Skip to content

review: security-auth docstring-contract rule, workflow-security hunts, and their corpus cases - #295

Open
ioanacrant wants to merge 10 commits into
mainfrom
ioana/sec624-security-auth-shared
Open

review: security-auth docstring-contract rule, workflow-security hunts, and their corpus cases#295
ioanacrant wants to merge 10 commits into
mainfrom
ioana/sec624-security-auth-shared

Conversation

@ioanacrant

@ioanacrant ioanacrant commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary:

Stack-generic additions to the shared security-auth lens, plus the eval corpus
cases that exercise them — one PR so the powered eval can run straight off this
ref. Stacked on #281; the consumer-surface counterparts live in that seam's
payload.

Prompt changes

  1. Docstring-contract-parity rule — a docstring or schema comment promising a
    security or privacy property ("without answers", "without PII") is a contract
    clients trust; a resolver that contradicts it means the schema lies. The rule
    fires when a symbol's documented guarantee disagrees with what the changed
    code actually assigns to it.
  2. Workflow-security huntspwn-request, push-ref-race,
    over-scoped-secret, unpinned-action, gated on .github/workflows/*.yml
    changes. Placed in security-auth rather than deploy-infra-config, which
    owns rollout safety (flag defaults, plaintext config secrets, destructive IaC)
    and nothing in workflow-execution semantics — happy to move if you disagree.
  3. Review-fix commit: explicit per-hunt gates, and concrete evidencing mechanics
    for over-scoped-secret (the validator's failure-scenario bar).

Corpus cases

Both cases are synthetic. They carry no real service code, paths, identifiers, or
author names — this repo is public, and every other case in the corpus is
synthetic for the same reason. Both are sized in line with existing cases (146
and 137 lines of tree across four files each).

  • incidents/incident-docstring-contract-parity — a GraphQL schema documents
    SimpleQuizItem.content as "the rendered content of the item without answers",
    and the changed resolver assigns it the output of a fetcher whose own docstring
    says it returns the item JSON "complete with answers". The PR description in the
    fixture defers stripping to a future frontend change, which would not stop the
    answers leaving the server. One mustCatchSpec anchored on a diff-added line,
    with altLocations on the in-diff fetcher and the deleted answer-stripped path.
    The unchanged schema file cannot carry a spec (the loader requires spec paths to
    appear in changedFiles), so it is staged as tree context alongside the content
    client — together they are the discoverability chain from the documented promise
    to the runtime behavior.

  • incidents/incident-workflow-pwn-request — a new workflow triggered on
    pull_request_target, which runs in the base repository's context with full
    secret access and a write-capable token for every PR including fork-authored
    ones. It checks out the attacker-controlled PR head (head.sha) with a bot
    token, then executes code from it (pnpm install lifecycle scripts via the
    setup action, and a fixer script from the PR branch) before pushing back. Any
    GitHub user can fork, open a PR, and obtain arbitrary code execution on a
    trusted runner: every secret in scope is exfiltratable and the bot token can
    push to protected branches. The fixture's comment rationalizes the trigger
    choice as a workaround for fork runs getting no secrets, so the case also tests
    whether the lens reasons about the trigger rather than accepting the author's
    stated justification.

    Single-spec by design. An earlier revision of this case used the plain
    pull_request trigger, where GitHub withholds secrets from fork runs — the
    attacker needs push access first, which makes the pattern a defensible tradeoff
    and "must block" contested ground truth. It now uses pull_request_target so
    the expected verdict is unambiguous. There is deliberately no unpinned-action
    spec: the unpinned-action hunt ships without a dedicated corpus case for now
    rather than with fabricated ground truth.

One changeset (minor), covering the lens changes and the corpus together.

Test plan:

  • Node CI green on this ref: 1073 tests, 0 failures (includes loader validation of
    both cases and the whole-corpus must-catch recall check replaying them to
    REQUEST_CHANGES), plus tsc --noEmit and eslint clean.
  • Verified beyond the suite: each case's embedded diff reverse-applies against its
    staged tree, and every anchor and evidence-trace line resolves to the content it
    claims.
  • The automatic live A/B runs on this PR (smoke subset; the adversarial
    prompt-injection hard gate must stay green). Single-run deltas are noise per the
    eval README.
  • Planned from this ref: identical-arm calibration of the two new cases
    (--force-arms, 3 repeats) before any band claim, then a powered repeated eval
    pricing the prompt changes against them.

Known CI failure

The PR Reviewer check fails on this PR by hitting the agent step's
timeout-minutes: 20 ceiling — the orchestrator fans out ten subagents and does
not finish inside the cap. It is not caused by these changes: the same step failed
identically on the preceding commit, and this PR's latest push reduced the diff
by ~3,600 lines without changing the runtime. Filed separately from this review.

Open questions:

  1. The pwn-request hunt text says "a pull_request-family trigger" and never
    names pull_request_target or workflow_run. The corpus case now turns
    entirely on pull_request_target, and workflow_run is the other classic
    privileged-trigger vector (artifact download, then execute). Worth naming both
    explicitly in the hunt?
  2. These hunts are inert where they would matter most: this repo's ROUTING
    enables no specialist lenses, so pwn-request cannot fire in the repo that
    ships actions into other repos' CI. Route .github/workflows/** to
    security-auth here as a follow-up?

Issue: SEC-624

jwbron and others added 9 commits July 21, 2026 15:16
…seam

A consuming repo may now define .github/aw/review/lenses/<lens>.md for any of
the eleven specialist lenses, plus lenses/correctness.md for the always-on
correctness-reviewer. Each file is runtime-imported (optional form) into a new
'Repo-specific rules and hunts' section of the matching reviewer prompt,
carrying that repo's surface-specific rules and extra tri-state hunts. Lens
names stay generic and shared; only payloads vary per repo.

Behavior-neutral for every current consumer: no repo carries a payload file
yet, and gh-aw's runtime resolves a missing optional import to nothing.
correctness-checks.md stays imported as a deprecated alias for
lenses/correctness.md (frontend carries one today); a repo should migrate the
file and carry at most one of the two.

Also in this change:
- Eval alignment: resolveRuntimeImports now matches production for the
  optional form (missing resolves to empty, not the '(not configured for this
  eval case)' note), so corpus case trees can carry payloads. The
  required-form fallback note is unchanged and remains the one documented
  deviation (production fails the run).
- disciplines.test.ts pins the payload import in every specialist lens
  section so the seam cannot be silently dropped.
- README documents the lenses/ surface and the three-way contribution rule
  (shared skeleton vs lens payload vs skills), and fixes two stale claims in
  the consumer-config section: correctness-checks.md was never documented,
  and the optional import form was said to have been dropped.

First planned payload use: migrating the #271 unbounded-reads hunt from the
shared skeleton to server repos' correctness/caching payloads, and frontend's
client-surface lenses/security-auth.md.
…, inert-payload warnings, precedence)

Addresses the four non-blocking reviewer findings on #281:

- Pin the correctness payload imports: disciplines.test.ts now asserts the
  correctness-reviewer section carries both the lenses/correctness.md import
  and its deprecated correctness-checks.md alias, so neither can be dropped
  silently (the specialist-lens assertion covered only the eleven lenses).
- Warn on silently-inert payloads: new lib/lens-payloads.ts computes
  fixed-format warnings for a lenses/ entry matching no imported payload
  (typo or unknown name), a specialist payload no ROUTING rule routes, and
  the correctness alias carried alongside its replacement. runCli readdirs
  the payload dir and appends these to routingConfig.warnings, the existing
  channel the orchestrator surfaces in the review body's note lines. Split
  into its own module to respect the max-lines cap on router.ts; the
  specialist roster is a parameter to avoid an import cycle.
- State payload precedence in the prompts: the eleven lens payload sections
  and the correctness framing now say payload rules are additive and never
  relax or override the shared rules, which win on conflict. README states
  the same, plus the alias's removal point (next major release).
- FsLike gains readdirSync; the router test fake answers it (and directory
  existsSync) from its input map.
…ound-two feedback)

Addresses the second-round reviewer findings on #281:

- ENOTDIR crash: existsSync is true for a regular file at
  .github/aw/review/lenses, and readdirSync would then throw before
  routing.json is written, failing every review run in that repo. The CLI
  now degrades that misconfiguration to a routing warning; the test fake's
  readdirSync mirrors node:fs and throws on a file path to prove it.
- Alias-alone deprecation nudge: an unmigrated repo (frontend today) would
  go silently inert when the correctness-checks.md import is dropped at the
  next major. lensPayloadWarnings now emits a rename nudge whenever the
  alias is carried without lenses/correctness.md, so the cliff is signposted
  on every review until the file moves.
- runCli wiring for the alias check is pinned by a fakeFs case carrying both
  correctness files; the both-files warning cannot silently drop out.
- README: the alias row no longer implies the alias is ignored when both
  files exist (both import; the router warns), and the warning list names
  the nudge and the dir-as-file degrade.
- Import order nit in router.test.ts fixed.

The additive/never-override contract stays prose-level by design: payloads
are consumer-owned trusted config in the same class as risk-classification.md
(restored from the base branch, authored under normal code review), and a
phrase blocklist or size cap would misfire on legitimate rule text. Answered
on the thread.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ioanacrant
ioanacrant requested a review from jwbron July 27, 2026 14:20
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: be03052

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khan-actions-bot
khan-actions-bot requested review from a team, jaredly and somewhatabstract and removed request for a team July 27, 2026 14:21
@ioanacrant
ioanacrant marked this pull request as draft July 27, 2026 14:39
@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

Other risky files (1 file)
File Reason
review.md Shared PR-reviewer prompt inherited by every consuming repo on release; the added security-auth docstring-contract rule and four workflow-security hunts change reviewer behavior everywhere. Additive-only, no frontmatter/permission surface touched.

Common patterns

11 files: Two new incident corpus cases — each a case.json (eval-harness metadata, embedded diff, and mustCatchSpecs) plus a tree/ snapshot of the incident's source files.

2 files: New changeset entries bumping the review package (minor for the lens additions, patch for the corpus).

Excluded from review (11 files)

Not individually reviewed — generated, formatting-only, or fully explained by a common pattern above:

  • .changeset/sec624-eval-corpus-cases.md — pattern-only (Common patterns)
  • .changeset/sec624-security-auth-additions.md — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-docstring-contract-parity/tree/services/progress/assessment_item.graphql — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-docstring-contract-parity/tree/services/progress/cross_service/assessment_item.go — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-docstring-contract-parity/tree/services/progress/resolvers/gap_detector.go — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-docstring-contract-parity/tree/services/progress/resolvers/gap_detector_test.go — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-workflow-pwn-request/tree/.github/actions/run-and-commit/action.yml — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-workflow-pwn-request/tree/.github/actions/setup/action.yml — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-workflow-pwn-request/tree/.github/workflows/validate-workflows.yml — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-workflow-pwn-request/tree/dev/tools/fix-workflows.ts — pattern-only (Common patterns)
  • workflows/review/eval/corpus/incidents/incident-workflow-pwn-request/tree/package.json — pattern-only (Common patterns)

- **`injection-sink`** — trace user-controlled input to a SQL/HTML/path/URL/shell/
deserialization sink without validation or parameterization. `found` on an unguarded
sink.
- **`pwn-request`** — when `.github/workflows/*.yml` changes: a workflow

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.

suggestion (non-blocking): All four new hunt gates read .github/workflows/*.yml, but GitHub also executes .yaml workflow files, so a lens following the gate literally would mark the hunts not-applicable for a .yaml-only change and miss the incident class they target. The same gap applies to composite actions under .github/actions/**/action.yml, where the pwn-request fixture itself stages the untrusted code the job runs. Widen the gate in all four bullets.

