penthera 1.4.0 → 1.5.0

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,329 @@
1
+ /**
2
+ * License policy for `penthera deps --license-policy` and the `licenses`
3
+ * section of penthera.policy.json.
4
+ *
5
+ * "licenses": {
6
+ * "preset": "permissive", // permissive | weak-copyleft-ok | no-agpl
7
+ * "allow": ["Python-2.0"], // added to the preset's allow list
8
+ * "deny": ["WTFPL"], // added to the preset's deny list
9
+ * "allowPackages": ["@acme/*"], // packages exempt from the policy (globs, @version pins)
10
+ * "unknown": "warn" // missing / non-SPDX licenses: "warn" (medium) or "block" (high)
11
+ * }
12
+ *
13
+ * An allow list, when non-empty, is exhaustive: every license in a package's
14
+ * expression must be on it (for OR, one branch is enough). A deny list always
15
+ * wins. `UNLICENSED` (npm's word for "no rights granted") breaches any policy
16
+ * unless the package is in allowPackages. Everything here is pure and offline:
17
+ * license fields come from the lockfile, else the installed package.json.
18
+ */
19
+ import { readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+
22
+ const SPDX = JSON.parse(readFileSync(new URL("./spdx.json", import.meta.url), "utf-8"));
23
+ const byLower = (ids) => new Map(ids.map((id) => [id.toLowerCase(), id]));
24
+ const LICENSES = byLower([...SPDX.licenses, ...SPDX.deprecated]);
25
+ const EXCEPTIONS = byLower(SPDX.exceptions);
26
+
27
+ // Deprecated GNU ids mean "-only"; "+" means "-or-later". Normalising both
28
+ // sides lets a deny of "GPL-3.0-only" catch a package that says "GPL-3.0".
29
+ const GNU = /^(A?GPL|LGPL|GFDL)-(\d\.\d)$/;
30
+ const LEGACY_WITH = /^(GPL-[23]\.0)-with-(GCC|autoconf|bison|classpath|font)-exception$/i;
31
+
32
+ // ── SPDX expression parser ────────────────────────────────────────────────
33
+
34
+ export class SpdxError extends Error {}
35
+
36
+ function tokenize(s) {
37
+ const out = [];
38
+ const re = /\s*(?:(\()|(\))|([A-Za-z0-9][A-Za-z0-9.+:-]*))\s*/y;
39
+ let i = 0;
40
+ while (i < s.length) {
41
+ re.lastIndex = i;
42
+ const m = re.exec(s);
43
+ if (!m || m[0].length === 0) {
44
+ if (/^\s*$/.test(s.slice(i))) break;
45
+ throw new SpdxError(`unexpected character "${s[i]}" at ${i}`);
46
+ }
47
+ i = re.lastIndex;
48
+ out.push(m[1] || m[2] || m[3]);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ const OPS = new Set(["AND", "OR", "WITH"]);
54
+ // SPDX operators are uppercase; lowercase "and"/"or" are common enough on npm
55
+ // ("MIT or Apache-2.0") that rejecting them only produces noise.
56
+ const op = (t) => (t && OPS.has(t.toUpperCase()) ? t.toUpperCase() : null);
57
+
58
+ /**
59
+ * Parse an SPDX license expression into a tree.
60
+ * { type: "license", id, plus, exception, known } (id canonical-cased when known)
61
+ * { type: "and" | "or", args: [...] }
62
+ * Precedence: WITH binds tightest, then AND, then OR. Throws SpdxError.
63
+ */
64
+ export function parseSpdx(input) {
65
+ if (typeof input !== "string" || !input.trim()) throw new SpdxError("empty expression");
66
+ const tokens = tokenize(input);
67
+ let pos = 0;
68
+ const peek = () => tokens[pos];
69
+
70
+ function license() {
71
+ const t = tokens[pos++];
72
+ if (t === undefined) throw new SpdxError("expression ends early");
73
+ if (t === "(" ) {
74
+ const e = orExpr();
75
+ if (tokens[pos++] !== ")") throw new SpdxError("missing )");
76
+ return e;
77
+ }
78
+ if (t === ")" || op(t)) throw new SpdxError(`expected a license, got "${t}"`);
79
+ let id = t;
80
+ let plus = false;
81
+ if (id.endsWith("+")) { plus = true; id = id.slice(0, -1); }
82
+ if (!id || id.includes("+")) throw new SpdxError(`bad license id "${t}"`);
83
+ const ref = /^(?:DocumentRef-[A-Za-z0-9.-]+:)?LicenseRef-[A-Za-z0-9.-]+$/.test(id);
84
+ const canon = LICENSES.get(id.toLowerCase());
85
+ const node = { type: "license", id: canon || id, plus, exception: null, known: !!canon || ref, ref };
86
+ if (op(peek()) === "WITH") {
87
+ pos++;
88
+ const ex = tokens[pos++];
89
+ if (ex === undefined || ex === "(" || ex === ")" || op(ex)) throw new SpdxError("WITH needs an exception id");
90
+ node.exception = EXCEPTIONS.get(ex.toLowerCase()) || ex;
91
+ if (!EXCEPTIONS.has(ex.toLowerCase())) node.known = false;
92
+ }
93
+ return node;
94
+ }
95
+ function chain(type, next) {
96
+ const args = [next()];
97
+ while (op(peek()) === type.toUpperCase()) { pos++; args.push(next()); }
98
+ if (args.length === 1) return args[0];
99
+ // Flatten (a AND (b AND c)) into one list.
100
+ return { type, args: args.flatMap((a) => (a.type === type ? a.args : [a])) };
101
+ }
102
+ const andExpr = () => chain("and", license);
103
+ const orExpr = () => chain("or", andExpr);
104
+
105
+ const tree = orExpr();
106
+ if (pos < tokens.length) throw new SpdxError(`unexpected "${tokens[pos]}"`);
107
+ return tree;
108
+ }
109
+
110
+ /** Every license leaf of a tree. */
111
+ export function leaves(tree) {
112
+ return tree.type === "license" ? [tree] : tree.args.flatMap(leaves);
113
+ }
114
+
115
+ /** Print a tree back as a canonical expression. */
116
+ export function formatSpdx(tree, parent = null) {
117
+ if (tree.type === "license") return `${tree.id}${tree.plus ? "+" : ""}${tree.exception ? ` WITH ${tree.exception}` : ""}`;
118
+ const s = tree.args.map((a) => formatSpdx(a, tree.type)).join(` ${tree.type.toUpperCase()} `);
119
+ return parent && parent !== tree.type ? `(${s})` : s;
120
+ }
121
+
122
+ /** The id a policy compares: deprecated GNU forms and "+" resolved. */
123
+ export function canonicalId(id, plus = false) {
124
+ const legacy = LEGACY_WITH.exec(id);
125
+ if (legacy) return `${legacy[1]}-only`;
126
+ const gnu = GNU.exec(id.replace(/\+$/, ""));
127
+ if (gnu) return `${gnu[1]}-${gnu[2]}-${plus || id.endsWith("+") ? "or-later" : "only"}`;
128
+ return (LICENSES.get(id.toLowerCase()) || id).replace(/\+$/, "");
129
+ }
130
+
131
+ // ── Reading a package's license field ─────────────────────────────────────
132
+
133
+ /** package.json `license` / legacy `licenses: [{ type }]` → an expression string or null. */
134
+ export function licenseField(meta) {
135
+ if (!meta || typeof meta !== "object") return null;
136
+ const one = (v) => (typeof v === "string" ? v : v && typeof v === "object" && typeof v.type === "string" ? v.type : null);
137
+ const l = one(meta.license);
138
+ if (l && l.trim()) return l.trim();
139
+ if (Array.isArray(meta.licenses)) {
140
+ const all = meta.licenses.map(one).filter(Boolean);
141
+ if (all.length) return all.length === 1 ? all[0] : `(${all.join(" OR ")})`;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Classify a raw license string.
148
+ * @returns {{ kind: "spdx"|"unlicensed"|"missing"|"custom"|"unknown", raw, expression?, tree? }}
149
+ * unknown: not valid SPDX, or names an id the SPDX list does not have
150
+ */
151
+ export function classifyLicense(raw) {
152
+ const s = typeof raw === "string" ? raw.trim() : "";
153
+ if (!s || /^(NONE|NOASSERTION)$/i.test(s)) return { kind: "missing", raw: s || null };
154
+ if (/^UNLICEN[SC]ED$/i.test(s)) return { kind: "unlicensed", raw: s };
155
+ if (/^SEE LICEN[SC]E IN\b/i.test(s)) return { kind: "custom", raw: s };
156
+ let tree;
157
+ try {
158
+ tree = parseSpdx(s);
159
+ } catch (e) {
160
+ return { kind: "unknown", raw: s, error: e.message };
161
+ }
162
+ const expression = formatSpdx(tree);
163
+ const unknownIds = leaves(tree).filter((l) => !l.known).map((l) => l.id + (l.exception ? ` WITH ${l.exception}` : ""));
164
+ if (unknownIds.length) return { kind: "unknown", raw: s, expression, tree, unknownIds };
165
+ return { kind: "spdx", raw: s, expression, tree };
166
+ }
167
+
168
+ // ── Presets and policy ────────────────────────────────────────────────────
169
+
170
+ const PERMISSIVE = [
171
+ "MIT", "MIT-0", "ISC", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "Apache-2.0", "Zlib",
172
+ "Unlicense", "CC0-1.0", "BlueOak-1.0.0", "BSL-1.0", "Python-2.0", "PSF-2.0",
173
+ "CC-BY-3.0", "CC-BY-4.0", "Unicode-DFS-2016", "Unicode-3.0", "X11", "WTFPL",
174
+ ];
175
+ const WEAK_COPYLEFT = [
176
+ "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-3.0-only", "LGPL-3.0-or-later",
177
+ "MPL-1.1", "MPL-2.0", "EPL-1.0", "EPL-2.0", "CDDL-1.0", "CDDL-1.1", "Artistic-2.0", "CC-BY-SA-4.0",
178
+ ];
179
+ const NETWORK_COPYLEFT = ["AGPL-1.0-only", "AGPL-1.0-or-later", "AGPL-3.0-only", "AGPL-3.0-or-later", "SSPL-1.0"];
180
+
181
+ export const LICENSE_PRESETS = Object.freeze({
182
+ permissive: { allow: PERMISSIVE, deny: [], about: "permissive licenses only (MIT, ISC, BSD, Apache-2.0, ...)" },
183
+ "weak-copyleft-ok": { allow: [...PERMISSIVE, ...WEAK_COPYLEFT], deny: [], about: "permissive plus weak copyleft (LGPL, MPL, EPL, CDDL)" },
184
+ "no-agpl": { allow: [], deny: NETWORK_COPYLEFT, about: "anything except AGPL and SSPL (network copyleft)" },
185
+ });
186
+
187
+ const LICENSE_FIELDS = ["preset", "allow", "deny", "allowPackages", "unknown"];
188
+
189
+ /** Validate and complete a `licenses` policy section. Throws on bad input. */
190
+ export function normalizeLicensePolicy(raw, label = "licenses") {
191
+ if (typeof raw === "string") raw = { preset: raw };
192
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${label}: expected an object or a preset name`);
193
+ const extra = Object.keys(raw).filter((k) => !LICENSE_FIELDS.includes(k));
194
+ if (extra.length) throw new Error(`${label}: unknown field(s) ${extra.join(", ")} (expected ${LICENSE_FIELDS.join(", ")})`);
195
+ const preset = raw.preset ?? null;
196
+ if (preset !== null && !LICENSE_PRESETS[preset]) {
197
+ throw new Error(`${label}: unknown preset "${preset}". Known: ${Object.keys(LICENSE_PRESETS).join(", ")}`);
198
+ }
199
+ const list = (field) => {
200
+ const v = raw[field] ?? [];
201
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string" || !x.trim())) throw new Error(`${label}: "${field}" must be an array of non-empty strings`);
202
+ return v.map((x) => x.trim());
203
+ };
204
+ const unknown = raw.unknown ?? "warn";
205
+ if (!["warn", "block"].includes(unknown)) throw new Error(`${label}: "unknown" must be "warn" or "block"`);
206
+ const base = preset ? LICENSE_PRESETS[preset] : { allow: [], deny: [] };
207
+ const allow = [...new Set([...base.allow, ...list("allow")])];
208
+ const deny = [...new Set([...base.deny, ...list("deny")])];
209
+ if (!allow.length && !deny.length) throw new Error(`${label}: give a preset, an allow list or a deny list`);
210
+ return { preset, allow, deny, allowPackages: list("allowPackages"), unknown };
211
+ }
212
+
213
+ const globRe = (pattern) => new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`, "i");
214
+
215
+ /** Does one policy entry ("MIT", "GPL-3.0", "AGPL-*", "GPL-2.0-only WITH Classpath-exception-2.0") match a leaf? */
216
+ function entryMatches(entry, leaf) {
217
+ const id = canonicalId(leaf.id, leaf.plus);
218
+ const [e, ex] = entry.split(/\s+WITH\s+/i);
219
+ if (ex && (!leaf.exception || leaf.exception.toLowerCase() !== ex.toLowerCase())) return false;
220
+ if (e.includes("*")) return globRe(e).test(id) || globRe(e).test(leaf.id);
221
+ return canonicalId(e).toLowerCase() === id.toLowerCase();
222
+ }
223
+
224
+ /** Is a package exempt via allowPackages (name globs, optional @version)? */
225
+ export function packageExempt(allowPackages, name, version) {
226
+ return allowPackages.find((entry) => {
227
+ const at = entry.lastIndexOf("@");
228
+ const [n, v] = at > 0 ? [entry.slice(0, at), entry.slice(at + 1)] : [entry, null];
229
+ return globRe(n).test(name) && (!v || v === "*" || v === version);
230
+ }) || null;
231
+ }
232
+
233
+ /**
234
+ * Check one parsed expression against a policy.
235
+ * @returns {{ ok: boolean, offending: string[] }} offending: the licenses that broke it
236
+ */
237
+ export function evaluateExpression(tree, policy) {
238
+ if (tree.type === "license") {
239
+ const denied = policy.deny.some((e) => entryMatches(e, tree));
240
+ const allowed = !policy.allow.length || policy.allow.some((e) => entryMatches(e, tree));
241
+ return denied || !allowed ? { ok: false, offending: [formatSpdx(tree)], denied } : { ok: true, offending: [] };
242
+ }
243
+ const parts = tree.args.map((a) => evaluateExpression(a, policy));
244
+ if (tree.type === "or") {
245
+ return parts.some((p) => p.ok) ? { ok: true, offending: [] } : { ok: false, offending: [...new Set(parts.flatMap((p) => p.offending))], denied: parts.some((p) => p.denied) };
246
+ }
247
+ const bad = parts.filter((p) => !p.ok);
248
+ return bad.length ? { ok: false, offending: [...new Set(bad.flatMap((p) => p.offending))], denied: bad.some((p) => p.denied) } : { ok: true, offending: [] };
249
+ }
250
+
251
+ // ── Findings ──────────────────────────────────────────────────────────────
252
+
253
+ const policyName = (p) => (p.preset ? `the "${p.preset}" preset` : "the license policy");
254
+
255
+ /** Read a package's license: lockfile field first, else its installed package.json. */
256
+ export function packageLicense(pkg, treeRoot) {
257
+ if (pkg.license) return { raw: pkg.license, from: "lockfile" };
258
+ if (treeRoot) {
259
+ try {
260
+ const meta = JSON.parse(readFileSync(join(treeRoot, pkg.path, "package.json"), "utf-8"));
261
+ return { raw: licenseField(meta), from: "package.json" };
262
+ } catch { /* not installed */ }
263
+ }
264
+ return { raw: null, from: null };
265
+ }
266
+
267
+ /**
268
+ * Apply a license policy to lockfile packages.
269
+ * @param {{ name, version, path, license? }[]} pkgs
270
+ * @param {object} policy from normalizeLicensePolicy
271
+ * @param {{ treeRoot?: string, lockfileHasLicenses?: boolean }} opts
272
+ * @returns {{ findings: object[], summary: object }}
273
+ */
274
+ export function checkLicenses(pkgs, policy, opts = {}) {
275
+ const findings = [];
276
+ const counts = {};
277
+ const summary = { policy: policy.preset || "custom", checked: 0, notChecked: 0, exempt: 0, breaches: 0, unlicensed: 0, missing: 0, unknown: 0, counts };
278
+ const unknownSeverity = policy.unknown === "block" ? "high" : "medium";
279
+ const seen = new Set();
280
+ for (const p of pkgs) {
281
+ const at = `${p.name}@${p.version}`;
282
+ if (seen.has(at)) continue;
283
+ seen.add(at);
284
+ const { raw, from } = packageLicense(p, opts.treeRoot);
285
+ // A v1 lockfile has no license fields: an uninstalled package was simply not checked.
286
+ if (!from && !opts.lockfileHasLicenses) { summary.notChecked++; continue; }
287
+ summary.checked++;
288
+ const c = classifyLicense(raw);
289
+ const shown = c.expression || c.raw || "none";
290
+ counts[shown] = (counts[shown] || 0) + 1;
291
+ const exemptBy = packageExempt(policy.allowPackages, p.name, p.version);
292
+ if (exemptBy) { summary.exempt++; continue; }
293
+
294
+ const base = { category: "license", source: "license-policy", method: "license-policy", url: p.path, package: p.name, version: p.version, license: c.raw };
295
+ const fix = `Replace ${p.name}, or exempt it with "allowPackages": ["${p.name}"] in the licenses section of penthera.policy.json if your legal review accepts it.`;
296
+ if (c.kind === "spdx") {
297
+ const r = evaluateExpression(c.tree, policy);
298
+ if (r.ok) continue;
299
+ summary.breaches++;
300
+ findings.push({
301
+ ...base,
302
+ title: `${at}: license ${c.expression} ${r.denied ? "denied by the license policy" : `not allowed by ${policyName(policy)}`}`,
303
+ severity: "high",
304
+ confidence: "confirmed",
305
+ description: `${p.name} is licensed ${c.expression}; ${r.offending.join(", ")} ${r.denied ? "is on the deny list" : "is not on the allow list"} (${from === "lockfile" ? "license field in the lockfile" : "installed package.json"}). ${fix}`,
306
+ });
307
+ } else if (c.kind === "unlicensed") {
308
+ summary.unlicensed++;
309
+ findings.push({
310
+ ...base,
311
+ title: `${at}: UNLICENSED (no rights granted)`,
312
+ severity: "high",
313
+ confidence: "confirmed",
314
+ description: `${p.name} declares "UNLICENSED": its authors grant no right to use, copy or ship it. This is normal for private packages and a problem for anything else. ${fix}`,
315
+ });
316
+ } else {
317
+ summary[c.kind === "missing" ? "missing" : "unknown"]++;
318
+ const what = c.kind === "missing" ? "no license" : c.kind === "custom" ? `a custom license (${c.raw})` : `a license that is not valid SPDX (${c.raw}${c.unknownIds ? `: unknown ${c.unknownIds.join(", ")}` : ""})`;
319
+ findings.push({
320
+ ...base,
321
+ title: `${at}: ${c.kind === "missing" ? "no license declared" : c.kind === "custom" ? "custom license" : `unknown license ${c.raw}`}`,
322
+ severity: unknownSeverity,
323
+ confidence: "likely",
324
+ description: `${p.name} declares ${what}, so the policy cannot be checked automatically. Read its license file before shipping it. ${fix}`,
325
+ });
326
+ }
327
+ }
328
+ return { findings, summary };
329
+ }
@@ -18,7 +18,8 @@ export function nameFromLockPath(key) {
18
18
 
19
19
  /**
20
20
  * Parse lockfile JSON into packages.
21
- * @returns {{ name, version, path, integrity, resolved, dev, optional, hasInstallScript, link }[]}
21
+ * @returns {{ name, version, path, integrity, resolved, dev, optional, hasInstallScript, link, license }[]}
22
+ * license: the lockfile's license field (v2/v3 only; v1 lockfiles have none)
22
23
  */
23
24
  export function parseLockfile(lock) {
24
25
  const out = [];
@@ -39,6 +40,7 @@ export function parseLockfile(lock) {
39
40
  optional: !!(v.optional || v.devOptional),
40
41
  hasInstallScript: !!v.hasInstallScript,
41
42
  link: !!v.link,
43
+ license: typeof v.license === "string" ? v.license : null,
42
44
  });
43
45
  }
44
46
  } else if (lock && lock.dependencies) {
@@ -56,6 +58,7 @@ export function parseLockfile(lock) {
56
58
  optional: !!v.optional,
57
59
  hasInstallScript: false,
58
60
  link: String(v.version || "").startsWith("file:"),
61
+ license: null,
59
62
  });
60
63
  if (v.dependencies) walk(v.dependencies, `${path}/`);
61
64
  }
@@ -24,6 +24,7 @@ export function formatDepsTerminal(deps, c = {}, opts = {}) {
24
24
  const cl = countsLine(deps.capabilityCounts);
25
25
  if (cl) L.push(` Can do ${k.dim(cl)}`);
26
26
  if (deps.typosquats.length) L.push(` Names ${k.yellow(`${deps.typosquats.length} look like popular packages`)}`);
27
+ if (deps.licenses) L.push(` Licenses ${licenseLine(deps.licenses, k)}`);
27
28
 
28
29
  const d = deps.diff;
29
30
  if (d) {
@@ -127,6 +128,18 @@ export function formatDiffMarkdown(deps) {
127
128
  }
128
129
 
129
130
  /** Dependencies section of the full Markdown report. */
131
+ /** One-line license policy summary for the terminal and Markdown. */
132
+ export function licenseLine(l, k = { red: plain, yellow: plain, green: plain, dim: plain }) {
133
+ const issues = [
134
+ l.breaches && k.red(`${l.breaches} breach the policy`),
135
+ l.unlicensed && k.red(`${l.unlicensed} UNLICENSED`),
136
+ l.missing && k.yellow(`${l.missing} without a license`),
137
+ l.unknown && k.yellow(`${l.unknown} unknown or custom`),
138
+ ].filter(Boolean);
139
+ const tail = [l.exempt && `${l.exempt} exempt`, l.notChecked && `${l.notChecked} not installed (v1 lockfile)`].filter(Boolean);
140
+ return `${l.checked} checked against "${l.policy}": ${issues.length ? issues.join(", ") : k.green("all allowed")}${tail.length ? k.dim(` · ${tail.join(" · ")}`) : ""}`;
141
+ }
142
+
130
143
  export function formatDepsMarkdown(deps) {
131
144
  const L = [];
132
145
  L.push("## Dependencies");
@@ -140,6 +153,10 @@ export function formatDepsMarkdown(deps) {
140
153
  for (const id of rows) L.push(`| ${label(id)} | ${deps.capabilityCounts[id]} |`);
141
154
  L.push("");
142
155
  }
156
+ if (deps.licenses) {
157
+ L.push(`**Licenses:** ${licenseLine(deps.licenses)}.`);
158
+ L.push("");
159
+ }
143
160
  if (deps.diff) {
144
161
  L.push("### Capability diff");
145
162
  L.push("");
@@ -0,0 +1 @@
1
+ {"source":"SPDX License List ids via spdx-license-ids@3.0.16 (CC0-1.0) and spdx-exceptions@2.3.0 (CC-BY-3.0, the SPDX project)","licenses":["0BSD","AAL","ADSL","AFL-1.1","AFL-1.2","AFL-2.0","AFL-2.1","AFL-3.0","AGPL-1.0-only","AGPL-1.0-or-later","AGPL-3.0-only","AGPL-3.0-or-later","AMDPLPA","AML","AMPAS","ANTLR-PD","ANTLR-PD-fallback","APAFML","APL-1.0","APSL-1.0","APSL-1.1","APSL-1.2","APSL-2.0","ASWF-Digital-Assets-1.0","ASWF-Digital-Assets-1.1","Abstyles","AdaCore-doc","Adobe-2006","Adobe-Glyph","Adobe-Utopia","Afmparse","Aladdin","Apache-1.0","Apache-1.1","Apache-2.0","App-s2p","Arphic-1999","Artistic-1.0","Artistic-1.0-Perl","Artistic-1.0-cl8","Artistic-2.0","BSD-1-Clause","BSD-2-Clause","BSD-2-Clause-Patent","BSD-2-Clause-Views","BSD-3-Clause","BSD-3-Clause-Attribution","BSD-3-Clause-Clear","BSD-3-Clause-HP","BSD-3-Clause-LBNL","BSD-3-Clause-Modification","BSD-3-Clause-No-Military-License","BSD-3-Clause-No-Nuclear-License","BSD-3-Clause-No-Nuclear-License-2014","BSD-3-Clause-No-Nuclear-Warranty","BSD-3-Clause-Open-MPI","BSD-3-Clause-Sun","BSD-3-Clause-flex","BSD-4-Clause","BSD-4-Clause-Shortened","BSD-4-Clause-UC","BSD-4.3RENO","BSD-4.3TAHOE","BSD-Advertising-Acknowledgement","BSD-Attribution-HPND-disclaimer","BSD-Inferno-Nettverk","BSD-Protection","BSD-Source-Code","BSD-Systemics","BSL-1.0","BUSL-1.1","Baekmuk","Bahyph","Barr","Beerware","BitTorrent-1.0","BitTorrent-1.1","Bitstream-Charter","Bitstream-Vera","BlueOak-1.0.0","Boehm-GC","Borceux","Brian-Gladman-3-Clause","C-UDA-1.0","CAL-1.0","CAL-1.0-Combined-Work-Exception","CATOSL-1.1","CC-BY-1.0","CC-BY-2.0","CC-BY-2.5","CC-BY-2.5-AU","CC-BY-3.0","CC-BY-3.0-AT","CC-BY-3.0-DE","CC-BY-3.0-IGO","CC-BY-3.0-NL","CC-BY-3.0-US","CC-BY-4.0","CC-BY-NC-1.0","CC-BY-NC-2.0","CC-BY-NC-2.5","CC-BY-NC-3.0","CC-BY-NC-3.0-DE","CC-BY-NC-4.0","CC-BY-NC-ND-1.0","CC-BY-NC-ND-2.0","CC-BY-NC-ND-2.5","CC-BY-NC-ND-3.0","CC-BY-NC-ND-3.0-DE","CC-BY-NC-ND-3.0-IGO","CC-BY-NC-ND-4.0","CC-BY-NC-SA-1.0","CC-BY-NC-SA-2.0","CC-BY-NC-SA-2.0-DE","CC-BY-NC-SA-2.0-FR","CC-BY-NC-SA-2.0-UK","CC-BY-NC-SA-2.5","CC-BY-NC-SA-3.0","CC-BY-NC-SA-3.0-DE","CC-BY-NC-SA-3.0-IGO","CC-BY-NC-SA-4.0","CC-BY-ND-1.0","CC-BY-ND-2.0","CC-BY-ND-2.5","CC-BY-ND-3.0","CC-BY-ND-3.0-DE","CC-BY-ND-4.0","CC-BY-SA-1.0","CC-BY-SA-2.0","CC-BY-SA-2.0-UK","CC-BY-SA-2.1-JP","CC-BY-SA-2.5","CC-BY-SA-3.0","CC-BY-SA-3.0-AT","CC-BY-SA-3.0-DE","CC-BY-SA-3.0-IGO","CC-BY-SA-4.0","CC-PDDC","CC0-1.0","CDDL-1.0","CDDL-1.1","CDL-1.0","CDLA-Permissive-1.0","CDLA-Permissive-2.0","CDLA-Sharing-1.0","CECILL-1.0","CECILL-1.1","CECILL-2.0","CECILL-2.1","CECILL-B","CECILL-C","CERN-OHL-1.1","CERN-OHL-1.2","CERN-OHL-P-2.0","CERN-OHL-S-2.0","CERN-OHL-W-2.0","CFITSIO","CMU-Mach","CNRI-Jython","CNRI-Python","CNRI-Python-GPL-Compatible","COIL-1.0","CPAL-1.0","CPL-1.0","CPOL-1.02","CUA-OPL-1.0","Caldera","ClArtistic","Clips","Community-Spec-1.0","Condor-1.1","Cornell-Lossless-JPEG","Cronyx","Crossword","CrystalStacker","Cube","D-FSL-1.0","DL-DE-BY-2.0","DL-DE-ZERO-2.0","DOC","DRL-1.0","DSDP","Dotseqn","ECL-1.0","ECL-2.0","EFL-1.0","EFL-2.0","EPICS","EPL-1.0","EPL-2.0","EUDatagrid","EUPL-1.0","EUPL-1.1","EUPL-1.2","Elastic-2.0","Entessa","ErlPL-1.1","Eurosym","FBM","FDK-AAC","FSFAP","FSFUL","FSFULLR","FSFULLRWD","FTL","Fair","Ferguson-Twofish","Frameworx-1.0","FreeBSD-DOC","FreeImage","Furuseth","GD","GFDL-1.1-invariants-only","GFDL-1.1-invariants-or-later","GFDL-1.1-no-invariants-only","GFDL-1.1-no-invariants-or-later","GFDL-1.1-only","GFDL-1.1-or-later","GFDL-1.2-invariants-only","GFDL-1.2-invariants-or-later","GFDL-1.2-no-invariants-only","GFDL-1.2-no-invariants-or-later","GFDL-1.2-only","GFDL-1.2-or-later","GFDL-1.3-invariants-only","GFDL-1.3-invariants-or-later","GFDL-1.3-no-invariants-only","GFDL-1.3-no-invariants-or-later","GFDL-1.3-only","GFDL-1.3-or-later","GL2PS","GLWTPL","GPL-1.0-only","GPL-1.0-or-later","GPL-2.0-only","GPL-2.0-or-later","GPL-3.0-only","GPL-3.0-or-later","Giftware","Glide","Glulxe","Graphics-Gems","HP-1986","HP-1989","HPND","HPND-DEC","HPND-Markus-Kuhn","HPND-Pbmplus","HPND-UC","HPND-doc","HPND-doc-sell","HPND-export-US","HPND-export-US-modify","HPND-sell-regexpr","HPND-sell-variant","HPND-sell-variant-MIT-disclaimer","HTMLTIDY","HaskellReport","Hippocratic-2.1","IBM-pibs","ICU","IEC-Code-Components-EULA","IJG","IJG-short","IPA","IPL-1.0","ISC","ImageMagick","Imlib2","Info-ZIP","Inner-Net-2.0","Intel","Intel-ACPI","Interbase-1.0","JPL-image","JPNIC","JSON","Jam","JasPer-2.0","Kastrup","Kazlib","Knuth-CTAN","LAL-1.2","LAL-1.3","LGPL-2.0-only","LGPL-2.0-or-later","LGPL-2.1-only","LGPL-2.1-or-later","LGPL-3.0-only","LGPL-3.0-or-later","LGPLLR","LOOP","LPL-1.0","LPL-1.02","LPPL-1.0","LPPL-1.1","LPPL-1.2","LPPL-1.3a","LPPL-1.3c","LZMA-SDK-9.11-to-9.20","LZMA-SDK-9.22","Latex2e","Latex2e-translated-notice","Leptonica","LiLiQ-P-1.1","LiLiQ-R-1.1","LiLiQ-Rplus-1.1","Libpng","Linux-OpenIB","Linux-man-pages-1-para","Linux-man-pages-copyleft","Linux-man-pages-copyleft-2-para","Linux-man-pages-copyleft-var","Lucida-Bitmap-Fonts","MIT","MIT-0","MIT-CMU","MIT-Festival","MIT-Modern-Variant","MIT-Wu","MIT-advertising","MIT-enna","MIT-feh","MIT-open-group","MIT-testregex","MITNFA","MMIXware","MPEG-SSG","MPL-1.0","MPL-1.1","MPL-2.0","MPL-2.0-no-copyleft-exception","MS-LPL","MS-PL","MS-RL","MTLL","MakeIndex","Martin-Birgmeier","McPhee-slideshow","Minpack","MirOS","Motosoto","MulanPSL-1.0","MulanPSL-2.0","Multics","Mup","NAIST-2003","NASA-1.3","NBPL-1.0","NCGL-UK-2.0","NCSA","NGPL","NICTA-1.0","NIST-PD","NIST-PD-fallback","NIST-Software","NLOD-1.0","NLOD-2.0","NLPL","NOSL","NPL-1.0","NPL-1.1","NPOSL-3.0","NRL","NTP","NTP-0","Naumen","Net-SNMP","NetCDF","Newsletr","Nokia","Noweb","O-UDA-1.0","OCCT-PL","OCLC-2.0","ODC-By-1.0","ODbL-1.0","OFFIS","OFL-1.0","OFL-1.0-RFN","OFL-1.0-no-RFN","OFL-1.1","OFL-1.1-RFN","OFL-1.1-no-RFN","OGC-1.0","OGDL-Taiwan-1.0","OGL-Canada-2.0","OGL-UK-1.0","OGL-UK-2.0","OGL-UK-3.0","OGTSL","OLDAP-1.1","OLDAP-1.2","OLDAP-1.3","OLDAP-1.4","OLDAP-2.0","OLDAP-2.0.1","OLDAP-2.1","OLDAP-2.2","OLDAP-2.2.1","OLDAP-2.2.2","OLDAP-2.3","OLDAP-2.4","OLDAP-2.5","OLDAP-2.6","OLDAP-2.7","OLDAP-2.8","OLFL-1.3","OML","OPL-1.0","OPL-UK-3.0","OPUBL-1.0","OSET-PL-2.1","OSL-1.0","OSL-1.1","OSL-2.0","OSL-2.1","OSL-3.0","OpenPBS-2.3","OpenSSL","PADL","PDDL-1.0","PHP-3.0","PHP-3.01","PSF-2.0","Parity-6.0.0","Parity-7.0.0","Plexus","PolyForm-Noncommercial-1.0.0","PolyForm-Small-Business-1.0.0","PostgreSQL","Python-2.0","Python-2.0.1","QPL-1.0","QPL-1.0-INRIA-2004","Qhull","RHeCos-1.1","RPL-1.1","RPL-1.5","RPSL-1.0","RSA-MD","RSCPL","Rdisc","Ruby","SAX-PD","SCEA","SGI-B-1.0","SGI-B-1.1","SGI-B-2.0","SGI-OpenGL","SGP4","SHL-0.5","SHL-0.51","SISSL","SISSL-1.2","SL","SMLNJ","SMPPL","SNIA","SPL-1.0","SSH-OpenSSH","SSH-short","SSPL-1.0","SWL","Saxpath","SchemeReport","Sendmail","Sendmail-8.23","SimPL-2.0","Sleepycat","Soundex","Spencer-86","Spencer-94","Spencer-99","SugarCRM-1.1.3","SunPro","Symlinks","TAPR-OHL-1.0","TCL","TCP-wrappers","TMate","TORQUE-1.1","TOSL","TPDL","TPL-1.0","TTWL","TTYP0","TU-Berlin-1.0","TU-Berlin-2.0","TermReadKey","UCAR","UCL-1.0","UPL-1.0","URT-RLE","Unicode-DFS-2015","Unicode-DFS-2016","Unicode-TOU","UnixCrypt","Unlicense","VOSTROM","VSL-1.0","Vim","W3C","W3C-19980720","W3C-20150513","WTFPL","Watcom-1.0","Widget-Workshop","Wsuipa","X11","X11-distribute-modifications-variant","XFree86-1.1","XSkat","Xdebug-1.03","Xerox","Xfig","Xnet","YPL-1.0","YPL-1.1","ZPL-1.1","ZPL-2.0","ZPL-2.1","Zed","Zeeff","Zend-2.0","Zimbra-1.3","Zimbra-1.4","Zlib","blessing","bzip2-1.0.6","check-cvs","checkmk","copyleft-next-0.3.0","copyleft-next-0.3.1","curl","diffmark","dtoa","dvipdfm","eGenix","etalab-2.0","fwlw","gSOAP-1.3b","gnuplot","iMatix","libpng-2.0","libselinux-1.0","libtiff","libutil-David-Nugent","lsof","magaz","metamail","mpi-permissive","mpich2","mplus","pnmstitch","psfrag","psutils","python-ldap","snprintf","ssh-keyscan","swrule","ulem","w3m","xinetd","xlock","xpp","zlib-acknowledgement"],"deprecated":["AGPL-1.0","AGPL-3.0","BSD-2-Clause-FreeBSD","BSD-2-Clause-NetBSD","GFDL-1.1","GFDL-1.2","GFDL-1.3","GPL-1.0","GPL-1.0+","GPL-2.0","GPL-2.0+","GPL-2.0-with-GCC-exception","GPL-2.0-with-autoconf-exception","GPL-2.0-with-bison-exception","GPL-2.0-with-classpath-exception","GPL-2.0-with-font-exception","GPL-3.0","GPL-3.0+","GPL-3.0-with-GCC-exception","GPL-3.0-with-autoconf-exception","LGPL-2.0","LGPL-2.0+","LGPL-2.1","LGPL-2.1+","LGPL-3.0","LGPL-3.0+","Nunit","StandardML-NJ","bzip2-1.0.5","eCos-2.0","wxWindows"],"exceptions":["389-exception","Autoconf-exception-2.0","Autoconf-exception-3.0","Bison-exception-2.2","Bootloader-exception","Classpath-exception-2.0","CLISP-exception-2.0","DigiRule-FOSS-exception","eCos-exception-2.0","Fawkes-Runtime-exception","FLTK-exception","Font-exception-2.0","freertos-exception-2.0","GCC-exception-2.0","GCC-exception-3.1","gnu-javamail-exception","GPL-3.0-linking-exception","GPL-3.0-linking-source-exception","GPL-CC-1.0","i2p-gpl-java-exception","Libtool-exception","Linux-syscall-note","LLVM-exception","LZMA-exception","mif-exception","Nokia-Qt-exception-1.1","OCaml-LGPL-linking-exception","OCCT-exception-1.0","OpenJDK-assembly-exception-1.0","openvpn-openssl-exception","PS-or-PDF-font-exception-20170817","Qt-GPL-exception-1.0","Qt-LGPL-exception-1.1","Qwt-exception-1.0","Swift-exception","u-boot-exception-2.0","Universal-FOSS-exception-1.0","WxWindows-exception-3.1"]}