weight-audit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weight_audit-0.1.0/LICENSE +21 -0
- weight_audit-0.1.0/PKG-INFO +126 -0
- weight_audit-0.1.0/README.md +113 -0
- weight_audit-0.1.0/pyproject.toml +22 -0
- weight_audit-0.1.0/setup.cfg +4 -0
- weight_audit-0.1.0/tests/test_accuracy.py +79 -0
- weight_audit-0.1.0/tests/test_classify.py +76 -0
- weight_audit-0.1.0/tests/test_cli_online.py +53 -0
- weight_audit-0.1.0/tests/test_hf_client.py +62 -0
- weight_audit-0.1.0/weight_audit/__init__.py +1 -0
- weight_audit-0.1.0/weight_audit/classify.py +92 -0
- weight_audit-0.1.0/weight_audit/cli.py +132 -0
- weight_audit-0.1.0/weight_audit/hf_client.py +70 -0
- weight_audit-0.1.0/weight_audit/license_db.py +278 -0
- weight_audit-0.1.0/weight_audit/policy.py +53 -0
- weight_audit-0.1.0/weight_audit/scanner.py +97 -0
- weight_audit-0.1.0/weight_audit/tiers.py +13 -0
- weight_audit-0.1.0/weight_audit.egg-info/PKG-INFO +126 -0
- weight_audit-0.1.0/weight_audit.egg-info/SOURCES.txt +20 -0
- weight_audit-0.1.0/weight_audit.egg-info/dependency_links.txt +1 -0
- weight_audit-0.1.0/weight_audit.egg-info/entry_points.txt +2 -0
- weight_audit-0.1.0/weight_audit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HiroCheck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weight-audit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: License compliance scanner for open-weight AI models (Llama, Gemma, Qwen, DeepSeek, and more)
|
|
5
|
+
Author: HiroCheck
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# weight-audit
|
|
15
|
+
|
|
16
|
+
Scan the open-weight AI models a project depends on (Llama, Gemma, Qwen,
|
|
17
|
+
DeepSeek, and others) for license compliance risk before a scale-cap or
|
|
18
|
+
acceptable-use restriction turns into a legal problem for a commercial
|
|
19
|
+
product.
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
Open-weight models are not all "open source." The Llama Community
|
|
24
|
+
License caps free commercial use at 700M monthly active users. Gemma,
|
|
25
|
+
Qwen, and DeepSeek carry acceptable-use policies. CC-BY-NC weights
|
|
26
|
+
forbid commercial use outright. Roughly 70% of models on Hugging Face
|
|
27
|
+
ship with no license metadata at all. Enterprise SCA platforms
|
|
28
|
+
(Endor Labs, Sonatype, Black Duck) now gate on this for large
|
|
29
|
+
customers -- this tool does the same check for the developers and
|
|
30
|
+
small AI startups those platforms don't sell to.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install weight-audit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
weight-audit scan . # scan a directory (manifest + source detection)
|
|
42
|
+
weight-audit scan . --policy policy.json # use a custom policy
|
|
43
|
+
weight-audit scan . --json # machine-readable output for CI
|
|
44
|
+
weight-audit scan . --online # also query the HF Hub API for models not in the offline DB
|
|
45
|
+
weight-audit bom . --output ml-bom.json # generate a CycloneDX ML-BOM
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Exit code is `1` if any model violates the policy, `0` otherwise --
|
|
49
|
+
safe to drop into CI:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
- run: weight-audit scan .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Detecting models
|
|
56
|
+
|
|
57
|
+
weight-audit looks for models in two places:
|
|
58
|
+
|
|
59
|
+
1. A `weight-audit-models.txt` manifest in the scanned directory --
|
|
60
|
+
one Hugging Face model id per line, with an optional declared
|
|
61
|
+
license as a second column:
|
|
62
|
+
```
|
|
63
|
+
meta-llama/Llama-3-8B-Instruct
|
|
64
|
+
some-org/attributed-model,cc-by-4.0
|
|
65
|
+
```
|
|
66
|
+
2. A best-effort scan of `.py` source for
|
|
67
|
+
`AutoModel.from_pretrained("org/model")` and `model_id = "org/model"`
|
|
68
|
+
patterns. This is a convenience for a first scan -- for reliable CI
|
|
69
|
+
gating, use a manifest.
|
|
70
|
+
|
|
71
|
+
### Policy
|
|
72
|
+
|
|
73
|
+
By default, anything at `strong-restriction` or above is a violation,
|
|
74
|
+
and models with no license metadata at all (`unknown`) are treated as
|
|
75
|
+
violations too. Override with a JSON file:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"fail_at_or_above": "weak-restriction",
|
|
80
|
+
"treat_unknown_as_violation": false
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How it classifies
|
|
85
|
+
|
|
86
|
+
Licenses are bucketed into four tiers: `permissive` < `weak-restriction`
|
|
87
|
+
< `strong-restriction` < `unknown`. 27 named model families (Llama,
|
|
88
|
+
Gemma, Qwen, DeepSeek, BLOOM, StarCoder, Baichuan, ChatGLM, Yi, Grok,
|
|
89
|
+
FLUX, and others) are matched against a hand-curated offline database
|
|
90
|
+
recording their *actual* restriction -- not just the license name --
|
|
91
|
+
so a scale cap or acceptable-use policy shows up as a concrete
|
|
92
|
+
obligation, not just a label. See `weight_audit/license_db.py` for the
|
|
93
|
+
exact rules and `weight_audit/classify.py` for the resolution order.
|
|
94
|
+
|
|
95
|
+
A broader SPDX/common-identifier fallback table (Apache-2.0, MIT,
|
|
96
|
+
CC-BY variants, OpenRAIL variants, and named Llama/Gemma license
|
|
97
|
+
strings) covers models outside the named-family list when a license
|
|
98
|
+
string is available, either from a manifest's `declared_license`
|
|
99
|
+
column or from `--online`.
|
|
100
|
+
|
|
101
|
+
### Online mode
|
|
102
|
+
|
|
103
|
+
`--online` queries the Hugging Face Hub API for models that aren't in
|
|
104
|
+
the offline database and don't have a declared license in the
|
|
105
|
+
manifest. It never overrides a license already recorded in the
|
|
106
|
+
manifest, and any network failure (timeout, 404, malformed response)
|
|
107
|
+
degrades to `unknown` rather than crashing the scan -- the offline
|
|
108
|
+
database remains the reliable default; `--online` only fills gaps.
|
|
109
|
+
|
|
110
|
+
## Limitations
|
|
111
|
+
|
|
112
|
+
The offline database is a hand-curated table of well-known model
|
|
113
|
+
families, not a registry mirror. Models outside the known families and
|
|
114
|
+
without a declared license (from a manifest or `--online`) are flagged
|
|
115
|
+
`unknown`. This is deliberate: "unknown" surfaces the real state of the
|
|
116
|
+
ecosystem (~70% of Hugging Face models carry no license) rather than
|
|
117
|
+
silently passing them.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
122
|
+
|
|
123
|
+
## Support
|
|
124
|
+
|
|
125
|
+
If this project is useful to you, you can support development here:
|
|
126
|
+
<https://buy.stripe.com/PLACEHOLDER>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# weight-audit
|
|
2
|
+
|
|
3
|
+
Scan the open-weight AI models a project depends on (Llama, Gemma, Qwen,
|
|
4
|
+
DeepSeek, and others) for license compliance risk before a scale-cap or
|
|
5
|
+
acceptable-use restriction turns into a legal problem for a commercial
|
|
6
|
+
product.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Open-weight models are not all "open source." The Llama Community
|
|
11
|
+
License caps free commercial use at 700M monthly active users. Gemma,
|
|
12
|
+
Qwen, and DeepSeek carry acceptable-use policies. CC-BY-NC weights
|
|
13
|
+
forbid commercial use outright. Roughly 70% of models on Hugging Face
|
|
14
|
+
ship with no license metadata at all. Enterprise SCA platforms
|
|
15
|
+
(Endor Labs, Sonatype, Black Duck) now gate on this for large
|
|
16
|
+
customers -- this tool does the same check for the developers and
|
|
17
|
+
small AI startups those platforms don't sell to.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install weight-audit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
weight-audit scan . # scan a directory (manifest + source detection)
|
|
29
|
+
weight-audit scan . --policy policy.json # use a custom policy
|
|
30
|
+
weight-audit scan . --json # machine-readable output for CI
|
|
31
|
+
weight-audit scan . --online # also query the HF Hub API for models not in the offline DB
|
|
32
|
+
weight-audit bom . --output ml-bom.json # generate a CycloneDX ML-BOM
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Exit code is `1` if any model violates the policy, `0` otherwise --
|
|
36
|
+
safe to drop into CI:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
- run: weight-audit scan .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Detecting models
|
|
43
|
+
|
|
44
|
+
weight-audit looks for models in two places:
|
|
45
|
+
|
|
46
|
+
1. A `weight-audit-models.txt` manifest in the scanned directory --
|
|
47
|
+
one Hugging Face model id per line, with an optional declared
|
|
48
|
+
license as a second column:
|
|
49
|
+
```
|
|
50
|
+
meta-llama/Llama-3-8B-Instruct
|
|
51
|
+
some-org/attributed-model,cc-by-4.0
|
|
52
|
+
```
|
|
53
|
+
2. A best-effort scan of `.py` source for
|
|
54
|
+
`AutoModel.from_pretrained("org/model")` and `model_id = "org/model"`
|
|
55
|
+
patterns. This is a convenience for a first scan -- for reliable CI
|
|
56
|
+
gating, use a manifest.
|
|
57
|
+
|
|
58
|
+
### Policy
|
|
59
|
+
|
|
60
|
+
By default, anything at `strong-restriction` or above is a violation,
|
|
61
|
+
and models with no license metadata at all (`unknown`) are treated as
|
|
62
|
+
violations too. Override with a JSON file:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"fail_at_or_above": "weak-restriction",
|
|
67
|
+
"treat_unknown_as_violation": false
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## How it classifies
|
|
72
|
+
|
|
73
|
+
Licenses are bucketed into four tiers: `permissive` < `weak-restriction`
|
|
74
|
+
< `strong-restriction` < `unknown`. 27 named model families (Llama,
|
|
75
|
+
Gemma, Qwen, DeepSeek, BLOOM, StarCoder, Baichuan, ChatGLM, Yi, Grok,
|
|
76
|
+
FLUX, and others) are matched against a hand-curated offline database
|
|
77
|
+
recording their *actual* restriction -- not just the license name --
|
|
78
|
+
so a scale cap or acceptable-use policy shows up as a concrete
|
|
79
|
+
obligation, not just a label. See `weight_audit/license_db.py` for the
|
|
80
|
+
exact rules and `weight_audit/classify.py` for the resolution order.
|
|
81
|
+
|
|
82
|
+
A broader SPDX/common-identifier fallback table (Apache-2.0, MIT,
|
|
83
|
+
CC-BY variants, OpenRAIL variants, and named Llama/Gemma license
|
|
84
|
+
strings) covers models outside the named-family list when a license
|
|
85
|
+
string is available, either from a manifest's `declared_license`
|
|
86
|
+
column or from `--online`.
|
|
87
|
+
|
|
88
|
+
### Online mode
|
|
89
|
+
|
|
90
|
+
`--online` queries the Hugging Face Hub API for models that aren't in
|
|
91
|
+
the offline database and don't have a declared license in the
|
|
92
|
+
manifest. It never overrides a license already recorded in the
|
|
93
|
+
manifest, and any network failure (timeout, 404, malformed response)
|
|
94
|
+
degrades to `unknown` rather than crashing the scan -- the offline
|
|
95
|
+
database remains the reliable default; `--online` only fills gaps.
|
|
96
|
+
|
|
97
|
+
## Limitations
|
|
98
|
+
|
|
99
|
+
The offline database is a hand-curated table of well-known model
|
|
100
|
+
families, not a registry mirror. Models outside the known families and
|
|
101
|
+
without a declared license (from a manifest or `--online`) are flagged
|
|
102
|
+
`unknown`. This is deliberate: "unknown" surfaces the real state of the
|
|
103
|
+
ecosystem (~70% of Hugging Face models carry no license) rather than
|
|
104
|
+
silently passing them.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
109
|
+
|
|
110
|
+
## Support
|
|
111
|
+
|
|
112
|
+
If this project is useful to you, you can support development here:
|
|
113
|
+
<https://buy.stripe.com/PLACEHOLDER>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "weight-audit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "License compliance scanner for open-weight AI models (Llama, Gemma, Qwen, DeepSeek, and more)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "HiroCheck"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
weight-audit = "weight_audit.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
include = ["weight_audit*"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Accuracy harness: known-violating and known-clean fixtures.
|
|
2
|
+
|
|
3
|
+
Mirrors license-radar's tests/test_parsers.py + accuracy pattern and
|
|
4
|
+
gha-audit's tests/test_accuracy.py: every fixture directory is a
|
|
5
|
+
manifest scan, and we assert the policy verdict is correct given the
|
|
6
|
+
*default* policy (fail_at_or_above=strong-restriction,
|
|
7
|
+
treat_unknown_as_violation=True).
|
|
8
|
+
|
|
9
|
+
Recall = violating fixtures correctly flagged / total violating fixtures
|
|
10
|
+
FP rate = clean fixtures incorrectly flagged / total clean fixtures
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from weight_audit.classify import classify
|
|
20
|
+
from weight_audit.policy import evaluate, load_policy
|
|
21
|
+
from weight_audit.scanner import scan
|
|
22
|
+
|
|
23
|
+
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
24
|
+
|
|
25
|
+
VIOLATING_FIXTURES = [
|
|
26
|
+
"violating-llama",
|
|
27
|
+
"violating-gemma",
|
|
28
|
+
"violating-qwen",
|
|
29
|
+
"violating-deepseek",
|
|
30
|
+
"violating-stability",
|
|
31
|
+
"violating-unknown",
|
|
32
|
+
"violating-cc-by-nc",
|
|
33
|
+
"source-detect-violating",
|
|
34
|
+
"violating-bloom",
|
|
35
|
+
"violating-starcoder",
|
|
36
|
+
"violating-baichuan",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
CLEAN_FIXTURES = [
|
|
40
|
+
"safe-mistral",
|
|
41
|
+
"safe-phi",
|
|
42
|
+
"safe-falcon",
|
|
43
|
+
"safe-cc-by",
|
|
44
|
+
"safe-mixed",
|
|
45
|
+
"source-detect-safe",
|
|
46
|
+
"safe-mpt",
|
|
47
|
+
"safe-olmo",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _scan_fixture(name: str) -> bool:
|
|
52
|
+
"""Return True if the fixture's policy evaluation flags a violation."""
|
|
53
|
+
refs = scan(FIXTURES_DIR / name)
|
|
54
|
+
assert refs, f"fixture {name} produced no model references -- fixture is broken"
|
|
55
|
+
verdicts = [classify(r.model_id, r.declared_license) for r in refs]
|
|
56
|
+
policy = load_policy(None)
|
|
57
|
+
results = evaluate(verdicts, policy)
|
|
58
|
+
return any(r.violates for r in results)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("name", VIOLATING_FIXTURES)
|
|
62
|
+
def test_violating_fixtures_are_flagged(name: str) -> None:
|
|
63
|
+
assert _scan_fixture(name) is True, f"{name} should violate the default policy but did not"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.parametrize("name", CLEAN_FIXTURES)
|
|
67
|
+
def test_clean_fixtures_are_not_flagged(name: str) -> None:
|
|
68
|
+
assert _scan_fixture(name) is False, f"{name} should NOT violate the default policy but did"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_accuracy_summary(capsys) -> None:
|
|
72
|
+
"""Not a real assertion -- prints recall / FP rate for visibility,
|
|
73
|
+
matching license-radar and gha-audit's accuracy reporting."""
|
|
74
|
+
tp = sum(1 for name in VIOLATING_FIXTURES if _scan_fixture(name))
|
|
75
|
+
fp = sum(1 for name in CLEAN_FIXTURES if _scan_fixture(name))
|
|
76
|
+
recall = tp / len(VIOLATING_FIXTURES)
|
|
77
|
+
fp_rate = fp / len(CLEAN_FIXTURES)
|
|
78
|
+
print(f"\nRecall: {recall:.1%} ({tp}/{len(VIOLATING_FIXTURES)})")
|
|
79
|
+
print(f"False-positive rate: {fp_rate:.1%} ({fp}/{len(CLEAN_FIXTURES)})")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from weight_audit.classify import Tier, classify
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_llama_is_strong_restriction():
|
|
5
|
+
v = classify("meta-llama/Llama-3-8B-Instruct")
|
|
6
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
7
|
+
assert "700M" in v.obligation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_mistral_is_permissive():
|
|
11
|
+
v = classify("mistralai/Mistral-7B-v0.1")
|
|
12
|
+
assert v.tier == Tier.PERMISSIVE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_unknown_model_is_unknown_tier():
|
|
16
|
+
v = classify("some-rando/never-heard-of-it")
|
|
17
|
+
assert v.tier == Tier.UNKNOWN
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_declared_cc_by_nc_overrides_to_strong_restriction():
|
|
21
|
+
v = classify("some-org/whatever-model", declared_license="CC-BY-NC-4.0")
|
|
22
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_declared_cc_by_is_weak_restriction():
|
|
26
|
+
v = classify("some-org/whatever-model", declared_license="cc-by-4.0")
|
|
27
|
+
assert v.tier == Tier.WEAK_RESTRICTION
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- expanded family coverage (added alongside --online support) ---
|
|
31
|
+
|
|
32
|
+
def test_bloom_is_strong_restriction():
|
|
33
|
+
v = classify("bigscience/bloom-7b1")
|
|
34
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_starcoder_is_strong_restriction():
|
|
38
|
+
v = classify("bigcode/starcoder2-15b")
|
|
39
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_mpt_is_permissive():
|
|
43
|
+
v = classify("mosaicml/mpt-7b")
|
|
44
|
+
assert v.tier == Tier.PERMISSIVE
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_olmo_is_permissive():
|
|
48
|
+
v = classify("allenai/olmo-7b")
|
|
49
|
+
assert v.tier == Tier.PERMISSIVE
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_baichuan_is_strong_restriction():
|
|
53
|
+
v = classify("baichuan-inc/baichuan2-13b")
|
|
54
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_yi_is_strong_restriction():
|
|
58
|
+
v = classify("01-ai/yi-34b")
|
|
59
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_flux_schnell_is_permissive_but_dev_is_restricted():
|
|
63
|
+
schnell = classify("black-forest-labs/flux.1-schnell")
|
|
64
|
+
dev = classify("black-forest-labs/flux.1-dev")
|
|
65
|
+
assert schnell.tier == Tier.PERMISSIVE
|
|
66
|
+
assert dev.tier == Tier.STRONG_RESTRICTION
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_declared_llama3_1_string_is_strong_restriction():
|
|
70
|
+
v = classify("some-mirror/repackaged-model", declared_license="llama3.1")
|
|
71
|
+
assert v.tier == Tier.STRONG_RESTRICTION
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_declared_cc_by_sa_is_weak_restriction():
|
|
75
|
+
v = classify("some-org/whatever-model", declared_license="cc-by-sa-4.0")
|
|
76
|
+
assert v.tier == Tier.WEAK_RESTRICTION
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import weight_audit.cli as cli_module
|
|
4
|
+
from weight_audit.cli import main
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_online_flag_resolves_unknown_model_via_mocked_hf(tmp_path: Path, monkeypatch, capsys):
|
|
8
|
+
# A manifest referencing a model with NO declared_license column,
|
|
9
|
+
# and NOT in the offline family table -- would be "unknown" offline.
|
|
10
|
+
manifest = tmp_path / "weight-audit-models.txt"
|
|
11
|
+
manifest.write_text("some-org/not-in-offline-db\n", encoding="utf-8")
|
|
12
|
+
|
|
13
|
+
def fake_fetch(model_id, fetch=None):
|
|
14
|
+
assert model_id == "some-org/not-in-offline-db"
|
|
15
|
+
return "cc-by-nc-4.0"
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr(cli_module, "fetch_declared_license", fake_fetch)
|
|
18
|
+
|
|
19
|
+
exit_code = main(["scan", str(tmp_path), "--online", "--json"])
|
|
20
|
+
out = capsys.readouterr().out
|
|
21
|
+
assert exit_code == 1 # cc-by-nc-4.0 -> strong-restriction -> violates default policy
|
|
22
|
+
assert "cc-by-nc-4.0" in out
|
|
23
|
+
assert "strong-restriction" in out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_without_online_flag_unknown_model_stays_unknown(tmp_path: Path, monkeypatch, capsys):
|
|
27
|
+
manifest = tmp_path / "weight-audit-models.txt"
|
|
28
|
+
manifest.write_text("some-org/not-in-offline-db\n", encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
def fail_if_called(model_id, fetch=None):
|
|
31
|
+
raise AssertionError("fetch_declared_license should not be called without --online")
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(cli_module, "fetch_declared_license", fail_if_called)
|
|
34
|
+
|
|
35
|
+
exit_code = main(["scan", str(tmp_path), "--json"])
|
|
36
|
+
out = capsys.readouterr().out
|
|
37
|
+
assert exit_code == 1 # unknown -> violates by default (treat_unknown_as_violation)
|
|
38
|
+
assert "\"tier\": \"unknown\"" in out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_manifest_declared_license_is_not_overwritten_by_online(tmp_path: Path, monkeypatch, capsys):
|
|
42
|
+
manifest = tmp_path / "weight-audit-models.txt"
|
|
43
|
+
manifest.write_text("some-org/has-declared-license,apache-2.0\n", encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
def fail_if_called(model_id, fetch=None):
|
|
46
|
+
raise AssertionError("should not call the network when manifest already has a declared_license")
|
|
47
|
+
|
|
48
|
+
monkeypatch.setattr(cli_module, "fetch_declared_license", fail_if_called)
|
|
49
|
+
|
|
50
|
+
exit_code = main(["scan", str(tmp_path), "--online", "--json"])
|
|
51
|
+
out = capsys.readouterr().out
|
|
52
|
+
assert exit_code == 0
|
|
53
|
+
assert "apache-2.0" in out
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from weight_audit.hf_client import fetch_declared_license
|
|
6
|
+
import urllib.error
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _fake_fetch_cardData(url: str) -> bytes:
|
|
10
|
+
return json.dumps({"cardData": {"license": "apache-2.0"}}).encode()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _fake_fetch_top_level_license(url: str) -> bytes:
|
|
14
|
+
return json.dumps({"license": "mit"}).encode()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _fake_fetch_tag_only(url: str) -> bytes:
|
|
18
|
+
return json.dumps({"tags": ["pytorch", "license:cc-by-4.0", "text-generation"]}).encode()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fake_fetch_no_license(url: str) -> bytes:
|
|
22
|
+
return json.dumps({"tags": ["pytorch"]}).encode()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _fake_fetch_malformed(url: str) -> bytes:
|
|
26
|
+
return b"not json{{{"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fake_fetch_404(url: str) -> bytes:
|
|
30
|
+
raise urllib.error.HTTPError(url, 404, "Not Found", hdrs=None, fp=None) # type: ignore[arg-type]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _fake_fetch_network_error(url: str) -> bytes:
|
|
34
|
+
raise urllib.error.URLError("connection refused")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_reads_cardData_license():
|
|
38
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_cardData) == "apache-2.0"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_reads_top_level_license():
|
|
42
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_top_level_license) == "mit"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_reads_license_tag():
|
|
46
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_tag_only) == "cc-by-4.0"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_returns_none_when_no_license_present():
|
|
50
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_no_license) is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_returns_none_on_malformed_response():
|
|
54
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_malformed) is None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_returns_none_on_404():
|
|
58
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_404) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_returns_none_on_network_error():
|
|
62
|
+
assert fetch_declared_license("org/model", fetch=_fake_fetch_network_error) is None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""License classification for open-weight AI models.
|
|
2
|
+
|
|
3
|
+
Mirrors license-radar's classify.py pattern: normalize to a known
|
|
4
|
+
identifier, bucket into a severity tier, and record the *actual*
|
|
5
|
+
restriction (not just the license name) so policy checks can act on it.
|
|
6
|
+
|
|
7
|
+
The data (named model families, declared-license lookup table) lives
|
|
8
|
+
in license_db.py so the ruleset can grow independently of this
|
|
9
|
+
control-flow logic.
|
|
10
|
+
|
|
11
|
+
Tiers (least to most restrictive), matching license-radar's naming
|
|
12
|
+
convention so users of both tools share a mental model:
|
|
13
|
+
|
|
14
|
+
permissive -- no meaningful restriction on commercial use
|
|
15
|
+
weak-restriction -- attribution / redistribution conditions only
|
|
16
|
+
strong-restriction -- usage caps, acceptable-use policies, or
|
|
17
|
+
non-commercial-only terms
|
|
18
|
+
unknown -- no license metadata found at all
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from .license_db import DECLARED_LICENSE_TIERS, KNOWN_FAMILIES, NON_COMMERCIAL_MARKERS
|
|
26
|
+
from .tiers import Tier
|
|
27
|
+
|
|
28
|
+
__all__ = ["Tier", "LicenseVerdict", "classify"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class LicenseVerdict:
|
|
33
|
+
model_id: str
|
|
34
|
+
license_id: str
|
|
35
|
+
tier: Tier
|
|
36
|
+
obligation: str # human-readable summary of the actual restriction
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def classify(model_id: str, declared_license: str | None = None) -> LicenseVerdict:
|
|
40
|
+
"""Classify a model id (+ optional declared license string from
|
|
41
|
+
the registry's metadata, e.g. from a manifest or --online lookup)
|
|
42
|
+
into a tier and a concrete obligation.
|
|
43
|
+
|
|
44
|
+
Resolution order:
|
|
45
|
+
1. Non-commercial marker in the declared license (unambiguous,
|
|
46
|
+
declared consistently across unrelated orgs).
|
|
47
|
+
2. Named model family (org/model prefix match) -- the highest-value
|
|
48
|
+
path, since it records the *actual* restriction, not just a label.
|
|
49
|
+
3. Declared license against the SPDX/common-identifier fallback table.
|
|
50
|
+
4. Unknown.
|
|
51
|
+
"""
|
|
52
|
+
normalized = model_id.strip().lower()
|
|
53
|
+
|
|
54
|
+
if declared_license:
|
|
55
|
+
lic = declared_license.strip().lower()
|
|
56
|
+
if any(marker in lic for marker in NON_COMMERCIAL_MARKERS):
|
|
57
|
+
return LicenseVerdict(
|
|
58
|
+
model_id=model_id,
|
|
59
|
+
license_id=declared_license,
|
|
60
|
+
tier=Tier.STRONG_RESTRICTION,
|
|
61
|
+
obligation="Non-commercial-only license (CC-BY-NC family); "
|
|
62
|
+
"commercial deployment is not permitted under these terms.",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for prefix, license_id, tier, obligation in KNOWN_FAMILIES:
|
|
66
|
+
if prefix in normalized:
|
|
67
|
+
return LicenseVerdict(
|
|
68
|
+
model_id=model_id,
|
|
69
|
+
license_id=license_id,
|
|
70
|
+
tier=tier,
|
|
71
|
+
obligation=obligation,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if declared_license:
|
|
75
|
+
lic = declared_license.strip().lower()
|
|
76
|
+
if lic in DECLARED_LICENSE_TIERS:
|
|
77
|
+
tier, obligation = DECLARED_LICENSE_TIERS[lic]
|
|
78
|
+
return LicenseVerdict(
|
|
79
|
+
model_id=model_id,
|
|
80
|
+
license_id=declared_license,
|
|
81
|
+
tier=tier,
|
|
82
|
+
obligation=obligation,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return LicenseVerdict(
|
|
86
|
+
model_id=model_id,
|
|
87
|
+
license_id=declared_license or "unknown",
|
|
88
|
+
tier=Tier.UNKNOWN,
|
|
89
|
+
obligation="No license metadata found. Treat as highest risk until "
|
|
90
|
+
"manually verified -- roughly 70% of Hugging Face models ship "
|
|
91
|
+
"with no license at all.",
|
|
92
|
+
)
|