Skip to content

feat(threatcrush-scan): verify the tarball hash before installing it - #959

Merged
ralyodio merged 1 commit into
masterfrom
tc-integrity
Aug 14, 2026
Merged

feat(threatcrush-scan): verify the tarball hash before installing it#959
ralyodio merged 1 commit into
masterfrom
tc-integrity

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Haven#532's maintainer declined the workflow and asked for "exact version + integrity hash". The version pin shipped; the hash did not. This is the missing half — and their framing was the correct one, because the two are not the same guarantee.

A version pin says which release to fetch. It does not say the bytes are the ones that release was published with, and the party answering "which version" is the party serving the bytes. Pinning alone narrows the window to a re-publish of the same version; it does not close it.

What the install does now

npm pack --pack-destination "$RUNNER_TEMP"    fetch, do not install
openssl dgst -sha512 | openssl base64         hash it locally
compare against threatcrushIntegrity          refuse on mismatch
npm install -g --ignore-scripts "$tarball"    install the bytes we hashed

Verified npm pack reproduces the published tarball byte for byte — its sha512 equals dist.integrity for 0.11.0 exactly — so the comparison is against the registry's own published digest, not a repack of it.

Two details worth keeping

Into RUNNER_TEMP, never the checkout. npm pack writes to the working directory by default, and a stray .tgz in the tree is something this workflow then scans and reports findings on.

The mismatch is not retried, where the download is. A blip and a mismatch are different events: one is the network, the other is the registry handing back bytes nobody signed off on — and retrying that just asks again until it succeeds.

The new input

threatcrushIntegrity defaults to the 0.11.0 hash and must be bumped with the spec. A hash from a different version fails closed, which is the right direction to fail but a confusing one to debug, so both the manifest and README say so. Read it from npm view <spec> dist.integrity. Empty skips verification, for a consumer pointing the spec at something they build themselves.

Verification

All three paths, against the block as written:

  • correct hash → installs
  • wrong hash → exits 1 without installing, printing expected and received
  • empty hash → warns and proceeds

Pack to 1.3.0 — a new input.

Pairs with profullstack/threatcrush#134, which fills the input. Without it, tcfeed's render stops with the pack has an input tcfeed cannot fill.

🤖 Generated with Claude Code

Haven's maintainer declined the workflow and asked for "exact version +
integrity hash". The version pin shipped; the hash did not. This is the
half that was missing, and their framing was the correct one — the two
are not the same guarantee.

A version pin says which release to fetch. It does not say the bytes are
the ones that release was published with, and the party answering "which
version" is the party serving the bytes. Pinning alone narrows the window
to a re-publish of the same version; it does not close it.

So the install is now download, hash, compare, install:

  npm pack --pack-destination "$RUNNER_TEMP"    fetch, do not install
  openssl dgst -sha512 | openssl base64         hash it locally
  compare against threatcrushIntegrity          refuse on mismatch
  npm install -g --ignore-scripts "$tarball"    install the bytes we hashed

Verified npm pack reproduces the published tarball byte for byte: its
sha512 equals dist.integrity for 0.11.0 exactly, so the comparison is
against the registry's own published digest and not a repack of it.

Two details worth keeping:

Into RUNNER_TEMP, never the checkout. `npm pack` writes to the working
directory by default, and a stray .tgz in the tree is something this
workflow then scans and reports findings on.

The mismatch is not retried, where the download is. A blip and a
mismatch are different events: one is the network, the other is the
registry handing back bytes nobody signed off on, and retrying that just
asks again until it succeeds.

threatcrushIntegrity defaults to the 0.11.0 hash and must be bumped with
the spec. A hash from a different version fails closed, which is the
right direction to fail but a confusing one to debug, so both the
manifest and the README say so. Empty skips verification, for a consumer
pointing the spec at something they build themselves.

Verified all three paths against the block as written: correct hash
installs, a wrong hash exits 1 without installing and prints expected
and received, and an empty hash warns and proceeds.

Pack to 1.3.0 — a new input.

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 ad1b6a0 into master Aug 14, 2026
8 checks passed
@ralyodio
ralyodio deleted the tc-integrity branch August 14, 2026 01:26
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