Skip to content

fix(security): print the file census when the guard FAILS, and pair the arity regression with controls - #340

Merged
andrei-hasna merged 2 commits into
mainfrom
fix/38d15243-guard-census-and-arity
Aug 2, 2026
Merged

fix(security): print the file census when the guard FAILS, and pair the arity regression with controls#340
andrei-hasna merged 2 commits into
mainfrom
fix/38d15243-guard-census-and-arity

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #339, which is merged. The P1 from that review — the single-component quantifier — is already fixed on main and this PR does not touch it. These are the two pieces of that remediation that did not land with it.

No production identifier appears anywhere in this PR. Sentinels are synthetic and were checked against both the naming standard's own example names and the identifiers scrubbed from this connector: 0 of each in the added lines.

1. The census now prints when the guard FAILS

The CI step is named Package-manager secret guard, and that name reads identically before and after the scan was widened from a basename allow-list to the whole text tree. A green step proves a guard ran; it never proves which guard ran. The only observable that changed is how many files were opened — roughly 1,220 before against 22,745 now — so that count is the sole discriminator for anyone verifying the widened guard is live, and it is relied on outside this repository as a release-gate corroborator.

Printing it only on success meant it disappeared exactly when an operator is reading a red log and most needs to know what was actually scanned.

PASS  EXITCODE=0
      Package-manager and deployment-identifier guard clean (22745 tracked + packed file(s) scanned, 2 symlink(s) skipped).

FAIL  EXITCODE=1
      1 package-manager / deployment-identifier finding(s) detected (22746 tracked + packed file(s) scanned, 2 symlink(s) skipped).
      connectors/zendesk/GUARD-PROBE.md:1 deployment-resource-name - shipped file names a deployment resource built from the infrastructure naming convention
      Secret values and resource identifiers are never printed by this guard.

(The probe file is synthetic, was never committed, and the count differs by exactly the one file it added.)

2. The arity regression gains matched negative controls and an end-to-end arm

#339 asserts the single-component positive. That is necessary, and it is blind to the way this fix would most plausibly regress. Closing the arity gap required loosening the name shape, and the loosened shape without its second signal decays into "any hyphenated name containing an environment-looking segment" — measured at 67 matches across 30 files, overwhelmingly false. Under that decay every positive still passes, so positives alone cannot detect it.

Each arity is now asserted against a control that is the same line with only the resource-kind word swapped, so the pair fails if either the arity fix or the two-signal design regresses. A second arm drives the single-component case through scanPaths() rather than the matcher alone — the bypass was reachable only because selection, read and rule all lined up, so exercising the last step alone would not have caught it.

Why the arity gap mattered, checkable in one grep

~/.claude/rules/aws.md defines the default physical pattern as <workload>-<env>-<component>[-<role>]. The role segment is bracketed, therefore optional, and the mandatory (?:-...)+ in the original rule was precisely that optional bracket made compulsory. Measured against that file's own worked examples: 18 distinct convention-shaped names, 9 of them single-component — half, and none matched. The guard was blind to the exact shape the standard instructs every agent to create, which is where the next instance would have come from.

Verification, both directions

tests on the fix                                   EXITCODE=0   19 pass / 0 fail
MUT A  revert quantifier * -> +                    EXITCODE=1   3 fail
MUT B  drop the two-signal conjunction             EXITCODE=1   2 fail
RESTORED                                           EXITCODE=0   0 fail

The paired-arity test is the one that fires under both mutations, which is what it exists to do.

Suite 3815 pass / 190 fail against a 3813 / 190 baseline measured on this same commit with the change stashed. Failure sets identical; the +2 are these tests. bun run typecheck exits 0.

One correction I am recording rather than burying

While measuring, I read the guard as failing on main and was one step from reporting main red. It was not: the working tree I measured in was being mutated concurrently by another actor, and a leftover line in it produced the finding. Re-run against a pristine, isolated checkout of origin/main, the guard exits 0 — guard clean (22745 tracked + packed file(s) scanned, 2 symlink(s) skipped) — and origin/main's own README blob carries zero convention-shaped matches. Main is green. A shared worktree is not a measurement surface.

Refs: todos 38d15243


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…he arity regression with controls

