Skip to content

Licenses and SBOM

The Licenses tab of Maintaining answers two questions a security or legal reviewer asks about a codebase: which open source licenses does it ship under, and is any of them one the organization has not approved.

Every maintenance sweep already reads the repository’s lockfiles for vulnerable dependencies. It now records every package it finds, vulnerable or not: name, version, ecosystem, the license deps.dev knows for it, whether the lockfile declares it for runtime or for development, and whether the repository declares it directly or another package pulls it in. For every lockfile that records its dependency graph, the sweep also walks that graph, so a package another one pulls in says which of the repository’s own dependencies bring it in and, where the lockfile or the manifest beside it says which declarations are for development, whether it ships (reached from a runtime dependency) or is for development only. That is a pnpm lockfile in the version 9 format (pnpm 9 and 10), uv.lock, package-lock.json and npm-shrinkwrap.json (versions 2 and 3), bun.lock, yarn.lock (Yarn 1 and Yarn 2 and later, with the package.json files beside it), composer.lock (with composer.json), poetry.lock (with pyproject.toml), Cargo.lock, Gemfile.lock, NuGet’s packages.lock.json and go.mod. For a go.mod, the sweep reads each required module’s own go.mod from the module proxy, as go mod graph does, and honors the repository’s GOPROXY, so modules behind its own proxy are read too. Cargo.lock, Gemfile.lock, packages.lock.json and go.mod do not say development from runtime, so their packages show what brings them in and no scope. A pom.xml or a requirements file (requirements.txt and its variants) records no graph, so the sweep builds one: a requirements file written by pip-compile or uv pip compile says what pulls each package in, and is read as written; otherwise each package the file declares is looked up on deps.dev, and a package in that declaration’s dependency graph counts as brought in by it. In a pom.xml, a test dependency is for development, a compile or runtime one ships, and provided or system say neither. A requirements file named for development or testing, such as requirements-dev.txt or requirements-test.txt, is development only. gradle.lockfile, Pipfile.lock, pubspec.lock and mix.lock record no graph: a package read from one of them shows its parent as unknown. A package whose parent is unknown says why: its file records no graph, the lockfile could not be read, nothing the repository declares brings it in, or completing the graph needed a file no source served, such as a private Go module’s go.mod. In a uv.lock, development groups and extras named like one (dev, test, lint, docs and similar) count as development; any other extra counts as runtime, since it may be how the product is deployed. A workspace member itself is the repository’s own deliverable and counts as runtime.

The sweep also says which files it found and read nothing from, and why, at the top of the Licenses tab: a Gradle build without a dependency lock (its dependencies exist only once Gradle runs); a manifest with no lockfile beside it, such as a package.json, Gemfile, Pipfile, Cargo.toml, composer.json, pyproject.toml or .NET project file; a requirements file that pip-compile did not write, whose packages deps.dev resolved rather than the file pinning them; and, in a very large repository, the directories after the first 20,000, which the scan does not look at.

A lockfile lists every platform’s build of a native package: sharp’s Windows build sits beside its Linux one. The sweep reads which platforms the repository deploys to: a Dockerfile or Containerfile means Linux, on any architecture and with either C library (a build may target any architecture, and an image’s name does not reliably say its C library), so only another operating system rules a package out: Windows, macOS, or a wasm-only build. A package is a build for other platforms only when it installs on none of those platforms: its own os, cpu and libc fields (in a pnpm, npm or bun lockfile, or a Yarn 2 conditions entry) exclude them all, or every path to it passes through a package that does (a helper only sharp’s Windows build depends on is marked too), or every uv.lock or poetry.lock path to it carries an environment marker that holds on none of them. A package with such fields that another package pulls in as an optional dependency is that package’s platform build: lightningcss-linux-x64-gnu is lightningcss’s, and sharp’s libvips build, pulled in through sharp’s Linux build, is sharp’s. It stays in the inventory and the SBOM, marked as such, but is not counted by license and raises no license finding, since it never ships. A repository with no container definition counts every platform, so nothing is marked; so does one with a Windows base image, a base named by a build argument with no default, a FROM the sweep does not read (anything but FROM [--platform=...] image [AS name], continuation lines joined), a container definition or directory that is a symbolic link, or more (or longer) container definitions than the sweep reads.

