Skip to main content

Package policies

A package policy decides, file by file, whether a package in your organisation’s registry may be downloaded. It covers the registry types fremforge hosts: npm, Maven, PyPI, NuGet, Cargo, Go, Conda, CRAN, Debian, RPM, Helm and generic packages, and container images (see Container images). Each organisation has one policy, with optional settings per package type.

Enforcement is not switched on yet. Every organisation runs in shadow mode: fremforge evaluates every download against your policy and records the ones it would have refused, but it refuses none of them. Choosing Enforce today records the same results as Shadow. This page will say when enforcement starts.

What a policy checks

When a file is published, fremforge unpacks it, lists the components bundled inside it and reads the package’s own declared licence (see SBOMs). Each component is matched against the OSV advisories, and matched again every day. The policy is evaluated against those results and the verdict is stored for the file: allow, warn or block. A change to the policy, a list or an exception re-evaluates every file at once, with no rescan.

At download, the stored verdict decides. This happens for downloads through the registry API (/api/packages/…, which every package manager uses) and from the web UI. Package metadata, such as an npm packument or a PyPI simple index, is never filtered. Once enforcement is on, a resolver that asks for a blocked version gets 403 for that file; it is not steered to another version.

Vulnerabilities

SettingMeaning
vuln_min_severityBlock a file with an open advisory at this severity or higher: low, medium, high or critical.
vuln_min_cvssBlock at this CVSS base score or higher (0–10). The score is computed from the advisory’s CVSS vector.
vuln_require_fix_availableOnly count advisories that have a fixed version.
vuln_min_epssOnly count advisories whose EPSS probability is at least this value (0–1). An advisory with no EPSS score does not count.

The vulnerability rule is on only when a severity or a CVSS threshold is set. With both set, an advisory that reaches either one counts.

Licences

Licences are SPDX identifiers, checked against both the declared licence and the licences detected in bundled components.

SettingMeaning
licence_allowIf not empty, only these licences are allowed.
licence_denyThese licences block.
licence_warnThese licences warn.
licence_unknown_actionWhat to do when a licence is not a valid SPDX expression or is missing: allow, warn or block.
licence_multiple_modeFor an expression with several licences (MIT OR GPL-3.0-only): any_allowed passes if one of them is allowed, all_allowed needs all of them.

Expressions are evaluated per operand. OR passes when any operand is allowed, AND needs every operand, and X WITH exception is evaluated as X.

Allow and deny lists

