// 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; }