periapsis 0.0.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.
- package/README.md +65 -0
- package/bin/periapsis.mjs +416 -0
- package/package.json +14 -0
- package/spdx_licenses_with_categories.json +4352 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Periapsis (draft)
|
|
2
|
+
|
|
3
|
+
License compliance radar for Node dependencies. Reads `package-lock.json` + installed `node_modules`, emits a full SBOM JSON, and fails CI when licenses violate your allowlist. Supports exceptions and traces upstream chains for each violation.
|
|
4
|
+
|
|
5
|
+
## Install / Run
|
|
6
|
+
|
|
7
|
+
Local clone:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install # only needed if you add dependencies later; currently none
|
|
11
|
+
npx periapsis --allowed ../allowed-licenses.example.json --violations-out ../sbom-violations.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
When published to npm (recommended):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx periapsis --allowed .sbom-allowlist.json --violations-out sbom-violations.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Initialize an allowlist based on SPDX categories:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx periapsis init --preset strict
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
- `--root <path>`: project root (defaults to `cwd`)
|
|
29
|
+
- `--lock <file>`: lockfile (default `package-lock.json`)
|
|
30
|
+
- `--out <file>`: SBOM JSON output (default `sbom-licenses.json`)
|
|
31
|
+
- `--allowed <file>`: allowlist JSON
|
|
32
|
+
- `--violations-out <file>`: write violations JSON (includes upstream chains)
|
|
33
|
+
- `--quiet`: suppress summary output
|
|
34
|
+
- `init --preset <level>`: write `allowedConfig.json` (strict | standard | permissive)
|
|
35
|
+
- `init --force`: overwrite existing `allowedConfig.json`
|
|
36
|
+
|
|
37
|
+
Allowlist file schema:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"allowedCategories": ["A", "B"],
|
|
42
|
+
"allowedLicenses": ["MIT", "Apache-2.0"],
|
|
43
|
+
"exceptions": ["package-name", "name@1.2.3"]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## GitHub Action (example)
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: 20
|
|
54
|
+
cache: npm
|
|
55
|
+
- run: npm ci
|
|
56
|
+
- run: npx periapsis --allowed .sbom-allowlist.json --violations-out sbom-violations.json
|
|
57
|
+
- run: |
|
|
58
|
+
node -e "const fs=require('fs');const p='sbom-violations.json';if(!fs.existsSync(p))process.exit(1);const d=JSON.parse(fs.readFileSync(p,'utf8'));const c=Array.isArray(d)?d.length:Array.isArray(d.violations)?d.violations.length:0;if(c>0){console.error(`Found ${c} license violations`);process.exit(1);}console.log('No license violations');"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Publish checklist
|
|
62
|
+
|
|
63
|
+
- Update `package.json` name/version, remove `"private": true` when ready.
|
|
64
|
+
- Add CI (lint/test) and changelog.
|
|
65
|
+
- Tag a release and publish to npm: `npm publish`.
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = {};
|
|
11
|
+
let i = 0;
|
|
12
|
+
if (argv[0] && !argv[0].startsWith('-')) {
|
|
13
|
+
args.command = argv[0];
|
|
14
|
+
i = 1;
|
|
15
|
+
}
|
|
16
|
+
for (; i < argv.length; i++) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (arg === '--help' || arg === '-h') {
|
|
19
|
+
args.help = true;
|
|
20
|
+
} else if (arg.startsWith('--')) {
|
|
21
|
+
const key = arg.slice(2);
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (next && !next.startsWith('--')) {
|
|
24
|
+
args[key] = next;
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
args[key] = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
console.log(`Usage: periapsis [options]
|
|
36
|
+
periapsis init [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--root <path> Project root (default: cwd)
|
|
40
|
+
--lock <file> Lockfile to read (default: package-lock.json)
|
|
41
|
+
--out <file> SBOM JSON output (default: sbom-licenses.json)
|
|
42
|
+
--allowed <file> JSON file containing allowed licenses and optional exceptions
|
|
43
|
+
--violations-out <file> Where to write violating packages (optional)
|
|
44
|
+
--quiet Suppress summary output
|
|
45
|
+
-h, --help Show this help
|
|
46
|
+
|
|
47
|
+
Init options:
|
|
48
|
+
--preset <level> strict | standard | permissive (default: strict)
|
|
49
|
+
--force Overwrite existing allowedConfig.json
|
|
50
|
+
|
|
51
|
+
Allowed licenses file:
|
|
52
|
+
Can be an array of strings or an object like:
|
|
53
|
+
{
|
|
54
|
+
"allowedCategories": ["A", "B"],
|
|
55
|
+
"allowedLicenses": ["MIT", "Apache-2.0"],
|
|
56
|
+
"exceptions": ["package-name", "other@1.2.3"]
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadJson(filePath, description) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Failed to read ${description} at ${filePath}: ${err.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadAllowedConfig(filePath) {
|
|
71
|
+
const raw = loadJson(filePath, 'allowed licenses file');
|
|
72
|
+
const list = Array.isArray(raw) ? raw : raw?.allowedLicenses || [];
|
|
73
|
+
if (!Array.isArray(list)) {
|
|
74
|
+
console.error(
|
|
75
|
+
'Allowed licenses file must be an array or contain { "allowedLicenses": [] }'
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const allowedCategoriesRaw = Array.isArray(raw?.allowedCategories)
|
|
80
|
+
? raw.allowedCategories
|
|
81
|
+
: [];
|
|
82
|
+
if (raw?.allowedCategories && !Array.isArray(raw.allowedCategories)) {
|
|
83
|
+
console.error('allowedCategories must be an array like ["A", "B"]');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const allowedLicenses = new Set(list.map((l) => String(l).trim()).filter(Boolean));
|
|
87
|
+
const allowedCategories = new Set(
|
|
88
|
+
allowedCategoriesRaw.map((c) => String(c).trim().toUpperCase()).filter(Boolean)
|
|
89
|
+
);
|
|
90
|
+
const exceptions = new Set(
|
|
91
|
+
(raw?.exceptions || []).map((e) => String(e).trim()).filter(Boolean)
|
|
92
|
+
);
|
|
93
|
+
return { allowedLicenses, allowedCategories, exceptions };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractLicense(pkgJson) {
|
|
97
|
+
if (!pkgJson) return null;
|
|
98
|
+
const lic = pkgJson.license || pkgJson.licenses;
|
|
99
|
+
if (!lic) return null;
|
|
100
|
+
if (typeof lic === 'string') return lic;
|
|
101
|
+
if (Array.isArray(lic)) {
|
|
102
|
+
return lic
|
|
103
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry?.type || entry?.name))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join(' OR ');
|
|
106
|
+
}
|
|
107
|
+
if (typeof lic === 'object') return lic.type || lic.name || null;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function licenseTokens(license) {
|
|
112
|
+
if (!license) return ['UNKNOWN'];
|
|
113
|
+
return license
|
|
114
|
+
.split(/\s*(?:\(|\)|\+|\/|\s+OR\s+|\s+AND\s+|,|;|\||\s+with\s+)/i)
|
|
115
|
+
.map((t) => t.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function loadSpdxCategories() {
|
|
120
|
+
const spdxPath = path.resolve(__dirname, '..', 'spdx_licenses_with_categories.json');
|
|
121
|
+
if (!fs.existsSync(spdxPath)) {
|
|
122
|
+
return new Map();
|
|
123
|
+
}
|
|
124
|
+
const list = loadJson(spdxPath, 'SPDX licenses with categories');
|
|
125
|
+
const map = new Map();
|
|
126
|
+
if (Array.isArray(list)) {
|
|
127
|
+
for (const entry of list) {
|
|
128
|
+
if (!entry) continue;
|
|
129
|
+
const id = entry.identifier ? String(entry.identifier).trim() : '';
|
|
130
|
+
const category = entry.defaultCategory ? String(entry.defaultCategory).trim() : '';
|
|
131
|
+
if (id && category) map.set(id, category.toUpperCase());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return map;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveDepPath(packages, parentKey, depName) {
|
|
138
|
+
const nested = parentKey ? path.posix.join(parentKey, 'node_modules', depName) : `node_modules/${depName}`;
|
|
139
|
+
if (packages[nested]) return nested;
|
|
140
|
+
const top = `node_modules/${depName}`;
|
|
141
|
+
if (packages[top]) return top;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildSbom({ root, lockPath }) {
|
|
146
|
+
if (!fs.existsSync(lockPath)) {
|
|
147
|
+
console.error(`Lockfile not found at ${lockPath}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const lock = loadJson(lockPath, 'lockfile');
|
|
151
|
+
const packages = lock.packages || {};
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const results = [];
|
|
154
|
+
const pathMap = new Map();
|
|
155
|
+
const reverseDeps = new Map(); // childPath -> Set<parentPath>
|
|
156
|
+
|
|
157
|
+
function addReverse(child, parent) {
|
|
158
|
+
if (!reverseDeps.has(child)) reverseDeps.set(child, new Set());
|
|
159
|
+
reverseDeps.get(child).add(parent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build SBOM entries and reverse dependency edges
|
|
163
|
+
for (const [key, meta] of Object.entries(packages)) {
|
|
164
|
+
if (key === '' || !key.startsWith('node_modules')) continue;
|
|
165
|
+
const absPath = path.join(root, key);
|
|
166
|
+
const pkgJsonPath = path.join(absPath, 'package.json');
|
|
167
|
+
let pkgJson = null;
|
|
168
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
169
|
+
try {
|
|
170
|
+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore parse errors, fallback to lock data
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const name =
|
|
176
|
+
meta.name ||
|
|
177
|
+
pkgJson?.name ||
|
|
178
|
+
key.replace('node_modules/', '').split('node_modules/').pop();
|
|
179
|
+
const version = meta.version || pkgJson?.version || 'UNKNOWN';
|
|
180
|
+
const id = `${name}@${version}`;
|
|
181
|
+
if (seen.has(id)) continue;
|
|
182
|
+
seen.add(id);
|
|
183
|
+
|
|
184
|
+
const license = extractLicense(pkgJson) || meta.license || 'UNKNOWN';
|
|
185
|
+
const repository = pkgJson?.repository || null;
|
|
186
|
+
const entry = { name, version, license, path: key, repository };
|
|
187
|
+
results.push(entry);
|
|
188
|
+
pathMap.set(key, entry);
|
|
189
|
+
|
|
190
|
+
const deps = meta.dependencies ? Object.keys(meta.dependencies) : [];
|
|
191
|
+
for (const depName of deps) {
|
|
192
|
+
const depPath = resolveDepPath(packages, key, depName);
|
|
193
|
+
if (depPath) addReverse(depPath, key);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
results.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
198
|
+
// Add root-level edges
|
|
199
|
+
const rootDeps = packages['']?.dependencies ? Object.keys(packages['']?.dependencies) : [];
|
|
200
|
+
for (const depName of rootDeps) {
|
|
201
|
+
const depPath = resolveDepPath(packages, '', depName);
|
|
202
|
+
if (depPath) addReverse(depPath, '__root__');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { sbom: results, pathMap, reverseDeps };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isLicenseAllowed(token, allowedConfig, spdxCategories) {
|
|
209
|
+
if (!allowedConfig) return true;
|
|
210
|
+
const { allowedLicenses, allowedCategories } = allowedConfig;
|
|
211
|
+
if (allowedLicenses.has(token)) return true;
|
|
212
|
+
if (allowedCategories.size === 0) return false;
|
|
213
|
+
const category = spdxCategories.get(token);
|
|
214
|
+
if (!category) return false;
|
|
215
|
+
return allowedCategories.has(category);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function evaluateCompliance(sbom, allowedConfig, spdxCategories) {
|
|
219
|
+
if (!allowedConfig) return { violations: [] };
|
|
220
|
+
if (
|
|
221
|
+
allowedConfig.allowedLicenses.size === 0 &&
|
|
222
|
+
allowedConfig.allowedCategories.size === 0
|
|
223
|
+
) {
|
|
224
|
+
return { violations: [] };
|
|
225
|
+
}
|
|
226
|
+
const { exceptions } = allowedConfig;
|
|
227
|
+
const violations = [];
|
|
228
|
+
for (const item of sbom) {
|
|
229
|
+
const id = `${item.name}@${item.version}`;
|
|
230
|
+
if (exceptions.has(item.name) || exceptions.has(id)) continue;
|
|
231
|
+
const tokens = licenseTokens(item.license);
|
|
232
|
+
const allowed = tokens.some((t) => isLicenseAllowed(t, allowedConfig, spdxCategories));
|
|
233
|
+
if (!allowed) violations.push(item);
|
|
234
|
+
}
|
|
235
|
+
return { violations };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getUpstreamChains(targetPath, reverseDeps, pathMap, { limit = 30 } = {}) {
|
|
239
|
+
const memo = new Map();
|
|
240
|
+
function dfs(node, trail = []) {
|
|
241
|
+
if (memo.has(node)) return memo.get(node);
|
|
242
|
+
const parents = reverseDeps.get(node);
|
|
243
|
+
if (!parents || parents.size === 0) return [[node]];
|
|
244
|
+
const chains = [];
|
|
245
|
+
for (const parent of parents) {
|
|
246
|
+
if (trail.includes(parent)) continue; // avoid cycles
|
|
247
|
+
const parentChains = dfs(parent, [...trail, node]);
|
|
248
|
+
for (const chain of parentChains) {
|
|
249
|
+
chains.push([...chain, node]);
|
|
250
|
+
if (chains.length >= limit) break;
|
|
251
|
+
}
|
|
252
|
+
if (chains.length >= limit) break;
|
|
253
|
+
}
|
|
254
|
+
memo.set(node, chains);
|
|
255
|
+
return chains;
|
|
256
|
+
}
|
|
257
|
+
const rawChains = dfs(targetPath);
|
|
258
|
+
// Convert to labels and strip root marker
|
|
259
|
+
const toLabel = (p) => {
|
|
260
|
+
if (p === '__root__') return null;
|
|
261
|
+
const entry = pathMap.get(p);
|
|
262
|
+
return entry ? `${entry.name}@${entry.version}` : p;
|
|
263
|
+
};
|
|
264
|
+
return rawChains
|
|
265
|
+
.map((chain) => chain.map(toLabel).filter(Boolean))
|
|
266
|
+
.filter((chain) => chain.length > 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function writeJson(filePath, data) {
|
|
270
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function summarize(sbom, violations) {
|
|
274
|
+
const counts = new Map();
|
|
275
|
+
for (const { license } of sbom) {
|
|
276
|
+
const key = license || 'UNKNOWN';
|
|
277
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
278
|
+
}
|
|
279
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
280
|
+
console.log(`Total packages: ${sbom.length}`);
|
|
281
|
+
console.log('Top licenses:');
|
|
282
|
+
for (const [lic, count] of sorted.slice(0, 8)) {
|
|
283
|
+
console.log(` ${lic}: ${count}`);
|
|
284
|
+
}
|
|
285
|
+
if (violations.length === 0) {
|
|
286
|
+
console.log('All packages comply with allowed licenses.');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log(`Violations (${violations.length}):`);
|
|
290
|
+
const rows = violations.map((v) => [`${v.name}@${v.version}`, v.license || 'UNKNOWN']);
|
|
291
|
+
const colWidths = [0, 0];
|
|
292
|
+
for (const [pkg, lic] of rows) {
|
|
293
|
+
colWidths[0] = Math.max(colWidths[0], pkg.length);
|
|
294
|
+
colWidths[1] = Math.max(colWidths[1], lic.length);
|
|
295
|
+
}
|
|
296
|
+
const divider = '-'.repeat(colWidths[0] + colWidths[1] + 5);
|
|
297
|
+
console.log(divider);
|
|
298
|
+
console.log(
|
|
299
|
+
`${'Package'.padEnd(colWidths[0])} | ${'License'.padEnd(colWidths[1])}`
|
|
300
|
+
);
|
|
301
|
+
console.log(divider);
|
|
302
|
+
for (const [pkg, lic] of rows) {
|
|
303
|
+
console.log(`${pkg.padEnd(colWidths[0])} | ${lic.padEnd(colWidths[1])}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(divider);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resolveAllowedConfigPath(root, allowedArg) {
|
|
309
|
+
if (allowedArg) {
|
|
310
|
+
return path.resolve(allowedArg.startsWith('.') ? path.join(root, allowedArg) : allowedArg);
|
|
311
|
+
}
|
|
312
|
+
return path.resolve(root, 'allowedConfig.json');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function initAllowedConfig({ root, allowedPath, preset, force }) {
|
|
316
|
+
const categories =
|
|
317
|
+
preset === 'standard'
|
|
318
|
+
? ['A', 'B']
|
|
319
|
+
: preset === 'permissive'
|
|
320
|
+
? ['A', 'B', 'C']
|
|
321
|
+
: ['A'];
|
|
322
|
+
if (fs.existsSync(allowedPath) && !force) {
|
|
323
|
+
console.error(`Allowed config already exists at ${allowedPath}. Use --force to overwrite.`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const payload = {
|
|
327
|
+
allowedCategories: categories,
|
|
328
|
+
allowedLicenses: [],
|
|
329
|
+
exceptions: []
|
|
330
|
+
};
|
|
331
|
+
writeJson(allowedPath, payload);
|
|
332
|
+
console.log(`Wrote allowed config to ${allowedPath}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function main() {
|
|
336
|
+
const args = parseArgs(process.argv.slice(2));
|
|
337
|
+
if (args.help) {
|
|
338
|
+
printHelp();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const root = args.root ? path.resolve(args.root) : process.cwd();
|
|
343
|
+
if (args.command === 'init') {
|
|
344
|
+
const preset = args.preset ? String(args.preset).toLowerCase() : 'strict';
|
|
345
|
+
if (!['strict', 'standard', 'permissive'].includes(preset)) {
|
|
346
|
+
console.error('Invalid preset. Use strict, standard, or permissive.');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const allowedPath = resolveAllowedConfigPath(root, args.allowed);
|
|
350
|
+
initAllowedConfig({ root, allowedPath, preset, force: Boolean(args.force) });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lockPath = path.resolve(root, args.lock || 'package-lock.json');
|
|
355
|
+
const outPath = path.resolve(root, args.out || 'sbom-licenses.json');
|
|
356
|
+
const violationsOut = args['violations-out']
|
|
357
|
+
? path.resolve(root, args['violations-out'])
|
|
358
|
+
: null;
|
|
359
|
+
const allowedPath = args.allowed
|
|
360
|
+
? path.resolve(args.allowed.startsWith('.') ? path.join(root, args.allowed) : args.allowed)
|
|
361
|
+
: null;
|
|
362
|
+
|
|
363
|
+
const allowedConfig = allowedPath ? loadAllowedConfig(allowedPath) : null;
|
|
364
|
+
const spdxCategories =
|
|
365
|
+
allowedConfig && allowedConfig.allowedCategories.size > 0 ? loadSpdxCategories() : new Map();
|
|
366
|
+
const { sbom, pathMap, reverseDeps } = buildSbom({ root, lockPath });
|
|
367
|
+
|
|
368
|
+
const { violations } = evaluateCompliance(sbom, allowedConfig, spdxCategories);
|
|
369
|
+
// Decorate violations with upstream chains
|
|
370
|
+
const violationsWithUpstream = violations.map((v) => {
|
|
371
|
+
const upstream = getUpstreamChains(v.path, reverseDeps, pathMap, { limit: 50 });
|
|
372
|
+
return { ...v, upstream };
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
writeJson(outPath, sbom);
|
|
376
|
+
if (violationsOut) {
|
|
377
|
+
writeJson(violationsOut, violationsWithUpstream);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!args.quiet) {
|
|
381
|
+
console.log(`Wrote SBOM to ${outPath}`);
|
|
382
|
+
if (allowedConfig) {
|
|
383
|
+
if (allowedConfig.allowedCategories.size) {
|
|
384
|
+
console.log(
|
|
385
|
+
`Allowed categories: ${[...allowedConfig.allowedCategories].sort().join(', ')}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (allowedConfig.allowedLicenses.size) {
|
|
389
|
+
console.log(`Allowed licenses: ${[...allowedConfig.allowedLicenses].join(', ')}`);
|
|
390
|
+
}
|
|
391
|
+
if (allowedConfig.exceptions.size) {
|
|
392
|
+
console.log(
|
|
393
|
+
`Exceptions (${allowedConfig.exceptions.size}): ${[...allowedConfig.exceptions].join(', ')}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (violationsOut && violations) {
|
|
398
|
+
console.log(`Wrote violations to ${violationsOut}`);
|
|
399
|
+
}
|
|
400
|
+
if (violationsWithUpstream.length) {
|
|
401
|
+
summarize(sbom, violationsWithUpstream || []);
|
|
402
|
+
// print a brief upstream view
|
|
403
|
+
console.log('Upstream chains (first path per violation):');
|
|
404
|
+
for (const v of violationsWithUpstream) {
|
|
405
|
+
const chain = v.upstream?.[0] || [];
|
|
406
|
+
console.log(
|
|
407
|
+
` ${v.name}@${v.version}: ${chain.length ? chain.join(' -> ') : 'no upstream (direct)'}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
summarize(sbom, violations || []);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "periapsis",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"periapsis": "bin/periapsis.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"description": "Lightweight SBOM/license checker with allowlist, exceptions, and upstream chains.",
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|