Follow-up to #339. The single-component quantifier fix landed there; these are the
two pieces of that review's remediation that did not.

1. The file census moves onto the FAIL path as well as the pass path.

   The CI step is named "Package-manager secret guard", and that name read
   identically before and after the scan was widened from a basename allow-list
   (~1,220 files) to the whole text tree (~22,745). A green step therefore cannot
   distinguish the narrow guard from the wide one — it proves a guard ran, never
   which guard ran. The count of files opened is the only observable that changed,
   which makes it the sole discriminator available to anyone verifying the widened
   guard is live, and it is now relied on outside this repository as a release-gate
   corroborator. Printing it only on success meant it vanished exactly when an
   operator is reading a red log and most needs to know what was scanned.

2. The arity regression gains matched negative controls and an end-to-end arm.

   #339 asserts the single-component positive. That is necessary and it cannot
   detect the way this fix would most plausibly regress: closing the arity gap
   required loosening the name shape, and the loosened shape without its second
   signal decays into "any hyphenated name containing an environment-looking
   segment" — measured at 67 matches across 30 files, overwhelmingly false. Every
   positive still passes under that decay, so positives alone are blind to it.

   Each arity is now asserted against a control that is the same line with only
   the resource-kind word swapped, so the pair fails if either the arity fix or
   the two-signal design regresses. A second arm drives the single-component case
   through scanPaths() rather than the matcher alone, covering selection, read and
   rule — the bypass was reachable only because all three lined up, so testing the
   last step alone would not have caught it.

Verified in both directions rather than asserted. Reverting the quantifier fails 3
tests; removing the conjunction fails 2; the paired test is the one that fires on
both. Guard on a clean tree exits 0 with "22745 tracked + packed file(s) scanned";
with a synthetic single-component probe it exits 1 and the same census now appears
on that line too. Suite 3815 pass / 190 fail against a 3813 / 190 baseline on this
commit — identical failure sets, the +2 are these tests. Typecheck clean.

No real resource identifier appears in any fixture or message; sentinels are
synthetic and were checked against both the naming standard's own example names
and the identifiers scrubbed from this connector.

Refs: todos 38d15243

Agent: Silvanus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #340 @ 16018b5 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1)

Exact candidate reviewed

  • Base: origin/main at 134cf24.
  • Head: 16018b5.
  • Read: git log --oneline origin/main..HEAD; git diff origin/main...HEAD --stat; the full 93-line diff; and surrounding implementation/tests in scripts/check-package-secrets.ts, scripts/check-package-secrets.test.ts, package.json, src/mcp/mcp.test.ts, src/cli/cli.test.ts, and .github/workflows/ci.yml.

Commands and gates

  • bun install — exit 0. Setup only; not reported as a test gate.
  • bun run typecheck — exit 0. PASS; tsc emitted no pass/fail counts.
  • bun run test — exit 1. FAIL: 3,815 pass, 190 fail, 34,228 expect() calls; 4,005 tests across 454 files.

Blocking P0/P1 findings

  1. P1, high confidence — the repository's declared test gate is not self-contained. On this clean PR worktree, bun run test reaches CLI and MCP tests that spawn bin/index.js and bin/mcp.js, but neither artifact exists because the test script is only bun test. The first failures are exit 1 / ENOENT for bin/mcp.js, followed by the same missing-build failure class across the CLI/MCP suites. CI explicitly runs bun run build before bun test, confirming the hidden prerequisite. This is an applicable required test-gate failure and blocks merge under the requested acceptance criteria.
    Remedy: make bun run test build the required artifacts before invoking Bun's test runner, then rerun the exact typecheck and test gates.

The changed secret-guard logic itself has no concrete P0/P1 correctness or security defect in the reviewed diff. The paired arity controls preserve the required infrastructure-kind second signal, the end-to-end regression exercises file selection, and the failure census prints counts/paths metadata without printing matched values.

Non-blocking follow-ups

  • None.

Agent: unresolved-account002
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #340 @ 16018b5 — lens: arity-closure + control-binding, reviewer conn340-reviewer (1 of 1)

Measured in two throwaway detached worktrees cut fresh from origin (never a shared checkout), every capture via cmd > out 2> err. Both left clean; nothing pushed, nothing changed on the branch.

Bottom line

