> For the complete documentation index, see [llms.txt](https://docs.nullify.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nullify.ai/capabilities/scpm.md).

# Supply Chain Posture Management

> **Maturity: Beta** — Defined in the production stack configuration (`iac/stacks/prod/module-scpm.tf`); triage, cooldown sweep and autofix are still per-tenant flagged (`hyperdrive/pkg/lib/featureflags/keys.go`, `scpm_*`).

## Overview

Supply Chain Posture Management (SCPM) hardens the way your software is built and assembled. Where dependency analysis asks *"does this package have a known CVE?"*, SCPM asks *"is this dependency, manifest, lock file, or CI/CD workflow trustworthy in the first place?"* It reads the manifests, lock files, and pipeline definitions in each repository and flags supply-chain weaknesses — typosquatted or confusable package names, unpinned or floating dependencies, risky install scripts, missing or stale lock files, restricted licenses, and insecure CI/CD configuration — long before any of them turns into a published advisory.

SCPM scans the **default branch** to keep a continuous posture baseline and scans **pull requests** (diff-scoped to the manifests a PR touches) so new supply-chain risk is caught before it merges. Findings are triaged for real-world impact, surfaced in the dashboard and notification targets, and — where a fix can be expressed as a code change — remediated automatically with a pull request.

## How It Works

1. **Manifest & pipeline discovery**: SCPM walks the repository and parses every supported manifest, lock file, registry configuration and CI/CD definition — npm/`package.json` with `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, pnpm workspaces and `.npmrc`; Python (`requirements.txt`, `pyproject.toml`, `setup.cfg`, `Pipfile`, Conda environments, and pip index configuration); Go modules (`go.mod`, `go.sum`, `go.work`); Maven and Gradle including Gradle version catalogs; `.csproj`/NuGet; `Cargo.toml` with Cargo registry configuration; Ruby (`Gemfile`, `Gemfile.lock`, `.gemspec`); Composer (PHP); Swift Package Manager; Dart (`pubspec.yaml`); Elixir (`mix.exs`); CocoaPods; sbt; git submodules; Dockerfiles, Docker Compose and Kubernetes workload manifests; the build scripts a repository runs outside a package manager; and the pipelines themselves — GitHub Actions, GitLab CI, Azure Pipelines, Bitbucket Pipelines, CircleCI, Travis CI, Buildkite, Jenkins and Tekton — plus Dependabot and Renovate configuration.
2. **Posture detection**: A flat ruleset evaluates each parsed manifest against the supply-chain risk categories below. On a pull request the scan is diff-scoped so only the manifests the PR changed produce findings; on the default branch it runs full-repo.
3. **Triage**: Each detection is enriched with repository and business context, screened for false positives, classified for severity and exploitability, and assigned a priority. Triage combines LLM reasoning with deterministic rules so classification is reproducible.
4. **Delivery**: Real findings are written to the platform, flow into the dashboard, and fan out to Jira, Slack, Teams, and GitHub through the notification configuration, with ownership inferred from CODEOWNERS.
5. **Remediation**: For findings that map cleanly to a manifest edit (for example pinning a floating version or a CI/CD action), SCPM drafts an autofix pull request that matches the surrounding style.

SCPM is **event-driven**: it reacts to scan and finding lifecycle changes as they happen rather than running on a fixed daily or weekly cron, so posture stays in step with changes to your repositories.

## Supply-Chain Risk Coverage

SCPM ships a managed ruleset grouped by category rather than enumerated individually:

### Package Trust

* **Typosquatting** — package names that closely resemble a significantly more popular package, a common malicious-publish vector.
* **Dependency confusion** — internal/private package names that could be shadowed by a public registry package.
* **Supply-chain intelligence signals** — dependency hijacking via git-fork versions, and Go `replace` directives that redirect a required module to a fork under a different owner. A `replace` onto a local path (`./`, `../`) is how a Go monorepo wires its own packages together and is not reported.

### Dependency Pinning & Provenance

* **Unpinned / floating dependencies** — version specifiers that can resolve to different versions across builds.
* **Git/HTTP-sourced dependencies** — packages pulled from a git or HTTP URL instead of a registry, bypassing registry security controls.
* **Missing lock files** — manifests with no lock file, allowing non-deterministic resolution.
* **Unpinned Docker base images** — Dockerfiles and Compose files that reference a tag instead of a digest, exposing builds to tag mutation.
* **Dependency cooldown** — Dependabot ecosystem blocks with no cooldown, or a cooldown shorter than the configured window, that would auto-merge a freshly published (and potentially compromised) version too quickly.

### Install & Build Scripts

* **Install scripts** — packages that execute scripts during installation.
* **Unpinned installs in build scripts** — build steps that fetch or install dependencies without pinning.

### Licensing

* **Missing or restricted licenses** — the repository's own manifest declaring no license, or a project-declared license denied by org policy. With no accepted-licenses list configured, the default policy denies strong copyleft only (AGPL, GPL, SSPL, by exact SPDX id); weak and source-available licenses (LGPL, MPL, EPL, CDDL, Elastic, EUPL) are not flagged unless a tenant configures an allowlist. This evaluates the project's own declared license, not the licenses of its dependencies — see `scpm-license-dependency-restricted` for that, and `scpm-metadata-copyleft-license` for the separate rule that reports the full copyleft spectrum, weak included, on every scan.

### CI/CD Posture

A large, dedicated body of GitHub Actions / GitLab CI / Azure Pipelines checks — unpinned or unverified actions, script and YAML injection, artifact token leakage, excessive or over-provisioned permissions, dangerous triggers, cache poisoning, privileged checkout, Docker-socket exposure, self-hosted-runner risk, OIDC over-scoping, and missing build attestation, among others.

{% hint style="info" %}
Advisory-driven vulnerability matching (OSV/GHSA CVE data) is owned by [Dependency Analysis](/capabilities/code-reviews/dependency-analysis.md), not SCPM. SCPM does raise known malware: `scpm-known-malware` holds a repository's exactly-pinned dependencies against Nullify's malware index, which is a different source from the CVE feed and answers before any advisory exists. Everything else SCPM reports is a posture or trust signal rather than a known vulnerability.
{% endhint %}

## How SCPM Fits With Dependency Analysis

SCPM and dependency analysis (SCA) are complementary halves of dependency security:

* **Dependency Analysis (SCA)** answers *is this package version affected by a known vulnerability?* — it matches your dependency graph against enriched CVE/advisory feeds and reasons about reachability and exposure.
* **SCPM** answers *is the way you consume dependencies and run your pipelines trustworthy?* — typosquats, confusable names, unpinned versions, missing lock files, risky install scripts, license policy, and CI/CD hardening.

A package can be advisory-clean yet still be a supply-chain risk (a typosquat, an unpinned floating range, an install script, a workflow that leaks a token), which is exactly the gap SCPM covers. The two run over the same repositories and the same manifest set, so findings from both land side by side in the dashboard and route through the same triage, ticketing, and remediation flows.

## Finding Lifecycle

Every SCPM detection becomes a finding that moves through a consistent lifecycle:

1. **Detected** — a scan produces a new finding; pull-request scans flag findings the PR introduces.
2. **Triaged** — the finding is screened, classified, and prioritised, and re-triaged if a later scan changes the picture.
3. **Ticketed & notified** — findings that meet your configured thresholds create tickets and notify Slack and Teams.
4. **Remediated** — an autofix produces a fix and, where it applies, opens a pull request.
5. **Resolved or suppressed** — a finding can be allowlisted, un-allowlisted, or archived when it no longer reproduces.

When a scan that covered the whole branch no longer sees a finding, SCPM marks it fixed and closes any ticket attached to it. If a later scan finds it again, the finding is reported as reintroduced and its ticket is reopened — or, when the ticketing platform will not reopen a ticket that old, a fresh ticket is opened with a link back to the original. A finding you have allowlisted, archived, or marked a false positive is never reopened by a scan.

Triage runs on your default branch only. A pull-request scan reports what the pull request would introduce and blocks on it, but the verdict — the priority, the false-positive call, the reasoning — is produced for the finding that survives the merge, on the default branch's first scan after it lands. A pull request's findings are transient: the branch is deleted, the findings go with it, and any verdict produced against them would be discarded with them. The practical effect is that a finding a pull request introduces is blocked on its own merits immediately and carries its full triage detail from the first default-branch scan afterwards.

### When a repository or installation is removed

* **A branch is deleted** — its findings are removed, since they can never be observed again.
* **A repository is removed from the installation, or deleted** — its findings are removed.
* **The Nullify app is uninstalled** — findings are **kept**. Uninstalling stops new scans but preserves your triage verdicts, allowlist decisions, and ticket links, so reinstalling picks up where you left off. This matches how SAST and SCA findings behave.

To delete findings deliberately rather than as a side effect of uninstalling, remove the repository or ask Nullify support for a data erasure.

### Querying & acting on findings

The SCPM API mirrors the other detection services:

* `GET /scpm/findings` (plus `/scpm/findings/preview` and `/scpm/findings/detailed`) to list findings.
* `GET /scpm/findings/{findingId}`, `GET /scpm/findings/{findingId}/triage`, and `GET /scpm/findings/{findingId}/events` to inspect a single finding and its history.
* `PATCH /scpm/findings/{findingId}` to update a finding.
* `POST /scpm/findings/{findingId}/ticket` and `DELETE /scpm/findings/{findingId}/ticket` to manage tickets.
* `POST /scpm/findings/{findingId}/allowlist`, `POST /scpm/findings/{findingId}/unallowlist`, and the bulk `POST /scpm/findings/allowlist` to suppress findings.

### Allowlisting a finding

To allowlist (ignore) a finding, send an `allowlistReason` and an `allowlistType`:

```json
{
  "allowlistReason": "Internal package, not a public typosquat",
  "allowlistType": "UserFalsePositive"
}
```

`allowlistType` is one of `AI`, `UserFixed`, `UserAssumeRisk`, `UserFalsePositive`, or `UserOther`. The bulk endpoint additionally takes a `findingIds` array.

{% hint style="info" %}
Findings are allowlisted per-finding from the dashboard or API. A coordinate-based package allowlist can also suppress matching findings before they are created. A repository can additionally carry `.nullify/scpm-ignore.yml`, which sets a rule to `warn` or `off` or lowers its severity for that repository only. It can weaken enforcement and never strengthen it, and it is read from the pull request's merge base rather than the pull request's own checkout, so a pull request cannot add an entry that silences the finding it is introducing. Rules that describe an attack — see [The enforcement floor](#the-enforcement-floor) — cannot be weakened by it at all.
{% endhint %}

## Remediation (Autofix)

When a finding can be resolved by a manifest or pipeline change, SCPM drafts a fix and opens a pull request through the shared autofix pipeline:

* `POST /scpm/findings/{findingId}/autofix/fix` generates a fresh fix against the current target branch. Legacy `useCachedFix` inputs are ignored.
* Fix previews are retired. Both `POST …/autofix/cache` and `POST …/autofix/cache/create_pr` are removed; use `/autofix/fix` for fresh generation against the finding’s current branch. `GET …/autofix/cache/diff` returns 404 without reading storage.
* `GET /scpm/findings/{findingId}/autofix/status`, `…/state`, and `…/activity` track progress.

Autofix pull-request limits are platform-managed (3 open Nullify fix PRs per repository, 5 new fix PRs per remediation plan) and are not configurable per repository. See [Remediations](/capabilities/remediations.md) for how fixes work with developers, ownership routing, and CI feedback.

## Real-time reaction

Supply-chain posture stays in step with your repositories: as scans complete and findings move through their lifecycle, notifications, ticketing, and [Security Program Management](/capabilities/program-management.md) react in real time rather than waiting for a nightly batch.

## Dependency cooldowns

A freshly published package version is a supply-chain risk in its own right — worm-speed compromises spread through new releases before any advisory exists. SCPM can flag direct, dev and lockfile-resolved dependencies that are newer than a configurable recency window (7 days by default), independent of whether any CVE has been filed. A recent publish is reported at HIGH for the whole window and never escalates with how new the version is: the age is recorded on the finding, but the top severity is reserved for evidence of an attack rather than evidence of newness. What does escalate is missing coverage — a repository that ships dependencies in an ecosystem no updater cooldown covers is reported at HIGH for the gap itself.

You control this per tenant through the SCPM settings API (`GET`/`PUT /scpm/settings`): toggle the cooldown gate on or off, set the window in days, and add per-ecosystem overrides.

This complements the narrower **Dependabot cooldown** conformance rule above (which checks that your Dependabot config doesn't auto-merge freshly published versions too quickly).

## Configuration

SCPM settings live in the dashboard and admin API, not in any repository config file:

* **Ticket creation thresholds** (severity/priority per finding type): **Configure → Ticketing**.
* **CI/PR gate** (fail the build, enable PR reviews): **Configure → Pull Requests**.
* **Notification destinations**: **Configure → Connectors** (Slack, Teams, Jira).
* **Dependency cooldown window**: SCPM settings API (`PUT /scpm/settings`).
* **Enforcement floor**: fixed at `CRITICAL` — see below.

Ignore decisions are made per-finding (see [Allowlisting a finding](#allowlisting-a-finding)).

### The enforcement floor

The enforcement floor is the minimum severity a supply-chain finding must reach before it can **block a pull request**. It is **`CRITICAL`**.

That is deliberately conservative. A single repository's first SCPM scan routinely surfaces hundreds of `HIGH` findings — unpinned actions, floating version ranges, missing lock files — that are real posture issues but were already there before the pull request. Blocking on all of them the day SCPM is switched on would stop every merge in the organisation.

What the floor does and does not do:

* **It does not hide findings.** Every finding below the floor is still detected, triaged, stored, and visible in the dashboard and the API. Only merge-blocking is affected.
* **Malicious-package findings ignore it.** Typosquats, dependency confusion, known-malicious actions, known malware, phantom and AI-hallucinated packages, and rapid-publish findings block at their own severity whatever the floor is set to. These describe an attack rather than a hygiene gap, so the floor — which exists to stop the high-volume hygiene families gating a repository by surprise — is not allowed to disarm them. Setting your own policy for one of those rules replaces this default.
* **It applies to the PR gate.** A finding below the floor is reported on the pull request but does not fail the check run. When findings are excluded this way, the check run says so and names how many.
* **It applies to automatic ticketing.** Auto-created tickets use the same floor, so lowering it also starts ticketing the newly-eligible findings — including ones detected long before the change, since the floor is read when the ticket is considered, not when the finding was found.
* **An in-repo exception wins over it.** A path excluded through `.nullify/scpm-ignore.yml` is excluded regardless of severity.

{% hint style="info" %}
If your check runs are passing but you expect supply-chain findings to be blocking, the floor is the first thing to check — the check-run summary reports how many findings it excluded.
{% endhint %}

## Metadata and malware analysis

Package metadata can surface unmaintained, deprecated, or trivial dependencies in addition to manifest and pipeline weaknesses. Container configuration checks include insecure HTTP registry use. A metadata warning is a posture signal, not a confirmed malicious-package verdict.

SCPM also exposes on-demand dependency and container malware analysis and batch result lookup. See the [SCPM API](/api-reference/api-reference/scpm.md) for these operations and the `acceptedLicenses`, `packageAllowlist`, and `cooldownWarnOnly` settings. Coordinate-based package policy is separate from allowlisting one finding.

## Related

* [Threat Intelligence](/capabilities/threat-intelligence.md): shared research of new vulnerabilities and supply-chain compromises, assessed against your environment.
