secskills
secskills / core / auditing-supply-chain

auditing-supply-chain

core verified 2026-07-27

Audit software supply chain risk — dependency and transitive package review, typosquatting and dependency confusion, lockfile and SBOM analysis, CI/CD pipeline and GitHub Actions security, build provenance, and secrets exposure. Use when assessing third-party package risk, reviewing a build pipeline, investigating a malicious package, or hardening release infrastructure.

$ /plugin install secskills-core

Your build pipeline runs more untrusted code than your application does. A single unpinned action, a postinstall script, or a workflow with a writable token is a path from a stranger's commit to your production artifacts and your signing keys.

When to Use

When NOT to Use

for offensive assessment

managing-vulnerabilities; reachability analysis here feeds its ranking

Two Different Risks

Keep them separate; they need different responses.

Known-vulnerable dependencyMalicious dependency
DetectionCVE databases, npm audit, osv-scannerBehavioural review, install scripts, publisher anomalies
SignalLoud and well-tooledQuiet; scanners usually miss it
ResponsePatch, or justify the riskIncident — assume credentials on the build host are burned
Time pressureDays to weeksHours

Most programs handle the first and are blind to the second. Give the second explicit attention.

Dependency Review

# Known vulnerabilities, ecosystem-agnostic
osv-scanner --lockfile=package-lock.json --lockfile=go.sum --lockfile=Cargo.lock
trivy fs --scanners vuln,secret,misconfig .
grype dir:.

# Ecosystem-native
npm audit --omit=dev && npm ls --all --depth=99 | wc -l   # count transitives
pip-audit -r requirements.txt
cargo audit
govulncheck ./...    # reachability-aware: only reports vulns you actually call
mvn dependency-check:check

govulncheck-style reachability analysis matters: a vulnerability in a code path you never execute is a patching task, not a risk. Prioritize by reachability plus exposure, not by CVSS alone.

Transitive depth is the real surface. Direct dependencies are chosen and reviewed; transitive ones are inherited. Count them, and know which maintainers you are implicitly trusting.

Detecting Malicious Packages

Triage signals, roughly in order of how strongly they indicate malice:

SignalHow to check
Install-time script executionpostinstall/preinstall in package.json; setup.py with network or exec calls; build.rs
Obfuscated or minified source in a non-minified packageRead the published tarball, not the repo — they differ
Network calls at import/require timeStatic grep for HTTP/DNS in module top-level
Environment and credential accessReads of ~/.aws, .npmrc, .git-credentials, process.env dumps
New maintainer or a version published from a new accountRegistry metadata, publish history
Name close to a popular packageLevenshtein distance against top-N package list
Published artifact ≠ repository sourceCompare the tarball to the tagged commit
Version jump with no corresponding commitsRegistry vs VCS history
# Review what actually ships, not what the repo shows
npm pack <pkg> && tar -xzf <pkg>.tgz && rg -n 'child_process|eval\(|Buffer\.from\(.*base64|https?://' package/
pip download --no-deps --no-binary :all: <pkg> && tar -xzf <pkg>.tar.gz
rg -n 'os\.system|subprocess|urllib|requests|__import__|exec\(' <pkg>/setup.py

# Block install scripts by default in CI
npm ci --ignore-scripts
pip install --require-hashes -r requirements.txt

Dependency confusion: if an internal package name is not also registered (or reserved) on the public registry, and the resolver can reach the public registry, an attacker can publish a higher version and win resolution.

# Enumerate internal-looking names and check public availability
rg -o '"@?[a-z0-9-]+/[a-z0-9-]+"' package.json | sort -u
# Fix: scoped registries with strict scope→registry mapping, and
# `.npmrc` / `pip.conf` that never falls back to the public index for
# internal scopes

Lockfiles and Pinning

the lockfile (npm ci, pip install --require-hashes, cargo --locked, go mod verify) rather than resolving fresh.

review flag, not noise.

does not protect against a re-published version in registries that permit it.

CI/CD Pipeline Security

This is where the highest-impact findings usually are.

GitHub Actions

# Unpinned third-party actions — anyone who controls the tag controls your CI
rg -n 'uses:\s+(?!actions/)[^@]+@(?!v?[0-9a-f]{40})' .github/workflows/

# The dangerous trigger: pull_request_target runs with repo secrets and
# write-capable tokens, in the base repo context
rg -n 'pull_request_target|workflow_run' -A15 .github/workflows/

# Script injection: untrusted event data interpolated directly into a shell
rg -n '\$\{\{\s*github\.event\.(issue|pull_request|comment|head_commit)' .github/workflows/

Three findings to check for on every repository:

  1. pull_request_target + checkout of the PR head. This executes a

stranger's code with your secrets. It is a critical finding whenever the workflow also runs build or test steps from the checked-out tree.

  1. Untrusted interpolation into run:. ${{ github.event.issue.title }}

inside a shell block is command injection with a public entry point. Pass through an env: variable and quote it instead.

  1. Over-broad permissions. Default GITHUB_TOKEN scope should be

contents: read, elevated per-job only where needed. Check for permissions: write-all and for the absence of any permissions: block.

Also review: self-hosted runners on public repos (persistent compromise, no isolation between jobs), secrets available to fork-triggered workflows, cache poisoning across branches, and artifact upload of build directories that contain credentials.

General pipeline

# Secrets in history, not just in the tree
gitleaks detect --source . --redact
trufflehog git file://. --only-verified

# IaC and container config
trivy config . && checkov -d .
hadolint Dockerfile

Check: who can trigger a deploy, whether deploy credentials are scoped per environment, whether the build is reproducible, whether artifacts are signed, and whether anyone can push directly to the release branch.

SBOM and Provenance

# Generate — from the build, not from the source tree, so it reflects reality
syft dir:. -o cyclonedx-json=sbom.json
cdxgen -o sbom.json

# Consume — an SBOM is only useful if you scan it on a schedule
grype sbom:sbom.json
osv-scanner scan source -L sbom.json   # v2 takes SBOMs via -L; --sbom is gone

An SBOM produced once for a compliance checkbox has no security value. The value is in re-scanning existing SBOMs when a new vulnerability lands, which answers "are we affected" in minutes instead of days.

Provenance (SLSA framing): can you prove which source commit produced a given artifact, on which builder, with which dependencies? Sign artifacts (cosign), record attestations, and verify signatures at deploy time. An unverified signature is decoration.

cosign sign --key <key> <image>
cosign verify --key <pub> <image>
cosign verify-attestation --type slsaprovenance <image>

Responding to an Upstream Compromise

1. Determine exposure: did any build pull the affected version? Check
   lockfiles across branches AND build logs — the lockfile shows intent, the
   build log shows what was actually installed.
2. Assume credential compromise on any host that ran the package's install
   scripts. Rotate: registry tokens, cloud keys, signing keys, SSH keys.
3. Preserve build logs and runner images before they roll off.
4. Check outbound network from build hosts for the exfil window.
5. Pin and rebuild; verify the rebuilt artifact differs only as expected.
6. Only then publish an advisory.

Rotation is not optional because the package "only ran in CI." CI is where the production credentials live.

Rationalizations to Reject

build servers with full credentials. That is a worse target than production.

makes it a target. Several of the largest incidents were top-100 packages.

package that was malicious from its first publish has no CVE.

pinning takes minutes.

exactly the dangerous case.

through error messages, source maps, job logs, and public forks.

Deliverable

concentration

scope, and injection sinks each explicitly stated as present or absent

ATT&CK Coverage

Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.

Initial Access (TA0001)

Defense Evasion (TA0005)

Credential Access (TA0006)

Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.

References