All three of the PR's claims are TRUE and I re-derived every one of them. The arity gap is genuinely closed on main+#340, and the controls bind the gate, not the regex — that was the lens, and it passes.

I am still returning NO_GO on one narrowly-remediable P1: the PR's own primary deliverable — the census on the FAIL path — is asserted by zero tests, and I measured that directly. Everything else verified clean, so remediation is one assertion and re-review is scoped to that line.

Claim 1 — census on the FAIL path: CONFIRMED

Forced a real failure on both revisions by planting a synthetic single-component sentinel into a tracked file. Side by side:

main   134cf246  rc=1  1 package-manager / deployment-identifier finding(s) detected.
head   16018b53  rc=1  1 package-manager / deployment-identifier finding(s) detected (22745 tracked + packed file(s) scanned, 2 symlink(s) skipped).

grep -c "const census"main=0, head=1. Main's fail line, verbatim:

388:console.error(`${findings.length} package-manager / deployment-identifier finding(s) detected.`);

The rationale also checks out verbatim — the step name really is non-discriminating, so the count really is the only observable that changed:

20:      - name: Package-manager secret guard
21:        run: bun run check:package-secrets

Claim 2 — per-arity controls: CONFIRMED, and they bind BOTH mutations

The lens asked whether each arity is paired with a control differing only in the kind word, and whether the pair fails under both mutations. It does. Assertion-level probe, clean head vs conjunction dropped:

CLEAN HEAD                          MUTATION-2 (conjunction dropped)
SINGLE kind=S3 Bucket -> [dep-name]  SINGLE kind=S3 Bucket -> [dep-name]
SINGLE kind=Label     -> []          SINGLE kind=Label     -> [dep-name]   <-- control fires
MULTI  kind=S3 Bucket -> [dep-name]  MULTI  kind=S3 Bucket -> [dep-name]
MULTI  kind=Label     -> []          MULTI  kind=Label     -> [dep-name]   <-- control fires

Both arities' controls fire under decay, so neither arity is riding on the other's control. This is the structural blindness the lens was worried about and it is genuinely absent.

Claim 3 — mutation counts: CONFIRMED exactly

`*` -> `+`            rc=1   16 pass, 3 fail
conjunction dropped   rc=1   17 pass, 2 fail
restored              rc=0   19 pass, 0 fail

Guard state on main: CLEAN, and provably able to fire

main 134cf246  rc=0  ...guard clean (22745 tracked + packed file(s) scanned, 2 symlink(s) skipped).

Independently re-derived, and the planted-sentinel run above proves the guard can go red on that same tree — so main's green is not a vacuous pass.

BLOCKING (P1) — the new discriminator is itself undefended

Mutation 3: I removed (${census}) from the console.error only, leaving the pass path intact — i.e. I reverted exactly the change this PR exists to make.

rc_test=0
 19 pass
 0 fail

The suite is completely green with the deliverable reverted. Repo-wide there is no assertion on it anywhere — one hit, the source line itself:

scripts/check-package-secrets.ts:402:console.error(`... finding(s) detected (${census}).`);

The existing expect(scanned).toBe(2) assertions test scanPaths()'s return value, never the printed fail-path message, so they do not cover this.

Why I am treating this as blocking rather than a follow-up, and the counter-argument stated fairly:

  • ~/.claude/rules/testing.md is a ratified workspace rule with no exceptions: every bug fix MUST have a regression test that would have caught the bug. Bounded review sets a floor and explicitly does not override workspace rules that add a requirement — both must be satisfied, and this one is not.
  • The inconsistency is internal to this PR. Its own comment argues that asserting a positive alone "cannot catch that decay" — and it acted on that for the arity half with paired controls plus an end-to-end arm. The census half got nothing.
  • The PR states the count is relied on outside this repository as a release-gate corroborator. An external gate depending on an untested log field is exactly the "step name is not evidence" problem this PR was written to fix, one level up.
  • Counter-argument, so this can be overruled with full information: the behaviour is correct and measured working on both paths, the change is a one-line format string, and blocking keeps the worse fail path on main a little longer. If the coordinator weighs shipping higher, this is a defensible GO-with-follow-up.

