License policy

One question per declared license: given how you ship this, does policy allow it?

  1. Everything is normalized to SPDX first
    1. What is deliberately not resolved
    2. Where the mappings come from
    3. Extending the tables
  2. What this does not answer
  3. Why the use case decides
  4. Where the rules live
  5. SPDX expressions
  6. When ospac is missing
  7. What is not here yet

Everything is normalized to SPDX first

Policy is keyed on SPDX identifiers, so a declaration has to reach SPDX or be reported as not reaching it. An SBOM states a license in whichever slot its generator reached for, and ecosystems invented their own operators along the way. All of this normalizes:

Declared Normalized How
mit, apache-2.0 MIT, Apache-2.0 SPDX parse
GPL-2.0+ GPL-2.0-or-later SPDX parse
MIT or Apache-2.0 MIT OR Apache-2.0 SPDX parse
Apache-2.0 with LLVM-exception Apache-2.0 WITH LLVM-exception SPDX parse
MIT \|\| Apache-2.0 MIT OR Apache-2.0 npm’s documented OR
MIT/Apache-2.0, MIT, Apache-2.0 MIT AND Apache-2.0 separator, read conservatively
Apache 2, Apache2 Apache-2.0 curated alias

A bare list does not say whether both licenses apply or either does. Policy takes the least restrictive operand of an OR and the most restrictive of an AND, so reading a list as OR when it meant AND under-reports obligations and can pass something that should have been denied. Lists are therefore read as AND, which over-reports and surfaces for review instead.

What is deliberately not resolved

BSD, GPL, LGPL, Apache, Public Domain, BSD-like, see LICENSE file.

Family names do not name a license. BSD is 2-clause or 3-clause and the choice changes obligations; GPL states neither version nor only/or-later. Resolving them would produce a confident answer the document does not support.

GPL is a special case worth knowing about: the underlying parser will resolve it, to GPL-1.0-or-later, because that is what the deprecated bare key meant. Nobody writing GPL in an SBOM today means version 1.0, so it is refused explicitly.

Unresolved text still reaches policy verbatim, because a policy may list the exact string and “unknown” is a reviewable answer. It is reported as unresolved either way.

Where the mappings come from

Three layers, lowest to highest, so the more specific always wins:

Layer Supplies Refreshed by
ospac, when installed ~712 official SPDX long names ospac’s own regeneration from SPDX releases
ossbomer built-ins folk spellings SPDX never publishes (apache2, expat, new bsd) a release here
Your overlays anything you need you

ospac is the source of truth for license metadata across these tools, so anything derived from SPDX is read from it rather than re-curated here. It is read through ospac.license_aliases() if that exists, otherwise from its shipped license records. Neither is required: ospac is the [oslc] extra while normalization is used by every profile, so the built-in tables stand alone and ospac is strictly additive.

Extending the tables

License spellings drift, and you know your own suppliers’ habits better than any shipped table does. Point OSSBOMER_LICENSE_ALIASES at one or more files (os.pathsep-separated):

# acme-licenses.yaml
aliases:
  "acme proprietary v2": LicenseRef-ACME-2.0
  "BSD-like": BSD-3-Clause        # you accept the risk on this one
never_resolve:
  - "internal"                     # never let this look like a license
separators:
  " plus ": " AND "
OSSBOMER_LICENSE_ALIASES=./acme-licenses.yaml \
  ossbomer validate --profile cisa-2026-min --file sbom.json

Overlays are applied after the built-ins and win on conflict, so a shipped mapping you disagree with can be overridden in either direction. Packages can also register an ossbomer.license_aliases entry point, the same way validators extend.

What this does not answer

The question is “does policy allow the license this SBOM declares”. It is not “is the declared license correct”.

That boundary is deliberate. Deciding whether a component is really MIT means reading the component’s source or binary, and this tool reads one document. An SBOM that declares MIT for a GPL-3.0 library will pass a license-* profile, because the policy engine was asked about MIT and MIT is what it was told.

So a PASS here means the declared licenses are acceptable for your distribution model. It does not mean the declarations are true. If the SBOM came from a scanner you do not control, the declarations carry that scanner’s confidence, not this tool’s.

Verifying declarations against the actual code is a different job, done by license detection tools rather than by a document validator. ossbomer does not attempt it and does not pretend to.

Why the use case decides

The same license is not the same answer everywhere, so a tool that answers “is GPL-3.0 allowed?” without knowing how you ship is guessing:

ossbomer validate --profile license-internal --file sbom.json   # GPL: fine
ossbomer validate --profile license-mobile   --file sbom.json   # GPL: denied
ossbomer validate --profile license-saas     --file sbom.json   # GPL and AGPL: denied

Internal use is not distribution, so most copyleft obligations never trigger. Running a network service does trigger AGPL source disclosure, which shipping a binary does not. App store terms conflict with GPL-3.0 in ways that a server deployment never encounters.

Where the rules live

The rules are in ospac, not in ossbomer. That separation is deliberate: you change the answer by pointing at a different policy directory, not by patching the tool or waiting on a release.

license_policy:
  engine: ospac            # omit to use only the inline rules below
  use_case: mobile         # passed to ospac as `distribution_type`
  policy_path: ./policies  # optional: your own ospac policy dir, relative to this file
  context:                 # optional: any other keys your policy matches on
    linking_type: dynamic_linking
  rules:                   # optional: overrides for specific identifiers
    - spdx_id: LGPL-2.1-only
      allowed: true
      reason: "Reviewed 2026-03; dynamically linked only."

The engine runs first, then inline rules override it. So you can allow something your policy denies, or deny something it allows, without forking the policy. Give a reason when you do; it shows up in the report and saves the next person from re-deriving your decision.

Inline rules work on their own. Drop engine and you have a small policy with no optional dependency at all.

An override needs a spdx_id, and it must be one SPDX identifier rather than an expression. Overrides are matched by exact identifier, so an expression would match nothing; both an expression key and a missing spdx_id are rejected when the profile loads rather than sitting there doing nothing. To decide on expressions, let the engine evaluate them, see below.

SPDX expressions

Declared licenses are frequently expressions, not single identifiers, and getting the operators wrong produces wrong compliance answers:

Declared Result Why
MIT OR GPL-3.0-only allowed You pick the operand, so the least restrictive one governs
MIT AND GPL-3.0-only denied for distribution Every operand applies, so the most restrictive governs
Apache-2.0 WITH LLVM-exception allowed Classified on the base license

When ospac is missing

A profile declaring engine: ospac without ospac installed exits 2 instead of skipping the layer:

pip install ".[oslc]"

This is intentional. A license check that silently does nothing and still reports PASS is worse than no check at all, because someone will ship on the strength of it.

What is not here yet

Package and PURL level risk is a separate concern and is still pending. The old 136 MB bundled advisory dataset and PackageRiskAnalyzer were removed. Risk will come back through the open PURL API as an opt-in network feature, so the default stays offline.