Monitoring for Suspicious GitHub Activity with Google Security Operations (Part 2)

David-French
Staff

Welcome to part two of this mini series where I’m demonstrating how to get started with monitoring a GitHub Enterprise environment for suspicious and notable behavior. In part one, I explained the importance of monitoring & detection for GitHub and walked through a process for ingesting GitHub audit logs in the Google Security Operations platform.

In this post, I’ll provide some more details on the free detection rules we’ve made available to help security teams bolster their detection capabilities for their GitHub environment. Then we’ll do a deep dive on one of the rules to explain how it works with Google Security Operations before testing it to validate that it detects the intended behavior.

Summary of GitHub Enterprise detection rules

Below is a summary of coverage that these rules provide. Please keep in mind that every organization is different; these rules will need to be tested and tuned to fit your environment. You can use these rules to get started with monitoring your GitHub environment and start learning what’s normal versus out of the ordinary.

Rule Name

MITRE ATT&CK Technique

Access Granted to GitHub Personal Access Token Followed by High Number of Cloned Non-Public Reposito...

Data from Information Repositories: Code Repositories (T1213.003)

GitHub Dependabot Vulnerability Alerts Disabled

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub Enterprise Audit Log Stream Destroyed

Impair Defenses: Disable or Modify Cloud Logs (T1562.008)

GitHub Enterprise Audit Log Stream Modified

Impair Defenses: Disable or Modify Cloud Logs (T1562.008)

GitHub Enterprise Deleted

Data Destruction (T1485)

High Number of Non-Public GitHub Repositories Cloned

Data from Information Repositories: Code Repositories (T1213.003)

High Number of Non-Public GitHub Repositories Downloaded

Data from Information Repositories: Code Repositories (T1213.003)

GitHub Organization Removed from Enterprise

Data Destruction (T1485)

GitHub Outgoing Organization Transfer Initiated

Transfer Data to Cloud Account (T1537)

GitHub Outgoing Repository Transfer Initiated

Transfer Data to Cloud Account (T1537)

GitHub Personal Access Token Auto Approve Policy Modified

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub Personal Access Token Created from Tor IP Address

Account Manipulation: Additional Cloud Credentials (T1098.001)

GitHub Repository Archived or Deleted

Data Destruction (T1485)

GitHub Repository Branch Protection Rules Disabled

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub Repository Deploy Key Created or Modified

Account Manipulation: Additional Cloud Credentials (T1098.001)

GitHub Secret Scanning Alert

Unsecured Credentials (T1552)

GitHub Secret Scanning Disabled or Bypassed

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub SSO Configuration Modified

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub Two-Factor Authentication Requirement Disabled

Impair Defenses: Disable or Modify Tools (T1562.001)

GitHub User Blocked from Accessing Organization Repositories

 

GitHub Application Installed

 

GitHub User Unlocked from Accessing Organization Repositories

 

GitHub Repository Visibility Changed to Public

 

GitHub Invitation Sent to Non-Company Email Domain

 

GitHub OAuth Application Access Restrictions Disabled

 

GitHub Enterprise or Organization Recovery Codes Activity

 

Detecting exfiltration of private code repositories

As I mentioned in part one, while working at another company, I received intelligence that a threat group’s tactics were to compromise a developer or software engineer’s user account, create a GitHub personal access token under the account, and proceed to clone all of the GitHub repositories that the compromised user had access to. The attackers used VPN services to anonymize their activity. Now we are going to put this intelligence to use as Detection Engineers.

Based on the above intelligence, there are a few opportunities available to us as defenders to detect this behavior. One opportunity is as follows:

  1. A GitHub user creates a new personal access token
  2. The personal access token is used to clone one or more private GitHub repositories

After reviewing the documentation for GitHub’s audit log, I’m led to believe that we can use the following events in a rule to detect this attack scenario:

Event Name

Description

personal_access_token.access_granted

A fine-grained personal access token was granted access to resources.

git.clone

A repository was cloned.

My next step before writing the logic for the new rule is to generate an example of the events I want to detect. I did this by creating a new GitHub personal access token under my user account and using the token to clone several private GitHub repositories from an organization that’s under my GitHub Enterprise account.

