Skip to content

fix(threatcrush-scan): stop resolving a repository's alerts on a failed scan - #960

Merged
ralyodio merged 1 commit into
masterfrom
sag-review
Aug 14, 2026
Merged

fix(threatcrush-scan): stop resolving a repository's alerts on a failed scan#960
ralyodio merged 1 commit into
masterfrom
sag-review

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Review from the SAG maintainers on Zleap-AI/SAG#93. Three of their four findings are fixed here; the fourth is documented rather than pretended away.

1. The empty SARIF upload — a defect, not a readiness complaint

This one already affects repositories that installed the pack.

On any failure the workflow synthesised a zero-result SARIF so the upload wouldn't error on a missing file, then uploaded it under category: threatcrush. Code scanning treats a new analysis in a category as the current truth for that category. An empty run doesn't read as "no data" — it resolves every open ThreatCrush alert the repository had.

A scanner that fails and marks its own previous findings as fixed is worse than one that never ran.

The inline comment defending that step covered the PR comment path — which does correctly say NOT RUN — and said nothing about the upload, because nobody had looked at the upload.

Now: no synthesised file at all, and the upload is gated on the scan step actually reporting clean or findings with a SARIF present. The artifact upload takes if-no-files-found: ignore, since nothing invents the file any more.

2. Actions pinned to full SHAs

Asking a repository to trust a pinned npm package while the workflow around it floats on mutable tags is an argument that doesn't survive being read. The job holds pull-requests: write and security-events: write; every action in it is the same class of grant the npm pin exists to close.

action sha
actions/checkout 11d5960
actions/setup-node 49933ea
github/codeql-action/upload-sarif f371297
actions/upload-artifact ea165f8
actions/github-script f28e40c

Manifest now says pinThirdPartyActions: required rather than optional.

3. Paginated comment lookup

listComments returns thirty and stops, so on a PR with more discussion than that the existing report falls off the page, isn't found, and every run posts another one — a bug that only shows up on the requests people actually engage with.

4. The dependency tree — correct, and not fixed

The hash covers the published tarball; npm install -g still resolves that package's own dependencies from ranges. They verified the hash matched and then pointed straight at the gap behind it.

Closing it means one of:

  • a committed lockfile with npm ci — complete, but 210 packages and ~2,500 lines of package-lock.json landing in the consuming repository
  • a bundled artifact — impossible in full here; better-sqlite3 is native and can't be bundled

Neither is a sane default, so it's written into the README as a stated limitation with both options, rather than left for the next reviewer to find.

Pack to 1.4.0.

🤖 Generated with Claude Code

…ed scan

Review from the SAG maintainers on Zleap-AI/SAG#93. Three of their four
findings are fixed here; the fourth is documented rather than pretended
away.

The first one is not a readiness complaint, it is a defect that hurts
repositories that already installed this.

  On any failure the workflow synthesised a zero-result SARIF so the
  upload would not error on a missing file, then uploaded it under
  category: threatcrush. Code scanning treats a new analysis in a
  category as the current truth for that category. An empty run does not
  read as "no data" — it resolves every open ThreatCrush alert the
  repository had. A scanner that fails and marks its own previous
  findings as fixed is worse than one that never ran.

The inline comment defending that step covered the PR comment path,
which does correctly say NOT RUN, and said nothing about the upload,
because nobody had looked at the upload. Now: no synthesised file at
all, and the upload is gated on the scan step actually reporting clean
or findings with a SARIF present. The artifact upload takes
if-no-files-found: ignore, since nothing invents the file any more.

Second, every action is pinned to a full commit SHA, and the manifest
says pinThirdPartyActions: required rather than optional. Asking a
repository to trust a pinned npm package while the workflow around it
floats on mutable tags is an argument that does not survive being read.
The job holds pull-requests: write and security-events: write; every
action in it is the same class of grant the npm pin exists to close.

  actions/checkout                  11d5960
  actions/setup-node                49933ea
  github/codeql-action/upload-sarif f371297
  actions/upload-artifact           ea165f8
  actions/github-script             f28e40c

Third, the existing-comment lookup is paginated. listComments returns
thirty and stops, so on a pull request with more discussion than that the
report falls off the page, is not found, and every run posts another
one — a bug that only appears on the requests people engage with.

Fourth is the dependency tree, and it is correct and not fixed. The hash
covers the published tarball; npm install -g still resolves that
package's own dependencies from ranges. Closing it means either a
committed lockfile (complete, but 210 packages and ~2,500 lines landing
in the consuming repository) or a bundled artifact (impossible in full
here — better-sqlite3 is native). Written into the README as a stated
limitation, with both options, rather than left for the next reviewer to
find.

Pack to 1.4.0.

Signed-off-by: Anthony Ettinger <anthony@chovy.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

311 finding(s)

HIGH/CRITICAL: 24 | MEDIUM: 51 | LOW: 236

