Dependency licences
Every analysis records which dependencies a project uses and under which licences. You see that inventory on a project’s Dependencies tab, in every edition.
With the Business or Enterprise plan, an organisation admin also sets a licence policy, for example “no strong or network copyleft”. A dependency that breaks the policy then becomes an ordinary Qualor issue: it shows on the merge request, gets the new-code treatment, can be accepted with won’t fix, and fails the quality gate.
| Community | Business | Enterprise | |
|---|---|---|---|
| The dependency inventory and the Dependencies tab | yes | yes | yes |
The share of packages with an unknown licence (unknown_licenses_ratio) |
yes | yes | yes |
A licence policy, its issues and the gate metrics forbidden_licenses and licensing_issues |
no | yes, feature license-policy |
yes, feature license-policy |
What the scanner collects
When Trivy runs (it does in the qualor/scanner image, see
Languages and analyzers), the scanner runs a
second, licence-only Trivy pass after the analyzers. It reads the licences that the packages
themselves declare, in the lockfile or in the installed packages. It runs offline and needs no
vulnerability database.
For each package it records the name, the version, the ecosystem (npm, pnpm, yarn, gomod,
nuget, composer and so on), the lockfile and line, the licences, and whether it is a direct or
a transitive dependency when the lockfile says so.
- Development dependencies are left out, as in vulnerability scanning: they are usually not shipped with your product.
- Your project’s own package is not listed.
- A lockfile the scan does not analyse is left out: one in a gitignored or excluded folder, such as a Go module cache kept inside the repository.
- Licences are written as SPDX identifiers. Common spellings are
translated:
GPLv3becomesGPL-3.0-only,GPL-2.0+becomesGPL-2.0-or-later,Apache 2.0becomesApache-2.0, and a bareBSDbecomesBSD-3-Clause. Text that names no licence, such asSEE LICENSE IN LICENSE.txt, is kept as written. - At most 50 000 packages are recorded per analysis, the direct ones first. Above that, the
analysis shows the warning
DEPENDENCIES_TRUNCATED.
The report carries package names, versions and licences, never the text of your files.
Which ecosystems give licences
Some lockfiles carry the licences of their packages. For the others, Trivy reads them from the
installed packages, so install your dependencies in the scan job, before qualor scan.
| Ecosystem | From the lockfile alone | With the dependencies installed in the job |
|---|---|---|
npm (package-lock.json) |
yes | yes |
Composer (composer.lock) |
yes | yes |
pnpm (pnpm-lock.yaml) |
no | yes, from node_modules |
Yarn, classic and Berry (yarn.lock) |
no | yes, from node_modules (for Berry, nodeLinker: node-modules: Plug’n’Play installs no node_modules) |
Go (go.mod) |
no | yes, from the Go module cache (go mod download) |
NuGet (packages.lock.json) |
no | yes, from the NuGet package folder (~/.nuget/packages, or NUGET_PACKAGES) |
Maven (pom.xml) |
no | yes, for the packages whose POM file the build downloaded into the local Maven repository (~/.m2/repository) |
Gradle (gradle.lockfile) |
no | partly, even with the Gradle cache (~/.gradle, or GRADLE_USER_HOME): Trivy reads only a package’s own POM, so a licence inherited from a parent POM is missed |
pip (requirements.txt) |
no | no, even with a virtual environment installed |
Cargo (Cargo.lock) |
no | no |
Bundler (Gemfile.lock) |
no | no |
A package without a licence is listed with the licence Unknown. The Dependencies tab says how many packages have a known licence.
The scanner finds the Go module cache through GOPATH or GOMODCACHE, or in ~/go when neither
is set. That is where go mod download puts it. It finds the local Maven repository in ~/.m2,
under the scan job’s HOME. If your job keeps that repository inside the project (for example
-Dmaven.repo.local=.m2/repository, to cache it), set HOME to the folder that holds .m2 for
the scan, or keep the repository outside the project. A C# build in the qualor/scanner-dotnet
image restores its packages into ~/.nuget/packages, so qualor dotnet end reads them with no
extra step.
Install your dependencies before the scan
GitLab CI, in the qualor/scanner image (keep the lines for the package managers your repository
uses):
qualor:
stage: test
image: { name: qualor/scanner:<version>, entrypoint: [''] }
variables: { GIT_DEPTH: 0 }
script:
- npm ci # npm
- corepack pnpm install --frozen-lockfile # pnpm
- corepack yarn install --immutable # Yarn Berry (Yarn 1: yarn install --frozen-lockfile)
- go mod download # Go, in each module directory
- qualor scan
The image has npm, Yarn 1 and corepack, which fetches the pnpm or Yarn version your
package.json names (packageManager).
GitHub Actions, in the workflow of GitHub:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- run: corepack pnpm install --frozen-lockfile # or npm ci, go mod download …
- run: qualor scan
env:
QUALOR_URL: ${{ vars.QUALOR_URL }}
QUALOR_TOKEN: ${{ secrets.QUALOR_TOKEN }}
Installing the dependencies is what the JavaScript, TypeScript and Go analyzers need anyway, so one install step serves both.
Settings
analyzers:
trivy:
licenses: auto # auto (default), true or false
licenseTimeoutSeconds: 300
autoruns the licence pass whenever Trivy runs.truedoes the same, and also warns (LICENSE_SCAN_FAILED) when Trivy is not installed.falseskips the pass: the analysis then records no inventory.analyzers.trivy.enabled: falseturns off both Trivy passes.- The pass reads every installed package, so a large
node_modulestakes time: about 100 seconds for 800 MB in our measurement.licenseTimeoutSeconds(default 300) bounds it on its own. - A pass that fails or times out never fails the scan. The vulnerability findings are kept, the analysis shows the warning, and the Dependencies tab keeps the previous inventory.
The analysis lists the pass as the engine trivy-licenses, with its status.
The Dependencies tab
Project → Dependencies lists the packages of a branch or a merge request (choose it in the Branch picker): package, version, ecosystem, lockfile (a link to that line in the Code tab), licences, and Direct or Transitive. The header says how many of the packages have a known licence.
Filter by Licence, Licence group or ecosystem, tick Unknown only or Direct only, or search by package name. Until a scan of the branch has recorded an inventory, the tab says so.
With a licence policy (Business or Enterprise), the tab adds a Policy column, Allowed, Forbidden or Unknown, and the Forbidden only filter. A forbidden package links to its issues. The column always shows the current policy, so it reflects a change at once.
The licence policy (Business and Enterprise)
An organisation admin sets the policy in Settings → Licence policy. It applies to every project
of the organisation, and nobody can weaken it from a qualor.yml in their own merge request. There
is no policy until an admin saves one.
- Mode. Forbid listed licences (a deny list) forbids the licences of the forbidden groups and the Forbidden licences list. Allow only listed licences (an allow list) forbids every licence that is not in Allowed licences.
- Forbidden groups. Qualor’s licence groups, below. Expand a group to see its licences.
- Forbidden licences and Allowed licences. SPDX identifiers, such as
GPL-3.0-only, or terms such asGPL-2.0-only WITH Classpath-exception-2.0. In deny-list mode, an allowed licence is allowed even when a forbidden group lists it: forbid Weak copyleft and allowLGPL-2.1-only, for example. In allow-list mode only Allowed licences counts: the groups and the forbidden list do not apply. Each list holds at most 500 entries. - Warn about dependencies with unknown licences. Each package whose licence the scan could not read becomes an issue of severity Info. It is off by default.
- Remove policy. The next analysis of each project closes its licence issues.
A policy change applies from each project’s next analysis, like a quality profile change.
The licence groups
The groups are a starting point, not legal advice. Qualor reviews them by hand and updates them with its releases. Permissive licences, such as MIT, Apache-2.0 or BSD-3-Clause, belong to no group.
| Group | What its licences have in common | Licences |
|---|---|---|
Network copyleft (network-copyleft) |
the duty to share the source also applies when the software is only offered over a network | AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0-only, AGPL-3.0-or-later, SSPL-1.0, OSL-1.0, OSL-1.1, OSL-2.0, OSL-2.1, OSL-3.0, EUPL-1.1, EUPL-1.2, CPAL-1.0, RPL-1.1, RPL-1.5 |
Strong copyleft (strong-copyleft) |
software combined with the code must be released under the same terms | GPL-1.0-only, GPL-1.0-or-later, GPL-2.0-only, GPL-2.0-or-later, GPL-3.0-only, GPL-3.0-or-later, Sleepycat, QPL-1.0 |
Weak copyleft (weak-copyleft) |
changes to the licensed files or library must be shared, but not the programs that use them | LGPL-2.0-only, LGPL-2.0-or-later, LGPL-2.1-only, LGPL-2.1-or-later, LGPL-3.0-only, LGPL-3.0-or-later, MPL-1.0, MPL-1.1, MPL-2.0, MPL-2.0-no-copyleft-exception, EPL-1.0, EPL-2.0, CDDL-1.0, CDDL-1.1, CPL-1.0, MS-RL, GPL-2.0-only WITH Classpath-exception-2.0, GPL-2.0-or-later WITH Classpath-exception-2.0 |
Non-commercial or source-available (non-commercial-or-source-available) |
they restrict commercial use or competing services, so they are not open source in the usual sense | CC-BY-NC-1.0, CC-BY-NC-2.0, CC-BY-NC-2.5, CC-BY-NC-3.0, CC-BY-NC-4.0, CC-BY-NC-ND-1.0, CC-BY-NC-ND-2.0, CC-BY-NC-ND-2.5, CC-BY-NC-ND-3.0, CC-BY-NC-ND-3.0-DE, CC-BY-NC-ND-3.0-IGO, CC-BY-NC-ND-4.0, CC-BY-NC-SA-1.0, CC-BY-NC-SA-2.0, CC-BY-NC-SA-2.0-DE, CC-BY-NC-SA-2.0-FR, CC-BY-NC-SA-2.0-UK, CC-BY-NC-SA-2.5, CC-BY-NC-SA-3.0, CC-BY-NC-SA-3.0-DE, CC-BY-NC-SA-3.0-IGO, CC-BY-NC-SA-4.0, BUSL-1.1, Elastic-2.0, Commons-Clause, PolyForm-Noncommercial-1.0.0, PolyForm-Small-Business-1.0.0 |
Commons-Clause is not an SPDX licence but a condition added to one, as in
Apache-2.0 WITH Commons-Clause: any licence with it belongs to the non-commercial group. The
Classpath exception moves the GPL into weak copyleft. The deprecated spellings count as their
current ones: GPL-2.0 is GPL-2.0-only, and GPL-2.0+ is GPL-2.0-or-later.
Qualor checks policy entries against the SPDX licence list it ships (version 3.29.0 of 2026-09-16), updated with each release.
How a package’s licences are judged
A package is allowed, forbidden or unknown:
- Several licences on one package all apply: it is forbidden when any of them is.
A OR B(a choice of licences) is forbidden only when every alternative is. SoMIT OR GPL-2.0-or-laterpasses a policy that forbids strong copyleft.A AND Bis forbidden when either is forbidden.A WITH exceptionis judged as the whole term first, then asA. SoGPL-2.0-only WITH Classpath-exception-2.0is weak copyleft, not strong copyleft.- No licence at all is unknown.
- Text that names no licence (
SEE LICENSE IN LICENSE.txt,Public Domain) is unknown in deny-list mode, and forbidden in allow-list mode, since it is not on the allowed list.
Licence issues
With the feature active, each analysis judges the inventory against the policy and reports:
| Rule | Severity | When |
|---|---|---|
license:forbidden |
high | the package’s licence is forbidden, for example “gpl-money 2.1.0 is licensed GPL-3.0-only, which the organisation’s licence policy forbids (Strong copyleft).” |
license:unknown |
info | the package has no licence Qualor could read, only with Warn about dependencies with unknown licences |
Both rules have the engine license and the quality Licensing, which has no rating: a
forbidden licence never lowers the security rating and never counts as a security issue. The
policy decides which packages are reported, so quality profiles do not apply to these rules.
- The issue sits on the package’s line in the lockfile. It is new code when that line is: the
change added the package or changed its version. A forbidden package that was already there
before the policy does not fail merge requests, but it is counted on the branch. When the
scanner could not find the package’s line in the lockfile, the issue is never new code, so
new_*gate conditions do not catch it on a merge request; branch-level conditions still do. - A package at a new version is a new issue.
- Licence issues appear in the merge request decoration like any other issue, but not in the
GitLab report artifacts the scanner writes (
--gitlab-code-qualityand the others), since the server makes them. - Compliance reports and portfolio reports leave them out.
Making an exception. To accept one package, set its issue to won’t fix, with a comment that says why (for example, the legal team’s approval). It stays won’t fix across analyses, until the package changes version. To accept a licence everywhere, add it to Allowed licences.
With warnUnknown on, install your dependencies before the scan. A project scanned without
them, for example a pnpm project without node_modules, gets an Info issue for every package.
Gate metrics
| Metric | Meaning |
|---|---|
licensing_issues, new_licensing_issues |
open licence issues (license:forbidden and license:unknown) |
forbidden_licenses, new_forbidden_licenses |
open license:forbidden issues |
unknown_licenses_ratio |
the share of the inventory, in percent, whose licence is unknown (overall only; no value without an inventory; every edition) |
Licence issues also count in issues, high_issues and info_issues. So the built-in
“Qualor way” gate, with new_issues > 0, already fails a merge request that adds a forbidden
licence. To check licences on their own, add new_forbidden_licenses gt 0 to your gate (see
Quality gates).
When the licence lapses
Without license-policy, the inventory and the Dependencies tab keep working. The policy is
kept but no longer applied: analyses neither add licence issues nor close the open ones, and
forbidden_licenses keeps counting the open ones. Settings → Licence policy shows a note that
the policy needs the Business plan. With a renewed key, the next analysis applies the policy again.
Who can change the policy
Organisation admins, through the permission org.licenses.manage; with a personal token, the token
needs the Admin scope. Every member of the organisation can read the policy. With audit-log,
each change is recorded as organization.license_policy_changed, with the policy before and after
(Roles and the audit log).
The API
# the inventory of the main branch (or ?branch=<name>, ?mr=<number>), forbidden packages only
q "$QUALOR_URL/api/v0/projects/<project id>/dependencies?verdict=forbidden&pageSize=100"
# the policy, the groups with their licences, and the SPDX ids a policy may name
q "$QUALOR_URL/api/v0/organizations/<org id>/license-policy"
# set the policy (an organisation admin; "policy": null removes it)
q -X PUT "$QUALOR_URL/api/v0/organizations/<org id>/license-policy" -d '{"policy":{
"mode":"denylist","forbiddenGroups":["network-copyleft","strong-copyleft","weak-copyleft"],
"forbidden":["CC-BY-SA-4.0"],"allowed":["LGPL-2.1-only","MPL-2.0"],"warnUnknown":false}}'
(q is the helper of Webhooks and REST API.)
GET /projects/{id}/dependenciesfilters bybranchormr,license,group,ecosystem,unknown=true|false,direct=true|false,q(the package name), andverdict(allowed,forbiddenorunknown; only with a policy). It pages withpageandpageSize(at most 500). The answer holdsrows,page,pageSize,total, asummaryof the whole inventory (total,known,unknownand the 20 most frequent licences),inventory(false until a scan recorded one) andverdicts(true when the rows carry the policy’sverdict).GET /organizations/{id}/license-policyreturnsavailable(false withoutlicense-policy),policy(ornull),groupsandspdxIds.PUT /organizations/{id}/license-policyanswers 403FEATURE_NOT_LICENSEDwithoutlicense-policy, and 400INVALID_LICENSE_IDfor an entry that is not an SPDX identifier orid WITH exceptionterm;details.invalidlists them. Entries are stored in their canonical spelling.{"policy":null}removes the policy, also one that can no longer be read. It answers 409LICENSE_POLICIES_UNREADABLEwhen the stored policies of all organisations (the instance settinglicensePolicy) cannot be read; nothing is changed until that row is fixed or deleted.
Warnings
| Warning | Meaning | What to do |
|---|---|---|
LICENSE_SCAN_FAILED |
the licence pass could not run, or failed; with licenses: true, also when Trivy is not installed |
read the scan log; the vulnerability findings are not affected |
LICENSE_SCAN_TIMEOUT |
the pass took longer than licenseTimeoutSeconds |
raise analyzers.trivy.licenseTimeoutSeconds, or exclude directories you do not ship |
DEPENDENCIES_TRUNCATED |
more than 50 000 packages; the direct ones are kept first. Licence issues are still opened, but none is closed while the inventory is cut | exclude lockfiles you do not ship (sources.exclude) |
DEPENDENCIES_OUT_OF_SCOPE |
lockfiles in folders the scan does not analyse (gitignored, such as an in-repository package cache) were left out of the inventory; lockfiles under sources.exclude or the built-in excludes (such as a .NET build’s bin/Debug/) are left out without it |
none needed; add the folder to sources.exclude to silence it and let the pass skip it |
DEPENDENCIES_SKIPPED |
packages whose name, version or lockfile path is too long for the report | none needed; they are left out of the inventory |
DEPENDENCY_LICENSES_SKIPPED |
licence strings longer than 256 characters, or beyond 16 for one package, were left out | check those packages’ licences by hand |
LICENSE_POLICY_INVALID |
the organisation’s stored licence policy could not be read, so the licences were not judged; open licence issues stay as they are | set the policy again in Settings → Licence policy, which shows it as missing, or remove it (PUT … {"policy":null}); if saving answers LICENSE_POLICIES_UNREADABLE, an operator must fix or delete the instance setting licensePolicy first |
DEPENDENCY_PATHS_INVALID |
the server could not place some flagged packages, because their lockfile path is not a repository path | only a report written by another tool can cause it; check that tool’s lockfile paths |