The repository’s own code is not a third-party package. A uv workspace member or a local directory dependency is marked as the repository’s own: it is not counted by license, raises no license finding, and the SBOM lists it without a public package URL, because the license the data source knows for that name is a public package’s, not this repository’s. This is the repository’s package inventory, replaced by the next sweep whose dependency scan completes.

A repository’s inventory keeps at most 20,000 packages. Past that, the tab says the counts are a floor (“At least … packages”), and the downloaded SBOM marks itself incomplete, so neither reads as the whole repository.

The license comes from deps.dev, which reads it from the package registry. When deps.dev cannot name it, or names something that is not a standard license, Taiga reads what the package itself declares first, and then its files, in this order:

  • For an npm, Cargo, NuGet, RubyGems, Composer or Maven package, the license its own registry declares (npmjs.com, crates.io, nuget.org, rubygems.org, Packagist, or the POM on Maven Central, which inherits its parent POM’s licenses when it names none). deps.dev does not cover Composer packages at all, so for them this is usually where the license comes from. Several declared licenses are judged as described under License violations below.
  • For a PyPI package, its License-Expression (PEP 639) on PyPI, and otherwise its PyPI license classifiers, mapped to SPDX identifiers as PEP 639 lays out.
  • The package’s own files. Taiga downloads the published version from its public registry, checks it against the hash the registry publishes for it where the registry publishes one, and reads its license files and copyright statements with ScanCode. The license lookups skip a package the repository installs from a private registry, a mirror, git or a local path, so its name is not sent to any of these sources.

Some PyPI classifiers name a license family without saying which one: “BSD License” can be BSD 2-Clause, BSD 2-Clause Plus Patent or BSD 3-Clause. Such a classifier is settled, where possible, by the package’s own license files, or by the license of the package’s source repository when that is one the classifier can stand for. Otherwise it is kept as declared, and reads as allowed only when every license it can stand for is on the approved list, not allowed when none is, and unknown otherwise. Each package says where its license was read when it was not deps.dev.

Taiga keeps what each source said about each package version, and asks the sources in the background rather than during a sweep. A package a sweep finds for the first time shows deps.dev’s license until the sources have answered, usually within minutes, and then shows theirs without waiting for another sweep; a license finding the answer calls for is raised by the next sweep. Once a source has answered, its answer stays until a newer one replaces it, so a source being unreachable never changes what a package shows. A source that fails is asked again, first after five minutes and then at doubling intervals of up to six hours. After 12 failures in a row, about a day and a half, the lookup ends as if the source had said nothing, and the source is asked again a week later.

A license no source could name is recorded as Unknown. A license a package declares in its own words rather than as a standard license, such as its own terms or a pointer to a file, is recorded as non-standard, with the words it declares and, where it gives one, a link to read the terms. Neither is counted against the repository: “we do not know” is not “not allowed”.

A license outside the approved list raises a license violation finding: one for each repository whose packages carry it. Its packages are the finding’s evidence, a package’s platform builds counted under it at its version, so lightningcss and its six platform builds are one package of an MPL 2.0 finding, not seven findings. One decision covers the license in that repository: ignoring the finding, or approving the license. A license finding’s link opens the license’s page in its product (below), where that decision is made; a resolved or replaced one opens the finding itself. The approved list is the organization’s, under control TIC-6.2 of its Third-party and AI policy, “Open source licenses must be on the organization’s approved license list before adoption”.

