From e93cd671d7c1619ac6f3619d5b70a2243addb286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Og=C3=BCn=20Keskin?= Date: Mon, 1 Jun 2026 07:38:30 +0300 Subject: [PATCH] Add GitHub Actions hardening audit --- .github/workflows/ci.yml | 7 +- .github/workflows/contextforge-audit.yml | 22 +- .github/workflows/npm-publish.yml | 17 +- CHANGELOG.md | 6 + README.md | 16 +- action.yml | 24 ++- contextforge-actions-audit.md | 15 ++ contextforge-publish-readiness.md | 4 +- docs/actions-audit.md | 36 ++++ docs/adoption.md | 5 +- docs/artifacts.md | 3 + docs/github-action.md | 59 ++++-- docs/launch-post.md | 2 + docs/launch-snapshot.md | 4 +- docs/release-checklist.md | 1 + docs/research/adjacent-tools.md | 10 + docs/use-cases.md | 5 + llms-full.txt | 8 +- llms.txt | 4 +- package.json | 3 +- src/analyzers/githubActions.ts | 246 +++++++++++++++++++++++ src/cli.ts | 29 ++- src/init/githubAction.ts | 22 +- src/publish/npmReadiness.ts | 2 +- src/report/adoptionBrief.ts | 5 +- src/report/artifactMap.ts | 13 ++ src/report/githubActionsSarif.ts | 109 ++++++++++ src/report/launchKit.ts | 2 + src/report/launchSnapshot.ts | 4 +- tests/actionMetadata.test.ts | 6 + tests/actionsAudit.test.ts | 74 +++++++ tests/artifactMap.test.ts | 3 + tests/cli.test.ts | 27 ++- tests/init.test.ts | 6 +- tests/npmReadiness.test.ts | 4 +- tests/packageMetadata.test.ts | 1 + tests/workflows.test.ts | 9 +- 37 files changed, 751 insertions(+), 62 deletions(-) create mode 100644 contextforge-actions-audit.md create mode 100644 docs/actions-audit.md create mode 100644 src/analyzers/githubActions.ts create mode 100644 src/report/githubActionsSarif.ts create mode 100644 tests/actionsAudit.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac2345..7e21c81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: +permissions: + contents: read + env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -12,8 +15,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 package-manager-cache: false diff --git a/.github/workflows/contextforge-audit.yml b/.github/workflows/contextforge-audit.yml index 0d935cb..cdc7300 100644 --- a/.github/workflows/contextforge-audit.yml +++ b/.github/workflows/contextforge-audit.yml @@ -16,10 +16,10 @@ jobs: contextforge-audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 package-manager-cache: false @@ -44,6 +44,8 @@ jobs: if: always() - run: node dist/cli.js workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif if: always() + - run: node dist/cli.js actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif + if: always() - run: node dist/cli.js trace-audit --summary contextforge-trace-audit.md if: always() - run: node dist/cli.js review-kit --base main --output contextforge-review-kit.md @@ -53,7 +55,7 @@ jobs: - name: Write job summary if: always() run: cat contextforge-summary.md >> "$GITHUB_STEP_SUMMARY" - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: always() with: name: contextforge-audit @@ -77,22 +79,28 @@ jobs: contextforge-claude.sarif contextforge-workflow-audit.md contextforge-workflow.sarif + contextforge-actions-audit.md + contextforge-actions.sarif contextforge-trace-audit.md contextforge-review-kit.md contextforge-artifact-map.md - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-mcp.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-claude.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-workflow.sarif + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + with: + sarif_file: contextforge-actions.sarif diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 3c6fa61..517c8eb 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -24,13 +24,14 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + NPM_TAG: ${{ inputs.npm_tag }} jobs: preflight: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 registry-url: https://registry.npmjs.org @@ -47,10 +48,10 @@ jobs: - run: npm pack --dry-run - run: npm pack --json > npm-pack.json - name: Generate npm tarball attestation - uses: actions/attest@v4 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4 with: subject-path: 'contextforge-*.tgz' - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: always() with: name: contextforge-npm-publish-readiness @@ -65,8 +66,8 @@ jobs: runs-on: ubuntu-latest environment: npm-publish steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 registry-url: https://registry.npmjs.org @@ -83,8 +84,8 @@ jobs: - run: npm pack --dry-run - run: npm pack --json > npm-pack.json - name: Generate npm tarball attestation - uses: actions/attest@v4 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4 with: subject-path: 'contextforge-*.tgz' - name: Publish to npm - run: npm publish contextforge-*.tgz --access public --tag "${{ inputs.npm_tag }}" + run: npm publish contextforge-*.tgz --access public --tag "$NPM_TAG" diff --git a/CHANGELOG.md b/CHANGELOG.md index 80217c3..b6ccdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.69.0 - 2026-06-01 + +- Add `contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif` for GitHub Actions hardening proof. +- Detect mutable action refs, missing workflow permissions, `permissions: write-all`, `pull_request_target` risk, pwn-request checkout, and direct script interpolation of untrusted GitHub contexts. +- Dogfood the audit by pinning ContextForge workflows to full action SHAs, adding least-privilege CI permissions, routing npm publish tags through an environment variable, and uploading Actions SARIF to Code Scanning. + ## 0.68.0 - 2026-06-01 - Expand `contextforge workflow-audit` to treat issue titles, pull request titles, PR head refs, `github.head_ref`, `github.ref_name`, review-comment bodies, and discussion titles as untrusted agent inputs. diff --git a/README.md b/README.md index 824a5a3..8517a90 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ For the agentic workflow risk model and command details, see [docs/workflow-audit.md](docs/workflow-audit.md). For agentic GitHub workflow injection risk, see [contextforge-workflow-audit.md](contextforge-workflow-audit.md). +For GitHub Actions hardening risk, see +[contextforge-actions-audit.md](contextforge-actions-audit.md) and +[docs/actions-audit.md](docs/actions-audit.md). For session trace efficiency, see [contextforge-trace-audit.md](contextforge-trace-audit.md). For configurable session cost estimates, see @@ -118,6 +121,7 @@ contextforge surface-diff --base main --output contextforge-agent-surface-diff.m contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 contextforge pack --demo --task "review auth regression" --budget 600 --output contextforge-pack.md @@ -143,6 +147,7 @@ contextforge audit --demo --comment examples/pr-comment.md --badge contextforge- | MCP config risk | `contextforge-mcp-audit.md` | | Claude Code project settings risk | `contextforge-claude-audit.md` | | Agentic workflow injection risk | `contextforge-workflow-audit.md` | +| GitHub Actions hardening risk | `contextforge-actions-audit.md` | | Agent trace efficiency | `contextforge-trace-audit.md` | | Session cost estimate | `contextforge-cost-estimate.md` | | Context pack budget proof | `contextforge-pack.md` | @@ -348,7 +353,7 @@ contextforge pack --task "review auth regression" --budget 20000 --sessions --ou Or use the GitHub Action before npm publishing is complete: ```yaml -- uses: grnbtqdbyx-create/contextforge@v0.68.0 +- uses: grnbtqdbyx-create/contextforge@v0.69.0 with: min-context-score: 60 min-cache-score: 60 @@ -372,6 +377,7 @@ Or use the GitHub Action before npm publishing is complete: - **Audit MCP exposure:** publish `contextforge-mcp-audit.md` and `contextforge-mcp.sarif` so committed MCP configs cannot quietly ship hardcoded secrets, remote shell installers, unpinned package launches, auto-approval, broad tool permissions, or symlinked config files. - **Audit Claude Code settings:** publish `contextforge-claude-audit.md` and `contextforge-claude.sarif` so repo-committed Claude settings cannot quietly ship bypass modes, broad Bash permissions, remote shell hooks, or missing sensitive-file denies. - **Audit agentic workflows:** publish `contextforge-workflow-audit.md` and `contextforge-workflow.sarif` so GitHub workflows cannot quietly feed untrusted issue, PR, review, comment, title, input, or branch text into privileged agents. +- **Audit GitHub Actions hardening:** publish `contextforge-actions-audit.md` and `contextforge-actions.sarif` so agent-authored workflows cannot quietly ship mutable action tags, missing permissions, pwn-request checkout, or shell interpolation of untrusted GitHub context. - **Audit trace efficiency:** publish `contextforge-trace-audit.md` so repeated tool calls, huge outputs, tool-output-heavy traces, and low cache reuse are visible before the next long agent session. - **Estimate session cost:** publish `contextforge-cost-estimate.md` with runtime price inputs for uncached input, cached input, and output tokens. - **Publish the artifact map from CI:** attach `contextforge-artifact-map.md` beside proof-pack and review-kit outputs in reusable and generated GitHub workflows. @@ -436,6 +442,7 @@ and tuned for Codex/Claude repository work. | MCP findings should show up in GitHub Security. | `mcp-audit --sarif` writes `contextforge-mcp.sarif` with `mcp-exposure/*` rule ids for Code Scanning. | | Claude Code settings can over-trust a repo. | `claude-audit` checks shared `.claude/settings.json` permissions, hooks, bypass modes, and sensitive-file denies. | | Agentic GitHub workflows can ingest attacker-controlled text. | `workflow-audit` checks whether issue, PR, review, comment, title, workflow input, or branch/ref text flows into agentic jobs with write permissions or secrets. | +| Agent-authored CI can weaken the release path. | `actions-audit` checks workflow SHA pins, token permissions, `pull_request_target`, pwn-request checkout, and direct script interpolation. | | Claude Code subagents and custom slash commands can hide powerful project prompts. | `security-audit`, context health, and context packs include `.claude/agents/**/*.md` and `.claude/commands/**/*.md`. | | Copilot hooks can run shell commands during agent workflows. | `security-audit` scans `.github/hooks/*.json` and committed `.github/copilot/settings*.json` for unsafe shell, exfiltration, hidden directives, and permission weakening. | | VS Code workspace settings can carry Copilot instructions. | `security-audit` scans `.vscode/settings.json` and committed `*.code-workspace` files for risky Copilot review, commit, and PR instruction text. | @@ -479,12 +486,13 @@ contextforge surface-diff [--base main] [--json] [--output contextforge-agent-su contextforge mcp-audit [--demo] [--json] [--summary contextforge-mcp-audit.md] [--sarif contextforge-mcp.sarif] contextforge claude-audit [--demo] [--json] [--summary contextforge-claude-audit.md] [--sarif contextforge-claude.sarif] contextforge workflow-audit [--demo] [--json] [--summary contextforge-workflow-audit.md] [--sarif contextforge-workflow.sarif] +contextforge actions-audit [--json] [--summary contextforge-actions-audit.md] [--sarif contextforge-actions.sarif] contextforge trace-audit [--demo] [--json] [--summary contextforge-trace-audit.md] contextforge cost-estimate [--demo] [--json] [--summary contextforge-cost-estimate.md] [--input-price-per-mtok 0] [--cached-input-price-per-mtok 0] [--output-price-per-mtok 0] contextforge review-kit [--demo] [--base main] [--output contextforge-review-kit.md] contextforge artifact-map [--output docs/artifacts.md] contextforge publish-readiness [--json] [--summary contextforge-publish-readiness.md] -contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.68.0] [--force] +contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.69.0] [--force] ``` Local session scans are bounded by default. Use `--max-session-files` and @@ -569,7 +577,7 @@ See [docs/research/adjacent-tools.md](docs/research/adjacent-tools.md). ## Current Status -ContextForge v0.68.0 is a public MVP CLI with: +ContextForge v0.69.0 is a public MVP CLI with: - Claude Code and Codex JSONL fixture scanners - bounded local session scanning fallbacks @@ -599,6 +607,7 @@ ContextForge v0.68.0 is a public MVP CLI with: - reusable GitHub Action and dogfood workflow support for `contextforge-mcp-audit.md` and `contextforge-mcp.sarif` - reusable GitHub Action and dogfood workflow support for `contextforge-claude-audit.md` and `contextforge-claude.sarif` - reusable GitHub Action and dogfood workflow support for `contextforge-workflow-audit.md` and `contextforge-workflow.sarif` +- reusable GitHub Action and dogfood workflow support for `contextforge-actions-audit.md` and `contextforge-actions.sarif` - reusable GitHub Action and dogfood workflow support for `contextforge-review-kit.md` - reusable GitHub Action and dogfood workflow support for `contextforge-artifact-map.md` - PR-ready comments that summarize changed agent-readable surfaces and point reviewers at `contextforge-proof-pack.md`, `contextforge-review-kit.md`, and `contextforge-agent-surface-diff.md` @@ -710,6 +719,7 @@ ContextForge v0.68.0 is a public MVP CLI with: - **v0.66.0:** launch snapshots explain the why-now, adjacent-category, and proof-first story for README visitors. - **v0.67.0:** agentic workflow audits catch untrusted GitHub event text flowing into privileged AI workflows. - **v0.68.0:** workflow audits expand attacker-controlled coverage to titles and branch/ref text. +- **v0.69.0:** GitHub Actions audits catch mutable action refs, pwn-request checkout, missing permissions, and direct script interpolation. - **Next:** first approved npm publish and external launch outreach. Release preparation lives in [docs/release-checklist.md](docs/release-checklist.md). diff --git a/action.yml b/action.yml index f563816..52d03b8 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: 'ContextForge Audit' -description: 'Run ContextForge context health, cache stability, trace efficiency, security, MCP exposure, Claude settings, agentic workflow, HTML, JSON, SARIF, Markdown, PR comment, suggestions, badge, proof pack, scorecard, surface map, surface inventory, surface diff, review kit, artifact map, and agent action plan audits.' +description: 'Run ContextForge context health, cache stability, trace efficiency, security, MCP exposure, Claude settings, agentic workflow, GitHub Actions hardening, HTML, JSON, SARIF, Markdown, PR comment, suggestions, badge, proof pack, scorecard, surface map, surface inventory, surface diff, review kit, artifact map, and agent action plan audits.' author: 'Ogün Keskin' branding: @@ -95,6 +95,14 @@ inputs: description: 'Agentic GitHub workflow SARIF output path in the caller workspace.' required: false default: 'contextforge-workflow.sarif' + actions-audit: + description: 'GitHub Actions hardening audit Markdown output path in the caller workspace.' + required: false + default: 'contextforge-actions-audit.md' + actions-sarif: + description: 'GitHub Actions hardening SARIF output path in the caller workspace.' + required: false + default: 'contextforge-actions.sarif' trace-audit: description: 'Agent trace efficiency audit Markdown output path in the caller workspace.' required: false @@ -170,6 +178,12 @@ outputs: workflow-sarif: description: 'Path to the generated agentic GitHub workflow SARIF report.' value: ${{ inputs.workflow-sarif }} + actions-audit-md: + description: 'Path to the generated GitHub Actions hardening audit.' + value: ${{ inputs.actions-audit }} + actions-sarif: + description: 'Path to the generated GitHub Actions hardening SARIF report.' + value: ${{ inputs.actions-sarif }} trace-audit-md: description: 'Path to the generated agent trace efficiency audit.' value: ${{ inputs.trace-audit }} @@ -275,6 +289,14 @@ runs: node "$GITHUB_ACTION_PATH/dist/cli.js" workflow-audit \ --summary "${{ inputs.workflow-audit }}" \ --sarif "${{ inputs.workflow-sarif }}" + - name: Run ContextForge GitHub Actions audit + if: always() + shell: bash + run: | + cd "$GITHUB_WORKSPACE" + node "$GITHUB_ACTION_PATH/dist/cli.js" actions-audit \ + --summary "${{ inputs.actions-audit }}" \ + --sarif "${{ inputs.actions-sarif }}" - name: Run ContextForge trace efficiency audit if: always() shell: bash diff --git a/contextforge-actions-audit.md b/contextforge-actions-audit.md new file mode 100644 index 0000000..d7b56df --- /dev/null +++ b/contextforge-actions-audit.md @@ -0,0 +1,15 @@ +# ContextForge GitHub Actions Audit + +Status: **pass** + +Score: **100/100** + +Workflow files: `.github/workflows/ci.yml`, `.github/workflows/contextforge-audit.yml`, `.github/workflows/npm-publish.yml` + +| Type | Severity | File | Message | Suggestion | +| --- | --- | --- | --- | --- | +| none | low | | No GitHub Actions hardening findings. | Keep workflows pinned, least-privilege, and isolated from untrusted PR code. | + +## Next Actions + +- Keep GitHub Actions workflows pinned to full SHAs and least-privilege by default. diff --git a/contextforge-publish-readiness.md b/contextforge-publish-readiness.md index 5a767df..a32e4a3 100644 --- a/contextforge-publish-readiness.md +++ b/contextforge-publish-readiness.md @@ -2,11 +2,11 @@ Status: **warn** -Package: `contextforge@0.68.0` +Package: `contextforge@0.69.0` | Check | Status | Detail | | --- | --- | --- | -| Package metadata | pass | contextforge@0.68.0 is public-package ready with bin dist/cli.js | +| Package metadata | pass | contextforge@0.69.0 is public-package ready with bin dist/cli.js | | Package provenance metadata | pass | repository, homepage, and issue tracker point at grnbtqdbyx-create/contextforge for npm provenance readers | | Trusted publishing workflow | pass | npm Trusted Publishing uses GitHub OIDC, manual dispatch, dry-run default, and environment approval | | Release artifact attestation | pass | GitHub artifact attestation covers the packed npm tarball before the same tarball is published | diff --git a/docs/actions-audit.md b/docs/actions-audit.md new file mode 100644 index 0000000..c189f2b --- /dev/null +++ b/docs/actions-audit.md @@ -0,0 +1,36 @@ +# GitHub Actions Audit + +`contextforge actions-audit` scans committed GitHub Actions workflows for +CI/CD hardening issues that matter when coding agents can open PRs, edit +workflows, trigger automation, or prepare releases. + +```bash +contextforge actions-audit +contextforge actions-audit --json +contextforge actions-audit --summary contextforge-actions-audit.md +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif +``` + +It inspects `.github/workflows/*.yml` and `.github/workflows/*.yaml`. + +The audit flags: + +- actions that are not pinned to full commit SHAs +- missing top-level workflow `permissions:` +- `permissions: write-all` +- `pull_request_target` workflows +- `pull_request_target` workflows that checkout attacker-controlled PR head code +- untrusted GitHub contexts interpolated directly into `run:` shell steps + +Use the Markdown summary in PRs, launch issues, README proof surfaces, and +ContextForge Audit artifacts when reviewers need a fast answer to: "Can this +workflow safely run around agent-authored code?" Use +`--sarif contextforge-actions.sarif` when the same findings should appear in +GitHub Code Scanning beside repository-instruction, MCP, Claude settings, and +agentic workflow alerts. + +This is intentionally a deterministic hardening check, not a replacement for a +complete CI/CD threat model. It focuses on high-signal GitHub Actions footguns +that are especially risky in agent-heavy repositories: mutable action refs, +overbroad tokens, `pull_request_target`, direct script interpolation, and +privileged release automation. diff --git a/docs/adoption.md b/docs/adoption.md index 9ffe3f7..8cd1a9a 100644 --- a/docs/adoption.md +++ b/docs/adoption.md @@ -24,6 +24,7 @@ contextforge surface-diff --base main --output contextforge-agent-surface-diff.m contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 contextforge artifact-map --output docs/artifacts.md @@ -35,6 +36,7 @@ contextforge artifact-map --output docs/artifacts.md - Open `contextforge-mcp-audit.md` when the repo has MCP config files or agent tool setup; upload `contextforge-mcp.sarif` when GitHub Code Scanning should track those findings. - Open `contextforge-claude-audit.md` when the repo commits Claude Code project settings, hooks, or permissions. - Open `contextforge-workflow-audit.md` when GitHub workflows pass issue, PR, review, comment, title, workflow input, or branch/ref text into agent commands. +- Open `contextforge-actions-audit.md` when GitHub Actions workflows need SHA pinning, least-privilege permissions, pwn-request, or script-injection review. - Open `contextforge-trace-audit.md` when you want to see whether a Codex or Claude trace wasted context on repeated tools or bulky outputs. - Open `contextforge-cost-estimate.md` when you want to turn observed tokens into a configurable spend estimate without trusting stale hardcoded prices. - Open `docs/artifacts.md` when CI uploaded many files and you need the right next proof artifact. @@ -63,6 +65,7 @@ node dist/cli.js surface-diff --base main --output contextforge-agent-surface-di node dist/cli.js mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif node dist/cli.js claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif node dist/cli.js workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +node dist/cli.js actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif node dist/cli.js trace-audit --demo --summary contextforge-trace-audit.md node dist/cli.js cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 ``` @@ -72,7 +75,7 @@ After npm publish, the same proof path should work with `npx contextforge ...`. ## Star-Worthy Proof - The CLI is deterministic and local-first; it does not call an LLM to create audit results. -- The repository dogfoods its own GitHub Action and uploads scorecard, surface diff, MCP audit, MCP SARIF, Claude settings audit, agentic workflow audit, workflow SARIF, trace audit, proof-pack, review-kit, artifact-map, SARIF, JSON, HTML, and Markdown artifacts. +- The repository dogfoods its own GitHub Action and uploads scorecard, surface diff, MCP audit, MCP SARIF, Claude settings audit, agentic workflow audit, workflow SARIF, GitHub Actions audit, Actions SARIF, trace audit, proof-pack, review-kit, artifact-map, SARIF, JSON, HTML, and Markdown artifacts. - The launch snapshot explains the why-now story without asking visitors to read the whole repository first. - PR comments embed changed agent-surface summaries so reviewers see context drift before opening artifacts. - Release notes include validation commands and GitHub Actions run evidence. diff --git a/docs/artifacts.md b/docs/artifacts.md index e19d5ab..7578a41 100644 --- a/docs/artifacts.md +++ b/docs/artifacts.md @@ -27,6 +27,8 @@ Use this to decide which ContextForge artifact a maintainer, reviewer, CI bot, C | `contextforge-claude.sarif` | GitHub Code Scanning | you want Claude Code settings findings to appear beside code scanning alerts | `contextforge claude-audit --sarif contextforge-claude.sarif` | | `contextforge-workflow-audit.md` | Security reviewers and agent workflow maintainers | you need to see whether GitHub issue, PR, review, comment, title, workflow input, or branch/ref text flows into privileged AI workflows | `contextforge workflow-audit --summary contextforge-workflow-audit.md` | | `contextforge-workflow.sarif` | GitHub Code Scanning | you want agentic workflow injection findings to appear beside code scanning alerts | `contextforge workflow-audit --sarif contextforge-workflow.sarif` | +| `contextforge-actions-audit.md` | Security reviewers and release maintainers | you need to review GitHub Actions SHA pins, token permissions, pull_request_target risk, and direct script interpolation | `contextforge actions-audit --summary contextforge-actions-audit.md` | +| `contextforge-actions.sarif` | GitHub Code Scanning | you want GitHub Actions hardening findings to appear beside code scanning alerts | `contextforge actions-audit --sarif contextforge-actions.sarif` | | `contextforge-trace-audit.md` | Codex and Claude operators | you need to review repeated tool calls, bulky tool output, and cache reuse before another long agent session | `contextforge trace-audit --summary contextforge-trace-audit.md` | | `contextforge-cost-estimate.md` | Maintainers and budget reviewers | you need a shareable cost estimate from observed session tokens without hardcoded provider pricing | `contextforge cost-estimate --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10` | | `contextforge-pack.md` | Codex, Claude, and human reviewers | you need a task-specific context bundle with a visible token budget ledger | `contextforge pack --task "review auth regression" --budget 20000 --sessions --output contextforge-pack.md` | @@ -64,6 +66,7 @@ contextforge surface-diff --base main --output contextforge-agent-surface-diff.m contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 contextforge pack --demo --task "review auth regression" --budget 600 --output contextforge-pack.md diff --git a/docs/github-action.md b/docs/github-action.md index 991d4a0..154f822 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -8,8 +8,9 @@ agent surface support matrix, a repo-specific agent surface inventory, a PR-specific agent surface diff, a committed MCP exposure audit, a dedicated MCP SARIF file, a Codex/Claude review kit, a Claude Code settings audit, a dedicated Claude settings SARIF file, an -agentic GitHub workflow audit, a dedicated workflow SARIF file, and an -agent-readable action plan on every push or pull request. +agentic GitHub workflow audit, a dedicated workflow SARIF file, a GitHub +Actions hardening audit, a dedicated Actions SARIF file, and an agent-readable +action plan on every push or pull request. ## One-command Setup @@ -28,12 +29,14 @@ The audit workflow writes JSON, HTML, SARIF, Markdown summary, PR comment, suggestions JSON, SVG badge, proof-pack Markdown, scorecard Markdown, agent surface map Markdown, agent surface inventory Markdown, agent surface diff Markdown, MCP audit Markdown, MCP SARIF, Claude settings Markdown, Claude settings SARIF, -agentic workflow Markdown, workflow SARIF, trace audit Markdown, review-kit Markdown, artifact-map Markdown, and agent action plan artifacts. It +agentic workflow Markdown, workflow SARIF, GitHub Actions audit Markdown, +Actions SARIF, trace audit Markdown, review-kit Markdown, artifact-map +Markdown, and agent action plan artifacts. It refuses to overwrite existing files by default: ```bash contextforge init --github-action --force -contextforge init --github-action --action-ref grnbtqdbyx-create/contextforge@v0.68.0 +contextforge init --github-action --action-ref grnbtqdbyx-create/contextforge@v0.69.0 ``` `contextforge init --pr-comment-workflow` writes a separate @@ -65,10 +68,10 @@ jobs: contextforge-audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 - - uses: grnbtqdbyx-create/contextforge@v0.68.0 + - uses: grnbtqdbyx-create/contextforge@v0.69.0 with: min-context-score: 60 min-cache-score: 60 @@ -93,11 +96,13 @@ jobs: claude-sarif: contextforge-claude.sarif workflow-audit: contextforge-workflow-audit.md workflow-sarif: contextforge-workflow.sarif + actions-audit: contextforge-actions-audit.md + actions-sarif: contextforge-actions.sarif trace-audit: contextforge-trace-audit.md review-kit: contextforge-review-kit.md artifact-map: contextforge-artifact-map.md review-base-ref: main - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: always() with: name: contextforge-audit @@ -121,25 +126,31 @@ jobs: contextforge-claude.sarif contextforge-workflow-audit.md contextforge-workflow.sarif + contextforge-actions-audit.md + contextforge-actions.sarif contextforge-trace-audit.md contextforge-review-kit.md contextforge-artifact-map.md - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-mcp.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-claude.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-workflow.sarif + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + with: + sarif_file: contextforge-actions.sarif ``` The action builds ContextForge from the action checkout, then runs the built CLI @@ -179,6 +190,10 @@ missing sensitive-file deny rules. The `contextforge-workflow-audit.md` and `contextforge-workflow.sarif` artifacts show whether GitHub issue, PR, review, comment, title, workflow input, or branch/ref text flows into agentic jobs with write permissions or secrets. +The `contextforge-actions-audit.md` and `contextforge-actions.sarif` artifacts +show whether GitHub Actions workflows have mutable action refs, missing +permissions, pwn-request checkout, or direct shell interpolation of untrusted +GitHub context. The `contextforge-trace-audit.md` artifact summarizes repeated tool calls, bulky tool output, tool-output-heavy traces, and cache reuse from available Codex or Claude session records. @@ -250,8 +265,8 @@ jobs: contextforge-audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 24 package-manager-cache: false @@ -276,6 +291,8 @@ jobs: if: always() - run: node dist/cli.js workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif if: always() + - run: node dist/cli.js actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif + if: always() - run: node dist/cli.js trace-audit --summary contextforge-trace-audit.md if: always() - run: node dist/cli.js review-kit --base main --output contextforge-review-kit.md @@ -285,7 +302,7 @@ jobs: - name: Write job summary if: always() run: cat contextforge-summary.md >> "$GITHUB_STEP_SUMMARY" - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: always() with: name: contextforge-audit @@ -309,25 +326,31 @@ jobs: contextforge-claude.sarif contextforge-workflow-audit.md contextforge-workflow.sarif + contextforge-actions-audit.md + contextforge-actions.sarif contextforge-trace-audit.md contextforge-review-kit.md contextforge-artifact-map.md - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-mcp.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-claude.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: sarif_file: contextforge-workflow.sarif + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + with: + sarif_file: contextforge-actions.sarif ``` For early projects, start with permissive thresholds and raise them as the repo diff --git a/docs/launch-post.md b/docs/launch-post.md index 1f8ab7a..ad3753b 100644 --- a/docs/launch-post.md +++ b/docs/launch-post.md @@ -15,6 +15,7 @@ contextforge review-kit --base main --output contextforge-review-kit.md contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 contextforge audit --summary contextforge-summary.md --plan contextforge-agent-plan.md --comment contextforge-pr-comment.md --suggestions contextforge-suggestions.json --badge contextforge-badge.svg --base main @@ -54,6 +55,7 @@ If this helps your agent work with less waste and better handoffs, a star helps - `contextforge mcp-audit --sarif contextforge-mcp.sarif` makes MCP config risk visible in GitHub Code Scanning. - `contextforge claude-audit --sarif contextforge-claude.sarif` makes Claude Code project settings risk visible in GitHub Code Scanning. - `contextforge workflow-audit --sarif contextforge-workflow.sarif` makes agentic GitHub workflow injection risk visible in GitHub Code Scanning. +- `contextforge actions-audit --sarif contextforge-actions.sarif` makes GitHub Actions hardening gaps visible in GitHub Code Scanning. - `contextforge trace-audit --summary contextforge-trace-audit.md` shows repeated tool calls, large outputs, and cache reuse from Codex or Claude traces. - `contextforge cost-estimate --summary contextforge-cost-estimate.md` turns observed tokens into a configurable spend estimate. - GitHub topics match the target audience. diff --git a/docs/launch-snapshot.md b/docs/launch-snapshot.md index 1eb8712..741189f 100644 --- a/docs/launch-snapshot.md +++ b/docs/launch-snapshot.md @@ -30,6 +30,7 @@ A short, shareable page for people deciding whether this project is worth trying | Are MCP configs risky? | `contextforge-mcp-audit.md` and `contextforge-mcp.sarif` | | Are Claude Code settings risky? | `contextforge-claude-audit.md` and `contextforge-claude.sarif` | | Can GitHub event text reach a privileged agent workflow? | `contextforge-workflow-audit.md` and `contextforge-workflow.sarif` | +| Are GitHub Actions pinned and least-privilege? | `contextforge-actions-audit.md` and `contextforge-actions.sarif` | | Did the last agent session waste context? | `contextforge-trace-audit.md` | | What would a long session cost? | `contextforge-cost-estimate.md` | | Is the first npm publish ready? | `contextforge-publish-readiness.md` | @@ -44,6 +45,7 @@ contextforge surface-diff --base main --output contextforge-agent-surface-diff.m contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge publish-readiness --summary contextforge-publish-readiness.md contextforge artifact-map --output docs/artifacts.md @@ -53,7 +55,7 @@ contextforge artifact-map --output docs/artifacts.md ContextForge is a local-first CI gate for the repository context that Codex, Claude Code, Copilot, Cursor, Cline, Gemini, Windsurf, and MCP-powered agents ingest. -It checks instruction bloat, prompt/context poisoning, MCP and Claude settings risk, agentic workflow injection risk, changed agent surfaces in PRs, trace waste, token-budgeted context packs, and release-readiness proof artifacts. +It checks instruction bloat, prompt/context poisoning, MCP and Claude settings risk, agentic workflow injection risk, GitHub Actions hardening, changed agent surfaces in PRs, trace waste, token-budgeted context packs, and release-readiness proof artifacts. Repo: https://github.com/grnbtqdbyx-create/contextforge diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 0c93768..0a5fa2f 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -21,6 +21,7 @@ - [x] npm publish workflow packs, attests, uploads, and publishes the same release tarball. - [x] Launch snapshot gives README visitors a why-now, adjacent-category, and proof-first page. - [x] Agentic workflow audit catches untrusted GitHub event text flowing into privileged AI workflows. +- [x] GitHub Actions audit catches mutable action refs, missing permissions, pwn-request checkout, and direct script interpolation. - [x] MCP exposure audit catches committed MCP config secrets, unsafe shell installers, unpinned package launches, auto-approval, broad tool permissions, and symlinked config files. - [x] MCP exposure findings can be exported as SARIF for GitHub Code Scanning. - [x] Claude Code project settings can be audited as Markdown and SARIF artifacts. diff --git a/docs/research/adjacent-tools.md b/docs/research/adjacent-tools.md index e858e6c..0bb87f0 100644 --- a/docs/research/adjacent-tools.md +++ b/docs/research/adjacent-tools.md @@ -560,3 +560,13 @@ refs. The extra coverage follows and recent [agentic workflow injection research](https://arxiv.org/abs/2605.07135) showing that prompt payloads do not need to live only in Markdown bodies. +ContextForge v0.69.0 adds `contextforge actions-audit --summary +contextforge-actions-audit.md --sarif contextforge-actions.sarif`, because +agent-authored workflow edits need the same proof loop as repo context files. +The check is intentionally narrower than full CI/CD security scanners such as +[zizmor](https://github.com/woodruffw/zizmor) and focused on the footguns that +matter most for coding-agent repositories: mutable action refs, missing +permissions, `pull_request_target`, pwn-request checkout, and direct shell +interpolation of untrusted GitHub contexts. ContextForge dogfoods the feature by +pinning its own workflows to full action SHAs and uploading the new Actions +SARIF beside MCP, Claude settings, and agentic workflow alerts. diff --git a/docs/use-cases.md b/docs/use-cases.md index 8bc442f..2d91ec8 100644 --- a/docs/use-cases.md +++ b/docs/use-cases.md @@ -141,6 +141,7 @@ contextforge scorecard --output contextforge-scorecard.md contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif +contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif contextforge trace-audit --demo --summary contextforge-trace-audit.md contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10 contextforge pack --demo --task "review auth regression" --budget 600 --output contextforge-pack.md @@ -171,6 +172,10 @@ Success signal: - Security reviewers can open `contextforge-workflow-audit.md` or upload `contextforge-workflow.sarif` to catch GitHub issue, PR, review, comment, title, workflow input, or branch/ref text flowing into privileged AI workflows. +- Release reviewers can open `contextforge-actions-audit.md` or upload + `contextforge-actions.sarif` to catch mutable action refs, missing + permissions, pwn-request checkout, and direct script interpolation before + agent-authored workflow changes reach `main`. - Agent operators can open `contextforge-trace-audit.md` to see whether the demo trace wasted turns on repeated tools or bulky output before they try local Codex/Claude history. diff --git a/llms-full.txt b/llms-full.txt index 9b3375f..0c5e69a 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -36,6 +36,8 @@ separate tools: - agentic GitHub workflow checks for untrusted issue, pull request, review, comment, discussion, workflow input, title, or branch/ref text reaching model-backed jobs that can use repository write permissions or secrets +- GitHub Actions hardening checks for mutable action refs, missing permissions, + `pull_request_target` pwn-request checkout, and direct script interpolation - trace efficiency checks for repeated tool calls, bulky tool output, tool-output-heavy sessions, and low cache reuse - configurable cost estimates using caller-provided input, cached-input, and @@ -95,6 +97,7 @@ contextforge surface-diff [--base main] [--json] [--output contextforge-agent-su contextforge mcp-audit [--demo] [--json] [--summary contextforge-mcp-audit.md] [--sarif contextforge-mcp.sarif] contextforge claude-audit [--demo] [--json] [--summary contextforge-claude-audit.md] [--sarif contextforge-claude.sarif] contextforge workflow-audit [--demo] [--json] [--summary contextforge-workflow-audit.md] [--sarif contextforge-workflow.sarif] +contextforge actions-audit [--json] [--summary contextforge-actions-audit.md] [--sarif contextforge-actions.sarif] contextforge trace-audit [--demo] [--json] [--summary contextforge-trace-audit.md] contextforge cost-estimate [--demo] [--json] [--summary contextforge-cost-estimate.md] [--input-price-per-mtok 0] [--cached-input-price-per-mtok 0] [--output-price-per-mtok 0] contextforge adoption-brief [--output docs/adoption.md] [--project-name "My App"] @@ -143,6 +146,8 @@ cases so maintainers can see what the scanner is expected to catch. - `contextforge-claude.sarif`: GitHub Code Scanning SARIF for committed Claude Code settings findings - `contextforge-workflow-audit.md`: agentic GitHub workflow injection summary from `workflow-audit` - `contextforge-workflow.sarif`: GitHub Code Scanning SARIF for agentic workflow findings +- `contextforge-actions-audit.md`: GitHub Actions hardening summary from `actions-audit` +- `contextforge-actions.sarif`: GitHub Code Scanning SARIF for GitHub Actions hardening findings - `contextforge-trace-audit.md`: Codex/Claude trace efficiency summary from `trace-audit` - `contextforge-cost-estimate.md`: configurable session cost estimate from `cost-estimate` - `docs/adoption.md`: first-time evaluator page from `adoption-brief`, including the 30-second proof path, adjacent-tool positioning, and pre-npm try-it commands @@ -150,7 +155,7 @@ cases so maintainers can see what the scanner is expected to catch. - `docs/artifacts.md`: generated artifact catalog and fast paths from `artifact-map` - `contextforge-artifact-map.md`: CI-uploaded artifact catalog from reusable and generated workflows - `contextforge-publish-readiness.md`: npm Trusted Publishing and package provenance readiness summary from `publish-readiness` -- Reusable GitHub Action and generated audit workflows upload `contextforge-proof-pack.md`, `contextforge-scorecard.md`, `contextforge-agent-surface-map.md`, `contextforge-agent-surface-inventory.md`, `contextforge-agent-surface-diff.md`, `contextforge-mcp-audit.md`, `contextforge-mcp.sarif`, `contextforge-claude-audit.md`, `contextforge-claude.sarif`, `contextforge-workflow-audit.md`, `contextforge-workflow.sarif`, `contextforge-review-kit.md`, and `contextforge-artifact-map.md` alongside JSON, HTML, SARIF, summary, plan, comment, suggestions, and badge artifacts. Dogfood and generated workflows opt JavaScript actions into Node 24. +- Reusable GitHub Action and generated audit workflows upload `contextforge-proof-pack.md`, `contextforge-scorecard.md`, `contextforge-agent-surface-map.md`, `contextforge-agent-surface-inventory.md`, `contextforge-agent-surface-diff.md`, `contextforge-mcp-audit.md`, `contextforge-mcp.sarif`, `contextforge-claude-audit.md`, `contextforge-claude.sarif`, `contextforge-workflow-audit.md`, `contextforge-workflow.sarif`, `contextforge-actions-audit.md`, `contextforge-actions.sarif`, `contextforge-review-kit.md`, and `contextforge-artifact-map.md` alongside JSON, HTML, SARIF, summary, plan, comment, suggestions, and badge artifacts. Dogfood and generated workflows opt JavaScript actions into Node 24. - PR-ready comments embed a compact changed agent-surface summary and point reviewers at `contextforge-proof-pack.md`, `contextforge-review-kit.md`, and `contextforge-agent-surface-diff.md` so sticky review discussions can lead to the deeper doctor/audit proof packet, the changed-file review brief, and the changed agent-surface report. - `docs/launch-post.md`: generated build-in-public launch kit from `launch-kit` - `docs/comparison.md`: generated adjacent-tool positioning guide from `compare` @@ -162,6 +167,7 @@ cases so maintainers can see what the scanner is expected to catch. - GitHub Actions: CI and ContextForge Audit dogfood workflows - `docs/research/adjacent-tools.md`: research notes and differentiation - `docs/workflow-audit.md`: agentic GitHub workflow risk model, commands, and SARIF usage +- `docs/actions-audit.md`: GitHub Actions hardening risk model, commands, and SARIF usage ## License and Ownership diff --git a/llms.txt b/llms.txt index d9803f2..5bf7d2a 100644 --- a/llms.txt +++ b/llms.txt @@ -22,6 +22,7 @@ that Codex and Claude can act on. - `contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif`: scan committed MCP configs for hardcoded secrets, unsafe shell installers, unpinned package launches, auto-approval, broad tool permissions, and symlinked config files, with optional GitHub Code Scanning output. - `contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif`: scan committed Claude Code settings for risky default modes, broad Bash permissions, remote shell hooks, wildcard HTTP hooks, and missing sensitive-file denies. - `contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif`: scan GitHub Actions workflows for untrusted issue, PR, review, comment, discussion, workflow input, title, or branch/ref text reaching privileged agentic jobs. +- `contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif`: scan GitHub Actions workflows for SHA pinning gaps, missing permissions, pwn-request checkout, and direct script interpolation. - `contextforge security-audit --min-security-score 80`: scan repo instructions, Claude Code subagents, custom slash commands, Copilot prompts, hooks, and workspace settings for prompt/context poisoning. - `contextforge trace-audit --demo --summary contextforge-trace-audit.md`: summarize repeated tool calls, bulky tool output, tool-output dominance, and cache reuse from Codex/Claude traces. - `contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10`: estimate session spend with caller-provided prices. @@ -33,7 +34,7 @@ that Codex and Claude can act on. - `contextforge compare --output docs/comparison.md`: generate adjacent-tool positioning for README and launch use. - `contextforge improve --demo --json`: emit structured repo-rule suggestions for agents and bots. - `contextforge audit --summary contextforge-summary.md --plan contextforge-agent-plan.md --comment contextforge-pr-comment.md --suggestions contextforge-suggestions.json --badge contextforge-badge.svg --base main`: create CI, PR-review, changed agent-surface summary, structured suggestion, and status badge artifacts. -- The reusable GitHub Action and generated audit workflow publish `contextforge-proof-pack.md`, `contextforge-scorecard.md`, `contextforge-agent-surface-map.md`, `contextforge-agent-surface-inventory.md`, `contextforge-agent-surface-diff.md`, `contextforge-mcp-audit.md`, `contextforge-mcp.sarif`, `contextforge-claude-audit.md`, `contextforge-claude.sarif`, `contextforge-workflow-audit.md`, `contextforge-workflow.sarif`, `contextforge-review-kit.md`, and `contextforge-artifact-map.md` as CI artifacts, opt JavaScript actions into Node 24, and PR-ready comments embed changed agent-surface summaries. +- The reusable GitHub Action and generated audit workflow publish `contextforge-proof-pack.md`, `contextforge-scorecard.md`, `contextforge-agent-surface-map.md`, `contextforge-agent-surface-inventory.md`, `contextforge-agent-surface-diff.md`, `contextforge-mcp-audit.md`, `contextforge-mcp.sarif`, `contextforge-claude-audit.md`, `contextforge-claude.sarif`, `contextforge-workflow-audit.md`, `contextforge-workflow.sarif`, `contextforge-actions-audit.md`, `contextforge-actions.sarif`, `contextforge-review-kit.md`, and `contextforge-artifact-map.md` as CI artifacts, opt JavaScript actions into Node 24, and PR-ready comments embed changed agent-surface summaries. - `contextforge pack --task "review auth regression" --budget 20000 --sessions --output contextforge-pack.md`: build an explainable task context pack with a budget ledger. ## Key Docs @@ -50,6 +51,7 @@ that Codex and Claude can act on. - [MCP Audit](docs/mcp-audit.md): Committed MCP config exposure checks and SARIF output for agent tool safety. - [Claude Audit](docs/claude-audit.md): Committed Claude Code settings checks and SARIF output for shared permissions and hooks. - [Agentic Workflow Audit](docs/workflow-audit.md): GitHub Actions checks for untrusted event text reaching privileged AI workflows. +- [GitHub Actions Audit](docs/actions-audit.md): GitHub Actions hardening checks for SHA pinning, permissions, pwn-request, and script injection. - [Trace Audit](docs/trace-audit.md): Codex/Claude session efficiency checks for repeated tools, bulky outputs, and cache reuse. - [Cost Estimate](docs/cost-estimate.md): Configurable session cost estimates for input, cached input, and output tokens. - [Adoption Brief](docs/adoption.md): First-time evaluator page for maintainers deciding whether to try, star, or wire ContextForge into CI. diff --git a/package.json b/package.json index e06868a..d16613b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "contextforge", - "version": "0.68.0", + "version": "0.69.0", "description": "Agent context gate for Codex, Claude Code, GitHub Copilot, MCP, Cursor, Cline, Gemini, and Windsurf repos.", "type": "module", "packageManager": "pnpm@11.2.2", @@ -34,6 +34,7 @@ "contextforge-mcp-audit.md", "contextforge-claude-audit.md", "contextforge-workflow-audit.md", + "contextforge-actions-audit.md", "contextforge-trace-audit.md", "contextforge-cost-estimate.md", "contextforge-pack.md", diff --git a/src/analyzers/githubActions.ts b/src/analyzers/githubActions.ts new file mode 100644 index 0000000..be79a15 --- /dev/null +++ b/src/analyzers/githubActions.ts @@ -0,0 +1,246 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import type { Finding } from '../types.js'; +import { listFiles } from '../utils/files.js'; + +export type GithubActionsStatus = 'pass' | 'warn' | 'fail'; + +export interface GithubActionsAudit { + status: GithubActionsStatus; + score: number; + files: string[]; + findings: Finding[]; + nextActions: string[]; +} + +const WORKFLOW_DIR = '.github/workflows/'; +const WORKFLOW_EXTENSIONS = new Set(['.yml', '.yaml']); +const FULL_SHA_PATTERN = /^[a-f0-9]{40}$/i; +const USES_PATTERN = /^\s*(?:-\s*)?uses:\s*([^@\s]+)(?:@([^\s#]+))?/gm; +const PERMISSIONS_PATTERN = /(^|\n)\s*permissions\s*:/i; +const WRITE_ALL_PATTERN = /(^|\n)\s*permissions:\s*write-all\b/i; +const WRITE_PERMISSION_PATTERN = /(^|\n)\s*(contents|pull-requests|issues|actions|checks|id-token|deployments|packages|repository-projects|statuses):\s*write\b/i; +const PULL_REQUEST_TARGET_PATTERN = /(^|\n)\s*pull_request_target\s*:/i; +const CHECKOUT_PATTERN = /^\s*-\s*uses:\s*actions\/checkout@/im; +const PR_HEAD_REF_PATTERN = /github\.event\.pull_request\.head\.(sha|ref)|github\.head_ref/i; +const SECRET_PATTERN = /\bsecrets\.[A-Z0-9_]+\b/i; +const UNTRUSTED_CONTEXTS = [ + 'github.event.issue.title', + 'github.event.issue.body', + 'github.event.pull_request.title', + 'github.event.pull_request.body', + 'github.event.pull_request.head.ref', + 'github.event.pull_request.head.label', + 'github.head_ref', + 'github.ref_name', + 'github.event.comment.body', + 'github.event.review_comment.body', + 'github.event.review.body', + 'github.event.discussion.title', + 'github.event.discussion.body', + 'github.event.inputs', + 'inputs.' +]; + +export async function auditGithubActions(options: { rootDir?: string } = {}): Promise { + const rootDir = options.rootDir ?? process.cwd(); + const workflowFiles = await listWorkflowFiles(rootDir); + const findings: Finding[] = []; + + for (const filePath of workflowFiles) { + const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/'); + const content = await fs.readFile(filePath, 'utf8'); + findings.push(...findActionsRisks(relativePath, content)); + } + + const uniqueFindings = dedupeFindings(findings); + const penalty = uniqueFindings.reduce((total, finding) => total + (finding.severity === 'high' ? 30 : finding.severity === 'medium' ? 12 : 5), 0); + return { + status: statusForFindings(uniqueFindings), + score: Math.max(0, 100 - penalty), + files: workflowFiles.map((filePath) => path.relative(rootDir, filePath).split(path.sep).join('/')), + findings: uniqueFindings, + nextActions: nextActions(uniqueFindings) + }; +} + +export function formatGithubActionsAudit(audit: GithubActionsAudit): string { + const lines = [ + `ContextForge GitHub Actions audit: ${audit.status}`, + `Score: ${audit.score}/100`, + `Workflow files: ${audit.files.join(', ') || 'none'}`, + 'Findings:', + ...(audit.findings.length > 0 ? audit.findings.map((finding) => `- [${finding.severity}] ${finding.type}: ${finding.message}`) : ['- none']), + 'Next actions:', + ...audit.nextActions.map((action) => `- ${action}`) + ]; + return `${lines.join('\n')}\n`; +} + +export function createGithubActionsSummary(audit: GithubActionsAudit): string { + const lines = [ + '# ContextForge GitHub Actions Audit', + '', + `Status: **${audit.status}**`, + '', + `Score: **${audit.score}/100**`, + '', + `Workflow files: ${audit.files.length > 0 ? audit.files.map((file) => `\`${file}\``).join(', ') : 'none'}`, + '', + '| Type | Severity | File | Message | Suggestion |', + '| --- | --- | --- | --- | --- |', + ...(audit.findings.length > 0 + ? audit.findings.map( + (finding) => + `| ${escapeTableCell(finding.type)} | ${finding.severity} | ${escapeTableCell(finding.file ?? '')} | ${escapeTableCell(finding.message)} | ${escapeTableCell(finding.suggestion)} |` + ) + : ['| none | low | | No GitHub Actions hardening findings. | Keep workflows pinned, least-privilege, and isolated from untrusted PR code. |']), + '', + '## Next Actions', + '', + ...audit.nextActions.map((action) => `- ${action}`) + ]; + return `${lines.join('\n')}\n`; +} + +async function listWorkflowFiles(rootDir: string): Promise { + return listFiles(rootDir, (filePath) => { + const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/'); + return relativePath.startsWith(WORKFLOW_DIR) && WORKFLOW_EXTENSIONS.has(path.extname(filePath)); + }); +} + +function findActionsRisks(file: string, content: string): Finding[] { + const findings: Finding[] = []; + const hasPermissions = PERMISSIONS_PATTERN.test(content); + const usesPullRequestTarget = PULL_REQUEST_TARGET_PATTERN.test(content); + const hasWriteAll = WRITE_ALL_PATTERN.test(content); + const hasWritePermissions = WRITE_PERMISSION_PATTERN.test(content) || hasWriteAll; + const usesSecrets = SECRET_PATTERN.test(content); + + if (!hasPermissions) { + findings.push({ + file, + type: 'actions-missing-permissions', + severity: 'medium', + message: `${file} does not declare explicit workflow permissions.`, + suggestion: 'Add least-privilege top-level permissions, usually `contents: read`, and grant write scopes only in isolated jobs that need them.' + }); + } + + if (hasWriteAll) { + findings.push({ + file, + type: 'actions-write-all-permissions', + severity: 'high', + message: `${file} grants write-all permissions to the workflow token.`, + suggestion: 'Replace `permissions: write-all` with explicit least-privilege scopes and isolate any write-capable job from untrusted input.' + }); + } + + findings.push(...findUnpinnedActions(file, content)); + + if (usesPullRequestTarget && CHECKOUT_PATTERN.test(content) && PR_HEAD_REF_PATTERN.test(content)) { + findings.push({ + file, + type: 'actions-pwn-request-checkout', + severity: 'high', + message: `${file} uses pull_request_target while checking out attacker-controlled PR head code.`, + suggestion: 'Do not checkout or execute PR head code in pull_request_target workflows; split untrusted build and privileged reporting into separate workflows.' + }); + } else if (usesPullRequestTarget) { + findings.push({ + file, + type: 'actions-pull-request-target', + severity: hasWritePermissions || usesSecrets ? 'high' : 'medium', + message: `${file} uses pull_request_target, which runs with base-repository context.`, + suggestion: 'Keep pull_request_target workflows read-only and avoid secrets, write tokens, caches, or checkout of untrusted PR code.' + }); + } + + const scriptContexts = findRunInterpolations(content); + if (scriptContexts.length > 0) { + findings.push({ + file, + type: 'actions-script-injection', + severity: usesPullRequestTarget || usesSecrets ? 'high' : 'medium', + message: `${file} interpolates ${scriptContexts.join(', ')} directly into a run step.`, + suggestion: 'Move untrusted GitHub contexts into environment variables or action inputs before shell use, and quote variables inside run scripts.' + }); + } + + return findings; +} + +function findUnpinnedActions(file: string, content: string): Finding[] { + const findings: Finding[] = []; + for (const match of content.matchAll(USES_PATTERN)) { + const action = match[1]; + const ref = match[2] ?? ''; + if (action.startsWith('./') || action.startsWith('../') || action.startsWith('docker://')) continue; + if (!ref || !FULL_SHA_PATTERN.test(ref)) { + findings.push({ + file, + type: 'actions-unpinned-action', + severity: action.startsWith('actions/') || action.startsWith('github/') ? 'low' : 'medium', + message: `${file} uses ${action}${ref ? `@${ref}` : ''} without a full commit SHA pin.`, + suggestion: 'Pin third-party and marketplace actions to full commit SHAs, then update pins intentionally through dependency review.' + }); + } + } + return findings; +} + +function findRunInterpolations(content: string): string[] { + const contexts = new Set(); + const lines = content.split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const runMatch = line.match(/^(\s*)-\s*run:\s*(.*)$|^(\s*)run:\s*(.*)$/); + if (!runMatch) continue; + const indent = (runMatch[1] ?? runMatch[3] ?? '').length; + const initial = runMatch[2] ?? runMatch[4] ?? ''; + const block = [initial]; + for (let inner = index + 1; inner < lines.length; inner += 1) { + const nextLine = lines[inner]; + const nextIndent = nextLine.match(/^(\s*)/)?.[1].length ?? 0; + if (nextLine.trim() !== '' && nextIndent <= indent) break; + block.push(nextLine); + } + const text = block.join('\n'); + for (const context of UNTRUSTED_CONTEXTS) { + if (text.includes(context)) contexts.add(context); + } + } + return [...contexts].sort(); +} + +function statusForFindings(findings: Finding[]): GithubActionsStatus { + if (findings.some((finding) => finding.severity === 'high')) return 'fail'; + if (findings.length > 0) return 'warn'; + return 'pass'; +} + +function nextActions(findings: Finding[]): string[] { + if (findings.length === 0) return ['Keep GitHub Actions workflows pinned to full SHAs and least-privilege by default.']; + const actions = ['Review every GitHub Actions hardening finding before trusting agent-authored or agent-triggered workflows.']; + if (findings.some((finding) => finding.type === 'actions-unpinned-action')) actions.push('Pin actions to full commit SHAs and update them through review.'); + if (findings.some((finding) => finding.type === 'actions-pwn-request-checkout')) actions.push('Remove PR-head checkout from pull_request_target workflows.'); + if (findings.some((finding) => finding.type === 'actions-script-injection')) actions.push('Route untrusted GitHub contexts through env vars before shell use.'); + if (findings.some((finding) => finding.type.includes('permissions'))) actions.push('Declare least-privilege `permissions:` for every workflow.'); + return actions; +} + +function dedupeFindings(findings: Finding[]): Finding[] { + const seen = new Set(); + return findings.filter((finding) => { + const key = `${finding.file ?? ''}|${finding.type}|${finding.message}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function escapeTableCell(value: string): string { + return value.replaceAll('|', '\\|').replaceAll('\n', ' '); +} diff --git a/src/cli.ts b/src/cli.ts index acfe8c6..527bb41 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ import { auditContextSecurity } from './analyzers/contextSecurity.js'; import { auditMcpExposure, createMcpExposureSummary, formatMcpExposureAudit } from './analyzers/mcpExposure.js'; import { auditClaudeSettings, createClaudeSettingsSummary, formatClaudeSettingsAudit } from './analyzers/claudeSettings.js'; import { auditAgenticWorkflows, createAgenticWorkflowSummary, formatAgenticWorkflowAudit } from './analyzers/agenticWorkflow.js'; +import { auditGithubActions, createGithubActionsSummary, formatGithubActionsAudit } from './analyzers/githubActions.js'; import { auditTraceEfficiency, createTraceEfficiencySummary, formatTraceEfficiencyAudit } from './analyzers/traceEfficiency.js'; import { createCostEstimateSummary, estimateSessionCost, formatCostEstimate } from './analyzers/costEstimate.js'; import { createContextPack } from './pack/contextPack.js'; @@ -29,6 +30,7 @@ import { createLaunchSnapshot } from './report/launchSnapshot.js'; import { createClaudeSettingsSarif } from './report/claudeSettingsSarif.js'; import { createMcpExposureSarif } from './report/mcpSarif.js'; import { createAgenticWorkflowSarif } from './report/agenticWorkflowSarif.js'; +import { createGithubActionsSarif } from './report/githubActionsSarif.js'; import { createPrComment } from './report/prComment.js'; import { createProofPack } from './report/proofPack.js'; import { createAgentReadinessScorecard, createAgentReadinessScorecardData } from './report/scorecard.js'; @@ -115,6 +117,9 @@ async function main(): Promise { case 'workflow-audit': await commandWorkflowAudit(args); break; + case 'actions-audit': + await commandActionsAudit(args); + break; case 'trace-audit': await commandTraceAudit(args); break; @@ -361,6 +366,26 @@ async function commandWorkflowAudit(args: CliArgs): Promise { if (audit.status === 'fail') process.exitCode = 1; } +async function commandActionsAudit(args: CliArgs): Promise { + const audit = await auditGithubActions({ rootDir: process.cwd() }); + if (args.summary) { + await fs.mkdir(dirname(args.summary), { recursive: true }); + await fs.writeFile(args.summary, createGithubActionsSummary(audit)); + } + if (args.sarif) { + await fs.mkdir(dirname(args.sarif), { recursive: true }); + await fs.writeFile(args.sarif, `${JSON.stringify(createGithubActionsSarif(audit), null, 2)}\n`); + } + console.log(args.json ? JSON.stringify(audit, null, 2) : formatGithubActionsAudit(audit)); + const written = [args.summary, args.sarif].filter(Boolean); + if (written.length > 0) { + const message = `Wrote ${written.join(' and ')}`; + if (args.json) console.error(message); + else console.log(message); + } + if (audit.status === 'fail') process.exitCode = 1; +} + async function commandTraceAudit(args: CliArgs): Promise { const audit = auditTraceEfficiency(await loadRecords(args)); if (args.summary) { @@ -769,6 +794,7 @@ function defaultOutputForCommand(command: string): string { if (command === 'compare') return 'docs/comparison.md'; if (command === 'mcp-audit') return 'contextforge-mcp-audit.md'; if (command === 'workflow-audit') return 'contextforge-workflow-audit.md'; + if (command === 'actions-audit') return 'contextforge-actions-audit.md'; if (command === 'trace-audit') return 'contextforge-trace-audit.md'; if (command === 'cost-estimate') return 'contextforge-cost-estimate.md'; if (command === 'proof-pack') return 'contextforge-proof-pack.md'; @@ -871,6 +897,7 @@ Usage: contextforge mcp-audit [--demo] [--json] [--summary contextforge-mcp-audit.md] [--sarif contextforge-mcp.sarif] contextforge claude-audit [--demo] [--json] [--summary contextforge-claude-audit.md] [--sarif contextforge-claude.sarif] contextforge workflow-audit [--demo] [--json] [--summary contextforge-workflow-audit.md] [--sarif contextforge-workflow.sarif] + contextforge actions-audit [--json] [--summary contextforge-actions-audit.md] [--sarif contextforge-actions.sarif] contextforge trace-audit [--demo] [--json] [--summary contextforge-trace-audit.md] contextforge cost-estimate [--demo] [--json] [--summary contextforge-cost-estimate.md] [--input-price-per-mtok 0] [--cached-input-price-per-mtok 0] [--output-price-per-mtok 0] contextforge agents-md-audit [--demo] @@ -893,7 +920,7 @@ Usage: contextforge surface-inventory [--json] [--output contextforge-agent-surface-inventory.md] contextforge surface-diff [--base main] [--json] [--output contextforge-agent-surface-diff.md] contextforge publish-readiness [--json] [--summary contextforge-publish-readiness.md] - contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.68.0] [--force] + contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.69.0] [--force] Session scan safety: --max-session-files 50 newest JSONL files to scan per provider diff --git a/src/init/githubAction.ts b/src/init/githubAction.ts index dbc159a..9035307 100644 --- a/src/init/githubAction.ts +++ b/src/init/githubAction.ts @@ -1,7 +1,7 @@ import { access, mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; -export const DEFAULT_GITHUB_ACTION_REF = 'grnbtqdbyx-create/contextforge@v0.68.0'; +export const DEFAULT_GITHUB_ACTION_REF = 'grnbtqdbyx-create/contextforge@v0.69.0'; export interface GithubActionScaffoldOptions { rootDir: string; @@ -53,7 +53,7 @@ jobs: contextforge-audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 - uses: ${actionRef} @@ -81,10 +81,12 @@ jobs: claude-sarif: contextforge-claude.sarif workflow-audit: contextforge-workflow-audit.md workflow-sarif: contextforge-workflow.sarif + actions-audit: contextforge-actions-audit.md + actions-sarif: contextforge-actions.sarif trace-audit: contextforge-trace-audit.md review-kit: contextforge-review-kit.md artifact-map: contextforge-artifact-map.md - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: always() with: name: contextforge-audit @@ -108,25 +110,31 @@ jobs: contextforge-claude.sarif contextforge-workflow-audit.md contextforge-workflow.sarif + contextforge-actions-audit.md + contextforge-actions.sarif contextforge-trace-audit.md contextforge-review-kit.md contextforge-artifact-map.md - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${sarifGuard} with: sarif_file: contextforge.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${sarifGuard} with: sarif_file: contextforge-mcp.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${sarifGuard} with: sarif_file: contextforge-claude.sarif - - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 if: ${sarifGuard} with: sarif_file: contextforge-workflow.sarif + - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + if: ${sarifGuard} + with: + sarif_file: contextforge-actions.sarif `; } diff --git a/src/publish/npmReadiness.ts b/src/publish/npmReadiness.ts index 224ed3b..5151399 100644 --- a/src/publish/npmReadiness.ts +++ b/src/publish/npmReadiness.ts @@ -163,7 +163,7 @@ function releaseArtifactAttestationCheck(workflow: string): NpmPublishReadinessC const missing = requiredFragments(workflow, [ 'attestations: write', 'npm pack --json > npm-pack.json', - 'actions/attest@v4', + 'actions/attest@', "subject-path: 'contextforge-*.tgz'", 'npm publish contextforge-*.tgz --access public' ]); diff --git a/src/report/adoptionBrief.ts b/src/report/adoptionBrief.ts index 8dbed4c..045e83f 100644 --- a/src/report/adoptionBrief.ts +++ b/src/report/adoptionBrief.ts @@ -33,6 +33,7 @@ export function createAdoptionBrief(options: AdoptionBriefOptions): string { 'contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif', 'contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif', 'contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif', + 'contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif', 'contextforge trace-audit --demo --summary contextforge-trace-audit.md', 'contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10', 'contextforge artifact-map --output docs/artifacts.md', @@ -44,6 +45,7 @@ export function createAdoptionBrief(options: AdoptionBriefOptions): string { '- Open `contextforge-mcp-audit.md` when the repo has MCP config files or agent tool setup; upload `contextforge-mcp.sarif` when GitHub Code Scanning should track those findings.', '- Open `contextforge-claude-audit.md` when the repo commits Claude Code project settings, hooks, or permissions.', '- Open `contextforge-workflow-audit.md` when GitHub workflows pass issue, PR, review, comment, title, workflow input, or branch/ref text into agent commands.', + '- Open `contextforge-actions-audit.md` when GitHub Actions workflows need SHA pinning, least-privilege permissions, pwn-request, or script-injection review.', '- Open `contextforge-trace-audit.md` when you want to see whether a Codex or Claude trace wasted context on repeated tools or bulky outputs.', '- Open `contextforge-cost-estimate.md` when you want to turn observed tokens into a configurable spend estimate without trusting stale hardcoded prices.', '- Open `docs/artifacts.md` when CI uploaded many files and you need the right next proof artifact.', @@ -72,6 +74,7 @@ export function createAdoptionBrief(options: AdoptionBriefOptions): string { 'node dist/cli.js mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif', 'node dist/cli.js claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif', 'node dist/cli.js workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif', + 'node dist/cli.js actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif', 'node dist/cli.js trace-audit --demo --summary contextforge-trace-audit.md', 'node dist/cli.js cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10', '```', @@ -81,7 +84,7 @@ export function createAdoptionBrief(options: AdoptionBriefOptions): string { '## Star-Worthy Proof', '', '- The CLI is deterministic and local-first; it does not call an LLM to create audit results.', - '- The repository dogfoods its own GitHub Action and uploads scorecard, surface diff, MCP audit, MCP SARIF, Claude settings audit, agentic workflow audit, workflow SARIF, trace audit, proof-pack, review-kit, artifact-map, SARIF, JSON, HTML, and Markdown artifacts.', + '- The repository dogfoods its own GitHub Action and uploads scorecard, surface diff, MCP audit, MCP SARIF, Claude settings audit, agentic workflow audit, workflow SARIF, GitHub Actions audit, Actions SARIF, trace audit, proof-pack, review-kit, artifact-map, SARIF, JSON, HTML, and Markdown artifacts.', '- The launch snapshot explains the why-now story without asking visitors to read the whole repository first.', '- PR comments embed changed agent-surface summaries so reviewers see context drift before opening artifacts.', '- Release notes include validation commands and GitHub Actions run evidence.', diff --git a/src/report/artifactMap.ts b/src/report/artifactMap.ts index 4854f67..a5a5b1d 100644 --- a/src/report/artifactMap.ts +++ b/src/report/artifactMap.ts @@ -132,6 +132,18 @@ const artifactRows: ArtifactMapRow[] = [ useWhen: 'you want agentic workflow injection findings to appear beside code scanning alerts', producedBy: '`contextforge workflow-audit --sarif contextforge-workflow.sarif`' }, + { + artifact: 'contextforge-actions-audit.md', + audience: 'Security reviewers and release maintainers', + useWhen: 'you need to review GitHub Actions SHA pins, token permissions, pull_request_target risk, and direct script interpolation', + producedBy: '`contextforge actions-audit --summary contextforge-actions-audit.md`' + }, + { + artifact: 'contextforge-actions.sarif', + audience: 'GitHub Code Scanning', + useWhen: 'you want GitHub Actions hardening findings to appear beside code scanning alerts', + producedBy: '`contextforge actions-audit --sarif contextforge-actions.sarif`' + }, { artifact: 'contextforge-trace-audit.md', audience: 'Codex and Claude operators', @@ -247,6 +259,7 @@ export function createArtifactMap(): string { 'contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif', 'contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif', 'contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif', + 'contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif', 'contextforge trace-audit --demo --summary contextforge-trace-audit.md', 'contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10', 'contextforge pack --demo --task "review auth regression" --budget 600 --output contextforge-pack.md', diff --git a/src/report/githubActionsSarif.ts b/src/report/githubActionsSarif.ts new file mode 100644 index 0000000..62adb42 --- /dev/null +++ b/src/report/githubActionsSarif.ts @@ -0,0 +1,109 @@ +import type { GithubActionsAudit } from '../analyzers/githubActions.js'; +import type { Finding, Severity } from '../types.js'; + +export interface GithubActionsSarifLog { + $schema: string; + version: '2.1.0'; + runs: GithubActionsSarifRun[]; +} + +interface GithubActionsSarifRun { + tool: { + driver: { + name: string; + informationUri: string; + rules: GithubActionsSarifRule[]; + }; + }; + results: GithubActionsSarifResult[]; +} + +interface GithubActionsSarifRule { + id: string; + shortDescription: { text: string }; + fullDescription: { text: string }; + help: { text: string }; + defaultConfiguration: { level: GithubActionsSarifLevel }; +} + +interface GithubActionsSarifResult { + ruleId: string; + level: GithubActionsSarifLevel; + message: { text: string }; + locations: GithubActionsSarifLocation[]; +} + +interface GithubActionsSarifLocation { + physicalLocation: { + artifactLocation: { uri: string }; + region: { startLine: number }; + }; + message: { text: string }; +} + +type GithubActionsSarifLevel = 'error' | 'warning' | 'note'; + +export function createGithubActionsSarif(audit: GithubActionsAudit): GithubActionsSarifLog { + return { + $schema: 'https://json.schemastore.org/sarif-2.1.0.json', + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'ContextForge GitHub Actions', + informationUri: 'https://github.com/grnbtqdbyx-create/contextforge', + rules: rulesFromFindings(audit.findings) + } + }, + results: audit.findings.map((finding) => ({ + ruleId: ruleId(finding.type), + level: sarifLevel(finding.severity), + message: { text: finding.message }, + locations: [ + { + physicalLocation: { + artifactLocation: { uri: finding.file ?? '.github/workflows/workflow.yml' }, + region: { startLine: 1 } + }, + message: { text: finding.suggestion } + } + ] + })) + } + ] + }; +} + +function rulesFromFindings(findings: Finding[]): GithubActionsSarifRule[] { + const rules = new Map(); + for (const finding of findings) { + const id = ruleId(finding.type); + if (rules.has(id)) continue; + rules.set(id, { + id, + shortDescription: { text: titleCase(finding.type) }, + fullDescription: { text: finding.message }, + help: { text: finding.suggestion }, + defaultConfiguration: { level: sarifLevel(finding.severity) } + }); + } + return [...rules.values()]; +} + +function ruleId(type: string): string { + return `github-actions/${type}`; +} + +function sarifLevel(severity: Severity): GithubActionsSarifLevel { + if (severity === 'high') return 'error'; + if (severity === 'medium') return 'warning'; + return 'note'; +} + +function titleCase(value: string): string { + return value + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} diff --git a/src/report/launchKit.ts b/src/report/launchKit.ts index 196528c..62a8ca2 100644 --- a/src/report/launchKit.ts +++ b/src/report/launchKit.ts @@ -35,6 +35,7 @@ export function createLaunchKit(options: LaunchKitOptions): string { 'contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif', 'contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif', 'contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif', + 'contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif', 'contextforge trace-audit --demo --summary contextforge-trace-audit.md', 'contextforge cost-estimate --demo --summary contextforge-cost-estimate.md --input-price-per-mtok 2 --cached-input-price-per-mtok 0.2 --output-price-per-mtok 10', 'contextforge audit --summary contextforge-summary.md --plan contextforge-agent-plan.md --comment contextforge-pr-comment.md --suggestions contextforge-suggestions.json --badge contextforge-badge.svg --base main', @@ -74,6 +75,7 @@ export function createLaunchKit(options: LaunchKitOptions): string { '- `contextforge mcp-audit --sarif contextforge-mcp.sarif` makes MCP config risk visible in GitHub Code Scanning.', '- `contextforge claude-audit --sarif contextforge-claude.sarif` makes Claude Code project settings risk visible in GitHub Code Scanning.', '- `contextforge workflow-audit --sarif contextforge-workflow.sarif` makes agentic GitHub workflow injection risk visible in GitHub Code Scanning.', + '- `contextforge actions-audit --sarif contextforge-actions.sarif` makes GitHub Actions hardening gaps visible in GitHub Code Scanning.', '- `contextforge trace-audit --summary contextforge-trace-audit.md` shows repeated tool calls, large outputs, and cache reuse from Codex or Claude traces.', '- `contextforge cost-estimate --summary contextforge-cost-estimate.md` turns observed tokens into a configurable spend estimate.', '- GitHub topics match the target audience.', diff --git a/src/report/launchSnapshot.ts b/src/report/launchSnapshot.ts index 06705e6..c055705 100644 --- a/src/report/launchSnapshot.ts +++ b/src/report/launchSnapshot.ts @@ -39,6 +39,7 @@ export function createLaunchSnapshot(options: LaunchSnapshotOptions): string { '| Are MCP configs risky? | `contextforge-mcp-audit.md` and `contextforge-mcp.sarif` |', '| Are Claude Code settings risky? | `contextforge-claude-audit.md` and `contextforge-claude.sarif` |', '| Can GitHub event text reach a privileged agent workflow? | `contextforge-workflow-audit.md` and `contextforge-workflow.sarif` |', + '| Are GitHub Actions pinned and least-privilege? | `contextforge-actions-audit.md` and `contextforge-actions.sarif` |', '| Did the last agent session waste context? | `contextforge-trace-audit.md` |', '| What would a long session cost? | `contextforge-cost-estimate.md` |', '| Is the first npm publish ready? | `contextforge-publish-readiness.md` |', @@ -53,6 +54,7 @@ export function createLaunchSnapshot(options: LaunchSnapshotOptions): string { 'contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif', 'contextforge claude-audit --summary contextforge-claude-audit.md --sarif contextforge-claude.sarif', 'contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif', + 'contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif', 'contextforge trace-audit --demo --summary contextforge-trace-audit.md', 'contextforge publish-readiness --summary contextforge-publish-readiness.md', 'contextforge artifact-map --output docs/artifacts.md', @@ -62,7 +64,7 @@ export function createLaunchSnapshot(options: LaunchSnapshotOptions): string { '', `${projectName} is a local-first CI gate for the repository context that Codex, Claude Code, Copilot, Cursor, Cline, Gemini, Windsurf, and MCP-powered agents ingest.`, '', - 'It checks instruction bloat, prompt/context poisoning, MCP and Claude settings risk, agentic workflow injection risk, changed agent surfaces in PRs, trace waste, token-budgeted context packs, and release-readiness proof artifacts.', + 'It checks instruction bloat, prompt/context poisoning, MCP and Claude settings risk, agentic workflow injection risk, GitHub Actions hardening, changed agent surfaces in PRs, trace waste, token-budgeted context packs, and release-readiness proof artifacts.', '', `Repo: ${repoUrl}`, '', diff --git a/tests/actionMetadata.test.ts b/tests/actionMetadata.test.ts index 1d60ba1..848e80e 100644 --- a/tests/actionMetadata.test.ts +++ b/tests/actionMetadata.test.ts @@ -26,6 +26,8 @@ describe('GitHub Action metadata', () => { expect(action).toContain('claude-sarif:'); expect(action).toContain('workflow-audit:'); expect(action).toContain('workflow-sarif:'); + expect(action).toContain('actions-audit:'); + expect(action).toContain('actions-sarif:'); expect(action).toContain('trace-audit:'); expect(action).toContain('review-kit:'); expect(action).toContain('artifact-map:'); @@ -45,6 +47,8 @@ describe('GitHub Action metadata', () => { expect(action).toContain('claude-sarif:'); expect(action).toContain('workflow-audit-md:'); expect(action).toContain('workflow-sarif:'); + expect(action).toContain('actions-audit-md:'); + expect(action).toContain('actions-sarif:'); expect(action).toContain('trace-audit-md:'); expect(action).toContain('review-kit-md:'); expect(action).toContain('artifact-map-md:'); @@ -70,6 +74,8 @@ describe('GitHub Action metadata', () => { expect(action).toContain('--sarif \"${{ inputs.claude-sarif }}\"'); expect(action).toContain('node \"$GITHUB_ACTION_PATH/dist/cli.js\" workflow-audit'); expect(action).toContain('--sarif \"${{ inputs.workflow-sarif }}\"'); + expect(action).toContain('node \"$GITHUB_ACTION_PATH/dist/cli.js\" actions-audit'); + expect(action).toContain('--sarif \"${{ inputs.actions-sarif }}\"'); expect(action).toContain('node \"$GITHUB_ACTION_PATH/dist/cli.js\" trace-audit'); expect(action).toContain('--summary \"${{ inputs.trace-audit }}\"'); expect(action).toContain('node \"$GITHUB_ACTION_PATH/dist/cli.js\" review-kit'); diff --git a/tests/actionsAudit.test.ts b/tests/actionsAudit.test.ts new file mode 100644 index 0000000..8626806 --- /dev/null +++ b/tests/actionsAudit.test.ts @@ -0,0 +1,74 @@ +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { auditGithubActions, createGithubActionsSummary, formatGithubActionsAudit } from '../src/analyzers/githubActions.js'; +import { createGithubActionsSarif } from '../src/report/githubActionsSarif.js'; + +describe('GitHub Actions audit', () => { + it('detects unpinned actions, pwn-request checkout, and script injection', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-actions-risk-')); + await mkdir(path.join(rootDir, '.github/workflows'), { recursive: true }); + await writeFile( + path.join(rootDir, '.github/workflows/risky.yml'), + [ + 'name: Risky', + 'on:', + ' pull_request_target:', + 'permissions:', + ' contents: write', + 'jobs:', + ' test:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - uses: actions/checkout@v5', + ' with:', + ' ref: ${{ github.event.pull_request.head.sha }}', + ' - uses: third-party/example-action@main', + ' - run: echo "${{ github.event.pull_request.title }}"' + ].join('\n') + ); + + const audit = await auditGithubActions({ rootDir }); + const types = audit.findings.map((finding) => finding.type); + const summary = createGithubActionsSummary(audit); + const text = formatGithubActionsAudit(audit); + const sarif = createGithubActionsSarif(audit); + + expect(audit.status).toBe('fail'); + expect(audit.files).toEqual(['.github/workflows/risky.yml']); + expect(types).toContain('actions-unpinned-action'); + expect(types).toContain('actions-pwn-request-checkout'); + expect(types).toContain('actions-script-injection'); + expect(text).toContain('ContextForge GitHub Actions audit: fail'); + expect(summary).toContain('# ContextForge GitHub Actions Audit'); + expect(sarif.runs[0].tool.driver.name).toBe('ContextForge GitHub Actions'); + expect(sarif.runs[0].results.some((result) => result.ruleId === 'github-actions/actions-pwn-request-checkout')).toBe(true); + }); + + it('passes pinned read-only workflows', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-actions-safe-')); + await mkdir(path.join(rootDir, '.github/workflows'), { recursive: true }); + await writeFile( + path.join(rootDir, '.github/workflows/ci.yml'), + [ + 'name: CI', + 'on:', + ' pull_request:', + 'permissions:', + ' contents: read', + 'jobs:', + ' test:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3', + ' - run: pnpm test' + ].join('\n') + ); + + const audit = await auditGithubActions({ rootDir }); + + expect(audit.status).toBe('pass'); + expect(audit.findings).toHaveLength(0); + }); +}); diff --git a/tests/artifactMap.test.ts b/tests/artifactMap.test.ts index 793602b..09b4eac 100644 --- a/tests/artifactMap.test.ts +++ b/tests/artifactMap.test.ts @@ -23,7 +23,10 @@ describe('artifact map report', () => { expect(map).toContain('contextforge-claude.sarif'); expect(map).toContain('contextforge-workflow-audit.md'); expect(map).toContain('contextforge-workflow.sarif'); + expect(map).toContain('contextforge-actions-audit.md'); + expect(map).toContain('contextforge-actions.sarif'); expect(map).toContain('contextforge workflow-audit --summary contextforge-workflow-audit.md --sarif contextforge-workflow.sarif'); + expect(map).toContain('contextforge actions-audit --summary contextforge-actions-audit.md --sarif contextforge-actions.sarif'); expect(map).toContain('contextforge-trace-audit.md'); expect(map).toContain('contextforge-cost-estimate.md'); expect(map).toContain('npm metadata, provenance links, Trusted Publishing'); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 68136d5..7cf3d96 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -62,7 +62,7 @@ describe('CLI help command', () => { it('prints the current default GitHub Action ref in init examples', async () => { const { stdout } = await execFileAsync('pnpm', ['contextforge', 'help']); - expect(stdout).toContain('--action-ref grnbtqdbyx-create/contextforge@v0.68.0'); + expect(stdout).toContain('--action-ref grnbtqdbyx-create/contextforge@v0.69.0'); }); }); @@ -337,6 +337,31 @@ describe('CLI workflow-audit command', () => { }); }); +describe('CLI actions-audit command', () => { + it('writes a GitHub Actions hardening summary and SARIF when requested', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-actions-audit-')); + const summaryPath = path.join(rootDir, 'actions-audit.md'); + const sarifPath = path.join(rootDir, 'actions-audit.sarif'); + + const { stdout } = await execFileAsync('pnpm', [ + 'contextforge', + 'actions-audit', + '--summary', + summaryPath, + '--sarif', + sarifPath + ]); + const summary = await readFile(summaryPath, 'utf8'); + const sarif = JSON.parse(await readFile(sarifPath, 'utf8')) as { runs: Array<{ tool: { driver: { name: string } } }> }; + + expect(stdout).toContain('ContextForge GitHub Actions audit:'); + expect(stdout).toContain(`Wrote ${summaryPath} and ${sarifPath}`); + expect(summary).toContain('# ContextForge GitHub Actions Audit'); + expect(sarif.runs[0].tool.driver.name).toBe('ContextForge GitHub Actions'); + await rm(rootDir, { recursive: true, force: true }); + }); +}); + describe('CLI trace-audit command', () => { it('writes an agent trace efficiency summary when requested', async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-trace-audit-')); diff --git a/tests/init.test.ts b/tests/init.test.ts index f3de8f2..7c94944 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -35,6 +35,8 @@ describe('GitHub Action init scaffold', () => { expect(workflow).toContain('contextforge-claude.sarif'); expect(workflow).toContain('contextforge-workflow-audit.md'); expect(workflow).toContain('contextforge-workflow.sarif'); + expect(workflow).toContain('contextforge-actions-audit.md'); + expect(workflow).toContain('contextforge-actions.sarif'); expect(workflow).toContain('contextforge-trace-audit.md'); expect(workflow).toContain('contextforge-review-kit.md'); expect(workflow).toContain('contextforge-artifact-map.md'); @@ -48,6 +50,8 @@ describe('GitHub Action init scaffold', () => { expect(workflow).toContain('claude-sarif: contextforge-claude.sarif'); expect(workflow).toContain('workflow-audit: contextforge-workflow-audit.md'); expect(workflow).toContain('workflow-sarif: contextforge-workflow.sarif'); + expect(workflow).toContain('actions-audit: contextforge-actions-audit.md'); + expect(workflow).toContain('actions-sarif: contextforge-actions.sarif'); expect(workflow).toContain('trace-audit: contextforge-trace-audit.md'); expect(workflow).toContain('review-kit: contextforge-review-kit.md'); expect(workflow).toContain('artifact-map: contextforge-artifact-map.md'); @@ -84,7 +88,7 @@ describe('GitHub Action init scaffold', () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-init-default-ref-')); const result = await scaffoldGithubActionWorkflow({ rootDir }); - expect(await readFile(result.path, 'utf8')).toContain('uses: grnbtqdbyx-create/contextforge@v0.68.0'); + expect(await readFile(result.path, 'utf8')).toContain('uses: grnbtqdbyx-create/contextforge@v0.69.0'); }); it('is available through the init CLI command', async () => { diff --git a/tests/npmReadiness.test.ts b/tests/npmReadiness.test.ts index a641f37..24ff87b 100644 --- a/tests/npmReadiness.test.ts +++ b/tests/npmReadiness.test.ts @@ -49,14 +49,14 @@ describe('npm publish readiness', () => { ' - run: node dist/cli.js audit --min-context-score 70 --min-cache-score 70 --min-security-score 70', ' - run: npm pack --dry-run', ' - run: npm pack --json > npm-pack.json', - ' - uses: actions/attest@v4', + ' - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26', ' with:', " subject-path: 'contextforge-*.tgz'", ' publish:', ' if: ${{ inputs.dry_run == false }}', ' environment: npm-publish', ' steps:', - ' - run: npm publish contextforge-*.tgz --access public --tag "${{ inputs.npm_tag }}"' + ' - run: npm publish contextforge-*.tgz --access public --tag "$NPM_TAG"' ].join('\n') ); await writeFile( diff --git a/tests/packageMetadata.test.ts b/tests/packageMetadata.test.ts index 4ab13ec..211cd12 100644 --- a/tests/packageMetadata.test.ts +++ b/tests/packageMetadata.test.ts @@ -22,6 +22,7 @@ describe('package metadata', () => { expect(pkg.files).toContain('contextforge-mcp-audit.md'); expect(pkg.files).toContain('contextforge-claude-audit.md'); expect(pkg.files).toContain('contextforge-workflow-audit.md'); + expect(pkg.files).toContain('contextforge-actions-audit.md'); expect(pkg.files).toContain('contextforge-trace-audit.md'); expect(pkg.files).toContain('contextforge-cost-estimate.md'); expect(pkg.files).toContain('contextforge-pack.md'); diff --git a/tests/workflows.test.ts b/tests/workflows.test.ts index 56f2c06..0ba181d 100644 --- a/tests/workflows.test.ts +++ b/tests/workflows.test.ts @@ -27,9 +27,10 @@ describe('GitHub workflows', () => { expect(workflow).toContain('needs: preflight'); expect(workflow).toContain('environment: npm-publish'); expect(workflow).toContain('npm pack --json > npm-pack.json'); - expect(workflow).toContain('actions/attest@v4'); + expect(workflow).toContain('actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26'); expect(workflow).toContain("subject-path: 'contextforge-*.tgz'"); expect(workflow).toContain('npm publish contextforge-*.tgz --access public'); + expect(workflow).toContain('NPM_TAG: ${{ inputs.npm_tag }}'); expect(workflow).toContain('publish-readiness --summary contextforge-publish-readiness.md'); expect(workflow).toContain('contextforge-publish-readiness.md'); expect(workflow).toContain('npm-pack.json'); @@ -61,6 +62,8 @@ describe('GitHub workflows', () => { expect(workflow).toContain('contextforge-claude.sarif'); expect(workflow).toContain('workflow-audit --summary contextforge-workflow-audit.md'); expect(workflow).toContain('contextforge-workflow.sarif'); + expect(workflow).toContain('actions-audit --summary contextforge-actions-audit.md'); + expect(workflow).toContain('contextforge-actions.sarif'); expect(workflow).toContain('trace-audit --summary contextforge-trace-audit.md'); expect(workflow).toContain('review-kit --base main --output contextforge-review-kit.md'); expect(workflow).toContain('artifact-map --output contextforge-artifact-map.md'); @@ -74,6 +77,8 @@ describe('GitHub workflows', () => { expect(workflow).toContain('contextforge-claude.sarif'); expect(workflow).toContain('contextforge-workflow-audit.md'); expect(workflow).toContain('contextforge-workflow.sarif'); + expect(workflow).toContain('contextforge-actions-audit.md'); + expect(workflow).toContain('contextforge-actions.sarif'); expect(workflow).toContain('contextforge-agent-plan.md'); expect(workflow).toContain('contextforge-pr-comment.md'); expect(workflow).toContain('contextforge-suggestions.json'); @@ -86,8 +91,10 @@ describe('GitHub workflows', () => { expect(workflow).toContain('contextforge-mcp-audit.md'); expect(workflow).toContain('contextforge-claude-audit.md'); expect(workflow).toContain('contextforge-workflow-audit.md'); + expect(workflow).toContain('contextforge-actions-audit.md'); expect(workflow).toContain('contextforge-review-kit.md'); expect(workflow).toContain('contextforge-artifact-map.md'); expect(workflow).toContain('contextforge-trace-audit.md'); + expect(workflow).toContain('github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa'); }); });