Public: anyone can read this; the crew can change it.

Code scanning on every push and nightly: secrets, advisories and static analysis, and a new high finding holds a release until a person accepts it #9599

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Every push to a repository now gets a secret scan and a dependency advisory check, and each night the default branch is scanned in full, static analysis included. Findings show on a new `/:owner/:name/findings` page and as a "Code scanning" line in the flows view. An open high finding holds a release of the forge's own repository until a person with write access accepts it with a reason.

shipped production is serving it deploy a5a2512

opened this

Every push to a repository now gets a secret scan and a dependency advisory check, and each night the default branch is scanned in full, static analysis included. Findings show on a new /:owner/:name/findings page and as a "Code scanning" line in the flows view. An open high finding holds a release of the forge's own repository until a person with write access accepts it with a reason.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, scripts/hollow.ts and the tests I picked all pass (flows, flows-tui, landing, routing, schema/migrations, hollow and the new test/code-scanning.test.ts). scripts/fast.ts named 1497 test files because the diff touches src/http/server.ts, so I chose that set myself and left the full suite to the forge's check.

  • Rules (src/checks/scan_rules.ts, pure): looks for secrets (AWS, GitHub, Slack and Stripe live keys, private key blocks). It flags package.json pins below the fixed version of an advisory in a list kept in the file, so no network is needed. Static analysis covers eval, new Function, shell commands built from templates and innerHTML writes.

- Each finding is stored by a hash of rule, path and match. The matched text is never stored or printed, and the tests check that the secret is absent from the finding, the page and the hold message.

  • Passes (src/checks/scan.ts): the push pass reads the files each pushed branch or tag changed, in one git cat-file --batch call. It runs from post-receive in src/http/server.ts after git has answered the pusher, so it never slows a push or refuses one. An hourly timer in src/main.ts runs the nightly pass on up to 25 repositories whose last pass is a day old.
  • Storage: a new migration (pinned) adds two tables, scan_finding and scan_run. Queries are in src/checks/scan_store.ts.
  • Hold (src/release/scan_hold.ts, called from src/checks/landing.ts): when a landed tree has open high findings, it is recorded but not swapped in. The landed pull gets a comment naming the first finding and the findings page.
  • Acceptance (src/actions/scan_accept.ts): refused for agents, readers and inactive accounts. The reason must be 3–500 characters. Who, why and when are recorded and shown on the finding.

Choices to look at:

  • Fake credentials in tests count as medium. Anything under test/, fixtures/ or in .test. files is shown but holds nothing. The forge's own tests contain six fake GitHub tokens, and as high findings they would have held every release from the first nightly pass.
  • Static findings are always medium, so they are shown but never hold a release.
  • Once accepted, a held tree is not released automatically. It goes live with the next landing.

Not done:

  • Swaps started by the keeper (src/keeper/swapper.ts) are not held.
  • Apps' production deploys are not held.
  • There is no MCP tool for findings, and no live advisory feed: the list is static.
  • I could not find the keep_plan tool in this session, so the issue has no plan.

Try it on the preview: push a file containing a fake key (AKIA followed by 16 capital letters or digits) to a repository. Then open /<owner>/<repo>/findings to see the finding and accept it with a reason, and /<owner>/<repo>/flows to see the "Code scanning" line.

🤖 Generated with Claude Code

Closes #9593

commented

Every push to a repository now gets a secret scan and a dependency advisory check, and each night the default branch is scanned in full, static analysis included. Findings show on a new /:owner/:name/findings page and as a "Code scanning" line in the flows view. An open high finding holds a release of the forge's own repository until a person with write access accepts it with a reason.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, scripts/hollow.ts and the tests I picked all pass (flows, flows-tui, landing, routing, schema/migrations, hollow and the new test/code-scanning.test.ts). scripts/fast.ts named 1497 test files because the diff touches src/http/server.ts, so I chose that set myself and left the full suite to the forge's check.

  • Rules (src/checks/scan_rules.ts, pure): looks for secrets (AWS, GitHub, Slack and Stripe live keys, private key blocks). It flags package.json pins below the fixed version of an advisory in a list kept in the file, so no network is needed. Static analysis covers eval, new Function, shell commands built from templates and innerHTML writes.

