Almost everyone who runs an open-source scan for the first time has the same experience. The report lands, the copyleft section is mercifully short, and then there is a long tail of packages whose license is simply Unknown. Dozens of them. Sometimes hundreds. They have names you do not recognize and versions from years before you started the project. The natural reaction is to decide the scanner is being noisy and get on with your day.

Here is the uncomfortable thing about that reaction. It is often correct about the individual rows, and still the wrong conclusion. "Unknown" is not one finding repeated three hundred times. It is a bin that several unrelated situations get swept into, some of them genuinely harmless and some of them a restriction on what you are allowed to do with your own product.

What "Unknown" actually means

Start with the vocabulary, because the word does a lot of damage. Unknown does not mean "this package has no license." It means the scanner could not establish a license from the evidence it happened to look at. That is a statement about the scan, not about the package.

Once you say it that way, the useful follow-up appears:

What did the scanner actually see, and why was it not enough to name a license?

Answer that for a given row and it stops being "unknown" and becomes something specific, with an obvious next step. The reasons repeat, and they sort into a handful of recognizable situations.

The main categories of unknown licenses

Sorting rows into these groups is the whole trick, because the groups differ enormously in how much they should worry you. Some are noise you can label once and stop thinking about. Others are real restrictions on your product, printed in your report as the same one word. Each card below is the plain version of the category and a worked example of what it looks like in a real scan.

Build-time artifacts

Usually benign

Files a tool wrote rather than a person. Nothing gave them a license because nobody published them.

Example: dist/app.min.js comes back as Unknown. Your bundler produced it during the last build. Any obligations belong to the packages that went into it, and those are already in the report under their own names.

Ask: what generated this?

Operating system components

Usually out of scope

Parts of the machine or image around your project, picked up because the scan reached past your application. Distros also repackage and relicense, so a component can carry different terms here than it does upstream.

Example: you scan a container image instead of the application inside it, and get rows for bash, coreutils and a handful of kernel modules. They arrived with the base image. The real question is whether you redistribute that image, not whether you wrote the code.

Ask: is this even mine to ship?

Modified open-source licenses

Treat as serious

A familiar license with an extra clause bolted on. The scanner sees text that is nearly MIT, cannot match it exactly, and declines to call it MIT. That refusal is correct: the difference is the entire point.

Example: the LICENSE file is MIT word for word, plus one added sentence saying the software may not be used by companies offering a competing commercial service. That sentence may rule out your business model, and it reaches you as a row marked Unknown.

Ask: what does the extra clause say?

Proprietary or hybrid licenses

Needs a human

Commercial terms, internal corporate licenses, and dual-licensed components where your rights depend on which option you took. Your own first-party code usually lands here too, which is normal rather than a defect.

Example: a component is offered under either a copyleft license or a paid commercial one. The scanner cannot see which you bought, so it reports neither. Answering this row means finding the contract, not reading the code.

Ask: which terms do we actually hold?

Incomplete or ambiguous statements

The most deceptive

A string sitting in the license field that looks like an answer without being one. Naive tooling records it, treats the row as resolved, and stops asking.

Example: the package metadata says SEE LICENSE IN LICENSE.txt and the package ships no LICENSE.txt. Same family: "See website", "Free for evaluation", "All rights reserved". The field is populated and you still have no terms.

Ask: what are the real terms?

Binary-embedded fragments

Partial evidence

License text pulled out of a compiled file. It proves something was there. It does not reliably tell you what, or which version.

Example: a .so file contains a BSD-style copyright notice in its strings. Three libraries were linked into that file, so the notice belongs to one of them and you cannot tell which. Useful lead, not a license you can record.

Ask: a lead, or a conclusion?

Misplaced or missing license files

Findable with effort

The grant exists. It is just not where convention says to look: a header comment, a README, a docs page, a metadata field nobody filled in. Common in older and smaller packages.

Example: a package from 2014 ships no LICENSE file, and the third line of its README reads "Released under the MIT License". Nothing is wrong with the package. The scanner checked the standard place and the standard place was empty.

Ask: did anyone look past LICENSE?

Why you cannot triage this from the list

Every category above renders as the same word in the same column. A build artifact that means nothing and a modified license that forbids you from competing with the author are visually identical rows. That is the actual problem. It is not that people are careless about unknowns, it is that the report gives them nothing to be careful with.

Dismissing a list you cannot sort is not triage. It is a coin flip performed several hundred times.

And the flip is not even fair, because the categories are not evenly distributed. The benign ones are numerous and the serious ones are rare, which trains exactly the wrong instinct. Scroll past two hundred build artifacts and you will scroll past the one modified license too.

