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

wadcoms-normalize.mjs (4234B)


      1 /**
      2  * WADComs family normalisation — shared by the dataset builder
      3  * (build-dataset.mjs) and the one-time committed-data migration
      4  * (split-impacket.mjs), so a fresh regen and the checked-in JSON agree.
      5  *
      6  * Two normalisations live here:
      7  *
      8  *  1. splitImpacket — Impacket is not one tool, it is a *suite* of independent
      9  *     example scripts (secretsdump.py, ntlmrelayx.py, psexec.py, …). WADComs
     10  *     files them all under a single "Impacket" family, which collapses ~48
     11  *     distinct techniques onto one unreadable tool page. We split them back out
     12  *     by the actual `examples/<script>.py` each technique references, so every
     13  *     script gets its own tool exactly as impacket ships — `ntlmrelayx.py`'s
     14  *     ten relay variants group together, `secretsdump.py`'s two do, and so on.
     15  *
     16  *  2. canonicalizeFamilyCase — two WADComs families that differ only in case
     17  *     (e.g. `Enum4Linux` and `enum4linux`) slug to the same route, and the
     18  *     static tool page silently drops one. We fold case-variants of the same
     19  *     toolId onto a single canonical spelling (the one with more techniques).
     20  *
     21  * Both are pure and deterministic (no wall-clock, no randomness).
     22  */
     23 
     24 // Match `…/impacket/…/examples/<script>.py`. The script basename is impacket's
     25 // own canonical casing (secretsdump, GetUserSPNs, getST, ntlmrelayx, …).
     26 const IMPACKET_EXAMPLE_RE = /impacket\/(?:[^\s"']*\/)?examples\/([A-Za-z0-9_+.-]+)\.py/i;
     27 const PY_BASENAME_RE = /([A-Za-z0-9_+.-]+)\.py/;
     28 
     29 /**
     30  * Resolve the impacket example script a technique belongs to, preferring the
     31  * `examples/<script>.py` reference URL and falling back to the first `*.py`
     32  * token in the command. Returns null when neither resolves (the technique is
     33  * then left under the umbrella Impacket tool rather than mis-filed).
     34  */
     35 export function impacketScript(references = [], command = '') {
     36   for (const r of references) {
     37     const m = IMPACKET_EXAMPLE_RE.exec(String(r));
     38     if (m) return m[1];
     39   }
     40   const m = PY_BASENAME_RE.exec(String(command));
     41   return m ? m[1] : null;
     42 }
     43 
     44 /**
     45  * If a technique is filed under the umbrella `wadcoms:Impacket` family, refile
     46  * it under `wadcoms:Impacket-<script>` (toolName `Impacket-<script>`) so it
     47  * lands on the script's own page. Idempotent and a no-op for everything else.
     48  */
     49 export function splitImpacket(tech) {
     50   if (tech.toolId !== 'wadcoms:Impacket') return tech;
     51   const script = impacketScript(tech.references, tech.command);
     52   if (!script) return tech;
     53   return { ...tech, toolId: `wadcoms:Impacket-${script}`, toolName: `Impacket-${script}` };
     54 }
     55 
     56 /**
     57  * Fold case-duplicate toolIds within one namespace onto a single canonical
     58  * spelling. The canonical variant is the one carrying the most techniques
     59  * (tie-break: an uppercase-initial family, then first-seen). Rewrites toolId
     60  * and toolName in place on a shallow copy; every other field is untouched.
     61  */
     62 export function canonicalizeFamilyCase(techniques) {
     63   // key = lowercased toolId → { count, byId: Map<toolId,{count,name,first}> }
     64   const groups = new Map();
     65   let order = 0;
     66   for (const t of techniques) {
     67     const key = t.toolId.toLowerCase();
     68     if (!groups.has(key)) groups.set(key, new Map());
     69     const byId = groups.get(key);
     70     if (!byId.has(t.toolId)) byId.set(t.toolId, { count: 0, name: t.toolName, first: order++ });
     71     byId.get(t.toolId).count += 1;
     72   }
     73 
     74   const canonical = new Map(); // toolId → { toolId, toolName }
     75   for (const byId of groups.values()) {
     76     if (byId.size < 2) continue; // no case-variants, nothing to fold
     77     const variants = [...byId.entries()].map(([id, v]) => ({ id, ...v }));
     78     variants.sort((a, b) =>
     79       b.count - a.count ||
     80       (isUpperInitialFamily(b.id) - isUpperInitialFamily(a.id)) ||
     81       a.first - b.first,
     82     );
     83     const win = variants[0];
     84     for (const v of variants) canonical.set(v.id, { toolId: win.id, toolName: win.name });
     85   }
     86 
     87   if (!canonical.size) return techniques;
     88   return techniques.map((t) => {
     89     const c = canonical.get(t.toolId);
     90     return c ? { ...t, toolId: c.toolId, toolName: c.toolName } : t;
     91   });
     92 }
     93 
     94 function isUpperInitialFamily(toolId) {
     95   const family = toolId.split(':').slice(1).join(':');
     96   return /^[A-Z]/.test(family) ? 1 : 0;
     97 }