Every organization starts from a default made for regulated enterprises, by classification: the permissive licenses that Google’s and the Apache Software Foundation’s licence policies both allow without conditions. That is MIT, Apache 2.0, the BSD family, ISC, Zlib, Boost, PostgreSQL, NCSA, UPL, Blue Oak, the Python family, Unicode, Unlicense, CC0, CC BY 4.0, CDLA Permissive 2.0 and Artistic 2.0. Being on the list is not compliance: each license’s own conditions still apply, such as keeping notices or Apache 2.0’s notice on a modified file. Weak and strong copyleft licenses, such as MPL 2.0, the GPL family or the SIL Open Font License, are not on it: a package under one raises a finding that the product decides on. An organization admin approves and removes licenses on the organization’s Licenses page (below), or through the API (PATCH /v1/tenants/current/license-allowlist with approve or remove), replace the whole list (PUT to the same address), or go back to the default (PUT with null), as SPDX license identifiers such as MIT or Apache-2.0. An identifier that is not on the SPDX license list is refused when saving.

A change takes effect at once. Every repository’s stored package inventory is judged against the new list: a license now outside it raises its repository’s finding (or brings back the one it had, keeping when and why it went), a finding whose license the list now approves is resolved as approved, not as fixed, and one whose license can no longer be judged (an ambiguous PyPI classifier with only some of its licenses approved) as undecided. GET /v1/tenants/current/license-allowlist/effect?approve= (or ?remove=) says what a change would do before it is made: how many findings it opens and closes, one per license in a repository. Each change is recorded as the next version of the list, with who made it and what it changed. A license that two repositories read differently shows the more cautious standing.

A license finding is not a security advisory and is not prioritized like one. Its severity says how the license class reads for hosted software: high for network copyleft licenses such as AGPL and SSPL, which the policy sends to review before GPL, and medium for every other license outside the list, LGPL included. Nothing in a license finding is exploitable, and no automatic fix is offered: the remedy is a review of the license or a different package, which is a decision, not a version bump.

A license read from PyPI, a registry or the package’s own files is judged the same way, and raises the same finding when it is outside the list.

License expressions are evaluated as written. A package offered under MIT OR GPL-3.0-only satisfies the list through MIT and raises nothing; one under Apache-2.0 AND LGPL-3.0-or-later needs both and raises a finding. A RubyGems, Composer or Maven package that lists several licenses offers a choice: a gemspec’s licenses, a composer.json license array and a POM’s <licenses> each mean “any one of these”. Such a package reads as one expression, Ruby OR BSD-2-Clause: it is allowed when any of its licenses is approved, and a license it does not need is not counted as in use against the list. When none is approved it raises one finding naming the choice, which approving any one of them closes. For other ecosystems, a package’s licenses all apply, so it is allowed only when every one is.

License findings were once raised one per package and license. The next time a repository is swept, or the list changes a license it carries, those are resolved as superseded by the repository’s finding of their license; when every one of them was ignored, the new finding keeps the ignore and its reason.

A license finding’s class is one of network copyleft (AGPL, SSPL), strong copyleft (GPL), weak copyleft (LGPL, MPL, EPL, CDDL), or not classified. A package under Apache-2.0 AND LGPL-3.0-or-later is decided with the ones under LGPL-3.0-or-later alone: LGPL is the decision either way. A finding is titled by the part of its license that is outside the list, with its class when the license has one, and how many packages carry it: MPL-2.0 (weak copyleft) is not on the approved license list (14 packages). A license outside the known classes, such as Elastic-2.0, is named without one. Its page lists the packages under the license, a package’s platform builds counted under it (lightningcss 1.33.0 · 6 platform builds), each with whether it ships and which of the repository’s dependencies bring it in (from a lockfile whose graph the sweep reads) and the lockfile it was read from; it names the whole license, links it on the organization’s Licenses page (under Compliance) where the list is decided, and links the TIC-6.2 control it breaches. In place of a fix version it lists the choices a license leaves: ignore the finding with a reason, replace the packages under it, and, where the organization’s list can hold the decision (one license identifier, or each side of an AND of identifiers), approve it on that list, which the choice links to. A choice between licenses (OR), a license exception (WITH) or an identifier the scanner does not recognize has no approve option, since the list takes single identifiers only. Nor is approval offered while the organization’s list differs from the one the finding was judged against: the next sweep judges the package again, and what would need approving may have changed.

