From 8e1ad40be21338d5dbf3077ed84b9922dd625c4a Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 13 Aug 2026 12:16:20 +0000 Subject: [PATCH 1/2] fix(threatcrush-scan): pin the CLI, drop install scripts and the checkout token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarQube fails the pack's own workflow on a repository that runs it, which is a poor look for a security scan: githubactions:S6505 omitting --ignore-scripts allows lifecycle scripts to run during package installation githubactions:S8543 using dependencies without locking resolved versions Both are fair. The job holds `pull-requests: write` and `security-events: write`, and it was installing a floating `@latest` with lifecycle scripts enabled — so an npm publish by anyone in the CLI's dependency tree ran arbitrary code inside a write-scoped job on a stranger's runner, with a checkout credential still in .git/config. - install with --ignore-scripts. The CLI declares no install hook, and `scan` was verified to run correctly from an --ignore-scripts install. - default to an exact version rather than @latest. Whoever installs the pack renders the version current at the time; the repository upgrades when it decides to. - persist-credentials: false on checkout. Nothing in this job pushes. Co-Authored-By: Claude Opus 5 (1M context) --- packages/actions/threatcrush-scan/README.md | 2 +- .../threatcrush-scan/sh1pt.actionpack.yaml | 13 ++++++++++--- packages/actions/threatcrush-scan/workflow.yml | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/actions/threatcrush-scan/README.md b/packages/actions/threatcrush-scan/README.md index 277fe33b..e322da17 100644 --- a/packages/actions/threatcrush-scan/README.md +++ b/packages/actions/threatcrush-scan/README.md @@ -14,7 +14,7 @@ sh1pt actions install threatcrush-scan --repo owner/name --pr | --- | --- | --- | | `scanPath` | `.` | Path to scan, relative to the repository root. | | `nodeVersion` | `20` | See *Node 20, deliberately*, below. | -| `threatcrushPackageSpec` | `@profullstack/threatcrush@latest` | npm spec used to install the CLI. | +| `threatcrushPackageSpec` | `@profullstack/threatcrush@0.11.0` | npm spec used to install the CLI. Must be an exact version — a floating spec installs unreviewed code on someone else's runner, and static analysis fails the workflow for it. | | `failOn` | *(empty)* | Comma-separated severities that fail the job, e.g. `critical,high`. Empty is report-only. | | `uploadSarif` | `true` | Upload to the Security tab. | diff --git a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml index a556f44f..172b4dcb 100644 --- a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml +++ b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml @@ -5,7 +5,7 @@ description: >- Scans pull requests for hardcoded credentials, injection, SSRF, unsafe deserialisation and dependency tampering, and uploads SARIF to the Security tab. -version: 1.1.0 +version: 1.2.0 publisher: profullstack visibility: public license: MIT @@ -32,8 +32,15 @@ inputs: that fails without a full toolchain. threatcrushPackageSpec: type: string - default: '@profullstack/threatcrush@latest' - description: npm spec used to install the CLI. + default: '@profullstack/threatcrush@0.11.0' + description: >- + npm spec used to install the CLI. An exact version, not a range and not + `@latest`: a floating spec means the workflow committed to a repository + today installs something nobody reviewed tomorrow, and static analysis + reads it as exactly that — SonarQube's githubactions:S8543 fails a + quality gate on it. Whoever installs the pack should render the version + current at install time; from then on the repository upgrades when it + decides to, which is the whole point of a pin. failOn: type: string default: '' diff --git a/packages/actions/threatcrush-scan/workflow.yml b/packages/actions/threatcrush-scan/workflow.yml index 3e7fdba4..2d49fbb0 100644 --- a/packages/actions/threatcrush-scan/workflow.yml +++ b/packages/actions/threatcrush-scan/workflow.yml @@ -15,7 +15,14 @@ jobs: timeout-minutes: 15 steps: + # persist-credentials: false because nothing here pushes. Left at the + # default, checkout leaves a credential in .git/config for the rest of + # the job — and the rest of this job runs a scanner installed from the + # network over the contents of a pull request. A token that no step + # needs should not be sitting in the working tree while that happens. - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: @@ -25,10 +32,17 @@ jobs: # whether a security gate runs at all. Retry before giving up; a # transient registry blip is not a security signal and should not read # like one. + # + # --ignore-scripts because a lifecycle script is arbitrary code from the + # dependency tree, and this job holds `pull-requests: write` and + # `security-events: write`. The CLI does not need them: it declares no + # install hook of its own, and `scan` was verified to run correctly from + # an --ignore-scripts install. A security gate that opens a shell for + # its own supply chain is not a gate. - name: Install ThreatCrush run: | for attempt in 1 2 3; do - if npm install -g "{{threatcrushPackageSpec}}"; then + if npm install -g --ignore-scripts "{{threatcrushPackageSpec}}"; then exit 0 fi delay=$((attempt * 10)) From 49d1ab4558f66af18c0cc788450ad63553c9d7d5 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 13 Aug 2026 12:49:52 +0000 Subject: [PATCH 2/2] fix(threatcrush-scan): defer to the pin that already landed on master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #952 pinned threatcrushPackageSpec to 0.11.0 and bumped the manifest to 1.2.0 while this branch was open, with a better reason than the one here: the `workspace:` protocol slip in 0.7.0/0.7.1 is what @latest actually cost, and that is worth more in the file than a rule number. Both sides reached the same default, so the manifest and README go back to master's wording verbatim — which also un-conflicts the branch. What is left is the half master still does not have: --ignore-scripts on the install, and persist-credentials: false on checkout. Co-Authored-By: Claude Opus 5 (1M context) --- packages/actions/threatcrush-scan/README.md | 2 +- .../actions/threatcrush-scan/sh1pt.actionpack.yaml | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/actions/threatcrush-scan/README.md b/packages/actions/threatcrush-scan/README.md index e322da17..8407deb8 100644 --- a/packages/actions/threatcrush-scan/README.md +++ b/packages/actions/threatcrush-scan/README.md @@ -14,7 +14,7 @@ sh1pt actions install threatcrush-scan --repo owner/name --pr | --- | --- | --- | | `scanPath` | `.` | Path to scan, relative to the repository root. | | `nodeVersion` | `20` | See *Node 20, deliberately*, below. | -| `threatcrushPackageSpec` | `@profullstack/threatcrush@0.11.0` | npm spec used to install the CLI. Must be an exact version — a floating spec installs unreviewed code on someone else's runner, and static analysis fails the workflow for it. | +| `threatcrushPackageSpec` | `@profullstack/threatcrush@0.11.0` | npm spec used to install the CLI. Pinned rather than `@latest` so one bad publish cannot break every consumer at once; bump it in a pack release. | | `failOn` | *(empty)* | Comma-separated severities that fail the job, e.g. `critical,high`. Empty is report-only. | | `uploadSarif` | `true` | Upload to the Security tab. | diff --git a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml index 172b4dcb..7f213877 100644 --- a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml +++ b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml @@ -34,13 +34,12 @@ inputs: type: string default: '@profullstack/threatcrush@0.11.0' description: >- - npm spec used to install the CLI. An exact version, not a range and not - `@latest`: a floating spec means the workflow committed to a repository - today installs something nobody reviewed tomorrow, and static analysis - reads it as exactly that — SonarQube's githubactions:S8543 fails a - quality gate on it. Whoever installs the pack should render the version - current at install time; from then on the repository upgrades when it - decides to, which is the whole point of a pin. + npm spec used to install the CLI. Pinned, not `@latest`: a scanner that + runs on every pull request is a dependency, and `@latest` means one bad + publish breaks CI in every repo that installed this pack at once — which + is exactly what a `workspace:` protocol slip in 0.7.0/0.7.1 did. Bump this + deliberately, in a pack release, so the fleet re-syncs consumers to a + version that was checked first. failOn: type: string default: ''