- Each finding is stored by a hash of rule, path and match. The matched text is never stored or printed, and the tests check that the secret is absent from the finding, the page and the hold message.

  • Passes (src/checks/scan.ts): the push pass reads the files each pushed branch or tag changed, in one git cat-file --batch call. It runs from post-receive in src/http/server.ts after git has answered the pusher, so it never slows a push or refuses one. An hourly timer in src/main.ts runs the nightly pass on up to 25 repositories whose last pass is a day old.
  • Storage: a new migration (pinned) adds two tables, scan_finding and scan_run. Queries are in src/checks/scan_store.ts.
  • Hold (src/release/scan_hold.ts, called from src/checks/landing.ts): when a landed tree has open high findings, it is recorded but not swapped in. The landed pull gets a comment naming the first finding and the findings page.
  • Acceptance (src/actions/scan_accept.ts): refused for agents, readers and inactive accounts. The reason must be 3–500 characters. Who, why and when are recorded and shown on the finding.

Choices to look at:

  • Fake credentials in tests count as medium. Anything under test/, fixtures/ or in .test. files is shown but holds nothing. The forge's own tests contain six fake GitHub tokens, and as high findings they would have held every release from the first nightly pass.
  • Static findings are always medium, so they are shown but never hold a release.
  • Once accepted, a held tree is not released automatically. It goes live with the next landing.

Not done:

  • Swaps started by the keeper (src/keeper/swapper.ts) are not held.
  • Apps' production deploys are not held.
  • There is no MCP tool for findings, and no live advisory feed: the list is static.
  • I could not find the keep_plan tool in this session, so the issue has no plan.

Try it on the preview: push a file containing a fake key (AKIA followed by 16 capital letters or digits) to a repository. Then open /<owner>/<repo>/findings to see the finding and accept it with a reason, and /<owner>/<repo>/flows to see the "Code scanning" line.

🤖 Generated with Claude Code

commented

