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

build-index.mjs (2964B)


      1 /**
      2  * `npm run build:index` — project the committed dataset down to the fields the
      3  * catalog list needs and write it as a content-addressed file:
      4  *
      5  *   public/data/index-<sha256[0:8]>.json   fetched by the catalog island
      6  *   src/data/index-hash.json               { file, hash, bytes, count } — imported
      7  *                                          by catalog.astro to point at the file
      8  *
      9  * The hashed name lets vercel.json serve it `immutable`; a data change makes a
     10  * new name, so a stale cache is impossible. Runs at the head of `npm run build`
     11  * and `npm run dev`; validate-data.mjs fails if index-hash.json is stale.
     12  */
     13 import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, existsSync } from 'node:fs';
     14 import { createHash } from 'node:crypto';
     15 import { fileURLToPath } from 'node:url';
     16 import { dirname, join, resolve } from 'node:path';
     17 
     18 const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
     19 
     20 /** Mirror of LIST_FIELDS in src/lib/render-row.ts (test/build-index.test.mjs pins them). */
     21 export const LIST_FIELDS = [
     22   'id', 'toolId', 'toolName', 'name', 'source', 'platform', 'capability', 'nativeCategory',
     23   'command', 'description', 'usecase', 'mitre', 'privilege', 'added',
     24   'context', 'requires', 'services', 'environment', 'aliases', 'availability', 'verification',
     25   'reviewedAt', 'compatibility', 'expected', 'troubleshooting', 'sideEffects', 'restore', 'template', 'references', 'detection',
     26 ];
     27 
     28 export function project(t) {
     29   const out = {};
     30   for (const k of LIST_FIELDS) if (t[k] !== undefined) out[k] = t[k];
     31   return out;
     32 }
     33 
     34 /** Deterministic: same techniques → same bytes → same hash. */
     35 export function buildIndex(techniques) {
     36   const list = techniques.map(project);
     37   const json = JSON.stringify(list);
     38   const hash = createHash('sha256').update(json).digest('hex').slice(0, 8);
     39   return { json, hash, file: `index-${hash}.json`, bytes: Buffer.byteLength(json), count: list.length };
     40 }
     41 
     42 export function main() {
     43   const techniques = JSON.parse(readFileSync(join(ROOT, 'src/data/techniques.json'), 'utf8'));
     44   const idx = buildIndex(techniques);
     45   const pub = join(ROOT, 'public', 'data');
     46   mkdirSync(pub, { recursive: true });
     47   for (const f of readdirSync(pub)) if (/^index-[0-9a-f]{8}\.json$/.test(f) && f !== idx.file) unlinkSync(join(pub, f));
     48   writeFileSync(join(pub, idx.file), idx.json);
     49   const meta = { file: idx.file, hash: idx.hash, bytes: idx.bytes, count: idx.count };
     50   const metaPath = join(ROOT, 'src/data/index-hash.json');
     51   const prev = existsSync(metaPath) ? readFileSync(metaPath, 'utf8') : '';
     52   const next = JSON.stringify(meta, null, 2) + '\n';
     53   if (prev !== next) writeFileSync(metaPath, next);
     54   console.log(`build:index — public/data/${idx.file}: ${idx.count} rows, ${idx.bytes.toLocaleString('en-US')} B${prev !== next ? ' (index-hash.json updated)' : ''}`);
     55 }
     56 
     57 if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();