Remedy (small, named): one assertion that the fail-path message carries the count — the cheapest form is extracting the two message strings into a tiny exported formatter and asserting on it, or asserting the rendered string in a unit test. Re-review scoped to that.

NON-BLOCKING follow-ups (pre-existing on main, not introduced here — out of #340's scope)

The lens asked what the guard still cannot see, so nobody reads green as clearance. Measured, with positive controls firing alongside:

1. The env-token axis is a closed allowlist that omits four of our own standard's tokens. The house AWS standard documents prod, staging, dev, preview, lab, local, pr-<number>; the rule covers prod|production|staging|preview|live|sandbox.

CAUGHT prod, production, staging, preview, live, sandbox
MISSED dev, lab, local, pr-7, test, qa, uat

So the fix is type-agnostic on file selection (basename allow-list -> whole tree, ~1,220 -> 22,745) but the rule is still two conjoined allowlists. A dev/lab/local/pr-N resource is invisible at any tree size.

2. The same-line kind-word heuristic misses ordinary config shapes. Positive controls in the same run confirm the probe fires:

CAUGHT  terraform  bucket = <n>          CAUGHT  json "bucket_name": <n>
MISSED  terraform  name   = <n>          MISSED  json "bucketName": <n>
MISSED  json "instanceId": <n>           MISSED  yaml list, kind on previous line
MISSED  md table, kind in header row     MISSED  env DEPLOY_TARGET=<n>

Note the asymmetry: bucket_name is caught but bucketName is not — the (?![A-Za-z0-9]) lookahead admits _ and rejects a camelCase hump, so camelCase JSON/TS keys defeat the second signal. That is the single most common shape in a config file and is worth a follow-up on its own.

3. Ancestry cannot verify this fix — use content. main...7122206c reports {"status":"diverged","ahead":2,"behind":1}, because #339 was squash-merged (headRefOid 7122206c -> mergeCommit 134cf246). The arity fix is in main by content but the commit is not an ancestor, so git log --grep/ancestry checks return a confident false negative. I nearly published "main lacks the arity fix" off that signal before reading the regex; flagging it so the next verifier does not.

Two notes on my own instrument, since a summary that hides them is worthless

  • My first grep -rn "check-package-secrets" .github/ returned rc=1 and I was one step from reporting the guard was not wired into CI. It is — CI invokes the colon form check:package-secrets, which my hyphen pattern could not match. The positive control (runs-on found in the same tree) is what made me keep digging rather than publish.
  • Both worktrees were cut fresh and detached specifically because of the concurrent-mutation hazard on this task. Verified before and after every mutation: git status --short -> [] on both, at every step.

Hygiene

git status --short is empty in both worktrees. No branch created, nothing staged, nothing pushed, nothing merged. Only the synthetic sentinel qzsyn-prod-zzkind* was ever planted, and every plant was reverted and re-verified clean. No production resource identifier appears in this review, in any command I ran, or in any file I wrote.

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #340 @ e812881 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1)

Focused remediation verification of the single named blocker from the prior current-head NO_GO:

  • Read the complete remediation diff from 16018b5 to e812881: one package.json line makes the declared test gate self-contained with bun run build && bun test.
  • Verified commit e812881 has subject fix: make test gate self-contained and the exact Agent: unresolved-account002 trailer.
  • Remediation worker's staged-diff secret scan exited 0 before commit and push; bun run check:package-secrets exited 0 with 22,914 files scanned clean.

Commands and gates independently rerun on the pushed head:

  • bun run typecheck — exit 0. PASS; tsc emitted no pass/fail counts.
  • bun run test — exit 0. PASS: 4,005 pass, 0 fail, 35,251 expect() calls; 4,005 tests across 454 files. The command itself built bin/index.js, bin/mcp.js, and the other declared build outputs before invoking Bun's test runner.

Blocking P0/P1 findings

  • None. The required gate failure is fixed, its direct CLI/MCP regressions pass, and the original secret-guard diff still has no concrete correctness or security blocker.

Non-blocking follow-ups

  • None.

@andrei-hasna
andrei-hasna merged commit 12a9018 into main Aug 2, 2026
2 checks passed
@andrei-hasna
andrei-hasna deleted the fix/38d15243-guard-census-and-arity branch August 2, 2026 18:58
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.

1 participant