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

og.mjs (7045B)


      1 /**
      2  * Social card — `npm run og`.
      3  *
      4  * Renders public/og.png (1200×630) from the SVG template below, using the
      5  * site's own faces from src/fonts via a private fontconfig so the card is the
      6  * site (Archivo display, DM Mono labels) rather than whatever the build box
      7  * happens to have installed. The PNG is committed and served as a static
      8  * asset — it is NOT rendered at build time on purpose: Vercel's image has
      9  * different fonts, and a card that changes shape per environment is worse
     10  * than one that changes only when someone re-runs this.
     11  *
     12  * Re-run after the dataset changes so the counts on the card stay honest.
     13  */
     14 import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync } from 'node:fs';
     15 import { spawnSync } from 'node:child_process';
     16 import { fileURLToPath } from 'node:url';
     17 import { dirname, join, resolve } from 'node:path';
     18 import { tmpdir } from 'node:os';
     19 
     20 const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
     21 const FONTS = join(ROOT, 'src', 'fonts');
     22 const facets = JSON.parse(readFileSync(join(ROOT, 'src/data/facets.json'), 'utf8'));
     23 const c = facets.counts;
     24 const n = (x) => Number(x || 0).toLocaleString('en-US');
     25 
     26 // A private fontconfig that sees only the site's faces. sharp's bundled
     27 // FreeType registers WOFF2 (fc-list sees them) but cannot rasterise their
     28 // glyphs — every run came out as tofu — so decompress to TTF first. Needs
     29 // `woff2_decompress` (package "woff2") or fonttools+brotli on PATH; both are
     30 // tried. Must be in place before sharp (libvips → pango → fontconfig) loads.
     31 const fcDir = join(tmpdir(), 'daemonbins-og-fc');
     32 const ttfDir = join(fcDir, 'ttf');
     33 mkdirSync(join(fcDir, 'cache'), { recursive: true });
     34 mkdirSync(ttfDir, { recursive: true });
     35 function decompress(woff2, ttf) {
     36   let r = spawnSync('woff2_decompress', [woff2], { stdio: 'ignore' });
     37   if (r.status === 0) {
     38     // woff2_decompress writes <name>.ttf beside the input; move it.
     39     const beside = woff2.replace(/\.woff2$/, '.ttf');
     40     if (existsSync(beside)) { writeFileSync(ttf, readFileSync(beside)); spawnSync('rm', ['-f', beside]); return true; }
     41   }
     42   r = spawnSync('fonttools', ['ttLib.woff2', 'decompress', '-o', ttf, woff2], { stdio: 'ignore' });
     43   return r.status === 0 && existsSync(ttf);
     44 }
     45 let converted = 0;
     46 for (const f of readdirSync(FONTS).filter((f) => f.endsWith('.woff2'))) {
     47   const ttf = join(ttfDir, f.replace(/\.woff2$/, '.ttf'));
     48   if (existsSync(ttf) || decompress(join(FONTS, f), ttf)) converted++;
     49 }
     50 if (!converted) {
     51   console.warn('og: custom WOFF2 faces unavailable; using the system fallback fonts.');
     52 } else {
     53   writeFileSync(join(fcDir, 'fonts.conf'), `<?xml version="1.0"?>
     54 <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
     55 <fontconfig>
     56   <dir>${ttfDir}</dir>
     57   <cachedir>${join(fcDir, 'cache')}</cachedir>
     58 </fontconfig>
     59 `);
     60   process.env.FONTCONFIG_FILE = join(fcDir, 'fonts.conf');
     61 }
     62 const sharp = (await import('sharp')).default;
     63 
     64 // Rosé Pine (night) — the card is always the dark plate, like the code blocks.
     65 const P = {
     66   base: '#191724', surface: '#1f1d2e', overlay: '#26233a',
     67   text: '#e0def4', subtle: '#908caa', muted: '#6e6a86',
     68   love: '#eb6f92', rose: '#ebbcba', gold: '#f6c177', iris: '#c4a7e7', foam: '#9ccfd8', pine: '#3e8fb0',
     69 };
     70 // Noto Sans Display is the masthead wordmark face (tokens.css --font-wordmark);
     71 // the Archivo subsets do not survive the WOFF2→TTF trip, so the card uses the
     72 // wordmark face for all display text.
     73 const DISPLAY = "'Noto Sans Display', 'Archivo', 'Liberation Sans', sans-serif";
     74 const MONO = "'DM Mono', 'JetBrains Mono', 'Liberation Mono', monospace";
     75 
     76 const decks = [
     77   { tag: 'GTFOBins', acc: P.foam, n: c.bySource?.GTFOBins },
     78   { tag: 'LOLBAS', acc: P.iris, n: c.bySource?.LOLBAS },
     79   { tag: 'WADComs', acc: P.gold, n: c.bySource?.WADComs },
     80   { tag: 'LOOBins', acc: P.rose, n: c.bySource?.LOOBins },
     81   { tag: 'DÆMON', acc: P.love, n: c.bySource?.DAEMON },
     82 ];
     83 
     84 const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
     85   <defs>
     86     <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
     87       <path d="M40 0H0V40" fill="none" stroke="${P.text}" stroke-opacity="0.045" stroke-width="1"/>
     88     </pattern>
     89     <linearGradient id="wash" x1="0" y1="0" x2="1" y2="1">
     90       <stop offset="0" stop-color="${P.base}"/>
     91       <stop offset="1" stop-color="${P.surface}"/>
     92     </linearGradient>
     93   </defs>
     94   <rect width="1200" height="630" fill="url(#wash)"/>
     95   <rect width="1200" height="630" fill="url(#grid)"/>
     96   <!-- signal bands: the site's streak field, frozen -->
     97   <g opacity="0.55">
     98     <rect x="0" y="86" width="1200" height="2" fill="${P.iris}" opacity="0.35"/>
     99     <rect x="180" y="118" width="760" height="1" fill="${P.foam}" opacity="0.5"/>
    100     <rect x="0" y="540" width="1200" height="1" fill="${P.love}" opacity="0.35"/>
    101     <rect x="420" y="566" width="780" height="2" fill="${P.gold}" opacity="0.3"/>
    102   </g>
    103   <!-- masthead -->
    104   <g font-family="${MONO}" font-size="22" fill="${P.subtle}" letter-spacing="4">
    105     <text x="80" y="150">^: 00 · LIVING OFF THE LAND</text>
    106     <text x="1120" y="150" text-anchor="end">lotl.daemon-sec.xyz</text>
    107   </g>
    108   <!-- title -->
    109   <g font-family="${DISPLAY}" font-weight="700">
    110     <text x="76" y="300" fill="${P.text}" font-weight="800" font-size="150" letter-spacing="-7">D<tspan fill="${P.love}">Æ</tspan>MONBins</text>
    111   </g>
    112   <text x="80" y="368" fill="${P.subtle}" font-family="${DISPLAY}" font-weight="600" font-size="44" letter-spacing="-1">the Off-the-Land Almanac</text>
    113   <text x="1120" y="366" text-anchor="end" fill="${P.text}" font-family="${MONO}" font-size="26" letter-spacing="1">${n(c.techniques)} techniques · ${n(c.tools)} tools</text>
    114   <text x="80" y="420" fill="${P.muted}" font-family="${MONO}" font-size="22" letter-spacing="0.5">GTFOBins × LOLBAS × WADComs × LOOBins — merged, resynced, extended · MITRE ATT&amp;CK mapped</text>
    115   <!-- the five collections -->
    116   <g font-family="${MONO}">
    117     ${decks.map((d, i) => {
    118       const x = 55 + i * 220;
    119       return `<rect x="${x}" y="470" width="194" height="4" fill="${d.acc}"/>
    120     <text x="${x}" y="522" fill="${P.text}" font-family="${DISPLAY}" font-weight="700" font-size="46" letter-spacing="-2">${n(d.n)}</text>
    121     <text x="${x}" y="556" fill="${d.acc}" font-size="20" letter-spacing="3">${d.tag.toUpperCase()}</text>`;
    122     }).join('\n    ')}
    123   </g>
    124 </svg>
    125 `;
    126 
    127 const outSvg = join(ROOT, 'public', 'og.svg');
    128 const outPng = join(ROOT, 'public', 'og.png');
    129 writeFileSync(outSvg, svg);
    130 // librsvg maps the SVG's px to 72 dpi; rasterise at 2× and downsample to the
    131 // exact 1200×630 so type edges stay crisp on retina previews.
    132 const png = await sharp(Buffer.from(svg), { density: 144 })
    133   .resize(1200, 630, { fit: 'fill', kernel: 'lanczos3' })
    134   .png({ compressionLevel: 9, palette: false })
    135   .toBuffer();
    136 writeFileSync(outPng, png);
    137 const meta = await sharp(png).metadata();
    138 console.log(`wrote public/og.svg (${svg.length} B) and public/og.png (${png.length} B, ${meta.width}×${meta.height})`);