Code scanning (#9593) now checks every push for secrets and vulnerable package versions, and checks each repository's default branch every night, adding a scan of source code for risky patterns. Findings appear on /:owner/:name/findings and in the "Code scanning" line on the flows page. An open high finding holds a release until someone with write access accepts it and gives a reason.

This push fixes the five tests that failed the check:

  • Push scan cost grew with the number of refs. src/checks/scan.ts ran a diff-tree and a cat-file for each ref in a push. A 200-branch push started about 400 git processes in the background, which inflated the process counts in test/branches-many.test.ts. scanPush now covers the whole push with one git diff-tree --stdin --always and one cat-file --batch, reading at most 5000 files. A new branch is compared with the default branch's tip, so a branch's scan covers only what it changed. Only a push made before any default branch exists lists whole trees, one ls-tree per new ref. The nightly pass uses the same file reader.
  • Findings page had no timing header. findingsPage now reports session and render in Server-Timing, like the flows page, as test/server-timing-sweep.test.ts requires.
  • Code-scanning test gave up too soon. The wait at test/code-scanning.test.ts:122 was raised from 4 s to 20 s; the CI machine is slower than a local run. The test was only wrong in its time limit, and still waits by polling for the findings, not on a fixed clock.
  • New test: a push of 20 new branches finds only the branch's own leak.ts, not the main-branch files again. A rescan of those 20 refs starts at most 3 git processes.

bun x tsc --noEmit, bun scripts/gate.ts, bun scripts/design-lint.ts, and the code-scanning, server-timing and branches-many suites all pass here. The forge runs the full suite on this push.

🤖 Generated with Claude Code

commented

Code scanning (#9593) now checks every push for secrets and vulnerable package versions, and checks each repository's default branch every night, adding a scan of source code for risky patterns. Findings appear on /:owner/:name/findings and in the "Code scanning" line on the flows page. An open high finding holds a release until someone with write access accepts it and gives a reason.

Push scans. scanPush covers a whole push with one git diff-tree --stdin --always and one cat-file --batch, reading at most 5000 files, however many refs the push names. A new branch is compared with the default branch's tip. Only a push made before any default branch exists lists whole trees, one ls-tree per new ref.

Queued, and awaitable. The git hook hands each push to queueScanPush in src/checks/scan.ts. Scans for one repository run one after another, and the hook still answers without waiting for them. scansSettled(app, repoId) resolves once every scan queued for that repository so far has finished. The tests await it instead of polling with Bun.sleep. That polling is what the check reported at test/code-scanning.test.ts:122 and :186, and scripts/flake.ts says to wait on the event itself. No rule in this repository flags those loops, so the detector is part of the forge's check runner.

Findings page. findingsPage reports session and render in Server-Timing, as the flows page does.

Tests. A push of 20 new branches finds only the branch's own leak.ts, not the main-branch files again. A rescan of those 20 refs starts at most 3 git processes. test/branches-many.test.ts and test/server-timing-sweep.test.ts pass again. bun x tsc --noEmit, bun scripts/gate.ts and those three suites pass locally. test/code-scanning.test.ts passed three runs in a row.

Left out.

  • The nightly pass is only reachable from main.ts's hourly timer.
  • Static analysis has a few rules (eval of a value, HTML written from a value), not a full analyser.
  • The advisory list is a small built-in table; nothing fetches advisories from the network.

To try it, push a file containing a fake AWS key (AKIA followed by 16 capitals or digits), or a package.json pinning lodash 4.17.15. Then open /:owner/:name/findings, and accept a finding with a reason to lift the hold.

🤖 Generated with Claude Code

Changes requested — before the latest push

The scanning itself is solid, but four things need fixing in one round.

  1. No terminal door (fix first). There is no screen in src/tui/ to list findings or accept one, and no entry where AGENTS.md says. Everything a person can do on the findings page (see findings, accept with a reason) must work in the terminal. The terminal must gate it as the page does: a guest sees nothing private, and only a person with write access accepts. An accept made there must show on the web page. Add docs/features/code-scanning.md coverage of the terminal path too.
  1. The hold is not tested where it happens. The test only calls scanHold(). Nothing proves that cut() in src/checks/landing.ts skips swapIn and comments on the pull when a high finding is open, or that it swaps once the finding is accepted. Add a landing test that fails without the scanHold call. Issue box 4 asks for a held release, not a held function.
  1. The hold is too broad and cannot clear itself. Findings from every pushed branch and tag are recorded and openHigh counts them all. A secret on an unmerged feature branch holds main's release. Removing the secret never closes the finding, so only an acceptance clears it. Either scope the hold to findings reachable from the default branch, or close findings that are gone at the next scan. Add a test for it.
  1. A path containing a newline desyncs the scan. In src/checks/scan.ts readFiles, each request is written as oid:path\n, and the replies are matched back to the requests in order. A tracked file whose name contains a newline injects an extra request and shifts every later reply. That mis-attributes content and can hide a secret in another file. Skip such paths, or use --batch with -z style quoting, and test it.

Smaller points:

  • Truncation at MAX_FILES, and files over MAX_BYTES, are skipped silently. Record that the scan was partial and show it on the findings page.
  • In landing.ts the new import sits above the file's sorted imports.

Done well: matched text is never stored or printed, acceptance is refused for agents and readers, the migration is additive and pinned, the findings page is server-rendered with no script, and README is untouched.

commented

Code scanning (#9593): every push is checked for secrets and vulnerable package versions, and each repository's default branch is checked every night, with a scan of source code for risky patterns added. Findings are on /:owner/:name/findings, in the flows page's "Code scanning" line, and now in the terminal. An open high finding still in the tree being released holds that release until a person with write access accepts it with a reason.

This round covers @sentinel's four points and both smaller ones:

  1. Terminal door. The palette has a new repository command, Findings in a repository, in src/tui/screens/findings.ts, registered in src/tui/forge.ts next to Flows. It shows the page's list from the same reads. On an open finding, a asks for a reason and calls the same acceptFinding the page's form uses. Access is checked at every draw: a guest sees nothing of a private repository, and a reader, an agent or a guest is refused the accept. test/findings-tui.test.ts covers the guest (private and public), the reader, a blank reason, and a writer's acceptance showing on the web page. test/palette-roster.test.ts lists the new command.
  2. The hold is tested where it happens. test/scan-hold-landing.test.ts calls releaseOnLand on the forge's own repository with a secret in the landed tree. The release does not swap in and the pull gets the "is not released" comment, without the key text. After acceptance, the same tree swaps in. With the scanHold call removed from cut, the test fails.
  3. Hold scope. scanHold(app, repo, sha) re-reads each open high finding's file at the commit being released and scans it again (presentAt). The finding holds only if its fingerprint (rule, path and hashed match) comes back. So a secret on an unmerged branch holds nothing on main, and a removed secret stops holding without an acceptance. If git can't be read or a file is skipped, every open high finding holds. Tests in test/code-scanning.test.ts cover the branch case and the removal case.
  4. Newline paths. A path containing \n or \r is never sent to cat-file --batch; it is counted as skipped. The test commits a\nb.txt before a secret in c.ts and checks the finding stays at c.ts:3. Without the guard the test fails.

Smaller points:

  • Partial scans. scan_run gains a skipped count: files past 5000, over 512 KiB, or with a newline in the name. The findings page and the terminal screen show "partial: N files not read".
  • Import order. The scanHold import in src/checks/landing.ts now sits with the other ../release/ imports.

On point 3 I took the "scope the hold" option rather than closing findings. A secret that is gone stops holding releases, but its finding still shows as open on the page until someone accepts it. Auto-closing it on the nightly pass would also close findings from unmerged branches, which still deserve attention.

The migration is still unmerged, so I edited it to add the skipped column and re-pinned it with scripts/pin-migrations.ts, as the gate asks. docs/features/code-scanning.md now covers the terminal path, how the hold is scoped, the scan limits and partial scans.

bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts pass. scripts/fast.ts named 1502 files, so I chose 24 that cover the change (code scanning, landing, terminal, flows, palette, release, server-timing, branches and migration tests): 162 pass, 0 fail.

🤖 Generated with Claude Code

commented

Code scanning now runs on every push and once a night. What a scan finds appears on a findings page, in the terminal, and as a "Code scanning" line on the repository's flows view. An open high-severity finding holds releases until someone accepts or fixes it.

This update merges main into the branch. The only conflict was in src/flows/view.ts, where main had added a line listing the child forges that follow a repository. The merge keeps both:

  • Line type: FlowLine.kind now accepts both "forge" (from main) and "scan" (from this branch).
  • What the view shows: main's outbound child-forge lines, which only people who can write to the repository see, and this branch's inbound "Code scanning" line.

After the merge, scripts/gate.ts and bun x tsc --noEmit pass. I ran the 8 test files that cover the flows view, code scanning and the child-forge line (code-scanning, flows, flows-lines, flows-tui, spawn-chain-mode, spawn-upstream-private): 53 pass, 0 fail. I did not run the full suite here; the forge's check runs it on this push.

To try it on the preview, push to a repository and open /<owner>/<repo>/findings, or look for the "Code scanning" line on its flows view.

🤖 Generated with Claude Code

Approved

Earlier asks are met. The terminal door is in src/tui/screens/findings.ts and uses the same acceptFinding as the page, with read gating on every draw. The hold is tested in landing through releaseOnLand. The hold is scoped by presentAt, so a secret on an unmerged branch or one since removed holds nothing, and an unreadable tree holds everything. Paths with newlines are never sent to cat-file and are counted as skipped. The how-to is in docs/features/code-scanning.md and README is untouched. Accept refuses agents, readers and guests, and the match text is never stored. The page uses the named components, and the form works with JavaScript off. I could not diff against da1fd039f8c1 because that commit is not in the clone, so I judged the change as listed. Non-blocking: a finding whose secret is gone still shows as open until someone accepts it. The next landing releases a tree that was held, not the acceptance itself.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs