Code and data for the IEEE Software paper "Don't Trust the Label: License Laundering in AI Supply Chains" (Jewitt, Li, Rajbahadur, Adams, and Hassan; Queen's University).
Every number and figure in the paper is reproducible from this repository.
unzip data.zip # creates data/ (~217 MB unpacked)
pip install -r requirements.txt
cd analysis
python3 prune_data.py # scope the data -> data/filtered_chains_clean.jsonl
python3 category_distribution.py # license-string -> category distributions
python3 rq1_false_clarity.py # RQ1: Unknown laundering
python3 rq2_selective_laundering.py # RQ2: category laundering
python3 e2e_category_survival.py # end-to-end survival matrices
python3 named_examples.py # MiniLM / wukong100m / Wikipedia examples
python3 figure_data_dump.py # -> ../figure_data.md
python3 sankey_rq1.py # -> ../figure_1.pdf
python3 rq2_break_pairs_experiment.py # -> ../figure_2.pdf
python3 report_paper_numbers.py # prints the table belowRun them in that order. The six analysis scripts write into results.json; the
two figure scripts read it. prune_data.py must run first — it produces
data/filtered_chains_clean.jsonl, which every other script reads.
Runtime is a few minutes end to end, dominated by prune_data.py.
LicenseLaundering/
├── LICENSE # Apache-2.0 (code)
├── LICENSE-DATA # CC BY 4.0 (data), with scope note
├── data.zip # raw scope + license-category mapping; unzip in place
├── requirements.txt
├── analysis/
│ ├── prune_data.py # scoping and filtering
│ ├── category_distribution.py # license strings -> seven categories
│ ├── rq1_false_clarity.py # Unknown laundering (RQ1)
│ ├── rq2_selective_laundering.py # category laundering (RQ2)
│ ├── e2e_category_survival.py # end-to-end survival matrices
│ ├── named_examples.py # the named examples in the paper
│ ├── figure_data_dump.py # plain-text dump of every figure datum
│ ├── sankey_rq1.py # Figure 1
│ ├── rq2_break_pairs_experiment.py # Figure 2
│ ├── report_paper_numbers.py # regenerates the table below
│ └── results_manager.py # shared results.json helper
├── results.json # every value the paper cites (generated)
├── figure_data.md # every node, flow and matrix cell as text (generated)
├── figure_1.pdf # Figure 1 in the paper (generated)
└── figure_2.pdf # Figure 2 in the paper (generated)
results.json and figure_data.md are committed so the numbers can be checked
without unpacking the data or running anything.
Generated by python3 analysis/report_paper_numbers.py --markdown, run against a
clean unpack of data.zip. Nothing below is typed by hand — re-run the pipeline
and this table regenerates from results.json and the figure scripts.
Scope (prune_data.py, category_distribution.py)
| Quantity | Value |
|---|---|
| Datasets in analysis scope | 3,120 |
| Models in analysis scope | 5,556 |
| Applications in analysis scope | 24,076 |
| Chains before license filtering | 264,431 |
| Chains excluded (unrecognised licence strings) | 32,161 |
| Distinct unrecognised licence strings | 495 of 765 |
| Chains analysed | 232,270 |
Figure 1 — Unknown laundering (sankey_rq1.py)
| Quantity | Value |
|---|---|
| Total D→M→A chains analysed | 232,270 |
| Chains through ≥1 Unknown artifact | 144,631 (62.3%) |
| Fully-Known chains | 87,639 (37.7%) |
| Unknown datasets → Known models | 91,067 of 102,607 (88.8%) |
| Unknown models → Known applications | 19,243 of 23,951 (80.3%) |
| Known models → Unknown applications (drawn cohort) | 51,975 of 120,680 (43.1%) |
| Known models → Unknown applications (all chains) | 51,975 of 208,319 (24.9%) |
| Applications ending Unknown | 56,683 (39.2%) |
| Top 10% of Unknown datasets, dataset→model | 89.5% |
| Top 10% of Unknown models, model→application | 76.0% |
| Top 10% of Unknown datasets, end-to-end | 84.3% |
Figure 2 — category laundering (rq2_break_pairs_experiment.py)
| Quantity | Value |
|---|---|
| Dataset→Model retention (transition-level) | 62,160 of 95,076 (65.4%) |
| Model→Application retention (transition-level) | 75,703 of 87,658 (86.4%) |
| End-to-end Dataset→Application retention (transition-level) | 62,839 of 95,076 (66.1%) |
| Copyleft→Permissive (a) | 4,901 of 5,265 (93.1%) |
| ML License retained (a) | 158 of 283 (55.8%) |
| ML License→Permissive (b) | 2,155 of 2,768 (77.9%) |
| ML License retained (b) | 116 of 2,768 (4.2%) |
| Permissive end-to-end (c) | 61,234 of 64,369 (95.1%) |
| Sharealike end-to-end (c) | 739 of 15,678 (4.7%) |
| Fully-Known chains dropping ≥1 category | 32,901 of 87,639 (37.5%) |
| preserve all categories | 54,738 (62.5%) |
| drop at dataset→model only | 20,952 (23.9%) |
| drop at model→application only | 4,823 (5.5%) |
| drop at both hops | 7,126 (8.1%) |
Named examples (named_examples.py)
| Quantity | Value |
|---|---|
| MiniLM applications | 1,656 |
| MiniLM declared training datasets | 21, 9 with no licence, 3 Sharealike |
| wukong100m downstream models | 89, 0 carry CC-Restrictive forward |
| Wikipedia chains | 4,849, 86 preserve both (1.8%) |
Unknown laundering (Figure 1) — an artifact with no declared license whose downstream artifact carries a definitive one. Figure 1 draws only the 144,631 chains passing through at least one Unknown artifact, so every node percentage in it is a share of that subset, not of all 232,270 chains.
Category laundering (Figure 2) — a declared license category that does not survive a transition. Measured over the 87,639 fully-Known chains, where all three artifacts carry a known category.
Two denominators appear in the results and are easy to conflate:
- Chains — one dataset→model→application path. 232,270 in scope.
- Category transitions — one per category per chain. Artifacts can carry several categories, and each is tracked independently, so the 87,639 fully-Known chains yield 95,076 dataset→model and 87,658 model→application category transitions.
Figure 2's panels (a)–(c) count transitions; panel (d) counts chains.
Seven categories (see the paper's AI Supply Chain Composition section for sources):
| Category | Examples |
|---|---|
PERMISSIVE |
MIT, Apache-2.0, BSD-3-Clause |
COPYLEFT |
GPL, AGPL |
SHARE_ALIKE |
CC BY-SA, LGPL, MPL |
ML_LICENSE |
OpenRAIL, Llama Community License |
CC_RESTRICTIVE |
CC NC, NC-SA, NC-ND, ND |
PUBLIC_DOMAIN |
CC0, Unlicense |
UNKNOWN |
absent / blank / "other" / "unknown" |
data.zip unpacks to data/ (~217 MB):
filtered_complete_chains_cleaned.jsonl— the raw scope inherited from our prior audit, including base-model paths.matrix.json— the license-string → category mapping used during scoping.
prune_data.py writes a third file, data/filtered_chains_clean.jsonl, which is
the dataset every analysis script reads. It is generated, not shipped.
Two pruning steps scope the data: models serving only as base models with no application invoking them are removed, so every chain passes through exactly two transitions; then chains containing a license string that does not map to our classification are excluded. Both counts appear in the table above.
- The pipeline is deterministic. Figure 1 resolves multi-category artifacts by
random choice seeded at 42, and
report_paper_numbers.pyuses the same seed, so repeated runs give identical output. results.jsonis keyed by question number: Q1–Q19 scope and distributions, Q20–Q44 Unknown laundering, Q45–Q66 category laundering, Q67–Q68 end-to-end survival, Q69–Q71 named examples. Each entry carries adescriptionsaying what it measures.- Both figure scripts self-check before drawing: they re-derive every printed percentage from the counts printed beside it and fail rather than emit a figure whose labels disagree with its own data. Figure 1 runs 27 such assertions, Figure 2 runs 175.
- Two estimators of "top 10% concentration" appear in the code. The figure
interpolates the Lorenz curve at exactly 10% (89.5% for dataset→model); Q39/Q40
in
results.jsontake the topfloor(10%)artifacts, which lands 0.1 points lower because 10% of 1,059 datasets truncates to 105. The paper quotes the figure's.
| What | License |
|---|---|
Code in analysis/ |
Apache License 2.0 — see LICENSE |
Data: data.zip, results.json, figure_data.md |
CC BY 4.0 — see LICENSE-DATA |
Figures: figure_1.pdf, figure_2.pdf |
Outputs of the Apache-2.0 code, regenerable from it |
The dataset records metadata about third-party Hugging Face and GitHub artifacts — identifiers, declared license strings, and the notices ScanCode extracted. It contains no source code, weights, or dataset content from those artifacts, and the CC BY grant covers our collection and curation rather than the underlying facts. See LICENSE-DATA for the full scope note.
@article{jewitt2026licenselaundering,
title = {Don't Trust the Label: License Laundering in AI Supply Chains},
author = {Jewitt, James and Li, Hao and Rajbahadur, Gopi Krishnan and
Adams, Bram and Hassan, Ahmed E.},
journal = {IEEE Software},
year = {2026}
}