Detection rules › Panther
GitHub Workflow Using Self-Hosted Runner
Detects when a GitHub Actions workflow runs on a self-hosted runner.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1195.002 Supply Chain Compromise: Compromise Software Supply Chain |
| Execution | T1072 Software Deployment Tools |
| Lateral Movement | T1021 Remote Services, T1072 Software Deployment Tools |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- GitHub pull_request_target Workflow Usage (Panther)
- GitHub Workflow Contains Checkout Action (Panther)
- GitHub Workflow Downloading Artifacts (Panther)
Rule body yaml
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
Tests:
- Name: "Self-hosted runner on public repository"
ExpectedResult: true
Log:
action: "completed"
workflow_job:
id: 52841003143
name: "Build"
status: "completed"
conclusion: "success"
run_url: "https://github.com/example-org/example-repo/actions/runs/12345678/job/52841003143"
head_branch: "main"
head_sha: "abc123"
run_id: 12345678
runner_group_id: 1
runner_group_name: "Default"
runner_id: 42
runner_name: "my-runner-01"
started_at: "2025-10-15T18:31:06Z"
completed_at: "2025-10-15T18:41:54Z"
steps:
- name: "Setup"
status: "completed"
conclusion: "success"
repository:
id: 123456789
full_name: "example-org/example-repo"
private: false
visibility: "public"
allow_forking: true
sender:
login: "developer"
- Name: "Self-hosted runner on private forkable repository"
ExpectedResult: true
Log:
action: "completed"
workflow_job:
id: 52841003144
name: "Deploy"
status: "completed"
conclusion: "success"
run_id: 12345679
runner_group_id: 2
runner_group_name: "Production"
runner_id: 43
runner_name: "prod-runner-vm-02"
repository:
id: 123456790
full_name: "example-org/private-repo"
private: true
visibility: "private"
allow_forking: true
sender:
login: "developer"
- Name: "Self-hosted runner on private non-forkable repository"
ExpectedResult: true
Log:
action: "completed"
workflow_job:
id: 52841003145
name: "Test"
status: "completed"
conclusion: "success"
run_id: 12345680
runner_group_id: 1
runner_group_name: "Default"
runner_id: 44
runner_name: "internal-runner"
repository:
id: 123456791
full_name: "example-org/internal-repo"
private: true
visibility: "private"
allow_forking: false
sender:
login: "developer"
- Name: "GitHub-hosted runner (not self-hosted)"
ExpectedResult: false
Log:
action: "completed"
workflow_job:
id: 52841003146
name: "Lint"
status: "completed"
conclusion: "success"
run_id: 12345681
runner_group_id: 0
runner_group_name: "GitHub Actions"
runner_id: 1000000001
runner_name: "GitHub Actions 1000000001"
repository:
id: 123456792
full_name: "example-org/example-repo"
private: true
- Name: "Workflow job in progress (not completed)"
ExpectedResult: false
Log:
action: "in_progress"
workflow_job:
id: 52841003147
name: "Build"
status: "in_progress"
run_id: 12345682
runner_name: "my-runner-01"
repository:
id: 123456793
full_name: "example-org/example-repo"
- Name: "Self-hosted runner with empty name"
ExpectedResult: false
Log:
action: "completed"
workflow_job:
id: 52841003148
status: "completed"
run_id: 12345683
runner_name: ""
repository:
id: 123456794
full_name: "example-org/example-repo"
Detection logic
Condition
action eq "completed"
workflow_job.runner_name is_not_null
workflow_job.runner_name not starts_with "GitHub Actions"
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
workflow_job.runner_name | starts_with | GitHub Actions |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
action | eq |
|
workflow_job.runner_name | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
workflow_name | workflow_job.name |
workflow_job_id | workflow_job.id |
workflow_run_id | workflow_job.run_id |
workflow_url | workflow_job.html_url |
repository | repository.full_name |
repository_private | repository.private |
repository_visibility | repository.visibility |
head_branch | workflow_job.head_branch |
head_sha | workflow_job.head_sha |
conclusion | workflow_job.conclusion |
runner_name | workflow_job.runner_name |
runner_group_name | workflow_job.runner_group_name |
runner_id | workflow_job.runner_id |
runner_group_id | workflow_job.runner_group_id |
actor | sender.login |