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

validate-data.mjs (3940B)


      1 /**
      2  * `npm run validate:data` — validate the *committed* dataset without touching
      3  * vendor/ or regenerating anything. CI runs this on every push so a hand-edit
      4  * or a bad migration can never reach a deploy.
      5  *
      6  * Checks: every technique passes TechniqueSchema; ids are unique; every
      7  * technique's toolId exists in tools.json; every tool has >= 1 technique; the
      8  * facet counts in facets.json match the data; the vocabulary in
      9  * src/lib/taxonomy.ts still agrees with technique-schema.mjs; the committed
     10  * src/data/index-hash.json still describes this data (else `npm run build:index`).
     11  */
     12 import { readFileSync } from 'node:fs';
     13 import { fileURLToPath } from 'node:url';
     14 import { dirname, join, resolve } from 'node:path';
     15 import { validateTechniques, PLATFORMS, SOURCES, CAPABILITIES } from './technique-schema.mjs';
     16 import { buildIndex } from './build-index.mjs';
     17 
     18 const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
     19 const read = (p) => JSON.parse(readFileSync(join(ROOT, p), 'utf8'));
     20 
     21 const techniques = read('src/data/techniques.json');
     22 const tools = read('src/data/tools.json');
     23 const facets = read('src/data/facets.json');
     24 
     25 const problems = validateTechniques(techniques);
     26 
     27 // Referential integrity.
     28 const toolIds = new Set(tools.map((t) => t.id));
     29 const used = new Set();
     30 for (const t of techniques) {
     31   used.add(t.toolId);
     32   if (!toolIds.has(t.toolId)) problems.push(`orphan technique ${t.id}: toolId ${t.toolId} not in tools.json`);
     33 }
     34 for (const tool of tools) {
     35   if (!used.has(tool.id)) problems.push(`orphan tool ${tool.id}: no techniques`);
     36 }
     37 const seenTools = new Set();
     38 for (const tool of tools) {
     39   if (seenTools.has(tool.id)) problems.push(`duplicate tool id ${tool.id}`);
     40   seenTools.add(tool.id);
     41 }
     42 
     43 // Facet counts must describe this exact dataset.
     44 const c = facets.counts || {};
     45 if (c.techniques !== techniques.length) problems.push(`facets.counts.techniques=${c.techniques} but data has ${techniques.length}`);
     46 if (c.tools !== tools.length) problems.push(`facets.counts.tools=${c.tools} but data has ${tools.length}`);
     47 const added = techniques.filter((t) => t.added).length;
     48 if (c.added !== added) problems.push(`facets.counts.added=${c.added} but data has ${added}`);
     49 const bySource = {};
     50 for (const t of techniques) bySource[t.source] = (bySource[t.source] || 0) + 1;
     51 for (const s of SOURCES) {
     52   if ((c.bySource || {})[s] !== (bySource[s] || undefined) && !(bySource[s] === undefined && (c.bySource || {})[s] === undefined)) {
     53     problems.push(`facets.counts.bySource.${s}=${(c.bySource || {})[s]} but data has ${bySource[s] || 0}`);
     54   }
     55 }
     56 
     57 // The content-addressed list index must be the one this data produces.
     58 try {
     59   const idx = JSON.parse(readFileSync(join(ROOT, 'src/data/index-hash.json'), 'utf8'));
     60   const fresh = buildIndex(techniques);
     61   if (idx.hash !== fresh.hash || idx.file !== fresh.file) problems.push(`src/data/index-hash.json is stale (${idx.file} vs ${fresh.file}) — run npm run build:index`);
     62 } catch (e) {
     63   problems.push(`src/data/index-hash.json missing or unreadable — run npm run build:index (${e.message})`);
     64 }
     65 
     66 // Vocabulary drift between the data contract and the UI taxonomy.
     67 const taxonomy = readFileSync(join(ROOT, 'src/lib/taxonomy.ts'), 'utf8');
     68 for (const [label, list] of [['platform', PLATFORMS], ['source', SOURCES], ['capability', CAPABILITIES]]) {
     69   for (const v of list) {
     70     if (!taxonomy.includes(`'${v}'`)) problems.push(`${label} '${v}' is in technique-schema.mjs but not in src/lib/taxonomy.ts`);
     71   }
     72 }
     73 
     74 if (problems.length) {
     75   console.error(`validate:data — ${problems.length} problem(s):`);
     76   for (const p of problems.slice(0, 40)) console.error(`  ✗ ${p}`);
     77   if (problems.length > 40) console.error(`  … and ${problems.length - 40} more`);
     78   process.exit(1);
     79 }
     80 console.log(`validate:data — OK: ${techniques.length} techniques, ${tools.length} tools, ${added} NEW; schema, ids, tool links and facet counts all consistent.`);