copyleft-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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 copyleft-audit contributors
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,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: copyleft-audit
3
+ Version: 0.1.0
4
+ Summary: Fail your CI build when a dependency's licence breaks policy. Detects GPL, AGPL, SSPL and LGPL in installed packages. Zero dependencies, stdlib only.
5
+ Project-URL: Homepage, https://pypi.org/project/copyleft-audit/
6
+ Project-URL: Changelog, https://pypi.org/project/copyleft-audit/#history
7
+ Keywords: copyleft,gpl,agpl,lgpl,sspl,license,licence,license-compliance,license-checker,open-source-compliance,sbom,ci,audit,due-diligence,dependencies
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Legal Industry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # copyleft-audit
31
+
32
+ **Fail your CI build when a dependency's licence breaks policy.**
33
+ Detects GPL, AGPL, SSPL, LGPL, MPL and friends in the packages actually
34
+ installed in your environment — and exits non-zero so the pipeline stops.
35
+
36
+ Zero runtime dependencies. Standard library only. It will not touch your
37
+ dependency tree, and it makes no network calls.
38
+
39
+ ```bash
40
+ pip install copyleft-audit
41
+ copyleft-audit
42
+ ```
43
+
44
+ ## Why this exists
45
+
46
+ `pip-licenses` and similar tools *list* licences for a human to read. That is a
47
+ different job from *enforcing* a policy in CI. If you ship closed-source
48
+ software, the question is not "what licences are in here" but "does the build
49
+ stop when someone adds an AGPL package". This tool answers the second question:
50
+ one command, an exit code, no configuration file required.
51
+
52
+ Typical triggers for needing it: legal asks you to prove no copyleft code ships
53
+ in the product; an acquirer's due-diligence checklist asks the same; or you just
54
+ want the check to run automatically instead of once a year.
55
+
56
+ ## Usage
57
+
58
+ ```
59
+ copyleft-audit [--fail-on {strong,weak,unknown}] [--ignore PKG[,PKG...]]
60
+ [--format {text,json}] [--quiet]
61
+ ```
62
+
63
+ | Exit code | Meaning |
64
+ |---|---|
65
+ | `0` | Clean — no policy violations |
66
+ | `1` | Policy violation |
67
+ | `2` | Usage or internal error |
68
+
69
+ GitHub Actions:
70
+
71
+ ```yaml
72
+ - name: Licence gate
73
+ run: |
74
+ pip install copyleft-audit
75
+ copyleft-audit --fail-on strong
76
+ ```
77
+
78
+ Stricter, for a product that cannot even take weak copyleft or unresolvable
79
+ metadata:
80
+
81
+ ```bash
82
+ copyleft-audit --fail-on unknown --ignore mypackage,internal-sdk
83
+ ```
84
+
85
+ Machine-readable output, for feeding another tool:
86
+
87
+ ```bash
88
+ copyleft-audit --format json
89
+ ```
90
+
91
+ ```json
92
+ {
93
+ "packages": [{"name": "...", "version": "...", "license": "...",
94
+ "license_source": "...", "tier": "...", "ignored": false}],
95
+ "violations": ["... same shape ..."],
96
+ "warnings": ["... same shape ..."],
97
+ "summary": {"tool_version": "...", "fail_on": "strong", "scanned": 0,
98
+ "unreadable": 0, "ignored": [], "counts": {},
99
+ "violation_count": 0, "warning_count": 0, "exit_code": 0}
100
+ }
101
+ ```
102
+
103
+ The JSON schema is stable within the `0.1.x` line; new keys may be added, and
104
+ existing keys will not change meaning without a version bump.
105
+
106
+ ## Risk tiers
107
+
108
+ | Tier | Licences | Default |
109
+ |---|---|---|
110
+ | `STRONG_COPYLEFT` | GPL-2.0, GPL-3.0, AGPL-3.0, SSPL, OSL, EUPL | **fail** |
111
+ | `WEAK_COPYLEFT` | LGPL-2.1, LGPL-3.0, MPL-2.0, EPL-2.0, CDDL, CPL | warn |
112
+ | `PERMISSIVE` | MIT, BSD, Apache-2.0, ISC, PSF, Unlicense, CC0, Zlib, … | pass |
113
+ | `UNKNOWN` | no resolvable licence metadata | warn |
114
+
115
+ `--fail-on weak` promotes weak copyleft to a failure. `--fail-on unknown`
116
+ promotes both weak copyleft and unknown metadata to a failure.
117
+
118
+ ## How it decides, precisely
119
+
120
+ Licence metadata is read in this order, and the first source that resolves wins:
121
+ `License-Expression` (PEP 639) → `License ::` trove classifiers → the free-text
122
+ `License` field. The source used is reported in `license_source`, so you can see
123
+ what the verdict rests on.
124
+
125
+ Deliberate design choices worth knowing about:
126
+
127
+ - **`UNKNOWN` is a real answer.** When metadata is missing, empty, or not
128
+ recognised, the tool says `UNKNOWN`. It never guesses a tier. A confident
129
+ wrong answer here would be worse than no answer.
130
+ - **A package whose metadata cannot be parsed is reported as `UNKNOWN`, never
131
+ skipped.** Silently dropping a package would produce a false clean report.
132
+ - **`LGPL` and `AGPL` are never read as `GPL`.** Matching is on whole tokens and
133
+ masked phrases, not substrings — the classic mis-tiering bug in naive
134
+ scanners.
135
+ - **`MIT OR GPL-3.0` resolves to permissive**, because you may take the MIT
136
+ option. `AND` resolves to the stricter operand. Parenthesised SPDX expressions
137
+ are not parsed and are scored conservatively (highest risk wins).
138
+ - **Multiple licence classifiers are read as a choice**, so the lowest-risk one
139
+ governs — but every licence string found is printed, so nothing is hidden.
140
+
141
+ ## Limits — please read before trusting it
142
+
143
+ - It reads **declared package metadata only**. It does not scan source files,
144
+ vendored code, bundled binaries, or transitively linked native libraries. A
145
+ package that declares MIT while vendoring GPL code will read as MIT.
146
+ - It classifies what is **installed in the current environment**, so run it in
147
+ the same environment your build produces.
148
+ - **This is not legal advice**, and the output is not a legal opinion. It is a
149
+ metadata heuristic that helps engineers catch the obvious cases early. Real
150
+ licence compliance decisions belong with your counsel.
151
+
152
+ ## Paid: auditor-ready attestation report
153
+
154
+ The tool is free and stays free — nothing is gated behind payment.
155
+
156
+ If you need the output as a **document you can hand to an acquirer, an auditor,
157
+ or your own legal team**, there is a one-time paid report for a single project:
158
+ every installed dependency with its declared licence, the metadata field that
159
+ licence was read from, its risk tier, and an explicit list of everything that
160
+ came back `UNKNOWN` with what is missing. Delivered as PDF plus the raw JSON.
161
+ $149, one project, one-time.
162
+
163
+ → **[Order the attestation report](https://buy.stripe.com/3cI8wP9libJt5Xrdd7a7C08)**
164
+
165
+ Same honesty applies: the report documents declared metadata, and it is not a
166
+ legal opinion.
167
+
168
+ ## Disclosure
169
+
170
+ This package was written by an AI agent (Claude) operating autonomously, and is
171
+ published and maintained under human ownership. It is deliberately small,
172
+ dependency-free and standard-library-only so that it can be read end to end and
173
+ verified by inspection before you trust it in a pipeline. Please do read it.
174
+
175
+ Licensed MIT.
@@ -0,0 +1,146 @@
1
+ # copyleft-audit
2
+
3
+ **Fail your CI build when a dependency's licence breaks policy.**
4
+ Detects GPL, AGPL, SSPL, LGPL, MPL and friends in the packages actually
5
+ installed in your environment — and exits non-zero so the pipeline stops.
6
+
7
+ Zero runtime dependencies. Standard library only. It will not touch your
8
+ dependency tree, and it makes no network calls.
9
+
10
+ ```bash
11
+ pip install copyleft-audit
12
+ copyleft-audit
13
+ ```
14
+
15
+ ## Why this exists
16
+
17
+ `pip-licenses` and similar tools *list* licences for a human to read. That is a
18
+ different job from *enforcing* a policy in CI. If you ship closed-source
19
+ software, the question is not "what licences are in here" but "does the build
20
+ stop when someone adds an AGPL package". This tool answers the second question:
21
+ one command, an exit code, no configuration file required.
22
+
23
+ Typical triggers for needing it: legal asks you to prove no copyleft code ships
24
+ in the product; an acquirer's due-diligence checklist asks the same; or you just
25
+ want the check to run automatically instead of once a year.
26
+
27
+ ## Usage
28
+
29
+ ```
30
+ copyleft-audit [--fail-on {strong,weak,unknown}] [--ignore PKG[,PKG...]]
31
+ [--format {text,json}] [--quiet]
32
+ ```
33
+
34
+ | Exit code | Meaning |
35
+ |---|---|
36
+ | `0` | Clean — no policy violations |
37
+ | `1` | Policy violation |
38
+ | `2` | Usage or internal error |
39
+
40
+ GitHub Actions:
41
+
42
+ ```yaml
43
+ - name: Licence gate
44
+ run: |
45
+ pip install copyleft-audit
46
+ copyleft-audit --fail-on strong
47
+ ```
48
+
49
+ Stricter, for a product that cannot even take weak copyleft or unresolvable
50
+ metadata:
51
+
52
+ ```bash
53
+ copyleft-audit --fail-on unknown --ignore mypackage,internal-sdk
54
+ ```
55
+
56
+ Machine-readable output, for feeding another tool:
57
+
58
+ ```bash
59
+ copyleft-audit --format json
60
+ ```
61
+
62
+ ```json
63
+ {
64
+ "packages": [{"name": "...", "version": "...", "license": "...",
65
+ "license_source": "...", "tier": "...", "ignored": false}],
66
+ "violations": ["... same shape ..."],
67
+ "warnings": ["... same shape ..."],
68
+ "summary": {"tool_version": "...", "fail_on": "strong", "scanned": 0,
69
+ "unreadable": 0, "ignored": [], "counts": {},
70
+ "violation_count": 0, "warning_count": 0, "exit_code": 0}
71
+ }
72
+ ```
73
+
74
+ The JSON schema is stable within the `0.1.x` line; new keys may be added, and
75
+ existing keys will not change meaning without a version bump.
76
+
77
+ ## Risk tiers
78
+
79
+ | Tier | Licences | Default |
80
+ |---|---|---|
81
+ | `STRONG_COPYLEFT` | GPL-2.0, GPL-3.0, AGPL-3.0, SSPL, OSL, EUPL | **fail** |
82
+ | `WEAK_COPYLEFT` | LGPL-2.1, LGPL-3.0, MPL-2.0, EPL-2.0, CDDL, CPL | warn |
83
+ | `PERMISSIVE` | MIT, BSD, Apache-2.0, ISC, PSF, Unlicense, CC0, Zlib, … | pass |
84
+ | `UNKNOWN` | no resolvable licence metadata | warn |
85
+
86
+ `--fail-on weak` promotes weak copyleft to a failure. `--fail-on unknown`
87
+ promotes both weak copyleft and unknown metadata to a failure.
88
+
89
+ ## How it decides, precisely
90
+
91
+ Licence metadata is read in this order, and the first source that resolves wins:
92
+ `License-Expression` (PEP 639) → `License ::` trove classifiers → the free-text
93
+ `License` field. The source used is reported in `license_source`, so you can see
94
+ what the verdict rests on.
95
+
96
+ Deliberate design choices worth knowing about:
97
+
98
+ - **`UNKNOWN` is a real answer.** When metadata is missing, empty, or not
99
+ recognised, the tool says `UNKNOWN`. It never guesses a tier. A confident
100
+ wrong answer here would be worse than no answer.
101
+ - **A package whose metadata cannot be parsed is reported as `UNKNOWN`, never
102
+ skipped.** Silently dropping a package would produce a false clean report.
103
+ - **`LGPL` and `AGPL` are never read as `GPL`.** Matching is on whole tokens and
104
+ masked phrases, not substrings — the classic mis-tiering bug in naive
105
+ scanners.
106
+ - **`MIT OR GPL-3.0` resolves to permissive**, because you may take the MIT
107
+ option. `AND` resolves to the stricter operand. Parenthesised SPDX expressions
108
+ are not parsed and are scored conservatively (highest risk wins).
109
+ - **Multiple licence classifiers are read as a choice**, so the lowest-risk one
110
+ governs — but every licence string found is printed, so nothing is hidden.
111
+
112
+ ## Limits — please read before trusting it
113
+
114
+ - It reads **declared package metadata only**. It does not scan source files,
115
+ vendored code, bundled binaries, or transitively linked native libraries. A
116
+ package that declares MIT while vendoring GPL code will read as MIT.
117
+ - It classifies what is **installed in the current environment**, so run it in
118
+ the same environment your build produces.
119
+ - **This is not legal advice**, and the output is not a legal opinion. It is a
120
+ metadata heuristic that helps engineers catch the obvious cases early. Real
121
+ licence compliance decisions belong with your counsel.
122
+
123
+ ## Paid: auditor-ready attestation report
124
+
125
+ The tool is free and stays free — nothing is gated behind payment.
126
+
127
+ If you need the output as a **document you can hand to an acquirer, an auditor,
128
+ or your own legal team**, there is a one-time paid report for a single project:
129
+ every installed dependency with its declared licence, the metadata field that
130
+ licence was read from, its risk tier, and an explicit list of everything that
131
+ came back `UNKNOWN` with what is missing. Delivered as PDF plus the raw JSON.
132
+ $149, one project, one-time.
133
+
134
+ → **[Order the attestation report](https://buy.stripe.com/3cI8wP9libJt5Xrdd7a7C08)**
135
+
136
+ Same honesty applies: the report documents declared metadata, and it is not a
137
+ legal opinion.
138
+
139
+ ## Disclosure
140
+
141
+ This package was written by an AI agent (Claude) operating autonomously, and is
142
+ published and maintained under human ownership. It is deliberately small,
143
+ dependency-free and standard-library-only so that it can be read end to end and
144
+ verified by inspection before you trust it in a pipeline. Please do read it.
145
+
146
+ Licensed MIT.
@@ -0,0 +1,30 @@
1
+ """copyleft-audit: fail a CI build when a dependency's licence breaks policy.
2
+
3
+ Zero runtime dependencies, standard library only. This is a metadata-based
4
+ heuristic aid for engineers, not legal advice.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ from .classify import ( # noqa: F401
12
+ PERMISSIVE,
13
+ STRONG_COPYLEFT,
14
+ TIER_RISK,
15
+ UNKNOWN,
16
+ WEAK_COPYLEFT,
17
+ classify_metadata,
18
+ classify_text,
19
+ )
20
+
21
+ __all__ = [
22
+ "__version__",
23
+ "STRONG_COPYLEFT",
24
+ "WEAK_COPYLEFT",
25
+ "PERMISSIVE",
26
+ "UNKNOWN",
27
+ "TIER_RISK",
28
+ "classify_text",
29
+ "classify_metadata",
30
+ ]
@@ -0,0 +1,10 @@
1
+ """Allow `python -m copyleft_audit` as well as the console script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,256 @@
1
+ """Classify a licence string into a copyleft risk tier.
2
+
3
+ Pure standard library. This module performs no I/O of any kind: no network
4
+ access, no filesystem access, no subprocesses. It is a *metadata heuristic*
5
+ and not a legal determination.
6
+
7
+ Design rules, in order of importance:
8
+
9
+ 1. Never guess. If a licence string is absent, empty, or not recognised, the
10
+ answer is ``UNKNOWN``. A confident wrong answer is worse than no answer.
11
+ 2. Be conservative inside a single licence name: if several markers appear in
12
+ one string, the highest-risk one wins.
13
+ 3. Treat an SPDX ``OR`` as a genuine choice: ``MIT OR GPL-3.0`` resolves to the
14
+ lowest-risk operand, because the user may take that operand. ``AND``
15
+ resolves to the highest-risk operand.
16
+ 4. Never rely on substring matching for ``GPL``. ``AGPL`` and ``LGPL`` both
17
+ contain the substring ``GPL`` and mis-tiering them is the classic failure
18
+ of naive licence scanners. Matching here is on whole tokens and on masked
19
+ phrases, so ``LGPL`` can never be read as ``GPL``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+
26
+ __all__ = [
27
+ "STRONG_COPYLEFT",
28
+ "WEAK_COPYLEFT",
29
+ "PERMISSIVE",
30
+ "UNKNOWN",
31
+ "TIER_RISK",
32
+ "classify_text",
33
+ "classify_metadata",
34
+ ]
35
+
36
+ STRONG_COPYLEFT = "STRONG_COPYLEFT"
37
+ WEAK_COPYLEFT = "WEAK_COPYLEFT"
38
+ PERMISSIVE = "PERMISSIVE"
39
+ UNKNOWN = "UNKNOWN"
40
+
41
+ #: Ordering used by min()/max(). UNKNOWN sits between permissive and weak so
42
+ #: that it sorts sensibly, but it is excluded from every comparison that
43
+ #: resolves an expression -- see _known().
44
+ TIER_RISK = {PERMISSIVE: 0, UNKNOWN: 1, WEAK_COPYLEFT: 2, STRONG_COPYLEFT: 3}
45
+
46
+ #: Some projects paste an entire licence text into the free-text ``License``
47
+ #: metadata field. Only the head of such a blob is classified, because the body
48
+ #: of the LGPL quotes the GPL repeatedly and would otherwise mis-tier.
49
+ MAX_TEXT = 300
50
+
51
+ # Phrases are matched most-specific-first and masked out once matched, so that
52
+ # "Lesser General Public License" can never also fire "General Public License".
53
+ _PHRASES = (
54
+ ("AFFERO GENERAL PUBLIC LICENSE", STRONG_COPYLEFT),
55
+ ("SERVER SIDE PUBLIC LICENSE", STRONG_COPYLEFT),
56
+ ("OPEN SOFTWARE LICENSE", STRONG_COPYLEFT),
57
+ ("EUROPEAN UNION PUBLIC LICENCE", STRONG_COPYLEFT),
58
+ ("EUROPEAN UNION PUBLIC LICENSE", STRONG_COPYLEFT),
59
+ ("LESSER GENERAL PUBLIC LICENSE", WEAK_COPYLEFT),
60
+ ("LIBRARY GENERAL PUBLIC LICENSE", WEAK_COPYLEFT),
61
+ ("GENERAL PUBLIC LICENSE", STRONG_COPYLEFT),
62
+ ("MOZILLA PUBLIC LICENSE", WEAK_COPYLEFT),
63
+ ("ECLIPSE PUBLIC LICENSE", WEAK_COPYLEFT),
64
+ ("COMMON DEVELOPMENT AND DISTRIBUTION LICENSE", WEAK_COPYLEFT),
65
+ ("COMMON PUBLIC LICENSE", WEAK_COPYLEFT),
66
+ ("PYTHON SOFTWARE FOUNDATION LICENSE", PERMISSIVE),
67
+ ("APACHE SOFTWARE LICENSE", PERMISSIVE),
68
+ ("BOOST SOFTWARE LICENSE", PERMISSIVE),
69
+ ("UNIVERSAL PERMISSIVE LICENSE", PERMISSIVE),
70
+ ("HISTORICAL PERMISSION NOTICE", PERMISSIVE),
71
+ ("ZOPE PUBLIC LICENSE", PERMISSIVE),
72
+ ("ACADEMIC FREE LICENSE", PERMISSIVE),
73
+ ("PUBLIC DOMAIN", PERMISSIVE),
74
+ ("BSD LICENSE", PERMISSIVE),
75
+ ("MIT LICENSE", PERMISSIVE),
76
+ ("MIT NO ATTRIBUTION", PERMISSIVE),
77
+ )
78
+
79
+ # Whole-token markers, applied after digits are stripped and a trailing "v"
80
+ # (as in GPLv3 -> GPLV -> GPL) is removed.
81
+ _WORDS = {
82
+ "AGPL": STRONG_COPYLEFT,
83
+ "SSPL": STRONG_COPYLEFT,
84
+ "OSL": STRONG_COPYLEFT,
85
+ "EUPL": STRONG_COPYLEFT,
86
+ "LGPL": WEAK_COPYLEFT,
87
+ "GPL": STRONG_COPYLEFT,
88
+ "MPL": WEAK_COPYLEFT,
89
+ "EPL": WEAK_COPYLEFT,
90
+ "CDDL": WEAK_COPYLEFT,
91
+ "CPL": WEAK_COPYLEFT,
92
+ "MIT": PERMISSIVE,
93
+ "BSD": PERMISSIVE,
94
+ "APACHE": PERMISSIVE,
95
+ "ISC": PERMISSIVE,
96
+ "PSF": PERMISSIVE,
97
+ "NCSA": PERMISSIVE,
98
+ "AFL": PERMISSIVE,
99
+ "ZLIB": PERMISSIVE,
100
+ "UNLICENSE": PERMISSIVE,
101
+ "WTFPL": PERMISSIVE,
102
+ "POSTGRESQL": PERMISSIVE,
103
+ }
104
+
105
+ # Tokens whose digits are meaningful and must not be stripped. "CC" on its own
106
+ # is deliberately absent: CC0 is a public-domain dedication, but CC-BY-NC is
107
+ # not a permissive software licence, so anything else Creative Commons stays
108
+ # UNKNOWN rather than being guessed at.
109
+ _EXACT_TOKENS = {"CC0": PERMISSIVE}
110
+
111
+ _OR_RE = re.compile(r"\s+OR\s+", re.IGNORECASE)
112
+ _AND_RE = re.compile(r"\s+AND\s+", re.IGNORECASE)
113
+ _WITH_RE = re.compile(r"\s+WITH\s+.*$", re.IGNORECASE | re.DOTALL)
114
+ _TOKEN_RE = re.compile(r"[A-Z0-9]+")
115
+ _NON_ALPHA_RE = re.compile(r"[^A-Z]+")
116
+ _DIGIT_RE = re.compile(r"[0-9]")
117
+
118
+
119
+ def _known(tiers):
120
+ """Drop UNKNOWN, which must never win a min()/max() against a real tier."""
121
+ return [t for t in tiers if t != UNKNOWN]
122
+
123
+
124
+ def _norm_token(token):
125
+ """GPLV3 -> GPL, 0BSD -> BSD, LGPL-2.1 -> LGPL."""
126
+ stripped = _DIGIT_RE.sub("", token)
127
+ if stripped.endswith("V"):
128
+ stripped = stripped[:-1]
129
+ return stripped
130
+
131
+
132
+ def _atom_tier(text):
133
+ """Highest-risk tier detectable within a single licence name.
134
+
135
+ Conservative on purpose: within one name, any strong-copyleft marker wins.
136
+ """
137
+ upper = text.upper()
138
+ found = []
139
+
140
+ for token in _TOKEN_RE.findall(upper):
141
+ if token in _EXACT_TOKENS:
142
+ found.append(_EXACT_TOKENS[token])
143
+
144
+ norm = " " + " ".join(_NON_ALPHA_RE.sub(" ", upper).split()) + " "
145
+ for phrase, tier in _PHRASES:
146
+ padded = " " + phrase + " "
147
+ if padded in norm:
148
+ found.append(tier)
149
+ norm = norm.replace(padded, " ")
150
+
151
+ for token in norm.split():
152
+ word = _norm_token(token)
153
+ if word in _WORDS:
154
+ found.append(_WORDS[word])
155
+
156
+ known = _known(found)
157
+ if not known:
158
+ return UNKNOWN
159
+ return max(known, key=lambda t: TIER_RISK[t])
160
+
161
+
162
+ def _conjunction_tier(text):
163
+ """Resolve an SPDX AND: the strictest operand governs."""
164
+ tiers = [_atom_tier(_WITH_RE.sub("", part)) for part in _AND_RE.split(text)]
165
+ known = _known(tiers)
166
+ if not known:
167
+ return UNKNOWN
168
+ return max(known, key=lambda t: TIER_RISK[t])
169
+
170
+
171
+ def classify_text(text):
172
+ """Classify one licence string. Returns a tier constant.
173
+
174
+ Handles whitespace-delimited SPDX ``OR`` / ``AND`` operators. Parenthesised
175
+ expressions are not parsed -- SPDX grouping is out of scope for a
176
+ dependency-free heuristic -- so a string containing parentheses is scored
177
+ conservatively as a single atom (highest risk wins). This is also what
178
+ makes PyPI trove classifiers such as
179
+ ``GNU General Public License v3 (GPLv3)`` behave correctly.
180
+ """
181
+ if not text:
182
+ return UNKNOWN
183
+ raw = text.strip()
184
+ if not raw:
185
+ return UNKNOWN
186
+ if len(raw) > MAX_TEXT:
187
+ raw = raw[:MAX_TEXT]
188
+ if "(" in raw or ")" in raw:
189
+ return _atom_tier(raw)
190
+ tiers = [_conjunction_tier(part) for part in _OR_RE.split(raw)]
191
+ known = _known(tiers)
192
+ if not known:
193
+ return UNKNOWN
194
+ return min(known, key=lambda t: TIER_RISK[t])
195
+
196
+
197
+ def _classifier_tail(classifier):
198
+ """'License :: OSI Approved :: MIT License' -> 'MIT License'."""
199
+ parts = [p.strip() for p in classifier.split("::")]
200
+ return parts[-1] if parts else ""
201
+
202
+
203
+ def _shorten(text, limit=120):
204
+ flat = " ".join((text or "").split())
205
+ if len(flat) <= limit:
206
+ return flat
207
+ return flat[: limit - 3] + "..."
208
+
209
+
210
+ def classify_metadata(expression=None, classifiers=None, license_field=None):
211
+ """Classify one distribution from its metadata fields.
212
+
213
+ Sources are consulted in the order defined by PEP 639 practice:
214
+ ``License-Expression`` first, then ``License ::`` trove classifiers, then
215
+ the free-text ``License`` field. The first source that yields anything
216
+ other than UNKNOWN wins.
217
+
218
+ Several licence classifiers on one distribution are read as a disjunction
219
+ (the project offers a choice), so the lowest-risk one governs -- but every
220
+ licence string found is returned for display, so nothing is hidden from
221
+ the reader.
222
+
223
+ Returns ``(tier, licence_string, source)``.
224
+ """
225
+ if expression and expression.strip():
226
+ tier = classify_text(expression)
227
+ if tier != UNKNOWN:
228
+ return tier, _shorten(expression), "License-Expression"
229
+
230
+ licence_classifiers = [
231
+ c for c in (classifiers or [])
232
+ if c and c.strip().upper().startswith("LICENSE ::")
233
+ ]
234
+ if licence_classifiers:
235
+ tails = [_classifier_tail(c) for c in licence_classifiers]
236
+ tiers = [classify_text(t) for t in tails]
237
+ known = _known(tiers)
238
+ if known:
239
+ tier = min(known, key=lambda t: TIER_RISK[t])
240
+ return tier, _shorten("; ".join(tails)), "Classifier"
241
+
242
+ if license_field and license_field.strip():
243
+ tier = classify_text(license_field)
244
+ if tier != UNKNOWN:
245
+ return tier, _shorten(license_field), "License"
246
+
247
+ # Nothing conclusive. Show whatever was declared so a human can judge.
248
+ if licence_classifiers:
249
+ shown = _shorten("; ".join(_classifier_tail(c) for c in licence_classifiers))
250
+ elif license_field and license_field.strip():
251
+ shown = _shorten(license_field)
252
+ elif expression and expression.strip():
253
+ shown = _shorten(expression)
254
+ else:
255
+ shown = ""
256
+ return UNKNOWN, shown, "none"
@@ -0,0 +1,286 @@
1
+ """Command-line interface for copyleft-audit.
2
+
3
+ Reads the licence metadata of the distributions installed in the current
4
+ environment and exits non-zero when one of them breaks the configured policy.
5
+ Intended to be dropped into a CI job. Standard library only.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import re
13
+ import sys
14
+
15
+ from . import __version__
16
+ from .classify import (
17
+ PERMISSIVE,
18
+ STRONG_COPYLEFT,
19
+ UNKNOWN,
20
+ WEAK_COPYLEFT,
21
+ classify_metadata,
22
+ )
23
+
24
+ try: # Python 3.8+
25
+ from importlib import metadata as importlib_metadata
26
+ except ImportError: # pragma: no cover
27
+ importlib_metadata = None
28
+
29
+ EXIT_OK = 0
30
+ EXIT_VIOLATION = 1
31
+ EXIT_ERROR = 2
32
+
33
+ FAIL_ON_TIERS = {
34
+ "strong": (STRONG_COPYLEFT,),
35
+ "weak": (STRONG_COPYLEFT, WEAK_COPYLEFT),
36
+ "unknown": (STRONG_COPYLEFT, WEAK_COPYLEFT, UNKNOWN),
37
+ }
38
+
39
+ TIER_ORDER_FOR_DISPLAY = (STRONG_COPYLEFT, WEAK_COPYLEFT, UNKNOWN, PERMISSIVE)
40
+
41
+ _NAME_NORM_RE = re.compile(r"[-_.]+")
42
+
43
+
44
+ def normalize_name(name):
45
+ """PEP 503 name normalisation."""
46
+ return _NAME_NORM_RE.sub("-", (name or "").strip()).lower()
47
+
48
+
49
+ def _get(metadata, key):
50
+ try:
51
+ return metadata.get(key)
52
+ except Exception:
53
+ return None
54
+
55
+
56
+ def _get_all(metadata, key):
57
+ try:
58
+ return metadata.get_all(key) or []
59
+ except Exception:
60
+ return []
61
+
62
+
63
+ def collect_packages():
64
+ """Return (packages, unreadable_count) for the current environment.
65
+
66
+ A distribution whose metadata cannot be parsed is reported as UNKNOWN
67
+ rather than skipped. Silently dropping a package would produce a false
68
+ clean report, which is the single worst outcome for a tool like this.
69
+ """
70
+ if importlib_metadata is None:
71
+ raise RuntimeError("importlib.metadata is unavailable; Python 3.8+ is required")
72
+
73
+ seen = {}
74
+ unreadable = 0
75
+ for dist in importlib_metadata.distributions():
76
+ try:
77
+ metadata = dist.metadata
78
+ name = _get(metadata, "Name") if metadata is not None else None
79
+ except Exception:
80
+ unreadable += 1
81
+ continue
82
+ if not name:
83
+ unreadable += 1
84
+ continue
85
+ key = normalize_name(name)
86
+ if key in seen:
87
+ continue
88
+ try:
89
+ tier, licence, source = classify_metadata(
90
+ expression=_get(metadata, "License-Expression"),
91
+ classifiers=_get_all(metadata, "Classifier"),
92
+ license_field=_get(metadata, "License"),
93
+ )
94
+ version = _get(metadata, "Version") or ""
95
+ except Exception:
96
+ tier, licence, source, version = UNKNOWN, "", "unreadable", ""
97
+ unreadable += 1
98
+ seen[key] = {
99
+ "name": name,
100
+ "version": version,
101
+ "license": licence,
102
+ "license_source": source,
103
+ "tier": tier,
104
+ "ignored": False,
105
+ }
106
+ return [seen[k] for k in sorted(seen)], unreadable
107
+
108
+
109
+ def build_parser():
110
+ parser = argparse.ArgumentParser(
111
+ prog="copyleft-audit",
112
+ description=(
113
+ "Fail the build when an installed dependency's licence breaks policy. "
114
+ "Reads declared package metadata only. This is an engineering "
115
+ "heuristic, not legal advice."
116
+ ),
117
+ epilog="Exit codes: 0 clean, 1 policy violation, 2 usage or internal error.",
118
+ )
119
+ parser.add_argument(
120
+ "--fail-on",
121
+ choices=("strong", "weak", "unknown"),
122
+ default="strong",
123
+ help=(
124
+ "Lowest tier that fails the build. 'strong' (default) fails only on "
125
+ "strong copyleft; 'weak' also fails on weak copyleft; 'unknown' also "
126
+ "fails on packages with no resolvable licence metadata."
127
+ ),
128
+ )
129
+ parser.add_argument(
130
+ "--ignore",
131
+ default="",
132
+ metavar="PKG[,PKG...]",
133
+ help="Comma-separated distribution names to exempt from the policy check.",
134
+ )
135
+ parser.add_argument(
136
+ "--format",
137
+ dest="fmt",
138
+ choices=("text", "json"),
139
+ default="text",
140
+ help="Output format. 'json' emits a stable machine-readable schema.",
141
+ )
142
+ parser.add_argument(
143
+ "--quiet",
144
+ action="store_true",
145
+ help="Print only violations. A clean run prints nothing.",
146
+ )
147
+ parser.add_argument(
148
+ "--version",
149
+ action="version",
150
+ version="copyleft-audit " + __version__,
151
+ )
152
+ return parser
153
+
154
+
155
+ def _render_text(stream, packages, violations, warnings, summary, quiet):
156
+ if not quiet:
157
+ stream.write(
158
+ "copyleft-audit %s | policy: fail on %s | %d distributions scanned\n"
159
+ % (__version__, summary["fail_on"], summary["scanned"])
160
+ )
161
+ counts = summary["counts"]
162
+ stream.write(
163
+ " strong=%d weak=%d unknown=%d permissive=%d\n"
164
+ % (
165
+ counts[STRONG_COPYLEFT],
166
+ counts[WEAK_COPYLEFT],
167
+ counts[UNKNOWN],
168
+ counts[PERMISSIVE],
169
+ )
170
+ )
171
+ if summary["unreadable"]:
172
+ stream.write(
173
+ " note: %d distribution(s) had unreadable metadata and are "
174
+ "reported as UNKNOWN\n" % summary["unreadable"]
175
+ )
176
+ stream.write("\n")
177
+
178
+ if violations:
179
+ stream.write("VIOLATIONS (%d)\n" % len(violations))
180
+ for pkg in violations:
181
+ stream.write(
182
+ " %-28s %-12s %-16s %s\n"
183
+ % (
184
+ pkg["name"][:28],
185
+ pkg["version"][:12],
186
+ pkg["tier"],
187
+ pkg["license"] or "<no licence metadata>",
188
+ )
189
+ )
190
+ stream.write("\n")
191
+
192
+ if warnings and not quiet:
193
+ stream.write("WARNINGS, not failing under this policy (%d)\n" % len(warnings))
194
+ for pkg in warnings:
195
+ stream.write(
196
+ " %-28s %-12s %-16s %s\n"
197
+ % (
198
+ pkg["name"][:28],
199
+ pkg["version"][:12],
200
+ pkg["tier"],
201
+ pkg["license"] or "<no licence metadata>",
202
+ )
203
+ )
204
+ stream.write("\n")
205
+
206
+ if not quiet:
207
+ if violations:
208
+ stream.write("RESULT: FAIL - %d policy violation(s).\n" % len(violations))
209
+ else:
210
+ stream.write("RESULT: PASS - no policy violations.\n")
211
+ stream.write(
212
+ "Declared metadata only; no source scanning. Not legal advice.\n"
213
+ )
214
+
215
+
216
+ def main(argv=None):
217
+ parser = build_parser()
218
+ try:
219
+ args = parser.parse_args(argv)
220
+ except SystemExit as exc: # --help / --version / usage error
221
+ code = exc.code
222
+ return code if isinstance(code, int) else EXIT_ERROR
223
+
224
+ ignored = set()
225
+ for item in args.ignore.split(","):
226
+ if item.strip():
227
+ ignored.add(normalize_name(item))
228
+
229
+ try:
230
+ packages, unreadable = collect_packages()
231
+ except Exception as exc: # pragma: no cover
232
+ sys.stderr.write("copyleft-audit: internal error: %s\n" % exc)
233
+ return EXIT_ERROR
234
+
235
+ fail_tiers = FAIL_ON_TIERS[args.fail_on]
236
+ counts = {STRONG_COPYLEFT: 0, WEAK_COPYLEFT: 0, UNKNOWN: 0, PERMISSIVE: 0}
237
+ violations = []
238
+ warnings = []
239
+ for pkg in packages:
240
+ counts[pkg["tier"]] = counts.get(pkg["tier"], 0) + 1
241
+ pkg["ignored"] = normalize_name(pkg["name"]) in ignored
242
+ if pkg["tier"] == PERMISSIVE:
243
+ continue
244
+ if pkg["ignored"]:
245
+ continue
246
+ if pkg["tier"] in fail_tiers:
247
+ violations.append(pkg)
248
+ else:
249
+ warnings.append(pkg)
250
+
251
+ def _sort_key(pkg):
252
+ return (TIER_ORDER_FOR_DISPLAY.index(pkg["tier"]), normalize_name(pkg["name"]))
253
+
254
+ violations.sort(key=_sort_key)
255
+ warnings.sort(key=_sort_key)
256
+
257
+ exit_code = EXIT_VIOLATION if violations else EXIT_OK
258
+ summary = {
259
+ "tool": "copyleft-audit",
260
+ "tool_version": __version__,
261
+ "fail_on": args.fail_on,
262
+ "scanned": len(packages),
263
+ "unreadable": unreadable,
264
+ "ignored": sorted(ignored),
265
+ "counts": counts,
266
+ "violation_count": len(violations),
267
+ "warning_count": len(warnings),
268
+ "exit_code": exit_code,
269
+ }
270
+
271
+ if args.fmt == "json":
272
+ payload = {
273
+ "packages": packages,
274
+ "violations": violations,
275
+ "warnings": warnings,
276
+ "summary": summary,
277
+ }
278
+ sys.stdout.write(json.dumps(payload, indent=2, sort_keys=True) + "\n")
279
+ else:
280
+ _render_text(sys.stdout, packages, violations, warnings, summary, args.quiet)
281
+
282
+ return exit_code
283
+
284
+
285
+ if __name__ == "__main__": # pragma: no cover
286
+ sys.exit(main())
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: copyleft-audit
3
+ Version: 0.1.0
4
+ Summary: Fail your CI build when a dependency's licence breaks policy. Detects GPL, AGPL, SSPL and LGPL in installed packages. Zero dependencies, stdlib only.
5
+ Project-URL: Homepage, https://pypi.org/project/copyleft-audit/
6
+ Project-URL: Changelog, https://pypi.org/project/copyleft-audit/#history
7
+ Keywords: copyleft,gpl,agpl,lgpl,sspl,license,licence,license-compliance,license-checker,open-source-compliance,sbom,ci,audit,due-diligence,dependencies
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Legal Industry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # copyleft-audit
31
+
32
+ **Fail your CI build when a dependency's licence breaks policy.**
33
+ Detects GPL, AGPL, SSPL, LGPL, MPL and friends in the packages actually
34
+ installed in your environment — and exits non-zero so the pipeline stops.
35
+
36
+ Zero runtime dependencies. Standard library only. It will not touch your
37
+ dependency tree, and it makes no network calls.
38
+
39
+ ```bash
40
+ pip install copyleft-audit
41
+ copyleft-audit
42
+ ```
43
+
44
+ ## Why this exists
45
+
46
+ `pip-licenses` and similar tools *list* licences for a human to read. That is a
47
+ different job from *enforcing* a policy in CI. If you ship closed-source
48
+ software, the question is not "what licences are in here" but "does the build
49
+ stop when someone adds an AGPL package". This tool answers the second question:
50
+ one command, an exit code, no configuration file required.
51
+
52
+ Typical triggers for needing it: legal asks you to prove no copyleft code ships
53
+ in the product; an acquirer's due-diligence checklist asks the same; or you just
54
+ want the check to run automatically instead of once a year.
55
+
56
+ ## Usage
57
+
58
+ ```
59
+ copyleft-audit [--fail-on {strong,weak,unknown}] [--ignore PKG[,PKG...]]
60
+ [--format {text,json}] [--quiet]
61
+ ```
62
+
63
+ | Exit code | Meaning |
64
+ |---|---|
65
+ | `0` | Clean — no policy violations |
66
+ | `1` | Policy violation |
67
+ | `2` | Usage or internal error |
68
+
69
+ GitHub Actions:
70
+
71
+ ```yaml
72
+ - name: Licence gate
73
+ run: |
74
+ pip install copyleft-audit
75
+ copyleft-audit --fail-on strong
76
+ ```
77
+
78
+ Stricter, for a product that cannot even take weak copyleft or unresolvable
79
+ metadata:
80
+
81
+ ```bash
82
+ copyleft-audit --fail-on unknown --ignore mypackage,internal-sdk
83
+ ```
84
+
85
+ Machine-readable output, for feeding another tool:
86
+
87
+ ```bash
88
+ copyleft-audit --format json
89
+ ```
90
+
91
+ ```json
92
+ {
93
+ "packages": [{"name": "...", "version": "...", "license": "...",
94
+ "license_source": "...", "tier": "...", "ignored": false}],
95
+ "violations": ["... same shape ..."],
96
+ "warnings": ["... same shape ..."],
97
+ "summary": {"tool_version": "...", "fail_on": "strong", "scanned": 0,
98
+ "unreadable": 0, "ignored": [], "counts": {},
99
+ "violation_count": 0, "warning_count": 0, "exit_code": 0}
100
+ }
101
+ ```
102
+
103
+ The JSON schema is stable within the `0.1.x` line; new keys may be added, and
104
+ existing keys will not change meaning without a version bump.
105
+
106
+ ## Risk tiers
107
+
108
+ | Tier | Licences | Default |
109
+ |---|---|---|
110
+ | `STRONG_COPYLEFT` | GPL-2.0, GPL-3.0, AGPL-3.0, SSPL, OSL, EUPL | **fail** |
111
+ | `WEAK_COPYLEFT` | LGPL-2.1, LGPL-3.0, MPL-2.0, EPL-2.0, CDDL, CPL | warn |
112
+ | `PERMISSIVE` | MIT, BSD, Apache-2.0, ISC, PSF, Unlicense, CC0, Zlib, … | pass |
113
+ | `UNKNOWN` | no resolvable licence metadata | warn |
114
+
115
+ `--fail-on weak` promotes weak copyleft to a failure. `--fail-on unknown`
116
+ promotes both weak copyleft and unknown metadata to a failure.
117
+
118
+ ## How it decides, precisely
119
+
120
+ Licence metadata is read in this order, and the first source that resolves wins:
121
+ `License-Expression` (PEP 639) → `License ::` trove classifiers → the free-text
122
+ `License` field. The source used is reported in `license_source`, so you can see
123
+ what the verdict rests on.
124
+
125
+ Deliberate design choices worth knowing about:
126
+
127
+ - **`UNKNOWN` is a real answer.** When metadata is missing, empty, or not
128
+ recognised, the tool says `UNKNOWN`. It never guesses a tier. A confident
129
+ wrong answer here would be worse than no answer.
130
+ - **A package whose metadata cannot be parsed is reported as `UNKNOWN`, never
131
+ skipped.** Silently dropping a package would produce a false clean report.
132
+ - **`LGPL` and `AGPL` are never read as `GPL`.** Matching is on whole tokens and
133
+ masked phrases, not substrings — the classic mis-tiering bug in naive
134
+ scanners.
135
+ - **`MIT OR GPL-3.0` resolves to permissive**, because you may take the MIT
136
+ option. `AND` resolves to the stricter operand. Parenthesised SPDX expressions
137
+ are not parsed and are scored conservatively (highest risk wins).
138
+ - **Multiple licence classifiers are read as a choice**, so the lowest-risk one
139
+ governs — but every licence string found is printed, so nothing is hidden.
140
+
141
+ ## Limits — please read before trusting it
142
+
143
+ - It reads **declared package metadata only**. It does not scan source files,
144
+ vendored code, bundled binaries, or transitively linked native libraries. A
145
+ package that declares MIT while vendoring GPL code will read as MIT.
146
+ - It classifies what is **installed in the current environment**, so run it in
147
+ the same environment your build produces.
148
+ - **This is not legal advice**, and the output is not a legal opinion. It is a
149
+ metadata heuristic that helps engineers catch the obvious cases early. Real
150
+ licence compliance decisions belong with your counsel.
151
+
152
+ ## Paid: auditor-ready attestation report
153
+
154
+ The tool is free and stays free — nothing is gated behind payment.
155
+
156
+ If you need the output as a **document you can hand to an acquirer, an auditor,
157
+ or your own legal team**, there is a one-time paid report for a single project:
158
+ every installed dependency with its declared licence, the metadata field that
159
+ licence was read from, its risk tier, and an explicit list of everything that
160
+ came back `UNKNOWN` with what is missing. Delivered as PDF plus the raw JSON.
161
+ $149, one project, one-time.
162
+
163
+ → **[Order the attestation report](https://buy.stripe.com/3cI8wP9libJt5Xrdd7a7C08)**
164
+
165
+ Same honesty applies: the report documents declared metadata, and it is not a
166
+ legal opinion.
167
+
168
+ ## Disclosure
169
+
170
+ This package was written by an AI agent (Claude) operating autonomously, and is
171
+ published and maintained under human ownership. It is deliberately small,
172
+ dependency-free and standard-library-only so that it can be read end to end and
173
+ verified by inspection before you trust it in a pipeline. Please do read it.
174
+
175
+ Licensed MIT.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ copyleft_audit/__init__.py
5
+ copyleft_audit/__main__.py
6
+ copyleft_audit/classify.py
7
+ copyleft_audit/cli.py
8
+ copyleft_audit.egg-info/PKG-INFO
9
+ copyleft_audit.egg-info/SOURCES.txt
10
+ copyleft_audit.egg-info/dependency_links.txt
11
+ copyleft_audit.egg-info/entry_points.txt
12
+ copyleft_audit.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ copyleft-audit = copyleft_audit.cli:main
@@ -0,0 +1 @@
1
+ copyleft_audit
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "copyleft-audit"
7
+ version = "0.1.0"
8
+ description = "Fail your CI build when a dependency's licence breaks policy. Detects GPL, AGPL, SSPL and LGPL in installed packages. Zero dependencies, stdlib only."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ keywords = [
12
+ "copyleft",
13
+ "gpl",
14
+ "agpl",
15
+ "lgpl",
16
+ "sspl",
17
+ "license",
18
+ "licence",
19
+ "license-compliance",
20
+ "license-checker",
21
+ "open-source-compliance",
22
+ "sbom",
23
+ "ci",
24
+ "audit",
25
+ "due-diligence",
26
+ "dependencies",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Environment :: Console",
31
+ "Intended Audience :: Developers",
32
+ "Intended Audience :: Legal Industry",
33
+ "License :: OSI Approved :: MIT License",
34
+ "Operating System :: OS Independent",
35
+ "Programming Language :: Python :: 3",
36
+ "Programming Language :: Python :: 3.8",
37
+ "Programming Language :: Python :: 3.9",
38
+ "Programming Language :: Python :: 3.10",
39
+ "Programming Language :: Python :: 3.11",
40
+ "Programming Language :: Python :: 3.12",
41
+ "Programming Language :: Python :: 3.13",
42
+ "Programming Language :: Python :: 3 :: Only",
43
+ "Topic :: Software Development :: Quality Assurance",
44
+ "Topic :: System :: Software Distribution",
45
+ "Topic :: Utilities",
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://pypi.org/project/copyleft-audit/"
50
+ Changelog = "https://pypi.org/project/copyleft-audit/#history"
51
+
52
+ [project.scripts]
53
+ copyleft-audit = "copyleft_audit.cli:main"
54
+
55
+ [tool.setuptools]
56
+ packages = ["copyleft_audit"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+