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.
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.
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:
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"
)
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.
Reviewing the events section of the rule in Google SecOps' rule editor
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.
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.
Testing the rule in Google SecOps
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.
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.
Updating the rule's condition section
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:
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.
Viewing an IP address entity in Google SecOps
That’s it for part two where I covered the following:
Please feel free to reach out on the Google Cloud Security Community with any questions.
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
}