enrich-data.mjs (4329B)
1 import { readFileSync, writeFileSync } from 'node:fs'; 2 import { createHash } from 'node:crypto'; 3 import { fileURLToPath } from 'node:url'; 4 import { resolve, dirname } from 'node:path'; 5 import { validateTechniques, SOURCES, PLATFORMS, CAPABILITIES } from './technique-schema.mjs'; 6 7 const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); 8 const read = p => JSON.parse(readFileSync(resolve(root, p), 'utf8')); 9 const uniq = a => [...new Set(a)]; 10 export const stableId = (prefix, value) => `${prefix}:${createHash('sha256').update(value).digest('hex').slice(0, 16)}`; 11 12 export function normalizeTechnique(t) { 13 const row = { ...t }; 14 // A Unix command is not evidence of macOS compatibility. Dedicated LOOBins 15 // entries retain macOS; GTFOBins entries remain Linux pending per-command review. 16 if (row.source === 'GTFOBins') { 17 row.platform = row.platform.filter(p => p !== 'macOS'); 18 row.compatibility = 'Linux reference. macOS compatibility has not been checked for this command and execution context.'; 19 } 20 const itemMap = { No_Creds: 'No credentials', AES_Key: 'AES key', Hash: 'NTLM hash', PFX: 'Certificate', TGT: 'Kerberos ticket', TGS: 'Kerberos ticket', powershell: 'PowerShell', PowerShell: 'PowerShell' }; 21 row.requires = uniq((row.requires || []).filter(x => !['PrivEsc', 'Exploitation', 'target', 'service'].includes(x)).map(x => itemMap[x] || x)); 22 row.services = uniq((row.services || []).filter(x => x !== 'Enumeration')); 23 row.environment ||= /kubectl|crictl|^ctr$|runc|nerdctl|docker/i.test(row.toolName) ? ['Containers'] : row.platform.includes('ActiveDirectory') ? ['Active Directory'] : ['Local host']; 24 row.verification ||= 'Upstream reference'; 25 row.availability ||= row.source === 'LOOBins' ? 'Built in' : row.source === 'WADComs' || row.toolId.startsWith('wadcoms:') ? 'Installed tool' : 'Check installation'; 26 return row; 27 } 28 29 export function enrich() { 30 const previous = read('src/data/techniques.json'); 31 const existingTools = new Map(read('src/data/tools.json').map(t => [t.id, t])); 32 const snapshot = read('src/data/sources/loobins.json'); 33 const additions = read('src/data/catalog-additions.json'); 34 const replaceIds = new Set([...snapshot.techniques, ...additions].map(t => t.id)); 35 const techniques = [...previous.filter(t => t.source !== 'LOOBins' && !replaceIds.has(t.id)), ...snapshot.techniques, ...additions].map(normalizeTechnique); 36 const tools = new Map(); 37 for (const t of techniques) { 38 const old = existingTools.get(t.toolId); 39 t.aliases = uniq([...(t.aliases || []), ...(old?.aliases || [])]); 40 if (!tools.has(t.toolId)) tools.set(t.toolId, { ...old, id: t.toolId, name: t.toolName, source: t.source, platform: [], references: [], count: 0 }); 41 const tool = tools.get(t.toolId); 42 tool.platform = uniq([...tool.platform, ...t.platform]); 43 tool.references = uniq([...tool.references, ...t.references]); 44 tool.count++; 45 } 46 const problems = validateTechniques(techniques); 47 if (problems.length) throw new Error(problems.join('\n')); 48 const count = values => values.reduce((a, v) => ({ ...a, [v]: (a[v] || 0) + 1 }), {}); 49 const facets = read('src/data/facets.json'); 50 facets.platforms = PLATFORMS.filter(p => techniques.some(t => t.platform.includes(p))); 51 facets.sources = SOURCES.filter(s => techniques.some(t => t.source === s)); 52 facets.capabilities = CAPABILITIES.filter(c => techniques.some(t => t.capability.includes(c))); 53 facets.counts = { techniques: techniques.length, tools: tools.size, added: techniques.filter(t => t.added).length, 54 bySource: count(techniques.map(t => t.source)), byPlatform: count(techniques.flatMap(t => t.platform)), 55 byCapability: count(techniques.flatMap(t => t.capability)), toolsBySource: count([...tools.values()].map(t => t.source)) }; 56 facets.commits.loobins = snapshot.commit; 57 for (const [file, data] of [['techniques', techniques], ['tools', [...tools.values()]], ['facets', facets]]) { 58 writeFileSync(resolve(root, `src/data/${file}.json`), JSON.stringify(data, null, file === 'facets' ? 2 : 0) + '\n'); 59 } 60 console.log(`Enriched ${techniques.length} techniques across ${tools.size} tools (${snapshot.techniques.length} LOOBins, ${additions.length} curated additions).`); 61 } 62 if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) enrich();