159 lines
8.7 KiB
JavaScript
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;
|
|
}
|