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.
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
| Setting | Meaning |
|---|---|
vuln_min_severity | Block a file with an open advisory at this severity or higher: low, medium, high or critical. |
vuln_min_cvss | Block at this CVSS base score or higher (0–10). The score is computed from the advisory’s CVSS vector. |
vuln_require_fix_available | Only count advisories that have a fixed version. |
vuln_min_epss | Only 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.
| Setting | Meaning |
|---|---|
licence_allow | If not empty, only these licences are allowed. |
licence_deny | These licences block. |
licence_warn | These licences warn. |
licence_unknown_action | What to do when a licence is not a valid SPDX expression or is missing: allow, warn or block. |
licence_multiple_mode | For 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: packagematches the package you published.match_target: componentmatches 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_epssset 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
containeris inage_types(orage_typesis 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
| Mode | What happens |
|---|---|
off | Only known malware and name clashes are checked. |
shadow | Every rule is evaluated. Nothing is refused, and each download that would have been refused is recorded. This is the default. |
enforce | A 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_clashormalware, 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.
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 path | What it does |
|---|---|
GET /orgs/{org}/package-policy | The policy, with its version. |
PUT /orgs/{org}/package-policy | Change the policy. Send the version you read; a stale version gets 409 policy-version-conflict. |
GET, POST /orgs/{org}/package-policy/rules | List 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/approvers | The named approvers, besides the owners. PUT takes {"logins": [...]}. |
GET, POST /orgs/{org}/package-policy/exceptions | List exceptions (filter with status) or request one. |
GET /orgs/{org}/package-policy/exceptions/{id} | One exception. |
POST /orgs/{org}/package-policy/exceptions/{id}/approve | Approve. Refused as self-approve for the requester and not-an-approver for anyone else who may not. |
POST /orgs/{org}/package-policy/exceptions/{id}/reject | Reject. |
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/evaluate | Dry 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/decisions | The Would have blocked data: days (1–90, default 14), decision (blocked, would_block or pending_refused). |
GET /orgs/{org}/findings/packages | Advisory 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
- Malicious packages
- Container image scanning, which has its own pull block for container images
- SBOMs
- Registry access log