Suggested change
- **`pwn-request`** — when `.github/workflows/*.yml` changes: a workflow
- **`pwn-request`** — when `.github/workflows/*.{yml,yaml}` changes: a workflow
Low-confidence notes (3)
  • workflows/review/review.md:2717 — over-scoped-secret hunt ships with no eval corpus case; consider adding one. Note the pwn-request fixture is not a clean example — its contents/pull-requests/issues: write scopes are each used by a step (git push, comment posting).
  • workflows/review/review.md:2722 — unpinned-action overlaps deterministic linters (actionlint/zizmor/Dependabot); worth deciding whether a model hunt is the right layer, especially with no corpus case measuring its precision/recall.
  • workflows/review/review.md:2715 — the push-ref-race discriminator ("a ref name rather than the head commit SHA") may read as firing on most checkout-branch-then-push auto-commit workflows; consider stating the compliant compare-and-swap pattern (--force-with-lease pinned to the checked-out SHA).

{
"id": "incident-workflow-pwn-request",
"tags": [
"live"

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.

nitpick (non-blocking): This incident-repro case tags only ["live"], while its sibling added in this same PR carries ["incident", "live"] and every other case under corpus/incidents/ carries "incident". No harness code filters on the tag today, so nothing breaks, but the inconsistency would silently drop this case from any future incident-tag selection.

Suggested change
"live"
"incident",
"live"

deserialization sink without validation or parameterization. `found` on an unguarded
sink.
- **`pwn-request`** — when `.github/workflows/*.yml` changes: a workflow
combining a `pull_request`-family trigger, write permissions

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.

thought (non-blocking): security-auth is a path-gated specialist lens, so these hunts run only when a consumer's ROUTING maps .github/workflows/** to security-auth — the pwn-request corpus case has to inject routerConfig.lensRules for the hunt to fire at all. Unless a default routing rule ships or the required ROUTING entry is documented, the hunts stay dormant in production even though the eval reports them caught.

(`contents:write` / `pull-requests:write`), checkout of the PR head, and any
step executing untrusted code (install/build/lint that runs or mutates files).
`found` on the full combination — treat as blocking.
- **`push-ref-race`** — when `.github/workflows/*.yml` changes: a workflow

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.

suggestion (non-blocking): push-ref-race ships with no eval corpus case, so it is untested detection logic — in this repo the corpus cases are the regression tests for prompt behavior. The pwn-request fixture's bare git push embodies the pattern, but its case asserts only the pwn-request finding. Consider adding a dedicated case so a future edit that stops the reviewer emitting push-ref-race fails a gate rather than regressing silently. (Same applies to over-scoped-secret — see the note on the pwn-request hunt above.)

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

Baseline: origin/review-lens-payload-seam (review.md bd838960b9ea); candidate: working tree (review.md 3d467315a832).

Ruler: matcher deterministic+arbiter; corpus cc11988c0918 (9 cases).

Metric Baseline Candidate Delta
Must-catch recall 100% 100% +0%
Verdict agreement 100% 89% -11%
Noise (unmatched posted) 61% 57% -4%
Clean false flags 0 0
Judge mean quality 0.90 0.90 -0.00
Cost $9.66 $9.64
Wall clock 1335s 1148s
Cases run / skipped 9 / 0 9 / 0
Misses found-but-dropped 0 0
Findings anchor-snapped 0 0

Adversarial hard gate: PASSED on the candidate arm.

Single-run-stable rows: recall, verdict agreement, regressions, adversarial gate. Judge quality and noise are not: they jitter run-to-run at this corpus size, and a regressed reviewer can score HIGHER on judge quality (fewer, surer comments each read better). Recall against the labeled specs is the load-bearing metric.

Measured noise floor (identical arms, run 29069228968, 2026-07-10, 6 arm-samples, full corpus x3, pre-arbiter; budget skips left the samples on unequal case sets, so these v1 bands also carry case-mix variance): must-catch recall 54%-86% (sd 10%), verdict agreement 75%-100% (sd 9%), noise (unmatched posted) 50%-60% (sd 3%), judge mean quality 82%-86% (sd 2%). A single-run delta whose arms both sit inside a band is indistinguishable from run-to-run wobble; use --repeats to resolve smaller effects.

@ioanacrant ioanacrant changed the title review: security-auth docstring-contract rule, workflow-security hunts, and their corpus cases (SEC-624) review: security-auth docstring-contract rule, workflow-security hunts, and their corpus cases Jul 27, 2026
Issue: SEC-624

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ioanacrant
ioanacrant marked this pull request as ready for review July 27, 2026 15:18
@khan-actions-bot
khan-actions-bot requested a review from a team July 27, 2026 15:18
@ioanacrant
ioanacrant removed the request for review from a team July 27, 2026 16:32
@jwbron
jwbron changed the base branch from review-lens-payload-seam to main July 29, 2026 23:15
@khan-actions-bot
khan-actions-bot requested a review from a team July 29, 2026 23:16
@jeresig
jeresig requested review from jeresig and removed request for a team, jaredly and somewhatabstract July 30, 2026 11:49
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.

2 participants