A license finding is ignored, reopened and resolved like any other finding. On a package family’s page, one ignore with one reason covers every open package in it. It resolves when a sweep that evaluated licenses no longer sees the package under that license. A sweep that did not evaluate licenses leaves license findings as they were.

Compliance › Licenses in the organization sidebar lists every license in use across the organization’s products, in two groups:

  • Not approved: licenses outside the list, with how many packages and products carry each.
  • Approved: the list itself. An approved license no product uses is shown greyed, as Not in use.

Selecting a license shows its full name, whether it is approved, its category, and where it ships (how many of its packages ship to users and how many more may, or that only build and test tools use it). A package counts as shipped when it is a runtime dependency, and as possibly shipped when its lockfile does not say. The pane then has, before the Approve button, up to three labelled lines:

  • It asks: what the license asks of whoever uses it, in plain words.
  • Approve: when approving it for every product fits, such as “if your teams use its packages as they come, and every product you hand out ships its notices and says where its source code is available”. Shown to whoever can change the list, while the license is not approved.
  • Allow in a product: when to allow it in one product instead, such as “if a team patches one: only in that team’s products”. Shown to whoever can allow it in a product, while the license is not approved and a product has a finding to allow.

The lines are written for the licenses people meet often, such as LGPL (whether it is handed to users in an app, a device or a browser bundle, where people must be able to swap in their own version of the library), Elastic 2.0 (whether a product offers it to others as a service), SSPL (offered as a service, or handed out, it asks for the source of all of it), for license exceptions that change them (the Classpath and GCC runtime exceptions), and for each license family otherwise. They are a summary to decide with, not legal advice: the license’s own text is what binds.

Under it, each product that uses the license is a block, the first three open; past six, Show all lists the rest. A block says whether the product ships it, and its rows are the product’s repositories: how many packages carry the license, how many of them ship, and what brings it into the repository (the dependency the repository declares, with its package count, a package’s platform builds counted under it and said beside it: vite (2, 12 platform builds)). A repository row opens the product’s own page for the license, where its findings are decided. A product that carries the license under several license strings has no one page: a repository’s finding then opens the page of its own string, and a repository with none opens the product’s Licenses tab. You see the products you are a member of; an organization admin sees every product.

An organization admin approves or removes the selected license from the pane, which says beforehand what the change does to the findings (“Closes 8 findings”, “Opens 3 findings”). That is the same judgement the change then makes, so the effect shown is what happens. Removing a license that is in use asks for confirmation first, with the number of findings it opens. A change applies at once, and Undo reverses it as one more version of the list. To approve a license no product uses yet, type its SPDX identifier into the search and choose Approve. Everyone else can read the page but not change the list.

A license can also be allowed in one product without approving it for everyone: the product’s ⋯ menu offers Allow in the product, which ignores every license finding of that license there, with one reason, an optional note and an optional time after which the findings come back. The reasons are worded for a license (Accepted, Not handed out, No alternative, License read wrong). The block then says until when it is allowed, the pane says in how many products, and Reopen in the same menu opens the findings again. Both are recorded in the audit log. The approved list does not change, so another product using the license still raises its finding. Allowing takes the right to change the product, as ignoring one of its findings does.

Agents read and change the list through the Taiga MCP server: taiga_list_licenses lists the organization’s licenses, or where one is used, and taiga_change_license approves or removes one (with preview for the effect alone), for an organization admin’s key. For such a key, taiga_list_action_required also lists the licenses in use in a product that nobody approved (licenseReviews). In one product, taiga_list_licenses with a product reads a license as its page does, and taiga_allow_license allows it there or reopens its findings, for a key that can change the product.

The approved list also binds Taiga’s own work. When Taiga plans an initiative, the planner is given the list, looks up the license of every package a step adds, and plans a package only under an approved license: A OR B needs one side approved, A AND B every side. When the package it would pick carries any other license, it picks another one, or asks you in a plan question when no other package will do. Approving a license stays your decision.