I searched for the GitHub audit log events in Google Security Operaions to explore the various fields and values that I can utilize in the new rule. Using the UDM search below, I was able to identify the events I generated where access was granted to a GitHub personal access token and some GitHub repositories were cloned.

 

metadata.vendor_name = "GITHUB"
metadata.product_name = "GITHUB"
(
    metadata.product_event_type = "personal_access_token.access_granted" or
    metadata.product_event_type = "git.clone"
)

 

image.png
Searching for GitHub events in Google SecOps

After exploring the various field names and values in the GitHub events, we can craft a YARA-L rule that detects the behavior described earlier.

 

rule github_access_granted_to_personal_access_token_followed_by_high_number_of_cloned_non_public_repositories {

  meta:
    author = "Google Cloud Security"
    description = "Detects when a user grants access to a GitHub Personal Access Token prior to cloning several GitHub non-public GitHub repositories. An adversary may grant access to a Personal Access Token before attempting to steal the contents of several GitHub repositories using a an automated script or offensive tool."
    assumption = "Your GitHub enterprise audit log settings are configured to log the source IP address for events. Reference: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/displaying-ip-addresses-in-the-audit-log-for-your-organization"
    type = "alert"
    severity = "High"
    priority = "High"
    platform = "GitHub"
    data_source = "github"
    mitre_attack_tactic = "Collection"
    mitre_attack_technique = "Data from Information Repositories: Code Repositories"
    mitre_attack_url = "https://attack.mitre.org/versions/v14/techniques/T1213/003/"
    mitre_attack_version = "v14"
    reference = "https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise"

  events:
    // GitHub Personal Access Token (PAT) access granted event
    $github_pat.metadata.vendor_name = "GITHUB"
    $github_pat.metadata.product_name = "GITHUB"
    $github_pat.metadata.product_event_type = "personal_access_token.access_granted"

    // GitHub repository clone event
    $github_pat.metadata.vendor_name = "GITHUB"
    $github_clone.metadata.product_name = "GITHUB"
    $github_clone.metadata.product_event_type = "git.clone"
    $github_clone.additional.fields["repository_public"] = "false"
    $github_clone.target.resource.name = $github_repo_name

    // Join GitHub PAT access granted event to GitHub repository clone event
    $github_pat.principal.user.userid = $github_clone.principal.user.userid

    // Placeholder for match section
    $github_pat.principal.user.userid = $user_id

    // Ensure PAT access granted event occurred before repository clone events
    $github_pat.metadata.event_timestamp.seconds < $github_clone.metadata.event_timestamp.seconds

  match:
    $user_id over 30m

  outcome:
    $github_repo_name_distinct_count = count_distinct($github_repo_name)
    $risk_score = max(85)
    $mitre_attack_tactic = "Collection"
    $mitre_attack_technique = "Data from Information Repositories: Code Repositories"
    $mitre_attack_technique_id = "T1213.003"
    $event_count = count_distinct($github_clone.metadata.id)
    $principal_ip = array_distinct($github_pat.principal.ip)
    $principal_user_userid = array_distinct($github_pat.principal.user.userid)
    $principal_ip_country = array_distinct($github_pat.principal.ip_geo_artifact.location.country_or_region)
    $principal_ip_state = array_distinct($github_pat.principal.ip_geo_artifact.location.state)
    $principal_ip_city = array_distinct($github_pat.principal.location.city)
    $security_result_summary = array_distinct($github_pat.security_result.summary)

  condition:
    // Customize GitHub repo count to fit your environment
    $github_pat and $github_clone and $github_repo_name_distinct_count > 5

 

Below is a description of what is happening in the events section of the rule. I’ve included a screenshot of this section so that I can refer to the line numbers and make it easier for you to follow along.

image.png
Reviewing the events section of the rule in Google SecOps' rule editor

  • Lines 20-22: Search for GitHub personal access token creation events
  • Lines 25-28: Search for events where a non-public GitHub repository was cloned
  • Line 29: Create a placeholder variable named “$github_repo_name” that is mapped to the “target.resource.name” (GitHub repo name) field
  • Line 32: Join the GitHub personal access token event to the GitHub repo clone event(s)
  • Line 35: Create a placeholder variable named “$user_id” that’s mapped to the “principal.user.userid” (GitHub user ID) field
  • Line 38: Search for events where the GitHub personal access token was created before the GitHub repo clone event(s)

In the match section of the rule, we’re instructing the rule to return the GitHub user ID value when a match occurs within a 30 minute time window.

The outcome section allows us to define “outcome variables” with arbitrary names. These will be stored in the detections generated by the rule and provide context to a security analyst when triage and investigation is needed. The outcome variable to pay attention to in this example is “$github_repo_name_distinct_count” – this variable will store the number of distinct GitHub repos that were cloned by the user and will be used in the “condition” section of the rule.

The condition section ensures that the rule will trigger if a match is found for the “$github_pat” and “$github_clone” events defined in the events section of the rule and that more than 5 distinct private GitHub repositories were cloned.

image.png
Reviewing the match, outcome, and condition sections of the rule in Google SecOps' rules editor

Testing the rule via Google Security Operaions’ rules editor enables me to validate that the rule matches on the events that I generated earlier. The example detection below tells me that the GitHub user, “threat-punter” granted access to a personal access token before cloning six private GitHub repositories.

image.png
Testing the rule in Google SecOps

Tuning rules to improve their precision

In the real world of Detection Engineering, we are never truly done. We need to continuously monitor, test, and tune our rules to ensure their relevance, efficacy, and precision.

For this example detection use case, we’ll assume that the rule generates some false positives based on some activity that happens regularly in our organization. Let’s imagine that when a software engineer gets a new laptop, it’s typical for them to create a new GitHub personal access token and clone several private code repositories that they work on. This benign behavior causes the rule to generate pesky false positives that are a waste of precious time for our SOC analysts to triage. We can fix that.

Based on the intelligence we received, we know that the attacker’s like to use VPN services to anonymize their activity when stealing code repositories. We can incorporate this component into our rule to improve its precision.

Spur is a service that provides data on VPNs, residential proxies, and bots. I’ve ingested their feed into my instance of Google Security Operations so that I can utilize their data in my detections. The screenshot below shows an updated “events” section for the rule with newly added lines highlighted in red. A copy of this rule can be found in the appendix section of this post.

  • Line 30: Create a placeholder variable named “$ip” that is mapped to the “principal.ip” (GitHub user’s IP address) field
  • Line 33: Join the GitHub events with the IP address entities in the Spur data feeds using the $ip placeholder variable
  • Lines 36-39: Filter the events to include IP address entities from the Spur feeds that have the “VPN” tag (i.e. IP addresses that Spur has attributed to a VPN service)

image.png
Updating the rule's events section

The updated condition section for the rule ensures that it will trigger if the IP address in the GitHub events is found in the Spur feeds and is associated with a VPN service.

image.png
Updating the rule's condition section

Testing the updated rule

It’s crucial to test a rule when any changes are being proposed to understand how the rule’s efficacy and precision will be impacted and to validate that the rule still detects the behavior that it's designed to identify. After testing the rule via Google Secuirty Operations’ rules editor, I can see that the rule matches on the intended behavior:

  1. A GitHub user granted access to a personal access token
  2. The user account was used to clone over 5 non-public GitHub repositories
  3. The GitHub activity occurred from an IP address that Spur attributes to a VPN service

image.png
Reviewing detection after testing a rule in Google SecOps' rules editor

Reviewing the IP address entity shows that this IP address is attributed to the Mullvad VPN service. If this is a non-approved VPN service for our organization, the security team should triage this detection to verify whether the behavior is suspicious or not.

It’s important to note that this is just one example of how to use Google Security Operations’ entity graph and third-party data/intelligence feeds to create high efficacy detection rules. This method can be easily expanded to cover other detection use cases. For example, user/administrator authentication, MFA factor modification, or password reset events that come from an IP address that’s attributed to a non-company approved VPN service. Happy hunting.

image.png
Viewing an IP address entity in Google SecOps

Wrap up

That’s it for part two where I covered the following:

  • A summary of the free detection rules we’ve released to help security teams get started with monitoring their GitHub Enterprise environment
  • Crafting a YARA-L rule that can detect an adversary exfiltrating the contents of private GitHub repositories while using a VPN service
  • Utilizing the power of Google SecOps’ entity graph to enrich events using third-party data/intelligence feeds and create high efficacy detection rules

Please feel free to reach out on the Google Cloud Security Community with any questions.

Appendix

Latest version of YARA-L rule

 

rule github_access_granted_to_personal_access_token_followed_by_high_number_of_cloned_non_public_repositories {

  meta:
    author = "Google Cloud Security"
    description = "Detects when a user grants access to a GitHub Personal Access Token prior to cloning several GitHub non-public GitHub repositories. An adversary may grant access to a Personal Access Token before attempting to steal the contents of several GitHub repositories using a an automated script or offensive tool."
    assumption = "Your GitHub enterprise audit log settings are configured to log the source IP address for events. Reference: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/displaying-ip-addresses-in-the-audit-log-for-your-organization"
    type = "alert"
    severity = "High"
    priority = "High"
    platform = "GitHub"
    data_source = "github"
    mitre_attack_tactic = "Collection"
    mitre_attack_technique = "Data from Information Repositories: Code Repositories"
    mitre_attack_url = "https://attack.mitre.org/versions/v14/techniques/T1213/003/"
    mitre_attack_version = "v14"
    reference = "https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise"

  events:
    // GitHub Personal Access Token (PAT) access granted event
    $github_pat.metadata.vendor_name = "GITHUB"
    $github_pat.metadata.product_name = "GITHUB"
    $github_pat.metadata.product_event_type = "personal_access_token.access_granted"

    // GitHub repository clone event
    $github_pat.metadata.vendor_name = "GITHUB"
    $github_clone.metadata.product_name = "GITHUB"
    $github_clone.metadata.product_event_type = "git.clone"
    $github_clone.additional.fields["repository_public"] = "false"
    $github_clone.target.resource.name = $github_repo_name
    $github_clone.principal.ip = $ip

    // Join GitHub events with Spur data
    $spur.graph.entity.artifact.ip = $ip

    // IP address is in Spur's VPN IP address feed
    $spur.graph.metadata.vendor_name = "Spur"
    $spur.graph.metadata.product_name = "Spur Feeds"
    $spur.graph.metadata.entity_type = "IP_ADDRESS"
    $spur.graph.entity.artifact.tags = "VPN"
    
    // Join GitHub PAT access granted event to GitHub repository clone event
    $github_pat.principal.user.userid = $github_clone.principal.user.userid

    // Placeholder for match section
    $github_pat.principal.user.userid = $user_id

    // Ensure PAT access granted event occurred before repository clone events
    $github_pat.metadata.event_timestamp.seconds < $github_clone.metadata.event_timestamp.seconds

  match:
    $user_id over 30m

  outcome:
    $github_repo_name_distinct_count = count_distinct($github_repo_name)
    $risk_score = max(85)
    $mitre_attack_tactic = "Collection"
    $mitre_attack_technique = "Data from Information Repositories: Code Repositories"
    $mitre_attack_technique_id = "T1213.003"
    $event_count = count_distinct($github_clone.metadata.id)
    $principal_ip = array_distinct($github_pat.principal.ip)
    $principal_user_userid = array_distinct($github_pat.principal.user.userid)
    $principal_ip_country = array_distinct($github_pat.principal.ip_geo_artifact.location.country_or_region)
    $principal_ip_state = array_distinct($github_pat.principal.ip_geo_artifact.location.state)
    $principal_ip_city = array_distinct($github_pat.principal.location.city)
    $security_result_summary = array_distinct($github_pat.security_result.summary)

  condition:
    // Customize GitHub repo count to fit your environment
    $github_pat and $github_clone and $spur and $github_repo_name_distinct_count > 5
}

 

 

2 Comments