daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

commit f031238aa3e1046066ca697fd2f0b78b036eec41
parent 6d7bafcfec145c6a61ff6a9e2c35b0f6c5db3d3c
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Mon,  5 Oct 2026 13:25:19 +0100

JJ Update: removed distracting particle effect from posts, it only shows up in the main page hero and at the top of each page, while viewing a post you will not be abl to see it at all Mon Oct  5 13:25:19 BST 2026

Diffstat:
Msrc/components/HeroLoop.astro | 15+++++++++++----
Msrc/components/SectionBanner.astro | 228++-----------------------------------------------------------------------------
Asrc/components/SignalField.astro | 22++++++++++++++++++++++
Msrc/content.config.ts | 2++
Msrc/content/sheets/enumeration/2-4-cheatsheet-gitleaks.md | 6+++++-
Msrc/content/sheets/enumeration/2-5-cheatsheet-trufflehog.md | 6+++++-
Msrc/layouts/Base.astro | 60------------------------------------------------------------
Msrc/lib/sheets.ts | 10++++++++--
Msrc/pages/hacktricks/[...slug].astro | 4+++-
Msrc/pages/hacktricks/section/[section].astro | 4+++-
Msrc/pages/internal/[...slug].astro | 4+++-
Msrc/pages/payloads/[...slug].astro | 4+++-
Msrc/pages/sheets/[...slug].astro | 10+++++++---
Msrc/scripts/signal-field.ts | 90+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/styles/chrome.css | 45++++++++++-----------------------------------
Msrc/styles/global.css | 5+----
Atest/hero-and-shelves.test.mjs | 39+++++++++++++++++++++++++++++++++++++++
17 files changed, 159 insertions(+), 395 deletions(-)