Why ignoring them is a mistake

The case for taking unknowns seriously does not rest on any single row being alarming. It rests on three things being true at once.

  • No license means no permission. Copyright is default-deny. Code published with no license attached is not public domain and is not free to use. It is code you have no grant to redistribute at all. That is a stronger problem than most copyleft findings, and it sits in the list you are scrolling past.
  • Some unknowns are restrictions in disguise. A modified MIT with a non-commercial clause, or a "free for evaluation" string, is not an absence of terms. It is terms you have not read, and they can be less permissive than the GPL you carefully avoided.
  • Unknown licenses are not background noise. They are questions you should be ready to answer. Imagine an M&A reps and warranties meeting where the auditor asks whether there are any IP concerns in the list of unknown licenses. Saying that you do not know or have not looked at them is a completely avoidable problem.

The failure mode is not that somebody consciously accepts the risk. It is that the list is too long and too undifferentiated to read, so nobody reads any of it, and the rows that mattered were on page four.

What to do about it

  1. Sort before you dismiss. The goal is not to resolve every unknown, it is to know which category each one is in. Once a row is labeled a build artifact or a first-party proprietary component, dismissing it is a decision rather than a guess.

  2. Treat an ambiguous string as worse than an empty one. A blank license field is honest. "See website" is a blank field wearing a costume, and most tooling counts it as resolved. If your scanner reports a license you cannot map to real terms, that is an unknown regardless of what the column says.

  3. Keep package coordinates intact. A row that reaches your report without identifying coordinates is a row nothing can ever look up, in any registry, by any tool. It is condemned to unknown permanently no matter how well known the component is.

  4. Then read what is left. Once the benign categories are labeled and the resolvable ones resolved, the residue is small. Small enough to actually read. That is the point of the whole exercise: not to drive the number to zero, but to make the remaining number mean something.

How OSSScan approaches this

Our position is that a scanner's job is to be precise about what it observed and honest when it does not know. Two rules follow, and they shape a lot of the product.

Never guess a license. When the evidence does not support a conclusion, OSSScan fails closed to unknown and flags the component for review. It will not infer MIT because the neighbouring package is MIT, and it will not resolve a component from its name alone when it has no version to confirm the match. A confident wrong answer is worse than an honest gap, because the gap gets investigated and the wrong answer does not.

Separate the categories rather than pooling them. Build and CI tooling is tagged and counted apart from your dependencies. First-party and proprietary components are marked for manual review rather than filed under unknown. Strings that look like answers but are not, including "see the url", "all rights reserved", "custom" and "TBD", are explicitly not treated as resolved, and where the metadata offered a URL it is carried forward so a reviewer has a concrete lead rather than a dead end. And in Audit mode, which reads the license text on disk rather than trusting the metadata, text that disagrees with the declared license is reported as a disagreement. That is how a modified license, or copyleft vendored inside a permissive package, announces itself.

What remains goes through several layers of resolution: the component's own metadata, then the ecosystem registry, then ClearlyDefined's curated data. Only what fails all of them stays unknown. The registry and ClearlyDefined layers make network calls, so they run in Deep mode rather than in a quick local scan, and Deep mode is the one to use when you actually care about the size of the unknown list.

Packages · OSSScan
NameVersionTypeLicenseReview Priority
left-pad1.3.0npm MIT Low Priority
libfoo1.2.0npm GPL-3.0-only High Priority
your-app0.1.0npm LicenseRef-Proprietary Manual License Review
vendor-sdk2.4.0npm Unknown

Illustrative rows in OSSScan's real layout and priority labels. Build and CI tooling does not appear in this table at all; it is tagged and counted separately, which is the point. Of four rows only one is an open question, and it is short enough to be worth reading.

For that last row, OSSScan can hand the work to an AI agent through Bulk License Review. The agent reads the actual component, checks the upstream project, and writes up what it found, under explicit instructions never to invent a license and to flag anything it cannot resolve. That is the right division of labor: deterministic facts from the scanner, judgment from something that can read files and reason about them, and no pretending in either direction.

Bottom line

Unknown is not a verdict, it is a question the scan could not answer. Most of the time the honest answer is boring: a build artifact, a first-party component, a license sitting in a header comment instead of a LICENSE file. Occasionally it is a modified license with a clause that restricts you, or a component that grants you no rights at all. Those deserve a decision rather than a scroll.

You cannot tell which is which from an undifferentiated list of three hundred. So the goal is not to make peace with a long unknown list, and it is certainly not to drive the number to zero by guessing. It is to sort it, until what is left is short enough that reading it is obviously worth your time.

← Back to the OSSScan Blog