daemon-sec-lotl

DÆMONBins: GTFOBins × LOLBAS × WADComs × LOOBins in one filterable catalog
git clone https://git.daemon-sec.xyz/daemon-sec-lotl.git
Log | Files | Refs | Submodules | README | LICENSE

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();