diff --git a/src/components/HeroLoop.astro b/src/components/HeroLoop.astro @@ -1,4 +1,6 @@ --- +import SignalField from './SignalField.astro'; + interface Shelf { n: string; label: string; path: string; accent: string; desc: string } interface Props { shelves: Shelf[]; @@ -8,7 +10,8 @@ interface Props { const { shelves, name = 'DΛΣMӨП-RΣF', tagline = 'DÆMON-REF · Cheatsheet vault' } = Astro.props; const data = JSON.stringify({ shelves, name, tagline }); --- -<section class="hero-loop bleed" data-hero-loop data-hero={data} aria-label="Shelves"> +<section class="hero-loop bleed signal-hero" data-hero-loop data-hero={data} aria-label="Shelves"> + <SignalField /> <noscript> <div style="padding:56px 44px;max-width:1560px;margin:0 auto;"> <p style="font-family:var(--font-wordmark);font-weight:800;font-size:clamp(40px,9vw,120px);font-stretch:88%;letter-spacing:-0.02em;line-height:1;color:var(--text);text-transform:uppercase;">{name}</p> @@ -106,6 +109,7 @@ const data = JSON.stringify({ shelves, name, tagline }); ? { position: 'absolute', left: '72px', top: '424px', width: '916px' } : { marginTop: '32px' }, [...rows.map((r) => r.a), el('div', { borderTop: `1px solid ${P.line}` })]); nav.setAttribute('aria-label', 'Shelves'); + nav.setAttribute('data-index-zone', ''); // ── Plate ── const dots: HTMLElement[] = []; @@ -182,7 +186,7 @@ const data = JSON.stringify({ shelves, name, tagline }); el('div', metaBase({ color: P.muted }), 'Rosé Pine · Dawn · plates inverted'), el('div', metaBase({ color: P.muted }), 'Scroll ↓'), ]); - root = el('div', { position: 'absolute', inset: '0', background: P.base, color: P.ink, opacity: '0', overflow: 'hidden', fontFamily: ARCHIVO }, [hairline, topMeta, wmBlock, nav, plateHost, bottomMeta]); + root = el('div', { position: 'absolute', inset: '0', color: P.ink, opacity: '0', overflow: 'hidden', fontFamily: ARCHIVO }, [hairline, topMeta, wmBlock, nav, plateHost, bottomMeta]); } else { wordmark.style.fontSize = withPlate ? 'clamp(36px, 6.4vw, 88px)' : 'clamp(28px, 10vw, 80px)'; hairline = el('div', { height: '1px', marginTop: '14px', background: P.ink, transformOrigin: 'left', transform: 'scaleX(0)', opacity: '0.9' }); @@ -198,7 +202,7 @@ const data = JSON.stringify({ shelves, name, tagline }); el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Rosé Pine · Dawn'), el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Scroll ↓'), ]); - root = el('div', { display: 'flex', flexDirection: 'column', minHeight: `min(calc(100svh - ${MARQUEE}px), 620px)`, padding: 'clamp(28px, 4vw, 56px) clamp(20px, 3.8vw, 72px)', background: P.base, color: P.ink, opacity: '0', fontFamily: ARCHIVO }, [topMeta, hairline, centerWrap, bottomMeta]); + root = el('div', { display: 'flex', flexDirection: 'column', minHeight: `min(calc(100svh - ${MARQUEE}px), 620px)`, padding: 'clamp(28px, 4vw, 56px) clamp(20px, 3.8vw, 72px)', color: P.ink, opacity: '0', fontFamily: ARCHIVO }, [topMeta, hairline, centerWrap, bottomMeta]); } let lastStep = -1, lastAccent = ''; @@ -307,7 +311,10 @@ const data = JSON.stringify({ shelves, name, tagline }); withPlate = !canvas && window.innerWidth >= FLOW_PLATE_MIN; dark = document.documentElement.getAttribute('data-theme') === 'dark'; const P = dark ? NIGHT : DAWN, Q = dark ? DAWN : NIGHT; - host.textContent = ''; + // Preserve the field when the hero rebuilds for theme or viewport changes. + for (const child of Array.from(host.children)) { + if (!child.hasAttribute('data-signal')) child.remove(); + } if (canvas) { const stage = el('div', { position: 'absolute', left: '0', top: '0', transformOrigin: '0 0' }); host.style.height = `max(${MIN_HEIGHT}px, min(calc(100vh - ${MARQUEE}px), ${(H / W) * 100}vw))`; diff --git a/src/components/SectionBanner.astro b/src/components/SectionBanner.astro @@ -1,4 +1,6 @@ --- +import SignalField from './SignalField.astro'; + interface Stat { label: string; value: string | number; @@ -18,18 +20,9 @@ interface Props { } const { n, label, title, blurb, stats = [], accent = 'foam' } = Astro.props; -/** - * The page header every route below the home page lands on: dark plate, - * live signal field, left scrim, and a ledger on the right. - * - * Legibility on the plate is carried by two layers, not one — the canvas - * erases its own top and bottom bands, AND the scrim gradient sits over - * the noise and under the content. Either alone leaves the title sitting - * on a bright filament some fraction of the loop. - */ --- -<header class="banner plate bleed" style={`--acc: var(--${accent});`}> - <canvas class="banner__field" data-fuzz aria-hidden="true"></canvas> +<header class="banner plate bleed signal-hero" style={`--acc: var(--${accent});`}> + <SignalField /> <span class="banner__scrim" aria-hidden="true"></span> <span class="grain grain--local" aria-hidden="true"></span> <!-- CRT film over the signal field — the body row is already z-index 2, @@ -75,216 +68,3 @@ const { n, label, title, blurb, stats = [], accent = 'foam' } = Astro.props; font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); } </style> - -<script> - /** - * The banners' signal field: six pixel-sorted streaks drifting on an - * additive blend, plus bright filaments, looping seamlessly every nine - * seconds. - * - * Ported from the main site's logic class. Four things are load-bearing - * and break the look if changed: the loop-global phase (so two plates on - * one page stay in step), the DPR backing (at 1× the streaks are 1–2px - * and upscaling blurs the pixel-sorted look), the erasure scrim, and the - * synchronous frame zero. - */ - - /** Band geometry — six streaks, three hues between them. `hue` is a - * *slot* (1 = iris, 2 = foam, 3 = love), not a colour: which colour a - * slot resolves to, and how it composites, is read from CSS at draw - * time so the field follows the mode along with everything else. */ - const BANDS = [ - { cy: 0.10, ch: 0.20, hue: 1, drift: 1, cyc: 1, dens: 12 }, - { cy: 0.34, ch: 0.24, hue: 2, drift: -1, cyc: 2, dens: 15 }, - { cy: 0.22, ch: 0.14, hue: 3, drift: 2, cyc: 1, dens: 9 }, - { cy: 0.60, ch: 0.22, hue: 2, drift: 1, cyc: 1, dens: 13 }, - { cy: 0.80, ch: 0.16, hue: 1, drift: -1, cyc: 2, dens: 10 }, - { cy: 0.92, ch: 0.14, hue: 3, drift: 1, cyc: 2, dens: 9 }, - ] as const; - - interface Palette { - blend: GlobalCompositeOperation; - bands: [string, string, string]; - filaments: [string, string, string]; - gain: number; - } - - const FALLBACK: Palette = { - blend: 'lighter', - bands: ['196,167,231', '156,207,216', '235,111,146'], - filaments: ['224,222,244', '156,207,216', '196,167,231'], - gain: 1, - }; - - /** - * Resolve the palette off the canvas itself, so a plate inside a scope - * that pins the night palette draws the dark field even while the page - * around it is on paper. Reading the computed style is what makes that - * work: the cascade has already answered the question, and the canvas - * only has to ask the right element. - * - * Channels come back space-separated from CSS and the 2D context wants - * them comma-separated, hence the normalise. - */ - function readPalette(cv: HTMLCanvasElement): Palette { - const cs = getComputedStyle(cv); - const channels = (name: string, fallback: string) => { - const v = cs.getPropertyValue(name).trim(); - return v ? v.replace(/\s+/g, ',') : fallback; - }; - const blend = cs.getPropertyValue('--fuzz-blend').trim(); - const gain = parseFloat(cs.getPropertyValue('--fuzz-gain')); - return { - blend: (blend || FALLBACK.blend) as GlobalCompositeOperation, - bands: [ - channels('--fuzz-band-1', FALLBACK.bands[0]), - channels('--fuzz-band-2', FALLBACK.bands[1]), - channels('--fuzz-band-3', FALLBACK.bands[2]), - ], - filaments: [ - channels('--fuzz-fil-1', FALLBACK.filaments[0]), - channels('--fuzz-fil-2', FALLBACK.filaments[1]), - channels('--fuzz-fil-3', FALLBACK.filaments[2]), - ], - gain: Number.isFinite(gain) ? gain : FALLBACK.gain, - }; - } - - /** One frame of the field at `phase` ∈ [0,1). Every band's drift and - * breathe is a whole number of cycles per loop, so the 9s loop seams - * cleanly and any two plates on the page stay in step. */ - function frame(cv: HTMLCanvasElement, phase: number, amt: number, pal: Palette): void { - // clientWidth is 0 for the first frames after insert; fall back to the - // host box so the field is not blank until the second layout. - const host = cv.parentElement; - const w = (cv.clientWidth || host?.clientWidth || 0) | 0; - const h = (cv.clientHeight || host?.clientHeight || 0) | 0; - if (!w || !h) return; - - const dpr = Math.min(2, devicePixelRatio || 1); - const bw = Math.round(w * dpr); - const bh = Math.round(h * dpr); - if (cv.width !== bw || cv.height !== bh) { cv.width = bw; cv.height = bh; } - - const ctx = cv.getContext('2d'); - if (!ctx) return; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - - const frac = (x: number) => x - Math.floor(x); - const rnd = (s: number) => frac(Math.sin(s * 127.1) * 43758.5453); - - ctx.clearRect(0, 0, w, h); - ctx.globalCompositeOperation = pal.blend; - - for (let bi = 0; bi < BANDS.length; bi++) { - const b = BANDS[bi]; - const cy = b.cy * h; - const ch = b.ch * h; - for (let y = Math.max(0, Math.round(cy - ch)); y < Math.min(h, cy + ch); y += 2) { - const fall = 1 - Math.abs(y - cy) / ch; - const breathe = 0.5 + 0.5 * Math.sin(2 * Math.PI * (b.cyc * phase + y * 0.004 + bi * 0.3)); - const env = fall * (0.4 + 0.6 * breathe); - if (env <= 0.02) continue; - const n = Math.round(env * b.dens * amt); - for (let i = 0; i < n; i++) { - const s = y * 7.3 + i * 13.7 + bi * 101.3; - const x = frac(rnd(s) + phase * b.drift) * (w + 420) - 210; - const len = (18 + rnd(s + 1) * 340) * (0.4 + env); - const hx = 0.22 + 0.78 * Math.pow(Math.sin(Math.PI * ((x / w) * (1 + (bi % 3)) + phase + rnd(bi * 9))), 2); - const a = (0.06 + rnd(s + 2) * 0.34) * env * hx * 1.7 * pal.gain; - ctx.fillStyle = `rgba(${pal.bands[b.hue - 1]},${a.toFixed(3)})`; - ctx.fillRect(x, y, len, rnd(s + 3) > 0.84 ? 2 : 1); - } - } - } - - for (let i = 0; i < 54; i++) { - const s = i * 37.1; - const y = Math.round((rnd(s) * h) / 2) * 2; - const cyc = 1 + (i % 2); - const x = frac(rnd(s + 5) + phase * cyc) * (w + 700) - 350; - const a = (0.1 + rnd(s + 6) * 0.28) * amt * pal.gain; - ctx.fillStyle = `rgba(${pal.filaments[i % 3]},${a.toFixed(3)})`; - ctx.fillRect(x, y, 120 + rnd(s + 7) * 520, 1); - } - - // Scrim by erasure: destination-out bands at top and bottom so type - // near the plate edges always sits on clean ink. The percentage clamp - // matters — a fixed 40px erased most of the short plates. - ctx.globalCompositeOperation = 'destination-out'; - const solid = Math.min(40, h * 0.14); - const ramp = Math.min(26, h * 0.1); - ctx.fillStyle = 'rgba(0,0,0,1)'; - ctx.fillRect(0, 0, w, solid); - ctx.fillRect(0, h - solid, w, solid); - let g = ctx.createLinearGradient(0, solid, 0, solid + ramp); - g.addColorStop(0, 'rgba(0,0,0,1)'); - g.addColorStop(1, 'rgba(0,0,0,0)'); - ctx.fillStyle = g; - ctx.fillRect(0, solid, w, ramp); - g = ctx.createLinearGradient(0, h - solid - ramp, 0, h - solid); - g.addColorStop(0, 'rgba(0,0,0,0)'); - g.addColorStop(1, 'rgba(0,0,0,1)'); - ctx.fillStyle = g; - ctx.fillRect(0, h - solid - ramp, w, ramp); - ctx.globalCompositeOperation = 'source-over'; - } - - /** Density scale. The main site ships at 6; the constant exists for - * tuning, not for the UI. */ - const PRESS = 6; - /** The frame reduced motion keeps as the permanent texture — mid-loop, - * where the bands are at their fullest rather than at a seam. */ - const STILL = 0.3; - - function initFuzz(): void { - const canvases = document.querySelectorAll<HTMLCanvasElement>('[data-fuzz]:not([data-bound])'); - canvases.forEach((cv) => { - cv.dataset.bound = '1'; - const amt = Math.max(0.2, PRESS / 5); - const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; - - // Resolved once per mode rather than once per frame: getComputedStyle - // flushes style on a canvas repainting at 60fps, and there is nothing - // to re-read between mode changes. - let pal = readPalette(cv); - - // Draw frame zero synchronously. rAF never fires in a hidden tab, - // during print, or in a screenshot, and a schedule-only start leaves - // the canvas at its 300×150 default — a bare plate in every capture. - const still = () => frame(cv, STILL, amt, pal); - frame(cv, reduced ? STILL : (performance.now() / 9000) % 1, amt, pal); - - // The animated loop re-measures every frame, so it absorbs resizes on - // its own; the static frame needs the observer to stay crisp. - const ro = new ResizeObserver(() => { if (reduced) still(); }); - ro.observe(cv.parentElement ?? cv); - - // A bitmap does not inherit. The variables have already changed by - // the time this fires, so re-reading them is all it takes — but a - // still frame has to be redrawn by hand, or the plate keeps the old - // palette until something else resizes it. - const onMode = () => { pal = readPalette(cv); if (reduced) still(); }; - addEventListener('daemonmodechange', onMode); - - let raf = 0; - if (!reduced) { - const loop = (now: number) => { - frame(cv, (now / 9000) % 1, amt, pal); - raf = requestAnimationFrame(loop); - }; - raf = requestAnimationFrame(loop); - } - - document.addEventListener('astro:before-swap', () => { - cancelAnimationFrame(raf); - ro.disconnect(); - removeEventListener('daemonmodechange', onMode); - }, { once: true }); - }); - } - - document.addEventListener('astro:page-load', initFuzz); - if (document.readyState !== 'loading') initFuzz(); - else document.addEventListener('DOMContentLoaded', initFuzz); -</script> diff --git a/src/components/SignalField.astro b/src/components/SignalField.astro @@ -0,0 +1,22 @@ +<canvas class="signal-field" data-signal aria-hidden="true"></canvas> + +<script> + import { mountSignalField } from '../scripts/signal-field'; + + const cleanups = new Set<() => void>(); + function init(): void { + document.querySelectorAll<HTMLCanvasElement>('[data-signal]:not([data-bound])').forEach((canvas) => { + canvas.dataset.bound = '1'; + cleanups.add(mountSignalField(canvas)); + }); + } + + // Heroes belong to their page; release their GPU resources before Astro swaps it. + document.addEventListener('astro:before-swap', () => { + cleanups.forEach((cleanup) => cleanup()); + cleanups.clear(); + }); + document.addEventListener('astro:page-load', init); + if (document.readyState !== 'loading') init(); + else document.addEventListener('DOMContentLoaded', init, { once: true }); +</script> diff --git a/src/content.config.ts b/src/content.config.ts @@ -9,6 +9,8 @@ const sheets = defineCollection({ title: z.string(), description: z.string().default(''), category: z.string(), + // Additional shelves share the canonical sheet and URL. + alsoIn: z.array(z.string()).default([]), // Optional second-level grouping shown as sub-sections on a busy // category page (Active Directory has 130+ sheets). Derived from the // source vault path by scripts/stamp-subcategory.py. diff --git a/src/content/sheets/enumeration/2-4-cheatsheet-gitleaks.md b/src/content/sheets/enumeration/2-4-cheatsheet-gitleaks.md @@ -2,12 +2,16 @@ title: "2.4 - Cheatsheet - Gitleaks" description: "brew install gitleaks" category: enumeration -tags: ["enumeration"] +alsoIn: ["osint"] +tags: ["enumeration", "osint", "secret-scanning"] tools: ["Gitleaks"] difficulty: intermediate updated: "2026-08-10" source: "vault:Enumeration/GitHub-Enum/2.4 - Cheatsheet - Gitleaks.md" --- +## Installation + +```bash # macOS — Homebrew brew install gitleaks diff --git a/src/content/sheets/enumeration/2-5-cheatsheet-trufflehog.md b/src/content/sheets/enumeration/2-5-cheatsheet-trufflehog.md @@ -2,12 +2,16 @@ title: "2.5 - Cheatsheet - TruffleHog" description: "brew install trufflehog" category: enumeration -tags: ["enumeration"] +alsoIn: ["osint"] +tags: ["enumeration", "osint", "secret-scanning"] tools: ["Gitleaks", "TruffleHog"] difficulty: intermediate updated: "2026-08-10" source: "vault:Enumeration/GitHub-Enum/2.5 - Cheatsheet - TruffleHog.md" --- +## Installation + +```bash # macOS — Homebrew brew install trufflehog diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro @@ -137,30 +137,6 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href; <ClientRouter /> </head> <body> - <!-- The signal field: one WebGL canvas fixed to the viewport, under the - whole site. First child of <body> so it is the bottom of the stack; - the CSS and the layer ladder it belongs to are in chrome.css. - - `transition:persist` is the decision that makes this affordable. - The ClientRouter replaces the whole document on every internal - navigation, so without it each page would build a WebGL context, - compile the shader and throw all of it away a moment later — and - the field would restart from a cold phase every time, which reads - as a flicker rather than as atmosphere. Persisted, Astro carries - this exact element into the new document: one context, one rAF - loop, one continuous animation for the whole session. - - Two consequences follow from the element surviving, and both are - load-bearing. The `data-bound` guard below is on the canvas - itself, so the re-init on `astro:page-load` finds it already - marked and does nothing. And there is deliberately NO - `astro:before-swap` teardown: the swap is not an unmount here, and - tearing down would destroy the context the surviving element still - owns. Contrast SectionBanner.astro's fuzz canvas, which does not - persist — it is rebuilt per page, so it *must* cancel its rAF and - drop its listeners on before-swap or every navigation would leak - another loop. --> - <canvas class="signal-field" data-signal aria-hidden="true" transition:persist></canvas> <!-- Shared arrowhead for every inline-SVG flowchart (src/styles/flow.css). Defined once here so a diagram only has to reference `url(#flow-arrow)`. `context-stroke` makes the head take each edge's own stroke colour, so @@ -191,42 +167,6 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href; <SearchModal /> <script> import '../scripts/app.ts'; - import { mountSignalField } from '../scripts/signal-field'; - - /** - * Bind the signal field. This block imports, so Astro bundles it - * rather than treating it as inline source: it ships as an external - * module with nothing for the CSP to hash, and if the build does - * inline it, scripts/csp-hashes.mjs reads the hash out of the built - * HTML. Either way there is no hash to maintain by hand — unlike the - * two `is:inline` bootstraps in the head. - * - * The guard lives on the canvas, which is `transition:persist`ed, so - * the element the first load bound is the same element every later - * `astro:page-load` sees: it is already marked and the loop keeps - * running untouched. The `readyState` pair is the house init shape - * (app.ts, SectionBanner.astro) — it covers the first load, where a - * deferred module can run either side of `astro:page-load`. - * - * The teardown the module returns is deliberately dropped. There is - * no unmount point for a canvas that outlives every navigation, and - * the one case that does need to end — no WebGL, a shader that will - * not compile, a lost context — the module handles itself by tearing - * down and removing the canvas. The page ground is correct without - * it, so there is nothing here to fall back to. - */ - function initSignalField(): void { - document - .querySelectorAll<HTMLCanvasElement>('[data-signal]:not([data-bound])') - .forEach((cv) => { - cv.dataset.bound = '1'; - mountSignalField(cv); - }); - } - - document.addEventListener('astro:page-load', initSignalField); - if (document.readyState !== 'loading') initSignalField(); - else document.addEventListener('DOMContentLoaded', initSignalField); </script> </body> </html> diff --git a/src/lib/sheets.ts b/src/lib/sheets.ts @@ -12,19 +12,25 @@ export function sheetHref(entry: Sheet): string { return `sheets/${entry.id}`; } +export function sheetCategories(entry: Sheet): string[] { + return [...new Set([entry.data.category, ...entry.data.alsoIn])]; +} + /** Sheets grouped by category, in taxonomy order. */ export async function sheetsByCategory(): Promise<{ slug: string; items: Sheet[] }[]> { const sheets = await allSheets(); return CATEGORIES.map((c) => ({ slug: c.slug, - items: sheets.filter((s) => s.data.category === c.slug), + items: sheets.filter((s) => sheetCategories(s).includes(c.slug)), })); } export async function categoryCounts(): Promise<Record<string, number>> { const sheets = await allSheets(); const counts: Record<string, number> = {}; - for (const s of sheets) counts[s.data.category] = (counts[s.data.category] ?? 0) + 1; + for (const s of sheets) { + for (const category of sheetCategories(s)) counts[category] = (counts[category] ?? 0) + 1; + } return counts; } diff --git a/src/pages/hacktricks/[...slug].astro b/src/pages/hacktricks/[...slug].astro @@ -1,4 +1,5 @@ --- +import SignalField from '../../components/SignalField.astro'; import { render } from 'astro:content'; import Base from '../../layouts/Base.astro'; import HackTricksCredit from '../../components/HackTricksCredit.astro'; @@ -32,7 +33,8 @@ const toc = headings.filter((heading) => heading.depth === 2 || heading.depth == <a href={url(`hacktricks/section/${entry.data.sectionSlug}`)} style="color:var(--gold)">{entry.data.sectionSlug}</a> </nav> - <header class="sheet-head" data-pagefind-meta="category:HackTricks"> + <header class="sheet-head signal-hero" data-pagefind-meta="category:HackTricks"> + <SignalField /> <p class="eyebrow" style="color:var(--gold)">// HackTricks · {sectionTitle}</p> <h1 class="sheet-title" data-pagefind-meta="title">{entry.data.title}</h1> </header> diff --git a/src/pages/hacktricks/section/[section].astro b/src/pages/hacktricks/section/[section].astro @@ -1,4 +1,5 @@ --- +import SignalField from '../../../components/SignalField.astro'; import Base from '../../../layouts/Base.astro'; import RecordRow from '../../../components/RecordRow.astro'; import { hackTricksBySection, hackTricksHref } from '../../../lib/hacktricks'; @@ -22,7 +23,8 @@ const accents = ['gold', 'love', 'foam', 'pine', 'iris', 'rose']; <span class="muted">{section.slug}</span> </nav> - <header class="sheet-head"> + <header class="sheet-head signal-hero"> + <SignalField /> <p class="eyebrow" style="color:var(--gold)">// HackTricks · CC BY-NC 4.0</p> <h1 class="sheet-title">{section.title}</h1> <p class="muted">{section.count} pages · adapted from upstream with source attribution on every page.</p> diff --git a/src/pages/internal/[...slug].astro b/src/pages/internal/[...slug].astro @@ -1,4 +1,5 @@ --- +import SignalField from '../../components/SignalField.astro'; import { render } from 'astro:content'; import Base from '../../layouts/Base.astro'; import Icon from '../../components/Icon.astro'; @@ -41,7 +42,8 @@ const leaf = entry.id.split('/').pop()!.replace(/^index$/, d.sectionSlug); : <><a href={url(`internal/${d.sectionSlug}`)} style="color: var(--pine)">{d.sectionSlug}</a><span class="sep">/</span><span class="muted">{leaf}</span></>} </nav> - <header class="sheet-head" data-pagefind-meta="category:Internal"> + <header class="sheet-head signal-hero" data-pagefind-meta="category:Internal"> + <SignalField /> <p class="eyebrow" style="color: var(--pine)">// Internal · {sectionTitle}</p> <h1 class="sheet-title" data-pagefind-meta="title">{d.title}</h1> </header> diff --git a/src/pages/payloads/[...slug].astro b/src/pages/payloads/[...slug].astro @@ -1,4 +1,5 @@ --- +import SignalField from '../../components/SignalField.astro'; import { render } from 'astro:content'; import Base from '../../layouts/Base.astro'; import PayloadCredit from '../../components/PayloadCredit.astro'; @@ -36,7 +37,8 @@ const leaf = entry.id.split('/').pop()!.replace(/^index$/, d.topic); : <><a href={url(`payloads/${d.topicSlug}`)} style="color: var(--iris)">{d.topicSlug}</a><span class="sep">/</span><span class="muted">{leaf}</span></>} </nav> - <header class="sheet-head" data-pagefind-meta="category:Payloads"> + <header class="sheet-head signal-hero" data-pagefind-meta="category:Payloads"> + <SignalField /> <p class="eyebrow" style="color: var(--iris)">// Payloads · {topicTitle}</p> <h1 class="sheet-title" data-pagefind-meta="title">{d.title}</h1> </header> diff --git a/src/pages/sheets/[...slug].astro b/src/pages/sheets/[...slug].astro @@ -1,4 +1,5 @@ --- +import SignalField from '../../components/SignalField.astro'; import { render } from 'astro:content'; import Base from '../../layouts/Base.astro'; import Icon from '../../components/Icon.astro'; @@ -12,8 +13,10 @@ export async function getStaticPaths() { const groups = await sheetsByCategory(); const paths: any[] = []; for (const g of groups) { - for (const entry of g.items) { - const { prev, next } = prevNext(g.items, entry); + // Cross-listed sheets still generate one canonical page and pagination. + const primaryItems = g.items.filter((entry) => entry.data.category === g.slug); + for (const entry of primaryItems) { + const { prev, next } = prevNext(primaryItems, entry); paths.push({ params: { slug: entry.id }, props: { entry, prev, next } }); } } @@ -37,7 +40,8 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null; <span class="muted">{entry.id.split('/').pop()}</span> </nav> - <header class="sheet-head" data-pagefind-meta={`category:${cat.title}`}> + <header class="sheet-head signal-hero" data-pagefind-meta={`category:${cat.title}`}> + <SignalField /> <p class="eyebrow" style={`--acc: var(--${cat.accent});`}> <span class="eyebrow__n">{cat.tag}</span> <span class="eyebrow__mark">^:</span> diff --git a/src/scripts/signal-field.ts b/src/scripts/signal-field.ts @@ -1,6 +1,6 @@ /** - * The site's atmosphere, drawn in WebGL on one viewport-fixed canvas that - * sits behind every page: a network map. Hosts sit on an irregular + * The hero's atmosphere, drawn in WebGL on a canvas contained by its + * header: a network map. Hosts sit on an irregular * lattice, drift a little on their own and are pushed away from the * pointer, so the map bulges around the cursor and settles when it * leaves. The hosts within the cursor's reach lock in the archive's REC @@ -28,9 +28,7 @@ * An element carrying `data-index-zone` asks the field to part around it: * every host inside its box is pushed out through the nearest edge to a * margin beyond it, and links, hosts and locks fade to nothing inside. - * Nothing on this site carries the attribute yet, so `u_zone` stays at - * its empty sentinel and the keep-out is a no-op — see `findZone` below - * for why the mechanism is kept anyway. + * The home hero marks its shelf navigation as this clearing. * * ── Colour ── * No palette lives in the shader. The ink and the three accents are read @@ -59,7 +57,7 @@ * ── Motion ── * Under `prefers-reduced-motion: reduce` the loop never starts: one frame * is drawn at a fixed phase and redrawn only when the canvas resizes or - * the mode flips. The frame also pauses while the tab is hidden. With no + * the mode flips. The loop pauses offscreen and while the tab is hidden. With no * pointer on the page the cursor wanders the field on its own, so a touch * screen still sees hosts being locked. * @@ -513,26 +511,17 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { let raf = 0; let running = false; let visible = !document.hidden; + let inView = false; let lost = false; const live = !matchMedia('(prefers-reduced-motion: reduce)').matches; const start = performance.now(); - /* The canvas box, measured by the observers below and never inside a - frame or a pointer event: a layout read in either place forces the - browser to flush pending style and layout synchronously. - - The original stored the box's *page*-space top, because its canvas - scrolled with the page: the viewport-relative top changed on every - scroll while the page-relative one did not. This canvas is fixed to - the viewport, so that inverts — the viewport-relative top is the - constant, and the scroll offset must not be added back in. Pointer - coordinates are viewport-relative already, so the conversion below - gets simpler rather than harder. */ - const box = { left: 0, top: 0, width: 1, height: 1 }; + // Store page coordinates so pointer tracking stays aligned after scrolling. + const box = { left: 0, pageTop: 0, width: 1, height: 1 }; const measureBox = (): void => { const r = canvas.getBoundingClientRect(); box.left = r.left; - box.top = r.top; + box.pageTop = r.top + window.scrollY; box.width = r.width || 1; box.height = r.height || 1; }; @@ -550,24 +539,9 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { gl.uniform2f(uRes, w, h); }; - /* The clearing: an element with `data-index-zone`, measured against the - canvas whenever either changes shape. Nothing on this site carries - the attribute, so this resolves to nothing and `u_zone` keeps its - empty sentinel — `zoneSd` then returns on its first line and the - keep-out costs one uniform per fragment. - - It is kept rather than stripped because it is load-bearing behaviour - in the original, and because the home page's index section is the - structural analogue of the rows it was written for: if the field is - ever asked to part around them, this is what it will need. The one - thing to redo first is the measurement — a viewport-fixed canvas and - a zone that scrolls with the page part company on every scroll, so - the box would have to be re-measured against the scroll offset - rather than only when either element changes shape. - - The lookup is retried a few times rather than once: the original's - hero mounted its rows after it had measured itself, and anything - here that marks a zone from script would do the same. */ + // Observe the local shelf clearing; the home hero rebuilds its rows on + // theme and breakpoint changes without replacing this canvas. + const host = canvas.parentElement!; let zoneEl: HTMLElement | null = null; let zoneRo: ResizeObserver | null = null; const measureZone = (): void => { @@ -586,18 +560,20 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { (c.height / 2 - (z.top - c.top)) / H, ); }; - let zoneTimer = 0; - const findZone = (attempt = 0): void => { - const el = document.querySelector<HTMLElement>('[data-index-zone]'); + const findZone = (): void => { + const el = host.querySelector<HTMLElement>('[data-index-zone]'); + if (el === zoneEl) return; + zoneRo?.disconnect(); + zoneEl = el; if (el) { - zoneEl = el; zoneRo = new ResizeObserver(measureZone); zoneRo.observe(el); - measureZone(); - return; } - if (attempt < 8) zoneTimer = window.setTimeout(() => findZone(attempt + 1), 250); + measureZone(); + if (!running) still(); }; + const zoneChanges = new MutationObserver(findZone); + zoneChanges.observe(host, { childList: true }); const draw = (t: number): void => { if (lost) return; @@ -631,14 +607,9 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { draw(t); }; - /* One gate for the loop, as in the original — minus its in-view test. - The original observed its own canvas with an IntersectionObserver and - paused when the hero scrolled away; a canvas fixed to the viewport is - always intersecting, so that observer could only ever report true. - It is dropped deliberately rather than quietly: the tab-hidden pause - below is the one that still does work here. */ + // Stop spending GPU time once the reader scrolls past the hero. const sync = (): void => { - const want = live && visible; + const want = live && visible && inView; if (want && !running) { running = true; size(); @@ -657,17 +628,15 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { const onMove = (e: PointerEvent): void => { if (e.pointerType === 'touch') return; const x = ((e.clientX - box.left) / box.width) * 2 - 1; - const y = -(((e.clientY - box.top) / box.height) * 2 - 1); + const top = box.pageTop - window.scrollY; + const y = -(((e.clientY - top) / box.height) * 2 - 1); /* Off the field the cursor is handed back to the wanderer. */ if (x < -1.05 || x > 1.05 || y < -1.05 || y > 1.05) return; mouse.x = x; mouse.y = y; lastMove = e.timeStamp; }; - /* How far the page has been scrolled, in viewport heights — the field - slides up a little against it and then holds. `box.height` is the - viewport now rather than a hero's band, so one screen of scrolling - spends the whole term. */ + // Parallax is bounded to the height of this hero. const onScroll = (): void => { scroll = Math.max(0, Math.min(1, window.scrollY / box.height)); }; @@ -698,6 +667,12 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { if (!running) still(); }); ro.observe(canvas); + const io = new IntersectionObserver(([entry]) => { + inView = entry.isIntersecting; + measureBox(); + sync(); + }); + io.observe(canvas); /* No pointer listener under reduced motion: there is no loop to read the cursor, and the still frame is drawn at a fixed one. */ @@ -720,8 +695,9 @@ export function mountSignalField(canvas: HTMLCanvasElement): () => void { torn = true; running = false; cancelAnimationFrame(raf); - clearTimeout(zoneTimer); + zoneChanges.disconnect(); ro.disconnect(); + io.disconnect(); zoneRo?.disconnect(); window.removeEventListener('pointermove', onMove); window.removeEventListener('scroll', onScroll); diff --git a/src/styles/chrome.css b/src/styles/chrome.css @@ -9,7 +9,7 @@ .slab flat panel — 1px rule, square, no shadow .plate inverse (night) panel, defined in tokens.css .grain the film over everything - .signal-field the dot field under everything + .signal-field the dot field inside each hero .micro 10px mono label — the site's furniture type .eyebrow `03 ^: LABEL` lockup with a keyed rule .btn flat control @@ -49,42 +49,17 @@ } /* ---- The field ----------------------------------------------------------- */ -/* The other sheet pinned to the viewport, and the film's opposite number: - one WebGL canvas under the whole site, drawing the signal map (hosts on - an irregular lattice, links lighting where the pointer reaches). It is - mounted once in Base.astro and lives for the whole session; - `src/scripts/signal-field.ts` owns everything else about it. - - It is a ground, not a control, so it takes no events at all — the - pointer it follows is listened for on `window`, which leaves every link - and button on the page its own hover. - - The fixed layers, bottom to top, are the site's whole stacking contract - and the reason this one sits at 0: - - 0 .signal-field this canvas - 1 .grain the film — the canvas is part of its backdrop, - so the field is grained along with the page - 2 main, .site-header, .site-footer all page content (global.css) - 40 .to-top - 60 .site-header once stuck, and `.mast` (daemon.css) - 90 .scroll-progress - 100 .search-overlay - - 0 rather than -1 because the ladder already leaves 0 free, and because - "the bottom layer of the document" is what this is — not something - hiding behind it. Either index is in fact visible, and the reason is - worth writing down, since every page paints an opaque ground: `html` - sets no background, so `body`'s `--bg` propagates to the root element - and is painted as the canvas background, *below every child of body*. - A fixed child of body therefore paints over the page ground rather - than under it. What does hide the field is an opaque box in the - content layer — the banner plate, the marquee band, the footer — and - that is intended; the field reads in the paper between them. */ +/* Each hero contains and clips its own field. Negative stacking keeps the + canvas behind text while isolation keeps it above the hero's ground. */ +.signal-hero { + position: relative; + isolation: isolate; + overflow: hidden; +} .signal-field { - position: fixed; + position: absolute; inset: 0; - z-index: 0; + z-index: -1; width: 100%; height: 100%; display: block; diff --git a/src/styles/global.css b/src/styles/global.css @@ -773,16 +773,13 @@ main, .site-header, .site-footer { position: relative; z-index: 2; } .marquee__tab .dot { color: #9ccfd8; } /* ---- Section banner ------------------------------------------------------ */ -/* Dark plate, live noise field, left scrim. Legibility on the plate is - carried by two layers, not one: the canvas erases its own top and bottom - bands, AND this gradient sits over the noise, under the content. */ +/* The left scrim softens the hero field beneath the heading. */ .banner { position: relative; overflow: hidden; background: var(--plate); border-bottom: 1px solid var(--fg); } -.banner__field { position: absolute; inset: 0; width: 100%; height: 100%; display: block; } .banner__scrim { position: absolute; top: 0; bottom: 0; left: 0; diff --git a/test/hero-and-shelves.test.mjs b/test/hero-and-shelves.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import { Window } from 'happy-dom'; + +async function documentAt(route) { + const window = new Window(); + window.document.write(await readFile(new URL(`../dist/${route}index.html`, import.meta.url), 'utf8')); + return window.document; +} + +test('signal canvases belong to page heroes and never the reading area', async () => { + for (const route of ['', 'osint/', 'sheets/enumeration/2-4-cheatsheet-gitleaks/']) { + const document = await documentAt(route); + const fields = [...document.querySelectorAll('[data-signal]')]; + assert.equal(fields.length, 1, route); + assert.ok(fields[0].parentElement.classList.contains('signal-hero'), route); + assert.equal(document.querySelector('body > [data-signal], article [data-signal]'), null, route); + assert.equal(fields[0].hasAttribute('data-astro-transition-persist'), false, route); + } +}); + +test('OSINT and Enumeration both link to the original secret-scanner sheets', async () => { + for (const route of ['osint/', 'enumeration/']) { + const document = await documentAt(route); + for (const slug of ['2-4-cheatsheet-gitleaks', '2-5-cheatsheet-trufflehog']) { + assert.equal(document.querySelectorAll(`a.rec[href="/sheets/enumeration/${slug}"]`).length, 1, `${route}: ${slug}`); + } + } +}); + +test('secret-scanner instructions render as prose after the installation block', async () => { + for (const slug of ['2-4-cheatsheet-gitleaks', '2-5-cheatsheet-trufflehog']) { + const document = await documentAt(`sheets/enumeration/${slug}/`); + const headings = [...document.querySelectorAll('article h2')].map((h) => h.textContent); + assert.ok(headings.includes('Subcommands at a Glance'), slug); + assert.ok(document.querySelector('article table'), slug); + } +});