A fast, security-first static analyzer with heuristic auto-fix for GitHub Actions workflows.
Find injection, credential leakage, supply-chain, and pipeline-poisoning bugs in .github/workflows/ — and let -fix repair most of them for you.
- Security-first by design. Full coverage of the OWASP Top 10 CI/CD Security Risks — code/env/path/output injection, untrusted checkouts, artifact & cache poisoning, ref confusion, impostor commits, and more.
- Semantic, not regex. A real AST + expression parser + shell taint analyzer (
mvdan.cc/sh) — not string matching. Cross-step and cross-file taint propagation through$GITHUB_ENV, reusable workflow boundaries, and shell function arguments. - Auto-fix that ships PRs. 27+ rules carry an auto-fixer.
sisakulint -fix onrewrites the YAML in place;-fix dry-runpreviews the diff first. - Built for CI. SARIF output drops straight into reviewdog for inline PR review comments.
- AI-agent aware. Detects prompt injection, dangerous tool exposure, unsafe sandbox flags, and execution-order issues in claude-code-action and similar AI agent integrations (the Clinejection attack class).
| sisakulint | actionlint | zizmor | StepSecurity | CodeQL | |
|---|---|---|---|---|---|
| Workflow syntax / shell linting | ✅ | ✅ | partial | — | — |
| OWASP CI/CD Top-10 coverage | full | — | partial | runtime only | partial |
| Cross-file taint for reusable workflows | ✅ | — | — | — | ✅ |
Cross-step / cross-job taint via $GITHUB_ENV |
✅ | — | — | runtime | ✅ |
| Shell-aware taint (incl. function args) | ✅ | — | — | — | partial |
| AI agent action rules (Clinejection) | ✅ | — | — | — | — |
| Auto-fix | 27+ rules | — | — | N/A | |
| SARIF + reviewdog | ✅ | — | ✅ | ✅ | ✅ |
Static-analysis tools sit upstream of runtime tools — sisakulint catches bugs at PR review time, before any workflow runs.
- Quick start
- Remote pull request scans
- Installation
- What it detects
- Rule reference
- Example: detecting real vulnerabilities
- Auto-fix
- SARIF + reviewdog integration
- Configuration
- Architecture
- BlackHat Arsenal 2025
- Contributing
- License
- Citation
# macOS
brew tap sisaku-security/homebrew-sisakulint
brew install sisakulint
# Run in any repo with a .github/workflows/ directory
sisakulintOther ways to run:
sisakulint .github/workflows/release.yml # one file
sisakulint -fix dry-run # preview auto-fixes
sisakulint -fix on # apply auto-fixes
sisakulint -format "{{sarif .}}" # SARIF for CI
sisakulint -enable-rule missing-timeout-minutes # opt-in ruleExit codes: 0 = clean, 1 = findings, 2 = bad CLI args, 3 = fatal error.
Scan the exact PR head without cloning it yourself:
sisakulint -remote sisaku-security/sisakulint-agent -pr 18
# Service/CI callers should pin the webhook revision to prevent stale jobs from
# scanning a newer head. Repeat -remote-target to limit report/fix scope while
# every workflow in the snapshot remains available for cross-file analysis.
sisakulint \
-remote sisaku-security/sisakulint-agent \
-pr 18 \
-expected-head-sha 0123456789abcdef0123456789abcdef01234567 \
-remote-target .github/workflows/deploy.yml \
-format "{{sarif .}}"Pull request URLs are recognized as well (-remote https://github.com/owner/repo/pull/18).
The command downloads an archive at the resolved immutable head SHA, safely
materializes the complete repository, analyzes all workflows with local project
context, and reports only workflows changed in the PR. Private repositories can
authenticate with -github-token, SISAKULINT_GITHUB_TOKEN, GITHUB_TOKEN,
GH_TOKEN, an existing gh auth session, or Git credentials. Remote scanning
currently supports GitHub.com only; GitHub Enterprise Server and custom GitHub
API base URLs are not supported.
-fix dry-run can preview remote fixes. Applying -fix on requires a new,
explicit -remote-checkout-dir; sisakulint never writes directly to GitHub.
PR-mode fixes are limited to workflows changed in the PR and, when provided,
the -remote-target selection. Fixers which mutate repository files such as
.github/dependabot.yaml remain enabled for local scans but are disabled for
this workflow-only response contract.
brew tap sisaku-security/homebrew-sisakulint
brew install sisakulintgo install github.com/sisaku-security/sisakulint/cmd/sisakulint@latestRequires Go 1.25 or newer.
Download from the releases page and place it on $PATH:
# Linux example
curl -sSL -o sisakulint https://github.com/sisaku-security/sisakulint/releases/latest/download/sisakulint-linux-amd64
chmod +x sisakulint
sudo mv sisakulint /usr/local/bin/git clone https://github.com/sisaku-security/sisakulint.git
cd sisakulint
go build ./cmd/sisakulintThe easiest way to wire sisakulint into CI is the official sisaku-security/sisakulint-action. It installs the binary, runs the scan, renders findings as inline PR annotations, and (optionally) uploads SARIF to GitHub Code Scanning.
name: sisakulint
on:
pull_request:
paths: [".github/workflows/**"]
push:
branches: [main]
paths: [".github/workflows/**"]
permissions:
contents: read
pull-requests: write # inline PR annotations
security-events: write # only if upload-sarif: true
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: sisaku-security/sisakulint-action@596af4ab15e8c5b232c74aa97525a0302e7b7af4 # v1.0.0
with:
fail-on: high # none | low | medium | high | critical
upload-sarif: true # send SARIF to GitHub Code ScanningUseful inputs: version, working-directory, args, config-file, autofix (off / on / dry-run), fail-on, upload-sarif, sarif-file. See the action's README for the full list.
Alternative: pipe SARIF into reviewdog
If you'd rather use reviewdog for inline PR review comments instead of GitHub annotations:
- uses: reviewdog/action-setup@v1
- name: sisakulint + reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -sSL -o sisakulint \
https://github.com/sisaku-security/sisakulint/releases/latest/download/sisakulint-linux-amd64
chmod +x sisakulint
./sisakulint -format "{{sarif .}}" \
| reviewdog -f=sarif -reporter=github-pr-review -filter-mode=nofilterFull documentation: https://sisaku-security.github.io/lint/
50+ rules across syntax, configuration, credentials, injection, checkout, supply chain, poisoning, access control, and AI-agent action security.
Click to expand the full rule list
| Category | Rule | Severity | Description | Fix | Docs |
|---|---|---|---|---|---|
| Syntax | id | Low | ID collision detection for jobs/env vars | docs | |
| env-var | Low | Environment variable name validation | docs | ||
| permissions | High | Permission scopes and values validation | Yes | docs | |
| workflow-call | Medium | Reusable workflow call validation | docs | ||
| job-needs | Low | Job dependency validation | docs | ||
| expression | Medium | Expression syntax validation | docs | ||
| cond | Medium | Conditional expression validation | Yes | docs | |
| deprecated-commands | High | Deprecated workflow commands detection | docs | ||
| Config | timeout-minutes | Low | Ensures timeout-minutes is set (opt-in) | Yes | docs |
| cache-bloat | Low | Cache bloat with restore/save pair | Yes | docs | |
| Credentials | credentials | High | Hardcoded credentials detection | Yes | docs |
| secret-exposure | High | Excessive secrets exposure detection | Yes | docs | |
| unmasked-secret-exposure | High | Unmasked derived secrets detection | Yes | docs | |
| artipacked | Critical | Credential leakage via persisted checkout | Yes | docs | |
| secrets-in-artifacts | High | Sensitive data in artifact uploads | Yes | docs | |
| secrets-inherit | High | Excessive secrets inheritance | Yes | docs | |
| secret-exfiltration | Critical | Secret exfiltration via network commands | docs | ||
| secret-in-log | Critical | Secret values printed to build logs (taint-tracked) | Yes | docs | |
| Injection | code-injection-critical | Critical | Untrusted input in privileged triggers | Yes | docs |
| code-injection-medium | Medium | Untrusted input in normal triggers | Yes | docs | |
| envvar-injection-critical | Critical | Untrusted input to $GITHUB_ENV (privileged) | Yes | docs | |
| envvar-injection-medium | Medium | Untrusted input to $GITHUB_ENV (normal) | Yes | docs | |
| envpath-injection-critical | Critical | Untrusted input to $GITHUB_PATH (privileged) | Yes | docs | |
| envpath-injection-medium | Medium | Untrusted input to $GITHUB_PATH (normal) | Yes | docs | |
| output-clobbering-critical | Critical | Untrusted input to $GITHUB_OUTPUT (privileged) | Yes | docs | |
| output-clobbering-medium | Medium | Untrusted input to $GITHUB_OUTPUT (normal) | Yes | docs | |
| argument-injection-critical | Critical | Command-line argument injection (privileged) | Yes | docs | |
| argument-injection-medium | Medium | Command-line argument injection (normal) | Yes | docs | |
| Checkout | untrusted-checkout | Critical | Untrusted PR code in privileged contexts | Yes | docs |
| untrusted-checkout-toctou-critical | Critical | TOCTOU with labeled events | Yes | docs | |
| untrusted-checkout-toctou-high | High | TOCTOU with deployment environment | Yes | docs | |
| Supply Chain | commit-sha | High | Action version pinning validation | Yes | docs |
| action-list | Low | Organization allowlist/blocklist enforcement | docs | ||
| impostor-commit | Critical | Fork network impostor commit detection | Yes | docs | |
| ref-confusion | High | Branch/tag name collision detection | Yes | docs | |
| known-vulnerable-actions | Varies | Known CVE detection via GitHub Advisories | Yes | docs | |
| archived-uses | Medium | Archived action/workflow detection | docs | ||
| unpinned-images | Medium | Container image digest pinning | docs | ||
| dependabot-github-actions | Medium | Missing github-actions ecosystem in dependabot.yaml | Yes | docs | |
| reusable-workflow-taint | Critical | Untrusted inputs in reusable workflow calls | Yes | docs | |
| Poisoning | artifact-poisoning-critical | Critical | Artifact poisoning and path traversal | Yes | docs |
| artifact-poisoning-medium | Medium | Third-party artifact download in untrusted triggers | Yes | docs | |
| cache-poisoning | High | Unsafe cache patterns with untrusted inputs | Yes | docs | |
| cache-poisoning-poisonable-step | High | Untrusted code execution after unsafe checkout | Yes | docs | |
| Access Control | improper-access-control | High | Label-based approval and synchronize events | Yes | docs |
| bot-conditions | High | Spoofable bot detection conditions | Yes | docs | |
| unsound-contains | Medium | Bypassable contains() in conditions | Yes | docs | |
| dangerous-triggers-critical | Critical | Privileged triggers without mitigations | Yes | docs | |
| dangerous-triggers-medium | Medium | Privileged triggers with partial mitigations | Yes | docs | |
| Other | obfuscation | High | Obfuscated workflow pattern detection | Yes | docs |
| self-hosted-runners | High | Self-hosted runner security risks | docs | ||
| request-forgery-critical | Critical | SSRF vulnerabilities (privileged) | Yes | docs | |
| request-forgery-medium | Medium | SSRF vulnerabilities (normal) | Yes | docs | |
| AI Actions | ai-action-unrestricted-trigger | High | AI agent actions with allowed_non_write_users: "*" |
docs | |
| ai-action-excessive-tools | High | Dangerous tools (Bash/Write/Edit) under untrusted triggers | docs | ||
| ai-action-prompt-injection | High | Untrusted input interpolated into AI agent prompts | docs | ||
| ai-action-unsafe-sandbox | High | Unsafe sandbox / safety-strategy settings | docs | ||
| ai-action-execution-order | Medium | AI agent action not the last step in a job | docs |
Given a workflow with several common security mistakes:
name: PR Comment Handler
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
process-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Echo PR title
run: |
echo "Processing PR: ${{ github.event.pull_request.title }}"
- name: Run build
run: npm install && npm run buildsisakulint -enable-rule missing-timeout-minutes flags every line that matters, with a code snippet and a link to the rule docs:
.github/workflows/demo.yaml:1:1: workflow does not have explicit 'permissions' block.
Follow the principle of least privilege.
See https://sisaku-security.github.io/lint/docs/rules/permissions/ [permissions]
1 👈| name: PR Comment Handler
.github/workflows/demo.yaml:4:3: dangerous trigger (critical): pull_request_target +
issue_comment without any security mitigations.
See https://sisaku-security.github.io/lint/docs/rules/dangeroustriggersrulecritical/ [dangerous-triggers-critical]
4 👈| pull_request_target:
.github/workflows/demo.yaml:13:9: the action ref should be a full length commit SHA. [commit-sha]
13 👈| - uses: actions/checkout@v4
.github/workflows/demo.yaml:15:16: untrusted PR code checked out in pull_request_target context.
See https://sisaku-security.github.io/lint/docs/rules/untrustedcheckout/ [untrusted-checkout]
15 👈| ref: ${{ github.event.pull_request.head.sha }}
.github/workflows/demo.yaml:19:35: code injection (critical): github.event.pull_request.title
is interpolated into an inline script.
See https://sisaku-security.github.io/lint/docs/rules/codeinjectioncritical/ [code-injection-critical]
19 👈| echo "Processing PR: ${{ github.event.pull_request.title }}"
[sisaku:🤔] Detected 7 errors in 1 file checked
| Finding | OWASP | Severity | Auto-fix |
|---|---|---|---|
| Missing permissions block | CICD-SEC-02 | High | Yes |
| Dangerous privileged triggers | CICD-SEC-01 | Critical | Yes |
| Action not pinned to SHA | CICD-SEC-08 | High | Yes |
| Credential exposure (persist-credentials) | CICD-SEC-06 | Critical | Yes |
| Untrusted checkout | CICD-SEC-04 | Critical | Yes |
| Code injection | CICD-SEC-04 | Critical | Yes |
| Cache poisoning | CICD-SEC-09 | High | Yes |
Run sisakulint -fix on and most of these are repaired in place.
27+ rules ship with an auto-fixer. Two modes:
sisakulint -fix dry-run # show diff, don't write
sisakulint -fix on # apply changes to YAML filesAuto-fix-capable rules currently include: timeout-minutes, commit-sha, credentials, code-injection-*, envvar-injection-*, envpath-injection-*, output-clobbering-*, argument-injection-*, request-forgery-*, untrusted-checkout, untrusted-checkout-toctou-*, artifact-poisoning-*, cache-poisoning, cache-poisoning-poisonable-step, cache-bloat, artipacked, secrets-in-artifacts, secrets-inherit, secret-in-log, secret-exposure, unmasked-secret-exposure, improper-access-control, bot-conditions, unsound-contains, obfuscation, ref-confusion, impostor-commit, known-vulnerable-actions, dangerous-triggers-*, cond, permissions, dependabot-github-actions, reusable-workflow-taint (cross-file ChainFixer lifts callee ${{ inputs.X }} into a step-level env:).
A few representative fixes:
Code injection — move untrusted input into env:
Before:
- run: echo "Processing PR: ${{ github.event.pull_request.title }}"After:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Processing PR: $PR_TITLE"commit-sha — pin action tags to full SHA, preserve original tag as comment
Before:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3After:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3untrusted-checkout — add explicit ref in privileged contexts
Before:
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
steps:
- uses: actions/checkout@v4After:
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}Notes
- Always review the diff (or use
-fix dry-run) before committing.commit-shacalls the GitHub API to resolve tags; unauthenticated requests are limited to 60/hr.- A handful of rules (e.g.
expression,secret-exfiltration,self-hosted-runners,action-list, AI-action rules) are warning-only because the right fix depends on the user's policy.
sisakulint -format "{{sarif .}}" # stdout
sisakulint -format "{{sarif .}}" > results.sarif # to file
sisakulint -format "{{sarif .}}" | reviewdog -f=sarif -reporter=github-pr-reviewA complete GitHub Actions recipe is in Installation → As a GitHub Action.
Generate a starter config:
sisakulint -init # writes .github/action.yamlThe config file lets you set per-repo allowlists, block lists, and rule overrides. Useful flags:
sisakulint -ignore "permissions" -ignore "SC2086" # mute specific rules / patterns
sisakulint -enable-rule missing-timeout-minutes # enable an opt-in rule
sisakulint -boilerplate # print a hardened workflow skeleton
sisakulint -debug # dump AST traversal + rule decisionsAdd to your VS Code settings.json:
"yaml.schemas": {
"https://github.com/sisaku-security/homebrew-sisakulint/raw/main/settings.json": "/.github/workflows/*.{yml,yaml}"
}-
Always review changes: Even though autofix is automated, always review the changes made to your workflow files before committing them
-
Commit SHA fixes require internet: The
commit-sharule needs to fetch commit information from GitHub, so it requires an active internet connection -
Rate limiting: The commit SHA autofix makes GitHub API calls, which are subject to rate limiting. Unauthenticated requests are capped at 60 requests per hour. Set a token to lift the limit to 5,000 req/h:
SISAKULINT_GITHUB_TOKEN(preferred — scoped to this tool)GITHUB_TOKEN(read from the process environment; inside a GitHub Actions step you must map it explicitly, e.g.env: GITHUB_TOKEN: ${{ github.token }}— the runner does not exportsecrets.GITHUB_TOKENtorun:steps automatically)GH_TOKEN(set bygh auth login)-github-token "$(gh auth token)"(CLI flag, highest priority)
A fine-grained PAT with
public_repo(or justMetadata: Read-onlyon the targets you pin) is sufficient. When sisakulint detects no token at startup, it now prints a warning. If the rate limit is exhausted mid-run, sisakulint aborts with a non-zero exit and skips writing partial output for the affected file rather than leaving a mix of pinned and unpinned actions on disk. -
Backup your files: Consider committing your changes or backing up your workflow files before running autofix
-
Not all rules support autofix: Some rules like
expression,permissions,issue-injection,cache-poisoning, anddeprecated-commandsrequire manual fixes as they depend on your specific use case -
Auto-fix capabilities: Currently,
timeout-minutes,commit-sha,credentials,untrusted-checkout, andartifact-poisoningrules support auto-fix. More rules will support auto-fix in future releases
.github/workflows/*.yml
│
▼
[ AST parser ] ──► [ Expression parser (${{ }}) ]
│ │
▼ ▼
[ Shell parser (mvdan.cc/sh) ] [ Taint propagator ]
│ │
└───────────► [ Rule engine ] ◄───── 50+ rules
│
┌─────────────┴─────────────┐
▼ ▼
[ Error formatter ] [ Auto-fixer ]
│ │
▼ ▼
SARIF / pretty text in-place YAML rewrite
- AST parser —
pkg/ast,pkg/core/parse_*.go - Expression parser —
pkg/expressions(full GitHub Actions${{ }}grammar) - Shell parser & taint —
pkg/shell(scope-aware bash semantics, function-arg propagation) - Rule engine —
pkg/core/*rule.goimplementing the visitor pattern - Auto-fixer —
pkg/core/autofixer.go
sisakulint was showcased at BlackHat Asia 2025 Arsenal. The talk covers the SAST design, the semantic-analysis approach to GitHub Actions security, the auto-fix pipeline, and real-world OWASP CI/CD Top-10 case studies. Originally built as a SecHack365 2023 project under NICT.
Bug reports, rule proposals, and PRs are very welcome.
- File issues at https://github.com/sisaku-security/sisakulint/issues
- Adding a new rule: see
docs/RULES_GUIDE.mdand the example workflows underscript/actions/ - Run the test suite:
go test ./... - Run against the bundled examples:
sisakulint script/actions/
If sisakulint helps you keep a workflow safe, please ⭐️ the repo — it makes it much easier for other security teams to find.
Apache License 2.0 © sisaku-security contributors.
If you reference sisakulint in academic or industry work:
@software{sisakulint,
title = {sisakulint: CI-friendly static linter with SAST semantic analysis for GitHub Actions},
author = {sisaku-security contributors},
year = {2025},
url = {https://github.com/sisaku-security/sisakulint}
}
