split-impacket.mjs (5606B)
1 /** 2 * One-time, idempotent migration of the committed dataset: 3 * 4 * 1. Split the umbrella `wadcoms:Impacket` tool into one tool per impacket 5 * example script (secretsdump.py, ntlmrelayx.py, psexec.py, …). 6 * 2. Fold case-duplicate WADComs families (Enum4Linux / enum4linux) onto one 7 * canonical page so no tool's techniques are silently dropped by the 8 * static router. 9 * 10 * It reads and rewrites the checked-in JSON directly, because the full builder 11 * (build-dataset.mjs) needs the vendored upstreams under vendor/ which are not 12 * present at deploy/dev time. build-dataset.mjs applies the *same* 13 * normalisations (via wadcoms-normalize.mjs), so a future `npm run data` with 14 * vendor/ present produces an identical result. 15 * 16 * The WADComs tool records are fully derived from their techniques, so the 17 * migration rebuilds EVERY `wadcoms:` tool from the transformed techniques 18 * (mirroring the builder's toolsFromWad) and asserts that unaffected tools are 19 * bit-for-bit unchanged — leaving GTFOBins / LOLBAS / daemon tools alone. 20 * 21 * Safe to re-run: once the split is applied there is no `wadcoms:Impacket` 22 * left, and the case-fold is a no-op the second time. 23 */ 24 import { readFileSync, writeFileSync } from 'node:fs'; 25 import { fileURLToPath } from 'node:url'; 26 import { dirname, join, resolve } from 'node:path'; 27 import { splitImpacket, canonicalizeFamilyCase } from './wadcoms-normalize.mjs'; 28 29 const __dirname = dirname(fileURLToPath(import.meta.url)); 30 const ROOT = resolve(__dirname, '..'); 31 const SRC = join(ROOT, 'src', 'data'); 32 33 const uniq = (a) => [...new Set(a.filter((x) => x != null && x !== ''))]; 34 const read = (p) => JSON.parse(readFileSync(p, 'utf8')); 35 36 // Rebuild the per-tool record set for the wadcoms namespace from its 37 // techniques — a verbatim mirror of build-dataset.mjs's toolsFromWad. 38 function toolsFromWad(techniques) { 39 const map = new Map(); 40 for (const t of techniques) { 41 if (!map.has(t.toolId)) { 42 map.set(t.toolId, { id: t.toolId, name: t.toolName, source: t.source, platform: [], references: [], count: 0 }); 43 } 44 const tool = map.get(t.toolId); 45 tool.count++; 46 tool.platform = uniq([...tool.platform, ...t.platform]); 47 tool.references = uniq([...tool.references, ...t.references]); 48 if (t.source === 'DAEMON' && tool.source !== 'DAEMON') tool.source = 'WADComs'; 49 } 50 return [...map.values()]; 51 } 52 53 function main() { 54 const techniques = read(join(SRC, 'techniques.json')); 55 const tools = read(join(SRC, 'tools.json')); 56 const facets = read(join(SRC, 'facets.json')); 57 58 const isWad = (id) => id.startsWith('wadcoms:'); 59 60 // ---- Transform techniques ------------------------------------------------- 61 const before = techniques.filter((t) => t.toolId === 'wadcoms:Impacket').length; 62 const split = techniques.map(splitImpacket); 63 // Fold case-duplicate families over the WADComs namespace ONLY, exactly as 64 // build-dataset.mjs does (it runs canonicalizeFamilyCase over the WADComs 65 // techniques alone), so a fresh regen and this migration stay identical. 66 const foldedById = new Map( 67 canonicalizeFamilyCase(split.filter((t) => isWad(t.toolId))).map((t) => [t.id, t]), 68 ); 69 const techniques2 = split.map((t) => foldedById.get(t.id) ?? t); 70 71 const impacketTools = new Set( 72 techniques2.filter((t) => t.toolId.startsWith('wadcoms:Impacket-')).map((t) => t.toolId), 73 ); 74 console.log(`Impacket: ${before} techniques under wadcoms:Impacket -> ${impacketTools.size} script tools`); 75 76 // ---- Rebuild the wadcoms tool records ------------------------------------ 77 const wadTechs = techniques2.filter((t) => isWad(t.toolId)); 78 const rebuiltWad = toolsFromWad(wadTechs); 79 const rebuiltById = new Map(rebuiltWad.map((t) => [t.id, t])); 80 81 // Assert unaffected wadcoms tools are unchanged (bit-for-bit). 82 const oldWadById = new Map(tools.filter((t) => isWad(t.id)).map((t) => [t.id, t])); 83 let drifted = 0; 84 for (const [id, rebuilt] of rebuiltById) { 85 const old = oldWadById.get(id); 86 if (old && JSON.stringify(old) !== JSON.stringify(rebuilt)) { 87 // Expected only for a family whose membership actually changed (none but 88 // the impacket/enum4linux families should differ). 89 if (!/^wadcoms:(Impacket-|Enum4Linux$|enum4linux$)/.test(id)) { 90 console.error(` ! unexpected drift in ${id}`); 91 drifted++; 92 } 93 } 94 } 95 if (drifted) { console.error(`ABORT: ${drifted} unaffected wadcoms tool(s) drifted — refusing to write.`); process.exit(1); } 96 97 const nonWadTools = tools.filter((t) => !isWad(t.id)); 98 const tools2 = [...nonWadTools, ...rebuiltWad]; 99 100 // ---- Recompute the derived facet counts ---------------------------------- 101 const count = (arr) => arr.reduce((m, v) => ((m[v] = (m[v] || 0) + 1), m), {}); 102 facets.counts.tools = tools2.length; 103 facets.counts.toolsBySource = count(tools2.map((t) => t.source)); 104 105 // ---- Emit (match build-dataset.mjs formatting) --------------------------- 106 // public/data/techniques.json is a gitignored build artifact regenerated by 107 // `npm run data:public`, so only the source-of-truth files are written here. 108 writeFileSync(join(SRC, 'techniques.json'), JSON.stringify(techniques2)); 109 writeFileSync(join(SRC, 'tools.json'), JSON.stringify(tools2)); 110 writeFileSync(join(SRC, 'facets.json'), JSON.stringify(facets, null, 2)); 111 112 console.log(`Tools: ${tools.length} -> ${tools2.length} (wadcoms ${oldWadById.size} -> ${rebuiltWad.length})`); 113 console.log(`toolsBySource: ${JSON.stringify(facets.counts.toolsBySource)}`); 114 console.log('Wrote src/data/{techniques,tools,facets}.json'); 115 } 116 117 main();