Policy & Registry Configuration
Package Probe evaluates your scan results against a policy you define, and
detects where each dependency was installed from by reading each ecosystem’s
registry config. This page is the reference for both: the policy: block in
~/.packageprobe/config.yaml, and the per-ecosystem config files that back
registry-enforcement rules.
Policy is enforced in packageprobe-core, so every surface (CLI --policy, the
macOS app, Fleet) applies the same rules. Policy evaluation — including its
severity, license, and approved-registry rules — is gated by the single POLICY
entitlement (Compliance pack); typosquat findings additionally require
TYPOSQUAT_DETECTION (Developer pack). POLICY also gates the Third-Party
Licenses export: it absorbed the former LICENSE_COMPLIANCE and
REGISTRY_ENFORCEMENT codes, which are retired.
The policy: block
Section titled “The policy: block”Add a policy: block to ~/.packageprobe/config.yaml. Omit the whole block to
disable policy.
policy: max_severity: HIGH # fail if any vuln >= this severity allowed_licenses: # if set, ONLY these licenses pass - MIT - Apache-2.0 denied_licenses: # used only when allowed_licenses is absent - GPL-3.0 approved_registries: # glob-aware allow-list (recommended) - "https://*.artifactory.example.com/*" - "https://registry.npmjs.org/*" typosquat_detection: true # flag deps resembling popular packagesSeverity
Section titled “Severity”| Field | Type | Description |
|---|---|---|
max_severity | CRITICAL | HIGH | MEDIUM | LOW | Fail if any vulnerability is at or above this severity. UNKNOWN (no scorable CVSS / GHSA label) has rank 0 and is not caught by max_severity. |
deny_unknown_severity | bool (default false) | Additionally fail any vulnerability whose severity is UNKNOWN, so an un-triageable advisory can’t silently pass. |
Severity rules are vuln-dependent — they need a --vulns scan. Running
scan --policy without --vulns fails closed (exit 2) rather than skipping the
rule; pass --enrich-for-policy to auto-enable enrichment instead.
Exploitability (EPSS + KEV)
Section titled “Exploitability (EPSS + KEV)”These act on the EPSS score and CISA KEV listing attached during enrichment. See Vulnerability checking for how those signals are sourced.
| Field | Type | Description |
|---|---|---|
deny_kev | bool | Additive. Any CISA-KEV (actively-exploited) vulnerability fails, regardless of severity — even a KEV MEDIUM under max_severity: HIGH. |
max_epss | float 0.0–1.0 | Additive. Fail any vulnerability whose EPSS score exceeds this. |
epss_severity_gate.min_epss_to_enforce | float | Governed suppression. A HIGH+ finding with a present EPSS below this stops failing max_severity — relaxed, but kept visible in the report and audit-logged. KEV findings are never relaxed; a finding with no EPSS is never relaxed (absence is not evidence of low risk). |
deny_kev and max_epss can only add failures — a finding with no KEV/EPSS
signal is never failed by them, and that pass is not a claim it is safe.
Licenses
Section titled “Licenses”allowed_licenses and denied_licenses follow the standard allow/deny rule,
applied to the licenses Package Probe detected on each dependency:
- If
allowed_licensesis set, only those licenses pass (a detected license outside the set fails). - If only
denied_licensesis set, everything except those passes. allowed_licenseswins when both are set.
The check runs only on dependencies that have detected license data. A
dependency with no license evidence (several ecosystems don’t always surface a
license) is skipped — an allowed_licenses allow-list does not fail a dep
whose license is unknown. Treat “no license flagged” as “license unknown”, not
“license approved”.
License rules are evidence-free — they evaluate offline without a --vulns
scan. Like the rest of the policy: block — and like the Third-Party Licenses
export — they are gated by POLICY.
Registries
Section titled “Registries”Flag any dependency whose source_url didn’t come from an approved registry.
| Field | Type | Description |
|---|---|---|
approved_registries | list of glob | Recommended. Glob-aware allow-list (https://*.artifactory.example.com/*). A dependency with a source_url that matches none of these is flagged. |
allowed_registries | list of prefix | Legacy prefix-match allow-list. Prefer approved_registries. |
denied_registries | list of prefix | Prefix-match deny-list, used only when no allow-list is set. |
The registry check runs only on dependencies that have a resolved
source_url. A dependency with no source_url (some ecosystems and cataloger
paths don’t surface one — e.g. constraint-mode manifests without a lockfile) is
skipped by this check, not flagged. So an approved_registries allow-list is
not by itself a guarantee that every dependency came from an approved registry —
only that every dependency with known provenance did.
Like the rest of the policy: block, registry rules are gated by POLICY. The
per-ecosystem registry config Package Probe reads to determine source_url is
documented
below.
Maintenance & typosquatting
Section titled “Maintenance & typosquatting”| Field | Type | Description |
|---|---|---|
typosquat_detection | bool (default true) | Flag dependencies whose name resembles a popular package. Uses the TYPOSQUAT_DETECTION entitlement. |
typosquat_allowlist | list of string | Package names verified as legitimate — skipped during typosquat detection. |
max_dep_age_days is accepted in config for forward compatibility but is not
yet enforced by the policy evaluator — a stale dependency does not currently
produce a violation. Don’t rely on it as a gate.
Dependency-scope filtering
Section titled “Dependency-scope filtering”vuln_scopes controls which dependency scopes a vulnerability must belong to
for it to fail policy. Omit it and the default is [runtime, build, optional] —
development and test scopes are surfaced but not gated.
policy: vuln_scopes: - runtime - build - optionalA vulnerability on an out-of-scope dependency is downgraded to visible (kept in
the findings, audit-logged), not removed. Fail-safe: a dependency with an
unknown/empty scope is treated as runtime (in-scope), so scope filtering never
silently hides a vulnerability.
Governed suppressions
Section titled “Governed suppressions”Some fields suppress a would-be violation (rather than adding one). Every
suppression is governed: opt-in, kept visible in the report (never removed
from findings), audit-logged, and controllable by an admin
policy. The suppressing paths are epss_severity_gate
(above), vex (consume VEX statements), typosquat_suppress (id-keyed typosquat
downgrade), and vuln_scopes (scope exclusion). A relaxed finding is reported in a
dedicated bucket, never as “clean”.
Custom rules (the policy DSL)
Section titled “Custom rules (the policy DSL)”Beyond the declarative fields, policy.rules lets you write custom expressions.
Each rule’s when: expression runs once per dependency; when it returns true,
that dependency produces a violation.
policy: rules: - name: gpl-with-high message: "GPL-licensed dep with HIGH+ severity is not permitted" when: 'severity >= HIGH AND licenses matches "GPL-*"'
- name: artifactory-only message: "All dependencies must come from artifactory.example.com" when: 'source_url not starts_with "https://artifactory.example.com/"'
- name: no-kev message: "Dependency has an actively-exploited (CISA KEV) vulnerability" when: 'kev == true'Available per-dependency variables include name, version, source_url,
purl, licenses, severity, vuln_count, has_fix, has_vulns, vuln_ids,
kev, known_ransomware, epss, package_manager, and project_license.
Operators include ==, !=, ordinal compares, AND/OR/NOT, matches
(glob), contains, starts_with, ends_with, and in. Severity constants
compare ordinally: CRITICAL > HIGH > MEDIUM > LOW > UNKNOWN.
A rule referencing a vuln-dependent variable (severity, kev, epss, …) needs a
--vulns scan and fails closed on a scan that never enriched; a rule using only
evidence-free variables (name, licenses, source_url, …) stays evaluable
offline. The DSL fails softly — a rule that can’t be parsed or evaluated is
skipped with a logged warning (and, to be fail-secure, an unparseable rule is
treated as vuln-dependent) rather than crashing the scan.
The full DSL reference — every variable, operator, and 20 copy-paste examples —
lives in the repository at
docs/policy-dsl.md.
Admin-managed policy
Section titled “Admin-managed policy”An administrator can deploy a system-level policy that overrides user config. Admin fields always win; the macOS app shows the policy section read-only when an admin policy is present.
| OS | Path |
|---|---|
| macOS | /Library/Application Support/packageprobe/policy.yaml |
| Linux | /etc/packageprobe/policy.yaml |
| Windows | C:\ProgramData\packageprobe\policy.yaml |
The admin file uses the policy fields directly at the top level — it is a bare
policy document, not wrapped in a policy: key like config.yaml is:
# /etc/packageprobe/policy.yaml — no `policy:` wrappermax_severity: HIGHallowed_licenses: - MIT - Apache-2.0approved_registries: - "https://*.artifactory.example.com/*"Because admin fields win wholesale, an admin policy that omits a suppression block
(e.g. vex, epss_severity_gate) forbids the user’s — suppressions can be
centrally disabled. Fleet can also distribute a central managed policy that
composes as the outermost admin layer; see the repository’s
docs/managed-policy.md.
Per-ecosystem registry configuration
Section titled “Per-ecosystem registry configuration”To determine each dependency’s source_url (and detect private registries),
Package Probe reads the registry config files each ecosystem uses. It parses
project-level config (adjacent to the manifest) and user-level config
(in your home directory); credentials are always hashed before storage and never
kept in plaintext.
The authoritative, exhaustive spec — every file, field, and precedence level
per ecosystem, kept in sync with the parsers — lives in the repository at
docs/registry-conventions.md.
Consult it when configuring registry policy for a specific ecosystem. The
high-level surface each ecosystem’s parser reads:
| Ecosystem | Registry / credential surface (see the spec for exact files + fields) |
|---|---|
| Node.js (npm / yarn / pnpm) | .npmrc / .yarnrc.yml (project + user), pnpm’s rc; NPM_TOKEN |
| .NET (NuGet) | NuGet.Config at project (tree-walked), user, and machine levels |
| PHP (Composer) | composer.json repositories + auth.json (project + user) |
| Python (pip / poetry / uv / pipenv / pdm) | pip.conf, ~/.pypirc, pyproject.toml sources; PIP_INDEX_URL / PIP_EXTRA_INDEX_URL |
| Java (Maven / Gradle) | ~/.m2/settings.xml + POM repositories; Gradle maven { url } + gradle.properties |
| Rust (Cargo) | .cargo/config.toml (project + user) + ~/.cargo/credentials.toml; CARGO_REGISTRIES_<NAME>_TOKEN |
| Ruby (Bundler / RubyGems) | Gemfile sources, .bundle/config, ~/.gemrc, ~/.gem/credentials |
| Go (Go Modules) | GOPROXY / GOPRIVATE (and related env), ~/.netrc credentials |
| Swift (SPM) | resolved Git-host URLs from the project’s Package.resolved; ~/.netrc credentials |
| Dart (pub) | pub credentials.json (per-URL token); hosted URLs from the project manifest |
| CocoaPods | Podfile sources (default CDN https://cdn.cocoapods.org/); local spec-repo remotes; ~/.netrc |
Because the exact file/field set differs per ecosystem and evolves with the parsers, treat the table above as orientation and the linked spec as the source of truth.
Each detected registry becomes a RegistryConfig in the scan output with its url, level
(global/project/environment), the config_file it came from, an optional
username, a valid flag set by registry policy, and an auth_status
(detected / none / env_var_missing) that reports whether a credential was
present. Any credential is one-way hashed internally and never serialized into
the scan output — the JSON reports credential presence via auth_status, not the
secret itself.