The build is held to the same list. When a run changes a lockfile or a manifest, its security pass reads every package the branch installs, direct and transitive, and compares them with the lockfiles as they were when the branch started: only a package the run brought in counts, so the licenses a product already carried stay the Licenses tab’s findings. A package the run brought in under a license outside the list is never swapped for another one on Taiga’s own: the plan chose it, and which library a product uses is not the security pass’s decision. The pull request stays a draft, its description lists each package with its license under Licences not on your approved list, and the comment that hands it back to you names them. Autonomous merge never merges such a pull request on its own: the initiative says so and names the packages, and the pull request is yours to merge once you have replaced the package or approved the license.

The tab lists the licenses the product uses, grouped by whether each is allowed here, and says how many licenses wait on a decision. Each license is one line with its category and how many packages carry it, and opens its own page (below), where its packages are.

  • Not approved: each license outside a repository’s approved list that a finding is about. Allow at the row’s right allows the license in this product alone, as Allow in does on the organization’s Licenses page (above): every license finding of that license here is ignored with one reason, an optional note and an optional time after which the findings come back, and Undo on the confirmation takes exactly that back. The approved list does not change, so another product using the license still raises its finding. It is offered to whoever can change the product, while a finding is open, except where the repositories decide the license on different parts, so that no one identifier or license string names all its findings: its page then links each finding. A license finding whose packages the inventory does not list is a row too, and opens the finding.
  • Allowed in this product: each license whose findings here are all ignored, with until when, and Reopen, which opens those findings again. A license with a finding still open here stays under Not approved, even where another repository’s finding of it is ignored; its page shows both.
  • Unknown license: each package whose license no source names, with why: not on the public registry (a private package, or a version never published), still being looked up, or no source names a license; each package with a non-standard license, with the words it declares and a link to its terms where it gives one; and, by name, a license that could not be judged, such as a string that is not a license expression.
  • Approved: the licenses on the list, closed to its heading until opened, the largest first; the rest show on Load more. The repository’s own code is a row of its own here, Your own code, with how many packages it is: approved as yours, and not read against the list.
  • No longer applies: the license findings that were resolved, closed until opened. A finding resolves when its package is gone, is only built for platforms the repository does not deploy to, or its license was approved.

An expression reads the way the findings read it: Apache-2.0 AND MIT and MIT OR X11 are allowed, Apache-2.0 AND LGPL-3.0-or-later is not. An expression no one license identifier allows, such as a choice between licenses, is allowed as it is written. The SBOM and the third-party notices download from the page’s More actions menu, one of each per repository.

A license opens its own page, laid out as a finding’s page, with what it is in the main column and facts about it beside it. Its name carries one badge for where it stands in this product, Needs a decision or Allowed in this product, and the decision is at the page’s top right: Allow in this product, the same decision as on the tab, and once allowed Reopen, which opens the findings again. Where the repositories decide it on different parts, so that no one identifier or license string names all its findings, or for whoever cannot change the product, the page links the finding instead. A license the inventory no longer lists, such as a package a truncated inventory left out, has no page, and its finding is linked from where the page would be.

The page says what the license is against the approved list (“Mozilla Public License 2.0: not on your approved list”), how many of its packages ship to users, and the same labelled rows as the organization’s Licenses page: what the license asks, when to allow it in this product (shown to whoever can), or, once allowed, until when and why. An expression that no one license decides is named as written, with no rows.

Beside it, the page lists:

  • Family: its class, such as weak copyleft.
  • Packages: how many ship, how many only build and test tools use, and how many may ship because their lockfile does not say.
  • From: the ecosystems and the lockfiles its packages were read from.
  • Policy: the policy controls its findings breach, such as TIC-6.2, each marked Breached while a finding needs a decision and Exception once the license is allowed here. Each opens the control’s page in the product.
  • In the organization: how many packages in how many products use it, whether it is approved, and a link to where else it is used.

