Skip to content

test(gh-aw): enforce shell input security contract over compiled output (#1834) - #1866

Merged
bradygaster merged 2 commits into
devfrom
bradygaster-1834-shell-contract-gate
Aug 25, 2026
Merged

test(gh-aw): enforce shell input security contract over compiled output (#1834)#1866
bradygaster merged 2 commits into
devfrom
bradygaster-1834-shell-contract-gate

Conversation

@bradygaster

Copy link
Copy Markdown
Owner

What

Enforces workflows/squad.md §"Shell input security contract [MANDATORY]" with a fail-closed gate over compiled gh-aw output. The contract was declared by #1832 but had no observer — this is the seventh sibling of the "reads as enforcement, catches nothing" defect (#1824/#1812/#1801/#1822/#1827/#1833). Closes #1834.

How it works

The contract spans two artifacts, so the gate verifies two surfaces:

  1. Compiled lock (squad.lock.yml). UNTRUSTED_TEMPLATE_IN_RUN is a property of compiled output — GitHub Actions expands template expressions before the shell starts, so an attacker-controlled github.event.*.body/title expression left in a compiled run: block is the observable failure. The gate compiles the real workflow with gh aw compile … --strict (reusing the existing harness) and scans every run: block.
  2. Runtime-imported parser source (workflows/squad.md). The printf/eval/bash -c/awk hops live in the /squad parser one-liners, which gh-aw pulls in verbatim at runtime via {{#runtime-import}} and never inlines into the lock. That imported source is therefore the only surface on which those hops are observable, and the gate scans it directly. This is not a parallel compiler or a source proxy — those snippets are genuinely never compiled.

Detectors key on body references (github.event.(issue|comment|pull_request|discussion).(body|title) expressions, or body-shaped vars like SQUAD_TRIGGER_BODY/*_BODY/*_TITLE/$body), not arbitrary $, so gh-aw's own machinery (bash -c 'export PATH="$PATH"', source "${RUNNER_TEMP}/…") does not false-positive.

Failures name token + file + line + evidence and print a remediation grep that actually observes the violation.

Fail-closed (every mode fails, never skips)

  • gh aw missing → hard fail with the exact install command (gh extension install github/gh-aw).
  • Compile produced no squad.lock.yml → fail.
  • Zero run: blocks inspected → fail (a scanner with nothing to scan is a permanently-green gate).
  • Zero body-handling parser shell lines found → fail.

The old it.skipIf(!ghAwAvailable) — the exact "silently skipped while green" defect — is removed.

Positive control (required by the issue)

test/fixtures/gh-aw-shell-contract/violating.lock.yml has 4 run: blocks, one per token, including RETRO's mandated exact line run: printf '%s\n' "${{ github.event.issue.body }}". The gate's own test exercises it, proving the gate can turn red rather than assuming it.

Files

File Why
test/gh-aw-shell-contract.ts (new) Pure, unit-testable scanner: extractRunBlocks, scanRunBlocks, scanShellLines, extractBodyHandlingShell, formatViolations.
test/fixtures/gh-aw-shell-contract/violating.lock.yml (new) Positive control proving the gate can fail. Under test/fixtures/ so actionlint doesn't lint it.
test/gh-aw-quality.test.ts Un-skipped + strengthened describe: compile+config, compiled run-block scan, parser-source scan, positive-control, negative-control.
workflows/squad.md Verification paragraph downgraded from "not implemented" to describe the implemented gate and the compiled/runtime-import split honestly.
.github/workflows/squad-ci.yml Install-step comment updated: the gate now fails closed (not skipIf) and references #1834.

Validation

  • vitest run test/gh-aw-quality.test.ts115 passed (5 new contract tests green on real compiled output + real parser source).
  • Mutation test: injecting run: printf "${{ github.event.issue.body }}" into the compiled lock turned the gate RED, naming UNTRUSTED_TEMPLATE_IN_RUN and UNTRUSTED_PRINTF_FORMAT at squad.lock.yml:2418 with evidence; removed → GREEN.
  • tsc (bundler resolution + node types) and eslint clean on both changed test files.
  • check-workflow-input-interpolation.mjs passes on the edited squad.md.

Honest limitation

The printf/eval/awk hops cannot be observed in the compiled lock because gh-aw runtime-imports them; they are verified on the imported source instead. This is disclosed in the contract text, not hidden. No changeset needed — no packages/*/src touched.

The contract in workflows/squad.md was declared but unenforced. This adds a fail-closed gate that compiles the real workflow with `gh aw compile --strict` and scans the compiled `run:` blocks for attacker-controlled event expressions (UNTRUSTED_TEMPLATE_IN_RUN), plus scans the runtime-imported /squad parser source for the printf/eval/bash-c/awk hops that gh-aw never inlines into the lock. A committed positive-control fixture proves the gate can turn red; failures name token + file + line. Missing gh aw, absent lock, or zero inspected surfaces all fail rather than skip.

Closes #1834

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 25, 2026 00:30
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

⚠️ Architectural review: 1 info.

Severity Category Finding Files
ℹ️ info template-sync Template files changed in .github/workflows/ but not in other template locations. If these templates should stay in sync, consider updating the others too. Changed: .github/workflows/, Unchanged: templates/, .squad-templates/, packages/squad-cli/templates/

Automated architectural review — informational only.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 5018b14

PR Scope: 🔧 Infrastructure

⚠️ 2 item(s) to address before review

Status Check Details
Single commit 2 commits — consider squashing before review
Not in draft Ready for review
Branch up to date Up to date with dev
Copilot review No Copilot review yet — it may still be processing
Changeset present No source files changed — changeset not required
Scope clean No .squad/ or docs/proposals/ files
No merge conflicts No merge conflicts
Copilot threads resolved 1 active Copilot thread(s) resolved (2 outdated skipped)
CI passing All checks passing

Files Changed (5 files, +821 −51)

File +/−
.github/workflows/squad-ci.yml +6 −4
test/fixtures/gh-aw-shell-contract/violating.lock.yml +42 −0
test/gh-aw-quality.test.ts +404 −43
test/gh-aw-shell-contract.ts +349 −0
workflows/squad.md +20 −4

Total: +821 −51


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1866

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 5
Files added 2
Files modified 3
Files deleted 0
Modules touched 3

🎯 Risk Factors

  • 5 files changed (≤5 → LOW)
  • 3 modules touched (2-4 → MEDIUM)

📦 Modules Affected

ci-workflows (1 file)
  • .github/workflows/squad-ci.yml
root (1 file)
  • workflows/squad.md
tests (3 files)
  • test/fixtures/gh-aw-shell-contract/violating.lock.yml
  • test/gh-aw-quality.test.ts
  • test/gh-aw-shell-contract.ts

This report is generated automatically for every PR. See #733 for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new scanner has confirmed bypass/accuracy issues (tainted-variable matching and printf -- handling, plus run-block extraction behavior) that could let violations slip or produce incorrect scan results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a fail-closed security gate to enforce the “Shell input security contract [MANDATORY]” by scanning (1) compiled run: blocks in squad.lock.yml and (2) runtime-imported parser shell in workflows/squad.md, with a positive-control fixture proving the gate can turn red.

Changes:

  • Add a new scanner module to extract run: blocks / parser shell lines and detect the four contract anti-pattern tokens.
  • Strengthen gh-aw-quality tests to compile via gh aw, scan both enforcement surfaces, and fail closed (no more silent skips).
  • Update contract documentation and CI commentary to reflect the new enforcement and its compiled/runtime-import split.
File summaries
File Description
workflows/squad.md Updates contract text to accurately describe the now-implemented, two-surface gate.
test/gh-aw-shell-contract.ts New scanner that extracts/inspects compiled run: blocks and runtime-imported parser shell to detect contract violations.
test/gh-aw-quality.test.ts Adds the fail-closed gate tests, including real compilation, parser-source scan, and positive/negative controls.
test/fixtures/gh-aw-shell-contract/violating.lock.yml Positive-control compiled-shaped fixture proving the gate can fail and report token/file/line.
.github/workflows/squad-ci.yml Updates comment to reflect the gate now fails closed and depends on installing gh-aw.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/gh-aw-shell-contract.ts Outdated
Comment thread test/gh-aw-shell-contract.ts
Comment thread test/gh-aw-shell-contract.ts
Review found three cases where the #1834 gate reported green because the
detector never looked - the same "unmeasured is indistinguishable from
clean" failure the gate exists to eliminate.

- BODY_VAR allowed only one segment before the _BODY/_TITLE suffix, so
  multi-segment carriers ($SQUAD_EVENT_BODY, $GITHUB_EVENT_ISSUE_BODY)
  bypassed every detector while the doc comment claimed "any *_BODY".
  Since event carriers are conventionally multi-segment SCREAMING_SNAKE,
  the unenforced shape was the idiomatic one.
- firstPrintfArg() read `--` as the format operand, so `printf -- "$body"`
  (the idiom used when a body may start with `-`) never had its real
  format slot inspected. Option stripping now loops and honors `--`.
- extractRunBlocks() never advanced the cursor past a consumed block
  scalar, so a `run:` line inside a heredoc re-entered the header branch,
  opening a phantom block that double-reported violations and inflated
  the block count the fail-closed assertion depends on.

Adds 28 unit tests over the exported scanner (no `gh aw`, never skippable).
Mutation-verified: reverting each fix kills its tests (17 / 4 / 2), and
over-advancing the cursor kills the sibling-block guard. Sanctioned forms
including `printf -- '%s\n' "$body"` stay green, so no permanent red.

Closes #1834

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bradygaster
bradygaster merged commit 288130f into dev Aug 25, 2026
17 checks passed
@bradygaster
bradygaster deleted the bradygaster-1834-shell-contract-gate branch September 9, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce the gh-aw shell input security contract with a gate over compiled workflow output

2 participants