Skip to content

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.

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 packages
FieldTypeDescription
max_severityCRITICAL | HIGH | MEDIUM | LOWFail 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_severitybool (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.

These act on the EPSS score and CISA KEV listing attached during enrichment. See Vulnerability checking for how those signals are sourced.

FieldTypeDescription
deny_kevboolAdditive. Any CISA-KEV (actively-exploited) vulnerability fails, regardless of severity — even a KEV MEDIUM under max_severity: HIGH.
max_epssfloat 0.0–1.0Additive. Fail any vulnerability whose EPSS score exceeds this.
epss_severity_gate.min_epss_to_enforcefloatGoverned 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.

allowed_licenses and denied_licenses follow the standard allow/deny rule, applied to the licenses Package Probe detected on each dependency:

  • If allowed_licenses is set, only those licenses pass (a detected license outside the set fails).
  • If only denied_licenses is set, everything except those passes.
  • allowed_licenses wins 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.

Flag any dependency whose source_url didn’t come from an approved registry.

FieldTypeDescription
approved_registrieslist of globRecommended. Glob-aware allow-list (https://*.artifactory.example.com/*). A dependency with a source_url that matches none of these is flagged.
allowed_registrieslist of prefixLegacy prefix-match allow-list. Prefer approved_registries.
denied_registrieslist of prefixPrefix-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.

FieldTypeDescription
typosquat_detectionbool (default true)Flag dependencies whose name resembles a popular package. Uses the TYPOSQUAT_DETECTION entitlement.
typosquat_allowlistlist of stringPackage 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.

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
- optional

A 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.

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”.

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.

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.

OSPath
macOS/Library/Application Support/packageprobe/policy.yaml
Linux/etc/packageprobe/policy.yaml
WindowsC:\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:` wrapper
max_severity: HIGH
allowed_licenses:
- MIT
- Apache-2.0
approved_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.

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:

EcosystemRegistry / 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
CocoaPodsPodfile 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.