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.
Categories
Section titled “Categories”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.0lands inweak-copyleft, notcopyleft, 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, andCC-BY-NC*/CC-BY-ND*are source-available.
Configuration
Section titled “Configuration”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.
Where the policy lives
Section titled “Where the policy lives”A policy can be set in two places, and the resolution order is:
- The repository’s
.hibou.yamllicenses:section, if the matching gate has one. - Otherwise, the organization’s policy.
- 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.
SPDX expressions
Section titled “SPDX expressions”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-onlypasses a copyleft-denying policy. The component is dual-licensed: the consumer picks, the consumer picks MIT, and the copyleft terms never apply.ORresolves to the least restrictive alternative.MPL-2.0 AND MITis judged onMPL-2.0. Both sets of obligations apply at once, soANDresolves to the most restrictive operand. A policy that deniesweak-copyleftfails this component; one that only deniescopyleftand 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:
allowmatches operands, not expressions.allow: ["MIT"]does not rescueGPL-3.0-only AND MIT— every operand of anANDhas 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’smusl-utilsdeclaresMIT,BSD-2-ClauseandGPL-2.0-or-lateras three entries, so it reads as copyleft. Debian’sutil-linuxdeclares fifteen, two of which (public-domain,BSLA) this catalog does not recognize — so it reads as unknown, not copyleft, and is denied bydeny_unknownrather than by a category. - A free-form string stays one identifier.
SEE LICENSE IN LICENSE.txtis not tokenized into four words; it is a single unrecognized id, which you canallowverbatim once you have read the file.
Limitations
Section titled “Limitations”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.