Severity Rule Location
HIGH secret-generic-api-key packages/affiliates/sovrn/src/index.ts:28
HIGH js-nosql-injection packages/ai/amazon-bedrock/src/index.test.ts:121
HIGH secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:9
HIGH secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:10
HIGH secret-generic-credential packages/ai/amazon-bedrock/src/index.ts:11
HIGH js-host-header-trust packages/bots/wechat/src/index.ts:405
HIGH secret-generic-credential packages/bridges/matrix/src/index.ts:58
HIGH secret-generic-credential packages/bridges/matrix/src/index.ts:59
HIGH secret-generic-credential packages/captcha/captchasolver/src/index.ts:34
HIGH secret-generic-credential packages/cli/src/commands/secrets.ts:176
HIGH secret-generic-credential packages/cloud/linode/src/index.ts:15
HIGH secret-generic-credential packages/observability/sentry/src/index.ts:15
HIGH secret-generic-credential packages/outreach/producthunt/src/index.ts:103
HIGH secret-generic-credential packages/promo/posthog/src/index.ts:23
HIGH secret-generic-credential packages/security/snyk/src/index.ts:26
HIGH secret-generic-credential packages/social/hashnode/src/index.ts:4
HIGH secret-generic-credential packages/social/linkedin/src/index.ts:3
HIGH secret-generic-credential packages/social/linkedin/src/index.ts:4
HIGH secret-generic-credential packages/social/medium/src/index.ts:4
HIGH secret-generic-credential packages/social/snapchat/src/index.ts:5
HIGH secret-generic-credential packages/social/tiktok/src/index.ts:5
HIGH secret-generic-credential packages/targets/registry-ans/src/index.ts:49
HIGH secret-generic-credential sites/sh1pt.com/supabase/config.toml:303
HIGH secret-generic-credential sites/sh1pt.com/supabase/config.toml:335
MEDIUM redos-nested-quantifier packages/actions-fleet-core/src/action-pack/schema.ts:3
MEDIUM insecure-temp-file packages/agent-providers/opencode/src/__tests__/opencode.test.ts:19
MEDIUM insecure-temp-file packages/agent-providers/opencode/src/__tests__/opencode.test.ts:42
MEDIUM insecure-temp-file packages/agent-providers/opencode/src/__tests__/opencode.test.ts:45
MEDIUM insecure-temp-file packages/bridges/signal/src/index.test.ts:92
MEDIUM insecure-temp-file packages/bridges/signal/src/index.test.ts:118
MEDIUM insecure-temp-file packages/cli/src/input.test.ts:84
MEDIUM redos-nested-quantifier packages/core/src/setup-helpers.ts:583
MEDIUM insecure-temp-file packages/core/src/testing/harness.ts:15
MEDIUM insecure-temp-file packages/core/src/testing/harness.ts:16
MEDIUM insecure-temp-file packages/core/src/testing/harness.ts:30
MEDIUM insecure-temp-file packages/core/src/testing/harness.ts:42
MEDIUM insecure-temp-file packages/merch/printful/src/index.test.ts:9
MEDIUM insecure-temp-file packages/merch/printify/src/index.test.ts:11
MEDIUM insecure-temp-file packages/policy/src/linter.test.ts:8
MEDIUM redos-nested-quantifier packages/policy/src/rules/bundle-id.ts:3
MEDIUM insecure-temp-file packages/secrets/env-updater/src/index.test.ts:106
MEDIUM insecure-temp-file packages/social/facebook/src/index.test.ts:95
MEDIUM insecure-temp-file packages/social/instagram/src/index.test.ts:177
MEDIUM insecure-temp-file packages/social/pinterest/src/index.test.ts:91
MEDIUM insecure-temp-file packages/social/pinterest/src/index.test.ts:146
MEDIUM insecure-temp-file packages/social/threads/src/index.test.ts:108
MEDIUM insecure-temp-file packages/social/vimeo/src/index.test.ts:137
MEDIUM insecure-temp-file packages/social/x/src/index.test.ts:72
MEDIUM insecure-temp-file packages/social/x/src/index.test.ts:116
MEDIUM insecure-temp-file packages/targets/browser-safari/src/index.test.ts:22

…and 261 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit d89626f into master Aug 14, 2026
8 checks passed
ralyodio added a commit that referenced this pull request Aug 14, 2026
)

Re-targeted at master. This landed in #961, but #961 was based on
sag-review rather than master and #960 was squash-merged, so merging
#961 put the change on sag-review and left master without it. The
consumer side is already on threatcrush master, so right now
TCFEED_LEAST_PRIVILEGE=1 sets inputs the pack has no placeholders for
and silently changes nothing — the permission block is still static.

The change itself is unchanged from #961:

The permission block requested `pull-requests: write` and
`security-events: write` unconditionally, including in the configuration
where both the upload and the comment are switched off. A workflow that
asks for a write scope it will not use cannot call itself least
privilege, which is awkward for one whose request body argues about
supply-chain hygiene.

commentOnPr joins uploadSarif as a switch, and extraPermissions carries
the lines beneath `contents: read` — computed from the two rather than
set by hand, so the block cannot drift out of step with what the
workflow actually does. With both false the rendered workflow requests
`contents: read` and nothing else, and findings arrive in the job
summary and the SARIF artifact.

Verified by parsing the rendered output:

  default          {"contents":"read","pull-requests":"write","security-events":"write"}
  least privilege  {"contents":"read"}

Pack to 1.5.0.

Signed-off-by: Anthony Ettinger <anthony@chovy.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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