Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/actions/threatcrush-scan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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. Pinned rather than `@latest` so one bad publish cannot break every consumer at once; bump it in a pack release. |
| `threatcrushIntegrity` | *(sha512 of 0.11.0)* | SRI hash of that tarball. The workflow downloads, hashes and compares before installing, and refuses to install on a mismatch. Bump it with the spec — read it from `npm view <spec> dist.integrity`. Empty skips the check. |
| `failOn` | *(empty)* | Comma-separated severities that fail the job, e.g. `critical,high`. Empty is report-only. |
| `uploadSarif` | `true` | Upload to the Security tab. |

Expand Down
21 changes: 20 additions & 1 deletion packages/actions/threatcrush-scan/sh1pt.actionpack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0
version: 1.3.0
publisher: profullstack
visibility: public
license: MIT
Expand Down Expand Up @@ -40,6 +40,25 @@ inputs:
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.
threatcrushIntegrity:
type: string
default: 'sha512-EKcaxsgiydi7qCH0FhvNviKUpyVi/CImwNS6Kx3IbWMuUjUPCXISAUIzFnYo8BiC+jG9dxfFDMBlwZdhqhwWfQ=='
description: >-
Subresource-integrity hash of the tarball named by threatcrushPackageSpec,
in npm's own `sha512-<base64>` form. The workflow downloads, hashes and
compares before installing, and refuses to install on a mismatch.

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 the
first question is the party serving the bytes. This is the other half,
and it is what Haven's maintainer asked for when they declined on
"exact version + integrity hash".

Must be bumped together with threatcrushPackageSpec — a hash from a
different version fails closed, which is the correct direction to fail
but a confusing one to debug. Read it from
`npm view <spec> dist.integrity`. Empty skips verification, for a
consumer pointing the spec at something they build themselves.
failOn:
type: string
default: ''
Expand Down
52 changes: 47 additions & 5 deletions packages/actions/threatcrush-scan/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,60 @@ jobs:
# 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.
#
# Downloaded, hashed, and only then installed. A pinned version 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 tarball. The hash is the half a version pin
# cannot give you, which is the distinction Haven's maintainer drew
# when they asked for "exact version + integrity hash" rather than
# treating the pin as the answer.
#
# 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 on.
- name: Install ThreatCrush
run: |
set -euo pipefail
spec='{{threatcrushPackageSpec}}'
want='{{threatcrushIntegrity}}'

name=""
for attempt in 1 2 3; do
if npm install -g --ignore-scripts "{{threatcrushPackageSpec}}"; then
exit 0
if name=$(npm pack --silent --pack-destination "${RUNNER_TEMP}" "${spec}" | tail -1) \
&& [ -n "${name}" ] && [ -f "${RUNNER_TEMP}/${name}" ]; then
break
fi
name=""
delay=$((attempt * 10))
echo "::warning::ThreatCrush install attempt ${attempt}/3 failed; retrying in ${delay}s"
echo "::warning::ThreatCrush download attempt ${attempt}/3 failed; retrying in ${delay}s"
sleep "${delay}"
done
echo "::error::ThreatCrush install failed after 3 attempts"
exit 1
if [ -z "${name}" ]; then
echo "::error::ThreatCrush download failed after 3 attempts"
exit 1
fi
tarball="${RUNNER_TEMP}/${name}"

# Not retried, unlike the download. A blip and a mismatch are not the
# same event: 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.
if [ -n "${want}" ]; then
got="sha512-$(openssl dgst -sha512 -binary "${tarball}" | openssl base64 -A)"
if [ "${got}" != "${want}" ]; then
echo "::error::ThreatCrush integrity mismatch for ${spec}"
echo "::error::expected ${want}"
echo "::error::received ${got}"
echo "::error::refusing to install — this is not a transient failure"
exit 1
fi
echo "Integrity verified for ${spec}: ${got}"
else
echo "::warning::no integrity hash pinned for ${spec}; installing unverified"
fi

npm install -g --ignore-scripts "${tarball}"

# Recorded into every run log so a release that changes the interface
# shows up immediately, rather than silently scoring zero.
Expand Down
Loading