index.astro (11146B)
1 --- 2 import Base from '../../layouts/Base.astro'; 3 import SectionBanner from '../../components/SectionBanner.astro'; 4 import RecordRow from '../../components/RecordRow.astro'; 5 import CategoryNav from '../../components/CategoryNav.astro'; 6 import { CATEGORIES } from '../../lib/taxonomy'; 7 import type { CategoryDef } from '../../lib/taxonomy'; 8 import { sheetsByCategory, sheetHref, type Sheet } from '../../lib/sheets'; 9 import { url } from '../../lib/url'; 10 import pentestDownloads from '../../data/pentest-workflow-downloads.json'; 11 12 export async function getStaticPaths() { 13 const groups = await sheetsByCategory(); 14 return CATEGORIES.map((c, i) => ({ 15 params: { category: c.slug }, 16 props: { 17 cat: c, 18 items: groups.find((g) => g.slug === c.slug)?.items ?? [], 19 n: String(i + 1).padStart(2, '0'), 20 }, 21 })); 22 } 23 interface Props { cat: CategoryDef; items: Sheet[]; n: string } 24 const { cat, items, n } = Astro.props; 25 26 /* Kill-chain order for the sub-sections. A sheet whose subcategory isn't 27 listed falls to the end under its own name; a category with no 28 subcategories at all (every domain except Active Directory today) renders 29 one flat index, exactly as before. */ 30 const SUBCAT_ORDER = [ 31 'CPTS Attack Flow', 32 'Companion Guides', 33 'General CPTS Cheatsheets', 34 'Credential Access', 35 'Kerberos & Delegation', 36 'ACL Abuse', 37 'ADCS & Certificates', 38 'Domain Controller Attacks', 39 'Privilege & Group Abuse', 40 'Lateral Movement', 41 'Persistence', 42 'Trust Abuse', 43 'Advanced & Post-Exploitation', 44 'Tooling & Recon', 45 ]; 46 47 /* Within a section: numbered techniques first (attack-2 before attack-40, 48 esc1 before esc10 — numeric, not lexical), then everything else A→Z. The 49 prefix rank keeps all attack-* together, then all esc-*, and so on. */ 50 const PFX_RANK: Record<string, number> = { attack: 0, esc: 1, persist: 2, theft: 3, dpersist: 4 }; 51 function sortKey(entry: Sheet): [number, number, number, string] { 52 const slug = entry.id.split('/').pop() ?? ''; 53 if (entry.data.order !== undefined) return [-1, 0, entry.data.order, slug]; 54 const m = slug.match(/^([a-z]+)-?(\d+)/); 55 if (m && m[1] in PFX_RANK) return [0, PFX_RANK[m[1]], parseInt(m[2], 10), slug]; 56 return [1, 0, 0, (entry.data.title || slug).toLowerCase()]; 57 } 58 function cmp(a: Sheet, b: Sheet) { 59 const ka = sortKey(a), kb = sortKey(b); 60 for (let i = 0; i < ka.length; i++) { 61 if (ka[i] < kb[i]) return -1; 62 if (ka[i] > kb[i]) return 1; 63 } 64 return 0; 65 } 66 67 const hasSubcats = items.some((s) => s.data.subcategory); 68 type Group = { name: string; items: Sheet[] }; 69 let groups: Group[] = []; 70 if (hasSubcats) { 71 const buckets = new Map<string, Sheet[]>(); 72 for (const s of items) { 73 const key = s.data.subcategory ?? 'Other'; 74 (buckets.get(key) ?? buckets.set(key, []).get(key)!).push(s); 75 } 76 const ordered = [ 77 ...SUBCAT_ORDER.filter((k) => buckets.has(k)), 78 ...[...buckets.keys()].filter((k) => !SUBCAT_ORDER.includes(k)).sort(), 79 ]; 80 groups = ordered.map((name) => ({ name, items: [...buckets.get(name)!].sort(cmp) })); 81 } 82 83 const byDifficulty = (level: string) => items.filter((s) => s.data.difficulty === level).length; 84 const tools = new Set(items.flatMap((s) => s.data.tools)); 85 const bannerStats = [ 86 { label: 'Sheets', value: String(items.length).padStart(2, '0'), accent: true }, 87 ...(hasSubcats 88 ? [{ label: 'Sections', value: String(groups.length).padStart(2, '0') }] 89 : [{ label: 'Tools covered', value: String(tools.size).padStart(2, '0') }]), 90 { label: 'Beginner / adv', value: `${byDifficulty('beginner')} / ${byDifficulty('advanced')}` }, 91 ]; 92 93 /* Section accents cycle the palette so the sub-headers read as a colour 94 run down the page, the same key the masthead and marquee use. */ 95 const SECTION_ACCENTS = ['iris', 'foam', 'love', 'gold', 'rose', 'pine']; 96 const downloads = cat.slug === 'pentest-workflow' ? pentestDownloads : []; 97 const downloadBytes = downloads.reduce((sum, item) => sum + item.size, 0); 98 const downloadSize = `${(downloadBytes / 1024 / 1024).toFixed(1)} MiB`; 99 --- 100 <Base title={`${cat.title} — DÆMON//SEC`} description={cat.blurb}> 101 <SectionBanner n={n} label={cat.tag} title={cat.title} blurb={cat.blurb} accent={cat.accent} stats={bannerStats} /> 102 103 <div class="wrap"> 104 <nav class="breadcrumb" style="margin-top:1.6rem;"> 105 <a href={url('')}>~</a><span class="sep">/</span> 106 <span style={`color: var(--${cat.accent})`}>{cat.tag.toLowerCase()}</span> 107 </nav> 108 109 <div style="margin:1.4rem 0 2rem;"><CategoryNav active={cat.slug} /></div> 110 111 {items.length === 0 && ( 112 <div class="slab corners" style="margin:1.5rem 0 3rem; padding:1.2rem 1.3rem;"> 113 <p class="muted mono" style="font-size:0.85rem;"> 114 No sheets in this domain yet. More landing soon — 115 <a class="accent-text" href={url('')}>back to the vault</a>. 116 </p> 117 </div> 118 )} 119 120 {items.length > 0 && !hasSubcats && ( 121 <div class="index" style="margin-bottom:3rem;"> 122 {items.map((entry, i) => ( 123 <RecordRow 124 href={url(sheetHref(entry))} 125 n={String(i + 1).padStart(2, '0')} 126 title={entry.data.title} 127 kind={entry.data.difficulty} 128 accent={cat.accent} 129 meta={entry.data.updated ?? entry.id.split('/').pop()} 130 /> 131 ))} 132 </div> 133 )} 134 135 {items.length > 0 && hasSubcats && ( 136 <div style="margin-bottom:3rem;"> 137 {groups.map((g, gi) => { 138 const acc = SECTION_ACCENTS[gi % SECTION_ACCENTS.length]; 139 return ( 140 <section class="subcat" id={`s-${gi}`}> 141 <div class="section-head" style="margin-bottom:0.9rem;"> 142 <div> 143 <p class="eyebrow" style={`--acc: var(--${acc});`}> 144 <span class="eyebrow__n">{String(gi + 1).padStart(2, '0')}</span> 145 <span class="eyebrow__mark">^:</span> 146 <span>{g.items.length} {g.items.length === 1 ? 'sheet' : 'sheets'}</span> 147 <span class="eyebrow__rule" aria-hidden="true"></span> 148 </p> 149 <h2>{g.name}</h2> 150 </div> 151 </div> 152 <div class="index"> 153 {g.items.map((entry, i) => ( 154 <RecordRow 155 href={url(sheetHref(entry))} 156 n={String(i + 1).padStart(2, '0')} 157 title={entry.data.title} 158 kind={entry.data.difficulty} 159 accent={acc} 160 meta={entry.data.updated ?? entry.id.split('/').pop()} 161 /> 162 ))} 163 </div> 164 </section> 165 ); 166 })} 167 </div> 168 )} 169 170 {downloads.length > 0 && ( 171 <section class="download-library" id="signed-downloads" aria-labelledby="signed-downloads-title"> 172 <div class="download-library__head"> 173 <div> 174 <p class="eyebrow" style="--acc: var(--gold);"> 175 <span class="eyebrow__n">DL</span> 176 <span class="eyebrow__mark">^:</span> 177 <span>{downloads.length} signed files · {downloadSize}</span> 178 <span class="eyebrow__rule" aria-hidden="true"></span> 179 </p> 180 <h2 id="signed-downloads-title">Signed attachment library</h2> 181 <p class="muted download-library__lede"> 182 Every attachment referenced by the CPTS sheets is mirrored here. Each download has an individual 183 SHA-256 checksum and detached OpenPGP signature; the combined manifest covers the complete set. 184 </p> 185 </div> 186 <div class="download-library__manifest"> 187 <a class="btn" href={url('downloads/pentest-workflow/SHA256SUMS')} download>SHA256SUMS</a> 188 <a class="btn btn--ghost" href={url('downloads/pentest-workflow/SHA256SUMS.asc')} download>Manifest signature</a> 189 </div> 190 </div> 191 192 <div class="download-index" role="list"> 193 {downloads.map((item, i) => ( 194 <article class="download-row corners" role="listitem"> 195 <span class="download-row__n">{String(i + 1).padStart(2, '0')}</span> 196 <div class="download-row__main"> 197 <a class="download-row__file" href={url(item.href)} download>{item.name}</a> 198 <span class="download-row__hash" title={item.sha256}>{item.sha256}</span> 199 </div> 200 <span class="download-row__size">{item.sizeLabel}</span> 201 <div class="download-row__proof"> 202 <a href={url(item.checksumHref)} download>SHA-256</a> 203 <a href={url(item.signatureHref)} download>GPG</a> 204 </div> 205 </article> 206 ))} 207 </div> 208 </section> 209 )} 210 </div> 211 </Base> 212 213 <style> 214 .subcat + .subcat { margin-top: clamp(2.2rem, 4.5vw, 3.4rem); } 215 .download-library { 216 margin: clamp(2.8rem, 6vw, 5rem) 0 4rem; 217 scroll-margin-top: 6rem; 218 } 219 .download-library__head { 220 display: grid; 221 gap: 1.2rem 2rem; 222 align-items: end; 223 margin-bottom: 1.2rem; 224 padding-bottom: 1rem; 225 border-bottom: 1px solid var(--rule); 226 } 227 .download-library__head h2 { 228 margin-top: 0.35rem; 229 font-family: var(--font-display); 230 font-size: clamp(1.7rem, 4vw, 2.7rem); 231 letter-spacing: -0.035em; 232 } 233 .download-library__lede { max-width: 72ch; margin-top: 0.55rem; } 234 .download-library__manifest { display: flex; flex-wrap: wrap; gap: 0.55rem; } 235 .download-index { display: grid; gap: 0.45rem; } 236 .download-row { 237 position: relative; 238 display: grid; 239 grid-template-columns: 2.2rem minmax(0, 1fr) auto; 240 gap: 0.55rem 0.9rem; 241 align-items: center; 242 border: 1px solid var(--rule); 243 background: color-mix(in srgb, var(--surface) 76%, transparent); 244 padding: 0.75rem 0.85rem; 245 } 246 .download-row__n, 247 .download-row__size, 248 .download-row__hash, 249 .download-row__proof { 250 font-family: var(--font-mono); 251 font-size: var(--step-micro); 252 letter-spacing: var(--track-micro); 253 text-transform: uppercase; 254 } 255 .download-row__n { color: var(--gold); } 256 .download-row__main { min-width: 0; } 257 .download-row__file { 258 display: block; 259 overflow-wrap: anywhere; 260 color: var(--fg); 261 font-weight: 650; 262 } 263 .download-row__file:hover { color: var(--love); } 264 .download-row__hash { 265 display: block; 266 max-width: 100%; 267 margin-top: 0.2rem; 268 overflow: hidden; 269 color: var(--fg-faint); 270 text-overflow: ellipsis; 271 text-transform: none; 272 white-space: nowrap; 273 } 274 .download-row__size { color: var(--fg-muted); white-space: nowrap; } 275 .download-row__proof { 276 grid-column: 2 / -1; 277 display: flex; 278 gap: 0.75rem; 279 } 280 .download-row__proof a { color: var(--iris); } 281 .download-row__proof a:last-child { color: var(--foam); } 282 @media (min-width: 720px) { 283 .download-library__head { grid-template-columns: minmax(0, 1fr) auto; } 284 .download-library__manifest { justify-content: flex-end; } 285 .download-row { grid-template-columns: 2.2rem minmax(0, 1fr) 5.5rem 8rem; } 286 .download-row__proof { grid-column: auto; justify-content: flex-end; } 287 } 288 </style>