Its packages follow. A license with up to 100 packages lists each package under its own heading, open, the ones that ship first, with every copy of it the repositories carry: its version, the lockfile it was read from, and what brings it in. A package two repositories carry, or carry at two versions, is one heading with a row for each copy. A license with more packages groups them by what brings them in instead, the first groups open: the dependency the repository declares that pulls them in (all of sharp’s prebuilt builds sit under sharp), the packages the repository declares itself, or, where the lockfile’s graph could not say, under Parent unknown, with why. A package two declared dependencies bring in is listed under each. A search lists the license’s packages by name instead. A package’s platform builds read under it (lightningcss 1.33.0 · 6 platform builds), and count as that one package. Where packages differ, or a package has builds, each one opens to its copyright holders, where its license was read, why its scope is unknown when it is, its non-standard license’s own words, the lockfile it was read from, and the names of its builds.

Under its packages, its references link the license’s text on the SPDX site. Last, its activity lists what happened to the license, oldest first: when a sweep first found it in this product and when it last came back after it was gone, who allowed it in this product, with the reason and until when, and who approved it for the organization or removed it from the approved list, and when.

While the licenses feature is turned off, license findings are left out of every list and count, including the findings that need attention and policy standing. They are kept, and are listed and counted again when it is turned back on; a “findings need attention” item that cleared while they were hidden is raised again only when a sweep has something new.

Each repository’s inventory can be downloaded as a software bill of materials in CycloneDX 1.6 JSON (partial, and marked so, when the inventory reached its 20,000-package cap), the format procurement and compliance tooling reads. The document carries what the 2026 minimum elements for an SBOM ask for and the inventory can honestly say: when it was produced and by what, the repository it describes (versioned by the commit the sweep read), every package with its package URL (for every lockfile ecosystem the sweep reads, such as npm, PyPI, Go, Maven or Cargo; otherwise its ecosystem, name and version; none for the repository’s own code, which carries a taiga:first-party property instead) and its license (as an SPDX identifier or expression when it is one, and by name when it is not, such as a license the data source could not identify; marked declared when the package itself says it, through its registry, its PyPI metadata or classifiers, and concluded when Taiga concluded it from the package’s own license files or its source repository’s license), the lockfile each was read from, whether the lockfile declares it for runtime or development, the packages the repository declares directly, and, from a lockfile whose graph the sweep reads, the whole dependency graph (what each package depends on), which of the declared packages bring each other package in (as taiga:introduced-by properties), which packages are builds for other platforms only (a taiga:other-platforms-only property), each package’s digest where the lockfile pins one: the npm-style integrity hash of pnpm, npm and bun, and for uv the source distribution’s or the pure-Python wheel’s, its supplier: the owner of the package’s source repository as deps.dev links it (for example withastro for astro), where it links one, its copyright statements where its own files make them, and, as a taiga:license-source property, where its license was read. A package from another lockfile carries no digest and no graph entry of its own, rather than one that would claim it depends on nothing.

The same document is available to an agent through the Taiga MCP server, beside the package list and the license summary.

Beside each SBOM download in the More actions menu, Download notices saves the repository’s third-party notices as a text file: the attribution list a product carries, or hands over with its SBOM. It lists every third-party package the repository ships, with its version, its license (and where the license was read when that was not deps.dev), the copyright statements its own files make, and its package URL. A package the lockfiles declare for development only, the repository’s own code, and builds for platforms the repository does not deploy to are left out; a package whose scope the lockfile does not say is listed. Each package’s own license text is reproduced, once per distinct text, or, where its own text was not read, the standard text of each license it names, with the NOTICE file of every Apache 2.0 package that ships one. A license with no text to reproduce is linked to its page on spdx.org, or, when it is not an SPDX identifier, pointed to the package’s own files. A package with no known copyright statement says so. The notices are also available through the Taiga MCP server (taiga_get_notices).

Did you find what you needed?