Skip to content
Hibouhibou

License Gating

Hibou reads the license of every component in a snapshot’s SBOM and can fail a pull request that introduces a dependency your policy does not accept. The policy lives in the repository’s .hibou.yaml, next to the code, CWE and coverage gates.

version: 1
quality_gates:
- branches: ["main"]
enforcement: blocking
licenses:
deny_categories: ["copyleft", "network-copyleft", "source-available"]

The organization’s Licenses tab (/orgs/{provider}/{slug}/licenses) shows what you are actually under today, per category and per repository. It reports exposure, not enforcement — a component listed under copyleft there says nothing about whether any repository denies that category.

Enumerating every GPL variant in a deny-list is unmaintainable, so policies are written against seven categories. They are ordered from least to most restrictive, and that order is what “most restrictive” means everywhere below.

Category Meaning Examples
public-domain No conditions at all. CC0-1.0, Unlicense, WTFPL
permissive Attribution only; no obligation on your own source. MIT, Apache-2.0, BSD-3-Clause, ISC
weak-copyleft Reciprocal for changes to the component, but linking does not infect your code. MPL-2.0, LGPL-3.0-only, EPL-2.0, CDDL-1.1
copyleft Distributing a work that includes it obliges you to offer your source under the same terms. GPL-3.0-only, GFDL-1.3-only, CC-BY-SA-4.0
network-copyleft Same, and triggered by offering the software over a network — not just by shipping it. AGPL-3.0-only, SSPL-1.0
source-available Source you can read, with use restrictions. Not open source. BUSL-1.1, Elastic-2.0, CC-BY-NC-4.0, Commons-Clause
unknown No license metadata, or an identifier Hibou’s catalog does not recognize. Proprietary, SEE LICENSE IN LICENSE.txt, no value at all

Two refinements are worth knowing because they surprise people:

  • A GPL with a linking exception is treated as weak copyleft. GPL-2.0-only WITH Classpath-exception-2.0 lands in weak-copyleft, not copyleft, because that is how it behaves for a consumer who links it.
  • Creative Commons is split across three categories. CC-BY-* is permissive, CC-BY-SA-* is copyleft, and CC-BY-NC* / CC-BY-ND* are source-available.

Every key is optional and lives under a quality_gates entry’s licenses: block.

Key Type Default Effect
deny_categories list of category names empty Deny whole categories. The seven names above are the only accepted values; anything else is a config error.
deny list of SPDX ids or globs empty Deny specific ids on top of the categories, e.g. ["LGPL-2.1-*"] to carve one family out of a weak-copyleft you otherwise allow.
allow list of SPDX ids or globs empty Ids that pass even when a deny rule matches. Allow beats both deny and deny_categories.
deny_unknown boolean true Whether a component with no usable license fails the gate. Naming unknown in deny_categories has the same effect.
exempt_purls list of purls or globs empty Packages the policy does not apply to at all.
evaluate new | all new Whether the gate judges only what the change introduces, or the whole tree.

A policy that denies nothing — no categories, no ids, and deny_unknown: false — is skipped entirely rather than evaluated as a pass.

A failing license gate is advisory unless the gate entry sets enforcement: blocking. Advisory publishes an informational check that does not block the merge; use it while a new policy settles.

A policy can be set in two places, and the resolution order is:

  1. The repository’s .hibou.yaml licenses: section, if the matching gate has one.
  2. Otherwise, the organization’s policy.
  3. Otherwise, no license gate runs.

An organization policy therefore applies to a repository whose .hibou.yaml says nothing about licenses — that is the point of setting one. Set it under Organization → Settings → License policy.

A repository section replaces the organization’s policy rather than merging with it. Merging two partial policies would produce a third that neither side wrote, so “which policy applies here?” always has a one-word answer.

A license field is not a bag of words; it is an expression with precedence. AND binds tighter than OR, and the two mean opposite things for a policy:

  • MIT OR GPL-3.0-only passes a copyleft-denying policy. The component is dual-licensed: the consumer picks, the consumer picks MIT, and the copyleft terms never apply. OR resolves to the least restrictive alternative.
  • MPL-2.0 AND MIT is judged on MPL-2.0. Both sets of obligations apply at once, so AND resolves to the most restrictive operand. A policy that denies weak-copyleft fails this component; one that only denies copyleft and above lets it through.

Against a policy denying copyleft, network-copyleft and source-available:

Expression Effective category Result
MIT OR GPL-3.0-only permissive pass
MPL-2.0 AND MIT weak-copyleft pass
GPL-3.0-only AND MIT copyleft fail on GPL-3.0-only
AGPL-3.0-only network-copyleft fail
(MIT OR Apache-2.0) AND Unicode-DFS-2016 permissive pass
SEE LICENSE IN LICENSE.txt unknown fail while deny_unknown is true

Three consequences:

  • allow matches operands, not expressions. allow: ["MIT"] does not rescue GPL-3.0-only AND MIT — every operand of an AND has to clear the policy, and the GPL operand does not.
  • Separate list entries are conjunctive. When an SBOM gives a component several license entries instead of one expression, every entry’s obligations apply, so the component takes the most restrictive entry — there is no implied OR. Distribution packages do this constantly: Alpine’s musl-utils declares MIT, BSD-2-Clause and GPL-2.0-or-later as three entries, so it reads as copyleft. Debian’s util-linux declares fifteen, two of which (public-domain, BSLA) this catalog does not recognize — so it reads as unknown, not copyleft, and is denied by deny_unknown rather than by a category.
  • A free-form string stays one identifier. SEE LICENSE IN LICENSE.txt is not tokenized into four words; it is a single unrecognized id, which you can allow verbatim once you have read the file.

License gating is only as good as the SBOM it reads. These are properties of dependency scanners in general, not settings you can turn up.

Dev-only dependencies are typically omitted, so a clean result is not proof of a clean tree. Scanners default to production dependencies. Scanning a package-lock.json that declares lodash alongside typescript, vitest and several @types/* packages yields exactly one component: lodash. Build tooling, test frameworks and code generators — where copyleft and source-available licenses are most common — never reach the gate. If those matter to your policy, generate the SBOM with dev dependencies included.

A missing lockfile yields zero components, silently. A Rust crate scanned before cargo build has written Cargo.lock produces an SBOM with no components at all. The gate then passes because there is nothing to judge — not because the dependencies are acceptable. Generate SBOMs after the build resolves dependencies, and watch the component count per repository on the Licenses tab: an ecosystem showing zero is a pipeline bug, not a clean bill of health.

License data quality varies by ecosystem. npm and Maven Central carry a declared license field and are mostly reliable. Go modules often carry nothing at all — a scan of a two-dependency go.mod can still yield a transitive module with no license, and a project’s own module or reactor POM usually has none either. Linux distributions go the other way and declare every license found in the package, producing entries that categorize far more restrictively than the code you actually use. Jars and binaries discovered inside a container image generally carry no license metadata whatsoever.

The practical consequence: treat the first unknown count you see as a measurement of your SBOM pipeline, not of your legal exposure, and fix the generator before tightening deny_unknown.