aere-quantum/crypto-inventory/lib/cbom.mjs

159 lines
8.7 KiB
JavaScript

// Constructia CBOM-ului CycloneDX 1.6 din rezultatul scanarii.
import { createHash, randomUUID } from 'node:crypto';
export const ENUM = Object.freeze({
componentType: ['application', 'framework', 'library', 'container', 'platform', 'operating-system', 'device', 'device-driver', 'firmware', 'file', 'machine-learning-model', 'data', 'cryptographic-asset'],
assetType: ['algorithm', 'certificate', 'protocol', 'related-crypto-material'],
primitive: ['drbg', 'mac', 'block-cipher', 'stream-cipher', 'signature', 'hash', 'pke', 'xof', 'kdf', 'key-agree', 'kem', 'ae', 'combiner', 'other', 'unknown'],
mode: ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr', 'other', 'unknown'],
padding: ['pkcs5', 'pkcs7', 'pkcs1v15', 'oaep', 'raw', 'other', 'unknown'],
cryptoFunctions: ['generate', 'keygen', 'encrypt', 'decrypt', 'digest', 'tag', 'keyderive', 'sign', 'verify', 'encapsulate', 'decapsulate', 'other', 'unknown'],
protocolType: ['tls', 'ssh', 'ipsec', 'ike', 'sstp', 'wpa', 'other', 'unknown'],
materialType: ['private-key', 'public-key', 'secret-key', 'key', 'ciphertext', 'signature', 'digest', 'initialization-vector', 'nonce', 'seed', 'salt', 'shared-secret', 'tag', 'additional-data', 'password', 'credential', 'token', 'other', 'unknown'],
});
const P = 'aere:crypto-inventory:';
function slug(s) {
return String(s).toLowerCase().replace(/[^a-z0-9.+-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'x';
}
function cheieComponenta(g) {
return [g.assetType, g.name, g.primitive, g.classification, g.parameterSetIdentifier, g.curve, g.mode, g.padding,
g.protocolType, g.protocolVersion, g.material && g.material.type, g.certificate && g.certificate.fingerprintSha256, g.reason].map((x) => x ?? '').join('|');
}
function uuidDin(hex) {
const h = hex.slice(0, 32).split('');
h[12] = '5';
h[16] = '89ab'[parseInt(h[16], 16) & 3];
const s = h.join('');
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
}
export function construiesteCbom(rez, optiuni = {}) {
const grupuri = new Map();
for (const g of rez.findings) {
const k = cheieComponenta(g);
if (!grupuri.has(k)) grupuri.set(k, []);
grupuri.get(k).push(g);
}
const componente = [];
for (const [k, lista] of grupuri) {
const g = lista[0];
const ref = `crypto:${g.assetType}:${slug(g.certificate ? 'x509-' + (g.certificate.subjectName || 'certificate') : g.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;
const cp = { assetType: g.assetType };
if (g.assetType === 'algorithm') {
const ap = { primitive: ENUM.primitive.includes(g.primitive) ? g.primitive : 'unknown' };
if (g.parameterSetIdentifier) ap.parameterSetIdentifier = g.parameterSetIdentifier;
if (g.curve) ap.curve = g.curve;
if (g.mode) ap.mode = ENUM.mode.includes(g.mode) ? g.mode : 'other';
if (g.padding) ap.padding = ENUM.padding.includes(g.padding) ? g.padding : 'other';
const f = [...new Set(lista.flatMap((x) => x.cryptoFunctions || []))].filter((x) => ENUM.cryptoFunctions.includes(x)).sort();
if (f.length) ap.cryptoFunctions = f;
if (Number.isInteger(g.classicalSecurityLevel)) ap.classicalSecurityLevel = g.classicalSecurityLevel;
if (Number.isInteger(g.nistQuantumSecurityLevel)) ap.nistQuantumSecurityLevel = g.nistQuantumSecurityLevel;
cp.algorithmProperties = ap;
} else if (g.assetType === 'protocol') {
cp.protocolProperties = { type: ENUM.protocolType.includes(g.protocolType) ? g.protocolType : 'unknown' };
if (g.protocolVersion) cp.protocolProperties.version = g.protocolVersion;
} else if (g.assetType === 'related-crypto-material') {
const m = { type: ENUM.materialType.includes(g.material && g.material.type) ? g.material.type : 'unknown' };
if (g.material && g.material.format) m.format = g.material.format;
if (g.parameterSetIdentifier && /^\d+$/.test(g.parameterSetIdentifier)) m.size = Number(g.parameterSetIdentifier);
cp.relatedCryptoMaterialProperties = m;
} else if (g.assetType === 'certificate') {
const c = g.certificate || {};
const cert = {};
for (const camp of ['subjectName', 'issuerName', 'notValidBefore', 'notValidAfter', 'certificateFormat', 'certificateExtension']) if (c[camp]) cert[camp] = c[camp];
cp.certificateProperties = cert;
}
if (g.oid) cp.oid = g.oid;
const props = [
{ name: `${P}classification`, value: g.classification },
{ name: `${P}quantum-vulnerable`, value: String(!!g.quantumVulnerable) },
{ name: `${P}reason`, value: g.reason },
];
if (g.recommendation) props.push({ name: `${P}recommendation`, value: g.recommendation });
if (g.assetType !== 'algorithm') props.push({ name: `${P}key-algorithm`, value: g.name });
if (g.certificate && g.certificate.fingerprintSha256) props.push({ name: `${P}certificate-sha256-fingerprint`, value: g.certificate.fingerprintSha256 });
props.push({ name: `${P}languages`, value: [...new Set(lista.map((x) => x.language))].sort().join(',') });
props.push({ name: `${P}evidence-kinds`, value: [...new Set(lista.map((x) => x.evidenceKind))].sort().join(',') });
const rezolvate = [...new Set(lista.filter((x) => x.resolvedFrom).map((x) => `${x.file}:${x.line} <- ${x.resolvedFrom}`))];
if (rezolvate.length) props.push({ name: `${P}resolved-from`, value: rezolvate.join('; ') });
const nume = g.assetType === 'certificate' ? `X.509 certificate${g.certificate && g.certificate.subjectName ? ' ' + g.certificate.subjectName : ''}`
: g.assetType === 'related-crypto-material' ? `${g.name} ${g.material ? g.material.type : 'key material'}` : g.name;
componente.push({
type: 'cryptographic-asset',
'bom-ref': ref,
name: nume,
cryptoProperties: cp,
evidence: {
occurrences: lista.map((x) => {
const oc = { location: x.file, line: x.line, symbol: x.api };
if (x.context) oc.additionalContext = x.context;
return oc;
}),
},
properties: props,
});
}
// bibliotecile declarate, unite pe (ecosistem, nume, versiune)
const libs = new Map();
for (const b of rez.libraries) {
const k = `${b.ecosystem}|${b.name}|${b.version || ''}`;
if (!libs.has(k)) libs.set(k, []);
libs.get(k).push(b);
}
for (const [k, lista] of libs) {
const b = lista[0];
const c = {
type: 'library',
'bom-ref': `library:${b.ecosystem}:${slug(b.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`,
name: b.name,
};
if (b.version) c.version = b.version;
if (b.purl) c.purl = b.purl;
c.evidence = { occurrences: lista.map((x) => ({ location: x.file, line: x.line })) };
c.properties = [
{ name: `${P}declared-only`, value: 'true' },
{ name: `${P}note`, value: 'Declared in a dependency manifest. Declaration is not use: see the cryptographic-asset components for code that calls it.' },
{ name: `${P}provides`, value: b.provides },
];
componente.push(c);
}
componente.sort((a, b) => (a['bom-ref'] < b['bom-ref'] ? -1 : a['bom-ref'] > b['bom-ref'] ? 1 : 0));
const s = rez.stats;
const metaProps = [
['files-seen', s.filesSeen], ['code-files-analyzed', s.codeFilesTotal], ['text-files-pem-only', s.textFilesPemOnly],
['not-read-binary', s.notRead.binary], ['not-read-too-large', s.notRead.tooLarge], ['not-read-file-limit', s.notRead.fileLimit],
['not-read-unreadable', s.notRead.unreadable], ['symlinks-not-followed', s.symlinksNotFollowed],
['files-over-resolution-limit', s.resolutionLimitFiles], ['max-resolved-variables-per-file', rez.options.maxResolvedVariablesPerFile],
['dirs-excluded', Object.entries(s.dirsExcluded).map(([n, c]) => `${n}:${c}`).join(',') || 'none'],
['max-file-bytes', rez.options.maxFileBytes], ['max-files', rez.options.maxFiles],
['method', 'static pattern analysis of source text; no code from the scanned tree is executed'],
].map(([n, v]) => ({ name: `${P}${n}`, value: String(v) }));
const bom = {
$schema: 'http://cyclonedx.org/schema/bom-1.6.schema.json',
bomFormat: 'CycloneDX',
specVersion: '1.6',
serialNumber: '',
version: 1,
metadata: {
tools: { components: [{ type: 'application', name: rez.tool.name, version: rez.tool.version, description: 'Static cryptographic inventory of source code (CBOM)' }] },
component: { type: 'application', 'bom-ref': 'scanned-target', name: rez.rootName },
properties: metaProps,
},
components: componente,
};
if (optiuni.deterministic) {
bom.serialNumber = `urn:uuid:${uuidDin(createHash('sha256').update(JSON.stringify(componente)).digest('hex'))}`;
} else {
bom.metadata = { timestamp: rez.finishedAt, ...bom.metadata };
bom.serialNumber = `urn:uuid:${randomUUID()}`;
}
return bom;
}