aere-quantum/crypto-inventory/inventar.mjs

99 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
// Inventarul criptografic al unui cod sursa (CBOM CycloneDX 1.6). Fara dependinte externe.
// Folosire: node inventar.mjs scan <dosar> [--out cbom.json] [--summary] [--fail-on vulnerable]
// Codul de iesire: 0 = bine, 1 = conditia --fail-on indeplinita, 2 = eroare de folosire sau de scanare.
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { scaneaza, VERSIUNE, DOSARE_EXCLUSE_IMPLICIT } from './lib/scan.mjs';
import { construiesteCbom } from './lib/cbom.mjs';
import { rezumatText, verificaFailOn, numarPeClase } from './lib/rezumat.mjs';
import { evalueaza, CLS } from './lib/catalog.mjs';
export { scaneaza as scan, construiesteCbom as buildCbom, rezumatText as renderSummary, verificaFailOn as checkFailOn, numarPeClase as countByClass, evalueaza as classify, CLS as CLASSES, VERSIUNE as VERSION, DOSARE_EXCLUSE_IMPLICIT as DEFAULT_EXCLUDED_DIRS };
const AJUTOR = `aere-crypto-inventory ${VERSIUNE}
Usage:
node inventar.mjs scan <dir> [options]
Options:
--out <file> write the CycloneDX 1.6 CBOM to <file> (default: stdout, unless --summary is given)
--summary print a text summary to stdout
--fail-on <list> exit with code 1 if findings match: vulnerable, weak, unknown, any (comma-separated)
--max-file-bytes <n> skip (and count) files larger than n bytes (default 1048576)
--max-files <n> read at most n files; the rest are counted, not read (default 50000)
--exclude-dir <name> do not descend into directories with this name (repeatable)
--no-default-excludes also descend into ${DOSARE_EXCLUSE_IMPLICIT.join(', ')}
--deterministic serial number derived from content, no timestamp (reproducible output)
--findings-json <file> write the raw findings (one object per occurrence) as JSON
Exit codes: 0 ok, 1 --fail-on condition met, 2 usage or scan error.`;
export function main(argv) {
const a = argv.slice();
const cmd = a.shift();
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(AJUTOR + '\n'); return cmd ? 0 : 2; }
if (cmd === '--version') { process.stdout.write(VERSIUNE + '\n'); return 0; }
if (cmd !== 'scan') { process.stderr.write(`unknown command: ${cmd}\n${AJUTOR}\n`); return 2; }
let dir = null;
const o = { excludeDirs: [] };
let out = null;
let summary = false;
let failOn = null;
let determinist = false;
let findingsJson = null;
const numar = (x, nume) => {
if (!/^\d+$/.test(String(x))) throw new Error(`${nume} needs a non-negative integer`);
return Number(x);
};
try {
while (a.length) {
const x = a.shift();
if (x === '--out') out = a.shift();
else if (x === '--summary') summary = true;
else if (x === '--fail-on') failOn = a.shift();
else if (x === '--max-file-bytes') o.maxFileBytes = numar(a.shift(), x);
else if (x === '--max-files') o.maxFiles = numar(a.shift(), x);
else if (x === '--exclude-dir') o.excludeDirs.push(a.shift());
else if (x === '--no-default-excludes') o.noDefaultExcludes = true;
else if (x === '--deterministic') determinist = true;
else if (x === '--findings-json') findingsJson = a.shift();
else if (x.startsWith('--')) throw new Error(`unknown option: ${x}`);
else if (!dir) dir = x;
else throw new Error(`unexpected argument: ${x}`);
}
if (!dir) throw new Error('missing <dir>');
if (out === undefined || findingsJson === undefined || failOn === undefined) throw new Error('option is missing its value');
verificaFailOn([], failOn);
} catch (err) {
process.stderr.write(`error: ${err.message}\n${AJUTOR}\n`);
return 2;
}
let rez;
try {
rez = scaneaza(dir, o);
} catch (err) {
process.stderr.write(`error: ${err.message}\n`);
return 2;
}
const bom = construiesteCbom(rez, { deterministic: determinist });
const json = JSON.stringify(bom, null, 2) + '\n';
if (out) {
writeFileSync(out, json);
process.stderr.write(`CBOM written: ${resolve(out)} (${bom.components.length} components)\n`);
}
if (findingsJson) writeFileSync(findingsJson, JSON.stringify(rez.findings, null, 2) + '\n');
if (summary) process.stdout.write(rezumatText(rez) + '\n');
if (!out && !summary) process.stdout.write(json);
const f = verificaFailOn(rez.findings, failOn);
if (f.esec) {
process.stderr.write(`fail-on ${failOn}: ${f.motive.join('; ')}\n`);
return 1;
}
return 0;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exitCode = main(process.argv.slice(2));
}