I’ve been thinking about adding policy features to git-pkgs/actions, the GitHub Actions that check licenses, scan for vulnerabilities, and generate SBOMs during CI. The license action currently takes a comma-separated list of SPDX identifiers and the vulnerability action takes a severity string, which is fine for simple cases but obviously not enough once you need to ignore specific CVEs with expiry dates, ban particular packages regardless of license, allow exceptions for vetted transitive dependencies, or set different rules for different repositories.

I went looking for a format to adopt rather than invent. I’ve also been investigating what it would take to add dependency intelligence features to Forgejo, the forge that Codeberg and a growing number of self-hosted instances run, and if Forgejo gets a dependency graph it will need a policy layer with the same questions about licenses and vulnerabilities and banned packages. Building two tools against the same policy format was the goal, but that required finding one worth using.

I found about forty tools that make automated policy decisions about dependencies, and every single one has its own format.

License policy

cargo-deny uses deny.toml with [licenses] tables containing allow and deny arrays of SPDX identifiers. LicenseFinder stores decisions in doc/dependency_decisions.yml generated by CLI commands, with permitted_licenses and restricted_licenses as top-level keys. GitHub’s dependency review action takes allow-licenses or deny-licenses (mutually exclusive) as workflow parameters, or in a separate dependency-review-config.yml. Python’s liccheck reads [tool.liccheck] from pyproject.toml with authorized_licenses and unauthorized_licenses lists. The PHP Composer license-check plugin looks at extra.metasyntactical/composer-plugin-license-check in composer.json with allow-list and deny-list. GitLab and FOSSA both configure license approval policies through their web UIs with no file-based representation.

That’s the concept “don’t use GPL-3.0 in this project” expressed seven different ways across seven tools, and the vocabulary alone splits in every direction: allowed vs permitted vs authorized vs whitelisted on the accept side, denied vs banned vs restricted vs rejected vs unauthorized on the block side. Some require SPDX identifiers, some accept freeform strings, some use internal IDs.

Vulnerability policy

Snyk uses a .snyk file (YAML, v1.25.0 schema) where you ignore vulnerability IDs with a reason, expiry date, and dependency path. Trivy has trivy.yaml for configuration plus .trivyignore or .trivyignore.yaml for suppression, and also supports Rego-based custom policies. Grype uses .grype.yaml with ignore rules matching by vulnerability ID, fix-state, package name, version, type, or location. OSV-Scanner uses osv-scanner.toml with [[IgnoredVulns]] entries containing an ID, reason, and expiry. OWASP Dependency-Check uses suppression.xml with its own XSD schema, matching by Package URL regex, CPE, SHA1 hash, or vulnerability name, with optional expiration dates. Safety uses .safety-policy.yml with a different YAML structure again.

Ignoring a single CVE means writing a different file in a different format for whichever scanner your CI runs, and if you use more than one you maintain parallel ignore lists that express the same decisions in incompatible ways.

Package bans

Maven Enforcer puts banned dependencies in XML inside pom.xml as a <plugin> configuration, matching by groupId:artifactId:version patterns. cargo-deny has a [bans] section in deny.toml. GitHub’s dependency review action accepts deny-packages as a workflow parameter. npm, Yarn, and pnpm each have their own override mechanism (overrides, resolutions, pnpm.overrides) for forcing transitive dependency versions, all in package.json but with different field names and incompatible syntax. Composer uses a conflict field in composer.json. NuGet uses package source mapping in nuget.config (XML) to restrict which packages can come from which sources.

The full inventory

Across all the tools I surveyed:

  • cargo-deny: deny.toml (TOML)
  • bundler-audit: .bundler-audit.yml (YAML)
  • LicenseFinder: doc/dependency_decisions.yml (YAML)
  • npm audit: .npmrc settings
  • Socket: socket.yml (YAML)
  • Safety: .safety-policy.yml (YAML)
  • liccheck: pyproject.toml [tool.liccheck] or .ini
  • Maven Enforcer: pom.xml XML plugin config
  • Gradle License Report: allowed-licenses.json (JSON)
  • OWASP Dependency-Check: suppression.xml (XML with XSD)
  • NuGet Package Source Mapping: nuget.config (XML)
  • Composer license-check: composer.json extra field
  • GitHub Dependabot: .github/dependabot.yml (YAML)
  • GitHub Dependency Review: workflow params or dependency-review-config.yml (YAML)
  • Renovate: renovate.json / .renovaterc (JSON/JSON5)
  • Snyk: .snyk (YAML)
  • FOSSA: .fossa.yml (YAML) + web UI
  • Mend: .whitesource (JSON) + web UI
  • Black Duck: web UI only
  • Veracode SCA: web UI only
  • Checkmarx SCA: CLI threshold syntax (sca-high=10;sca-medium=20)
  • Sonatype Nexus Lifecycle: server-side only
  • Trivy: trivy.yaml + .trivyignore / .trivyignore.yaml (YAML)
  • Grype: .grype.yaml (YAML)
  • OSV-Scanner: osv-scanner.toml (TOML)
  • JFrog Xray: REST API / web UI (JSON)
  • ORT: .ort.yml (YAML) + evaluator.rules.kts (Kotlin script) + curations.yml
  • Dependency-Track: web UI
  • ScanCode: YAML policy file via CLI flag
  • FOSSology: web UI
  • GitHub Licensed: .licensed.yml (YAML)
  • Sigstore policy-controller: Kubernetes CRDs (YAML)
  • Kyverno: Kubernetes CRDs (YAML)
  • ratify: Kubernetes CRDs (YAML)

