Skip to content

Add a non-blocking secret and code scan to CI - #202

Open
ralyodio wants to merge 2 commits into
docmd-io:mainfrom
ralyodio:worktree-threatcrush-scan
Open

Add a non-blocking secret and code scan to CI#202
ralyodio wants to merge 2 commits into
docmd-io:mainfrom
ralyodio:worktree-threatcrush-scan

Conversation

@ralyodio

Copy link
Copy Markdown

Adds a non-blocking secret and code scan: every pull request, every push to main, and once a week. There is no security scanning in CI today.

What it is not

It cannot block a merge. The scan runs with --fail-on critical, so anything below critical is reported and never fails the job. On main and on the weekly run the SARIF goes to the Security tab; on pull requests the counts go to the job summary instead, because uploading SARIF needs a write token a fork PR does not get, and that step would otherwise fail on somebody's contribution rather than on anything about their change.

What it finds here today

I ran it against this repo first. 85 findings, 0 critical. Most of it is noise and I would rather say so than let a wall of yellow imply otherwise:

Count Rule My read
53 js-shell-exec-interpolation execSync(\node ${DOCMD} build`)intests/cli-contracts/**`. Test harness, not a surface. Noise.
12 insecure-temp-file /tmp paths inside workflow YAML and scripts. Noise.
5 js-open-redirect window.location.href = finalHref in docmd-main.js, where the href comes off a link on the page the theme itself rendered. Noise.
8 js-unescaped-html-sink innerHTML in the AI plugin's client. Worth a glance, since plugin output is the one thing a docs theme renders that the author did not write.
2 js-ssrf-outbound-request A fetch of a versions URL and a client-side HEAD. Both constant-origin. Noise.
1 manifest-install-lifecycle-script The rust engine's postinstall. Intentional.
1 js-dynamic-code-execution new Function(script.textContent) in docmd-main.js:819, running script tags out of the page it built. Intended by design.

Three that seemed worth naming rather than burying:

  • packages/core/src/commands/stop.ts:63execSync(\lsof -t -i:${port}`), and the same shape on the two lines under it. portcomes from config rather than from an attacker, so this is not a hole; it is the one place inpackages/where a config value reaches a shell string, andexecFile` with an argument array would close the question permanently.
  • packages/plugins/openapi/src/index.ts:277yaml.load(raw) on a user-supplied spec. On js-yaml v4 load is already the safe parser and this is a non-issue; on v3 it is not. The dependency is an optional require with no version constraint, so which one it is depends on what the user happens to have installed. Pinning js-yaml@^4 in the plugin's peer deps would settle it.
  • packages/plugins/ai/src/client/index.ts — the innerHTML sinks above.

None of these are why the workflow is worth having. The reason is the day a real key lands in a commit; the secret rules are the ones that catch that, and they are quiet on this repo today.

Verified rather than assumed

  • The scanner runs with no licence, no account and no network: exits 0 and writes the report.
  • npm install -g --ignore-scripts is enough for scan; without it npm builds better-sqlite3 from source, which only the scanner's daemon needs.
  • --fail-on critical really does exit 0 with the 2 high findings present, so the non-blocking claim is tested.
  • The SARIF is 2.1.0 with a proper driver, which is what upload-sarif wants, and the summary step was run against this repo's own report — the table above is its output.
  • Uses actions/checkout@v7 and actions/setup-node@v7, matching ci.yml.

About the scanner

ThreatCrush is a CLI I work on, so weigh the recommendation accordingly. Nothing about the shape depends on it: the workflow is one file, and pointing it at gitleaks, trufflehog or semgrep is a two-line change. If you would rather have one of those, say so and I will send that instead. If you would rather have none, close this and no hard feelings.

🤖 Generated with Claude Code

ralyodio and others added 2 commits August 12, 2026 04:04
docmd builds sites by running a CLI over somebody's content, and several of
its commands hand values straight to a shell. This watches that surface, and
the more expensive accident of a real credential reaching a public branch, on
every pull request, every push to main, and once a week.

It is deliberately not a gate. The scan fails only on a critical finding, so
a false positive can never hold a PR: results go to the Security tab on push
and to the job summary on a pull request, where uploading SARIF would need a
write token a fork does not get.

The scanner is pinned, and installed with --ignore-scripts so CI never builds
the native dependency that only its daemon uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run against a repo whose criticals are false positives, --fail-on critical
exits 1, so the promise that a false positive cannot hold a PR was not one
the workflow could keep. No --fail-on is passed now, which makes it true
structurally rather than by severity accounting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio

Copy link
Copy Markdown
Author

Correction, pushed just now. The PR said a false positive could never hold a PR because the scan ran with --fail-on critical. Testing that against another repo whose criticals are all false positives, the command exits 1 — so the claim held for this repo's current tree only, by luck, and not for the general case. The flag is gone: the scan reports and never fails, at any severity, which makes the claim structural. Turning it into a gate is one flag on one line and the workflow comment says so, but that is worth doing after triage rather than before.

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