A list rule names a package type (or any type), a name pattern (* and ? wildcards, for example @acme/* or org.apache.logging.log4j:log4j-core) and an optional version range in vers: syntax, for example vers:maven/<2.17.1. Ranges follow each ecosystem’s own version rules for npm, Cargo and Go (semver), PyPI (PEP 440), Maven and NuGet.

  • match_target: package matches the package you published.
  • match_target: component matches a component bundled inside it.

A deny rule blocks. An allow rule on a package lets it through the vulnerability, licence and age rules, and an allow rule on a component excuses that component from the vulnerability and licence rules. Allow rules never override known malware or a deny rule. A rule can carry an expiry date.

Minimum age

min_age_hours holds back files younger than this many hours after they were published, optionally only for the types in age_types. Age is decided at the moment of download, so a file becomes downloadable as soon as it is old enough.

Always on

Two checks run whatever the policy says, even with the mode set to Off:

  • Known malware. A file that bundles a package listed in an OSV MAL-* advisory is blocked. See malicious packages.
  • Name clash. An advisory that names your package’s own name and version, usually because a public package has the same name, gives a warning. It never blocks.

Container images

A container image is evaluated per manifest digest, against the same rules, with the image’s own inputs:

  • Vulnerabilities are the CVE findings of container image scanning for that digest (or its tag), with their severity and CVSS score. Image findings carry no EPSS score, so a policy with vuln_min_epss set does not count them. A dismissed finding does not count.
  • Malware is an open malicious-package finding on that digest.
  • Licences are those the image scan reports for the packages in the image. A package with no licence counts as unknown.
  • Allow and deny lists match the image as type container (its name and tag) and the packages inside it as components.
  • Minimum age counts from when the tag was pushed, if container is in age_types (or age_types is empty).

The image is checked when it is pulled (docker pull, or any client reading /v2/…/manifests/…), for every manifest the pull serves, including each platform of a multi-platform image. Images are in shadow mode only: a pull that breaks the policy is recorded under Would have blocked and is never refused, whatever mode you choose. Image findings are re-evaluated at least every 12 hours, and at once when you change the policy, a list or an exception.

This is separate from the existing image pull blocks, which are unchanged: the block_pull setting of container image scanning and the malicious-package block refuse pulls today.

Per-type settings

type_overrides sets any of the rules above differently for one package type, for example a stricter licence list for npm. "enabled": false turns the policy off for that type, apart from the malware check.

Modes

ModeWhat happens
offOnly known malware and name clashes are checked.
shadowEvery rule is evaluated. Nothing is refused, and each download that would have been refused is recorded. This is the default.
enforceA download that breaks the policy is refused with 403. The answer names the rules that applied and links to the policy page. Not active yet: until enforcement starts, enforce behaves like shadow.

pending_action decides what happens to a file whose evaluation has not finished yet, for example a file published seconds ago. allow (the default) lets it through. refuse answers 503 with Retry-After, in enforce mode only.

Exceptions

An exception lets a package through one rule for a limited time. It needs a second person: the requester cannot approve their own request, and the approver must be an owner of the organisation or on the organisation’s approver list.

An exception has:

  • a scope: one file, a version range of a package, every version of a package, or one bundled component (by purl);
  • a rule: vuln, licence, age, deny_list, name_clash or malware, optionally narrowed to one advisory ID or one SPDX licence;
  • a justification;
  • an expiry date at most 366 days away. It is required.

An exception is requested, then approved or rejected. An approved exception can be revoked at any time, and it is revoked automatically when it expires. Every step is written to the audit log (package_policy.exception.*).

The “Would have blocked” view

Open your organisation’s admin pages at Security → Policies → Package policies. The page has four views:

  • Policy sets the mode and the rules, and shows the current verdicts of your published files. To see what new settings would block before you save them, use the dry run in the API (below).
  • Allow and deny lists holds the list rules.
  • Exceptions shows requests, the approval queue and history. Your own requests show no Approve button.
  • Would have blocked lists the downloads the policy refused, or would have refused in shadow mode, over the last 14 days: one row per file, rule, user and day, with the number of downloads. This is the evidence to review before you switch an organisation to enforce.

Downloads refused or recorded this way are also in the registry access log with the decision would_block or blocked.

Preview. Until the shadow review completes, verdicts, the Would have blocked view, package findings and the evaluation results of the API are shown only to organisations taking part in the preview. For every other organisation they are computed and stored but not shown yet. Your policy, lists, approvers and exceptions can be set now and are applied to everything that is evaluated.

API

Use a token with policy:read or policy:write (findings:read for package findings). Base URL: https://frem.sh/_app/api/v1. See the API reference for the full request and response shapes.

Method and pathWhat it does
GET /orgs/{org}/package-policyThe policy, with its version.
PUT /orgs/{org}/package-policyChange the policy. Send the version you read; a stale version gets 409 policy-version-conflict.
GET, POST /orgs/{org}/package-policy/rulesList or add allow and deny rules. A rule needs list, name_glob and reason.
DELETE /orgs/{org}/package-policy/rules/{id}Remove a rule.
GET, PUT /orgs/{org}/package-policy/approversThe named approvers, besides the owners. PUT takes {"logins": [...]}.
GET, POST /orgs/{org}/package-policy/exceptionsList exceptions (filter with status) or request one.
GET /orgs/{org}/package-policy/exceptions/{id}One exception.
POST /orgs/{org}/package-policy/exceptions/{id}/approveApprove. Refused as self-approve for the requester and not-an-approver for anyone else who may not.
POST /orgs/{org}/package-policy/exceptions/{id}/rejectReject.
DELETE /orgs/{org}/package-policy/exceptions/{id}Revoke.
GET /orgs/{org}/package-policy/evaluation?type=&name=&version=The verdict of each file of one package version, with its reasons, findings and licences.
POST /orgs/{org}/package-policy/evaluateDry run. With {"policy": {...}} it reports what every current file would get under those settings, and writes nothing. With {"components": [{"purl": "...", "licence": "..."}]} it evaluates a list of components, for example from a CI step before you publish.
GET /orgs/{org}/package-policy/decisionsThe Would have blocked data: days (1–90, default 14), decision (blocked, would_block or pending_refused).
GET /orgs/{org}/findings/packagesAdvisory findings on registry packages. Page with cursor (the previous page’s next_cursor) and limit (up to 500).

A shadow-mode policy that blocks high and critical advisories and AGPL, and holds back new npm releases for 24 hours:

TOKEN=ffp_...
ORG=acme
V=$(curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://frem.sh/_app/api/v1/orgs/$ORG/package-policy" | jq .version)

curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  "https://frem.sh/_app/api/v1/orgs/$ORG/package-policy" \
  -d "{\"version\": $V, \"mode\": \"shadow\", \"vuln_min_severity\": \"high\",
       \"licence_deny\": [\"AGPL-3.0-only\", \"AGPL-3.0-or-later\"],
       \"min_age_hours\": 24, \"age_types\": [\"npm\"]}"

Deny log4j-core below 2.17.1 wherever it is bundled:

curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  "https://frem.sh/_app/api/v1/orgs/$ORG/package-policy/rules" \
  -d '{"list": "deny", "pkg_type": "maven", "name_glob": "org.apache.logging.log4j:log4j-core",
       "version_range": "vers:maven/<2.17.1", "match_target": "component",
       "reason": "Log4Shell"}'

Audit

Every change is in the audit log: package_policy.updated (with what changed), package_policy.rule.created and .deleted, package_policy.approvers.updated, the exception events, and package_policy.verdict_changed when a file moves from allow towards warn or block. A refused or would-be-refused download is recorded as package_policy.download_blocked or package_policy.download_would_block, once per file, rule, user and day.

Related