That spans TOML, YAML in at least ten different schemas, JSON, JSON5, XML with custom XSDs, Kotlin script, Rego, INI, semicolon-delimited CLI strings, workflow parameters, package.json sub-keys, pom.xml plugin sections, composer.json extra fields, nuget.config XML sections, Kubernetes CRDs, and proprietary web UIs with no file-based representation at all.

OPA

The one tool that could theoretically unify this is Open Policy Agent with Conftest, since OPA’s Rego language is a general-purpose policy engine that evaluates any structured data and you could write Rego rules against SBOMs, lockfiles, or dependency manifests. But OPA is the assembly language of policy rather than a standard for dependency decisions. It gives you a way to write rules but says nothing about what those rules should look like for package management, and there’s no shared Rego library for common operations like “deny this license” or “ignore this CVE until this date.” Everyone who uses OPA for dependency policy writes their own rules from scratch, and those rules aren’t portable between organizations any more than the tool-specific configs are.

Rego is at least a constrained language designed for policy evaluation, but ORT’s evaluator rules are full Kotlin scripts, which means your policy files are arbitrary code that can make HTTP requests, read the filesystem, or behave differently depending on what day of the week it is, and now you need to audit your policy files with the same scrutiny you’d give any other code you run in CI.

Existing standards

The supply chain standards community has been productive on the description side: PURL for package identity across ecosystems, CycloneDX and SPDX for component inventories, OSV for vulnerability records, OpenVEX for exploitability assessments, SLSA and in-toto for provenance attestations. You can generate a CycloneDX SBOM that lists every component with its license and known vulnerabilities, but there’s no standard way to write “if a component has this license, block it” or “if a vulnerability has this severity, ignore it until this date” or “if this specific package appears, reject the build.”

The Cyber Resilience Act makes this gap more consequential. The CRA requires organizations shipping software in the EU to document their components through SBOMs, handle vulnerabilities through coordinated disclosure, and demonstrate conformity through either self-assessment or third-party audit, with reporting obligations to ENISA taking effect September 2026 and full compliance by December 2027. I’ve written about the CRA before, partly in jest, but the compliance pressure is real.

BSI TR-03183 specifies that SBOMs should use CycloneDX or SPDX in JSON or XML, and explicitly separates vulnerability information from the SBOM itself, pointing to VEX and CSAF for vulnerability status communication. CEN/CENELEC is still developing the European standard that will nail down exact requirements. So the standards pipeline for CRA compliance is: SBOMs in CycloneDX or SPDX to describe what you ship, VEX or CSAF documents to communicate vulnerability status, and then nothing standardized for the policy rules that determine which licenses your organization accepts, which vulnerabilities have been assessed and approved, or which packages are banned.

Organizations need those policies to be machine-readable so CI can enforce them, and right now the only way to do that is to configure each tool separately in each tool’s own format. An organization using Trivy for vulnerability scanning and GitHub’s dependency review action for license checking has its CVE ignores in .trivyignore.yaml and its license policy in dependency-review-config.yml, and if they add cargo-deny for Rust projects that’s a third file with a third format, and if they also run Snyk that’s a .snyk file too, four tools expressing overlapping concerns with no way to keep them in sync.

What a standard might cover

I don’t have a spec, but the shape of one seems clear from looking at what every tool independently reinvented. A dependency policy needs to express license rules (allow or deny by SPDX identifier, with per-package exceptions), vulnerability rules (ignore specific advisories by CVE/GHSA/OSV ID, with a reason, an expiry date, and optionally a scope for which packages or paths the ignore applies to), package rules (ban or allow specific packages by name, optionally scoped to version ranges), severity thresholds (fail above a certain vulnerability severity), and scoping (apply different rules to different parts of the project, or different rules for direct vs transitive dependencies). The data model could reference existing standards throughout, using SPDX identifiers for licenses, PURLs for packages, OSV IDs for vulnerabilities, and CVSS for severity, since the building blocks already exist and the rules connecting them are what’s missing.

Relationship to SBOMs

We already have SBOMs that describe components and advisory databases that describe what’s wrong with those components. A policy format would close the loop by taking an SBOM and a set of advisories as input and producing accept/reject decisions as output. The closest existing analogy is how OpenVEX relates to vulnerability databases: OSV tells you a vulnerability exists, OpenVEX lets you say “yes, but it doesn’t affect us,” and a dependency policy format would sit at a similar level where the SBOM tells you what you have and the policy tells you what you’ll accept.

If this existed, tools like git-pkgs, Forgejo, GitHub’s dependency review, Trivy, Grype, and cargo-deny could all read the same policy file, and using multiple scanners wouldn’t mean maintaining parallel ignore lists. I’m going to need a policy format for git-pkgs regardless, and I’d rather design it as something other tools could adopt than add yet another entry to the list above. If you work on any of those tools and have opinions about what this should look like, I’d like to hear them.