Add red-team shadow dossiers site
This commit is contained in:
parent
cb928248cb
commit
17f096cc3e
33 changed files with 3854 additions and 2 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,14 +1,20 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
.env.staging
|
||||
.venv
|
||||
__pycache__/
|
||||
*.pyc
|
||||
dist/
|
||||
build/
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
.vite/
|
||||
|
||||
# Re-voice workspace
|
||||
tmp/
|
||||
*.log
|
||||
|
||||
|
|
|
|||
2
site/red-team-shadow-dossiers/.env.example
Normal file
2
site/red-team-shadow-dossiers/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
VITE_PUBLIC_BASE_URL=
|
||||
VITE_ENABLE_ROAST=false
|
||||
24
site/red-team-shadow-dossiers/.gitignore
vendored
Normal file
24
site/red-team-shadow-dossiers/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
46
site/red-team-shadow-dossiers/App.tsx
Normal file
46
site/red-team-shadow-dossiers/App.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { Hero } from './components/Hero';
|
||||
import { LeakViewer } from './components/LeakViewer';
|
||||
import { PricingTiers } from './components/PricingTiers';
|
||||
import { RoastGenerator } from './components/RoastGenerator';
|
||||
import { Footer } from './components/Footer';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen selection:bg-red-ink selection:text-white overflow-x-hidden">
|
||||
<Header />
|
||||
<main>
|
||||
<Hero />
|
||||
<LeakViewer />
|
||||
<RoastGenerator />
|
||||
<PricingTiers />
|
||||
|
||||
{/* Evidence CTA */}
|
||||
<section className="py-16 md:py-20 px-6 border-t hairline">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="rounded-xl border hairline bg-white p-8 md:p-10 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div>
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Verification</div>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight text-slate-900">Evidence index & trace verification</h2>
|
||||
<p className="text-slate-700 mt-2 max-w-2xl">
|
||||
Browse hashes, trace bundles, and reproducibility notes. If you want the bite, keep the proof.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="#dossiers"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center rounded-md bg-slate-900 text-white px-6 py-3 text-sm font-semibold hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Evidence index (staged)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
75
site/red-team-shadow-dossiers/CODEX_IMPLEMENTATION.md
Normal file
75
site/red-team-shadow-dossiers/CODEX_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# CODEX Implementation Guide (Server + Launch Wiring)
|
||||
|
||||
This repo is a **pre-launch staging front-end**. It is intentionally safe:
|
||||
- No “live” PDF/evidence links are active unless `dossier.status === 'live'`
|
||||
- No model API keys are shipped to the browser
|
||||
- The roast generator (if enabled) calls a server endpoint
|
||||
|
||||
## 1) Publish static artifacts
|
||||
|
||||
Target paths (recommended):
|
||||
- PDF: `/static/hosted/snyk-shadow-dossier.pdf`
|
||||
- Evidence index: `/static/hosted/evidence/index.html`
|
||||
|
||||
### What to ship
|
||||
- The free public PDF
|
||||
- Evidence index HTML + any referenced assets (bundles, hashes, trace files)
|
||||
|
||||
## 2) Flip dossiers to LIVE
|
||||
|
||||
Edit `constants.tsx`:
|
||||
|
||||
- Set:
|
||||
- `status: 'live'`
|
||||
- `pdfPath: '/static/hosted/<file>.pdf'`
|
||||
- `evidencePath: '/static/hosted/evidence/index.html'`
|
||||
|
||||
Optional:
|
||||
- Add `sha256` and `bytes` (use `node scripts/hash.mjs path/to.pdf`)
|
||||
- Add `gumroadUrl` for classified edition
|
||||
- Confirm `contactEmail`
|
||||
|
||||
## 3) Configure base URL (optional)
|
||||
|
||||
If you deploy under a domain/subdomain and want absolute links, set:
|
||||
|
||||
- `VITE_PUBLIC_BASE_URL=https://infrafabric.io` (or your subdomain)
|
||||
|
||||
If unset, the app uses relative links (works fine for same-origin hosting).
|
||||
|
||||
## 4) Roast Generator (optional)
|
||||
|
||||
### Current behavior
|
||||
- UI is hidden unless: `VITE_ENABLE_ROAST=true`
|
||||
- Client calls: `POST /api/roast` with JSON `{ "content": "..." }`
|
||||
- Expected response: `{ "text": "..." }`
|
||||
|
||||
### Implementing the endpoint
|
||||
You can implement this in whichever server environment you use. Examples are in `server_stubs/`.
|
||||
|
||||
Required behavior:
|
||||
- Validate request body
|
||||
- Rate limit (recommended)
|
||||
- Call your LLM provider using a **server-side** secret
|
||||
- Return JSON `{ text }`
|
||||
|
||||
### Provider dependency
|
||||
If you want Gemini, add `@google/genai` server-side and use `server_stubs/gemini_client_example.ts` as a starting point.
|
||||
|
||||
## 5) Replace staged UI labels (optional)
|
||||
|
||||
Staged labels are intentional pre-launch.
|
||||
Once live, you can:
|
||||
- Rename “Evidence (staged)” back to “Evidence”
|
||||
- Replace “Request early access” with “Download PDF”
|
||||
- Enable premium tier by adding Gumroad URL in `constants.tsx`
|
||||
|
||||
## 6) Final checklist
|
||||
|
||||
- [ ] PDF link works (no 404)
|
||||
- [ ] Evidence index link works
|
||||
- [ ] `status` set to `live`
|
||||
- [ ] Optional: SHA-256 + bytes present and correct
|
||||
- [ ] No secret keys shipped to client bundles
|
||||
- [ ] OpenGraph image exists at `/og-dossier.jpg`
|
||||
- [ ] Favicon present at `/favicon.svg`
|
||||
64
site/red-team-shadow-dossiers/README.md
Normal file
64
site/red-team-shadow-dossiers/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# InfraFabric Red Team — Shadow Dossiers (Front-End)
|
||||
|
||||
A minimalist React + Vite publication hub for InfraFabric’s “Shadow Dossiers” drops:
|
||||
- serious, paper/ink layout
|
||||
- staged/publication statuses (draft/scheduled/live)
|
||||
- verifiability hooks (evidence index + optional SHA-256 + size)
|
||||
- **no client-side model keys** (roast generator is feature-flagged and calls a server endpoint)
|
||||
|
||||
## Local dev
|
||||
|
||||
Prereqs: Node.js 18+
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open: http://localhost:3000
|
||||
|
||||
## Production (self-host)
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
PORT=8080 npm run start
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Create `.env.local` from `.env.example`:
|
||||
|
||||
- `VITE_PUBLIC_BASE_URL`
|
||||
Optional. When set (e.g. `https://infrafabric.io`) it will prefix dossier `pdfPath`/`evidencePath`.
|
||||
|
||||
- `VITE_ENABLE_ROAST`
|
||||
Defaults to `false`. When `true`, renders the Roast Generator UI which calls `POST /api/roast`.
|
||||
|
||||
## Staging behavior (important)
|
||||
|
||||
This bundle is **pre-launch**. Dossier buttons are gated by `dossier.status`:
|
||||
|
||||
- `draft` / `scheduled`: download + evidence links remain disabled, and the UI shows “staged.”
|
||||
- `live`: links activate (and optional SHA-256/bytes render if populated).
|
||||
|
||||
Update dossier status and paths in `constants.tsx`.
|
||||
|
||||
## Codex/server implementation checklist
|
||||
|
||||
See `CODEX_IMPLEMENTATION.md` for the complete checklist and stubs.
|
||||
|
||||
Quick summary:
|
||||
1. Publish PDFs and evidence pages at:
|
||||
- `/static/hosted/<dossier>.pdf`
|
||||
- `/static/hosted/evidence/index.html`
|
||||
2. Set `VITE_PUBLIC_BASE_URL` in the deployment environment if needed.
|
||||
3. (Optional) Implement `POST /api/roast` server-side and set `VITE_ENABLE_ROAST=true`.
|
||||
|
||||
## Hash helper
|
||||
|
||||
Compute SHA-256 + bytes for a PDF and paste into the dossier entry:
|
||||
|
||||
```bash
|
||||
node scripts/hash.mjs /path/to/file.pdf
|
||||
```
|
||||
18
site/red-team-shadow-dossiers/components/Disclaimer.tsx
Normal file
18
site/red-team-shadow-dossiers/components/Disclaimer.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Disclaimer: React.FC = () => (
|
||||
<div id="disclaimer" className="rounded-lg border hairline bg-white p-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<span className="mono text-[11px] uppercase tracking-wide text-slate-500">Legal notice</span>
|
||||
<span className="inline-flex items-center rounded-full border hairline px-2.5 py-1 mono text-[11px] text-slate-600">
|
||||
Satire / parody
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-slate-700">
|
||||
This site contains satirical critical commentary. The “Shadow Dossiers” are performance art intended to highlight
|
||||
systemic issues in governance, compliance, and security tooling. Nothing here is presented as a statement of fact
|
||||
about any specific organization. Verification artifacts (hashes, traces, evidence index) are provided to support
|
||||
reproducibility of the published materials.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
58
site/red-team-shadow-dossiers/components/Footer.tsx
Normal file
58
site/red-team-shadow-dossiers/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="py-12 px-6 border-t hairline bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-12 gap-10">
|
||||
<div className="md:col-span-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-9 h-9 rounded-md border hairline bg-slate-900 text-white flex items-center justify-center font-extrabold text-sm">
|
||||
IF
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-900">InfraFabric Red Team</div>
|
||||
<div className="mono text-[11px] text-slate-500">Shadow Dossiers</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed max-w-md">
|
||||
Satirical critical commentary, published alongside verification artifacts (hashes, traces, evidence index).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-7 grid sm:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-3">Links</div>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a className="underline hover:no-underline" href="#dossiers">Dossiers</a></li>
|
||||
<li><a className="underline hover:no-underline" href="#editions">Editions</a></li>
|
||||
<li><a className="underline hover:no-underline" href="#dossiers" target="_blank" rel="noopener noreferrer">Evidence index</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-3">Contact</div>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a className="underline hover:no-underline" href="mailto:dave@infrafabric.io">dave@infrafabric.io</a></li>
|
||||
<li><a className="underline hover:no-underline" href="mailto:redteam@infrafabric.io">redteam@infrafabric.io</a></li>
|
||||
<li><a className="underline hover:no-underline" href="https://x.com/IF_Dave" target="_blank" rel="noopener noreferrer">@IF_Dave</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-3">Notice</div>
|
||||
<p className="text-sm text-slate-700 leading-relaxed">
|
||||
This is satire/parody and critical commentary; not affiliated with any vendor. Nothing here is presented as a statement of fact.
|
||||
</p>
|
||||
<a className="mt-3 inline-block text-sm font-semibold underline hover:no-underline" href="#disclaimer">Read the disclaimer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 pt-6 border-t hairline">
|
||||
<p className="mono text-[11px] text-slate-500">
|
||||
© 2025 InfraFabric. Shadow Dossiers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
29
site/red-team-shadow-dossiers/components/Header.tsx
Normal file
29
site/red-team-shadow-dossiers/components/Header.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-[rgba(247,245,240,0.92)] backdrop-blur border-b hairline px-6 py-4">
|
||||
<div className="max-w-6xl mx-auto flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-md border hairline bg-white flex items-center justify-center font-extrabold text-slate-900 text-sm">
|
||||
IF
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold text-slate-900">InfraFabric Red Team</div>
|
||||
<div className="mono text-[11px] text-slate-500">Shadow Dossiers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center gap-6">
|
||||
<div className="hidden md:flex gap-6 text-sm text-slate-700">
|
||||
<a href="#dossiers" className="hover:text-slate-900">Dossiers</a>
|
||||
<a href="#editions" className="hover:text-slate-900">Editions</a>
|
||||
<a href="#dossiers" target="_blank" rel="noopener noreferrer" className="hover:text-slate-900">Evidence (staged)</a>
|
||||
<a href="mailto:dave@infrafabric.io" className="hover:text-slate-900">Contact</a>
|
||||
</div>
|
||||
<a href="https://x.com/IF_Dave" target="_blank" rel="noopener noreferrer" className="mono text-[11px] text-slate-500 hover:text-slate-800">@IF_Dave</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
50
site/red-team-shadow-dossiers/components/Hero.tsx
Normal file
50
site/red-team-shadow-dossiers/components/Hero.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { Disclaimer } from './Disclaimer';
|
||||
|
||||
export const Hero: React.FC = () => {
|
||||
return (
|
||||
<section className="pt-14 pb-16 px-6 border-b hairline">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid lg:grid-cols-12 gap-10 items-start">
|
||||
<div className="lg:col-span-7">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-4">
|
||||
Publication · Red Team Commentary · Verifiable Artifacts
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-6xl font-extrabold tracking-tight text-slate-900 leading-[1.05] mb-5">
|
||||
Shadow Dossiers
|
||||
</h1>
|
||||
<p className="text-lg text-slate-700 leading-relaxed max-w-2xl mb-8">
|
||||
Satirical, operator‑grade commentary on governance and compliance theater—published alongside an evidence index
|
||||
so readers can verify what’s being referenced.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href="#dossiers"
|
||||
className="inline-flex items-center justify-center rounded-md bg-slate-900 text-white px-5 py-3 text-sm font-semibold hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Browse dossiers
|
||||
</a>
|
||||
<a
|
||||
href="#dossiers"
|
||||
target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center rounded-md border hairline bg-white px-5 py-3 text-sm font-semibold text-slate-900 hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Evidence index
|
||||
</a>
|
||||
<a
|
||||
href="mailto:dave@infrafabric.io"
|
||||
className="inline-flex items-center justify-center rounded-md border hairline bg-transparent px-5 py-3 text-sm font-semibold text-slate-700 hover:bg-white transition-colors"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-5">
|
||||
<Disclaimer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
169
site/red-team-shadow-dossiers/components/LeakViewer.tsx
Normal file
169
site/red-team-shadow-dossiers/components/LeakViewer.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import React from 'react';
|
||||
import { DOSSIERS } from '../constants';
|
||||
import { resolvePublicUrl } from '../lib/urls';
|
||||
|
||||
const formatBytes = (bytes?: number) => {
|
||||
if (!bytes) return null;
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let b = bytes;
|
||||
let i = 0;
|
||||
while (b >= 1024 && i < units.length - 1) {
|
||||
b /= 1024;
|
||||
i += 1;
|
||||
}
|
||||
return `${b.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'live':
|
||||
return 'LIVE';
|
||||
case 'scheduled':
|
||||
return 'SCHEDULED';
|
||||
case 'draft':
|
||||
default:
|
||||
return 'DRAFT';
|
||||
}
|
||||
};
|
||||
|
||||
export const LeakViewer: React.FC = () => {
|
||||
return (
|
||||
<section id="dossiers" className="py-16 md:py-20 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Dossiers</div>
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">Current publications</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{DOSSIERS.map((d) => {
|
||||
const isLive = d.status === 'live';
|
||||
const pdfUrl = resolvePublicUrl(d.pdfPath);
|
||||
const evidenceUrl = resolvePublicUrl(d.evidencePath);
|
||||
|
||||
return (
|
||||
<article key={d.id} className="rounded-2xl border hairline bg-white shadow-sm overflow-hidden">
|
||||
<div className="grid md:grid-cols-12">
|
||||
<div className="md:col-span-4 p-6 md:p-8 bg-paper">
|
||||
<div className="rounded-xl border hairline bg-white overflow-hidden">
|
||||
<img
|
||||
src={d.previewImageUrl}
|
||||
alt={`Cover for Shadow Dossier: ${d.title}`}
|
||||
className="w-full h-auto block"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-8 p-6 md:p-8">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span className="mono text-[11px] text-slate-500">{d.leakDate}</span>
|
||||
<span className="text-slate-300">•</span>
|
||||
|
||||
<span className="inline-flex items-center rounded-full border hairline px-2.5 py-1 mono text-[11px] text-slate-600">
|
||||
{d.classification}
|
||||
</span>
|
||||
|
||||
<span className="inline-flex items-center rounded-full border hairline px-2.5 py-1 mono text-[11px] text-slate-600">
|
||||
{statusLabel(d.status)}
|
||||
</span>
|
||||
|
||||
<span className="text-slate-300">•</span>
|
||||
<span className="mono text-[11px] text-slate-500">{d.id}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl md:text-3xl font-extrabold tracking-tight text-slate-900 mb-3">
|
||||
{d.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-base md:text-lg text-slate-700 leading-relaxed mb-6">{d.summary}</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="rounded-xl border hairline bg-paper p-4">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Verification</div>
|
||||
|
||||
{d.sha256 && isLive ? (
|
||||
<div className="space-y-1">
|
||||
<div className="mono text-[11px] text-slate-500">SHA-256</div>
|
||||
<div className="mono text-[12px] text-slate-700 break-all">{d.sha256}</div>
|
||||
{d.bytes ? (
|
||||
<div className="mono text-[11px] text-slate-500">Size: {formatBytes(d.bytes)}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-slate-600 leading-relaxed">
|
||||
Integrity details will publish with the drop. Until then, link verification remains staged.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{evidenceUrl && isLive ? (
|
||||
<div className="mt-3">
|
||||
<a
|
||||
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
||||
href={evidenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Verify (evidence index)
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border hairline bg-paper p-4">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Links</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
{pdfUrl && isLive ? (
|
||||
<a
|
||||
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
||||
href={pdfUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm font-semibold text-slate-400 cursor-not-allowed" title="Not published yet">
|
||||
PDF not published
|
||||
</span>
|
||||
)}
|
||||
|
||||
{d.gumroadUrl ? (
|
||||
<a
|
||||
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
||||
href={d.gumroadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Classified edition
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
{d.contactEmail ? (
|
||||
<a
|
||||
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
||||
href={`mailto:${d.contactEmail}?subject=${encodeURIComponent(`Shadow Dossier: ${d.title}`)}`}
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isLive ? (
|
||||
<div className="mt-3 text-sm text-slate-600 leading-relaxed">
|
||||
This dossier is staged. Links will activate when the drop is published.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
80
site/red-team-shadow-dossiers/components/PricingTiers.tsx
Normal file
80
site/red-team-shadow-dossiers/components/PricingTiers.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { PRICING_TIERS } from '../constants';
|
||||
|
||||
export const PricingTiers: React.FC = () => {
|
||||
return (
|
||||
<section id="editions" className="py-16 md:py-20 px-6 border-t hairline bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Editions & services</div>
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">Choose an engagement</h2>
|
||||
<p className="text-slate-700 mt-3 max-w-2xl">
|
||||
The public drops are free. Paid editions fund ongoing work and include the working files. Custom dossiers are limited in capacity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{PRICING_TIERS.map((tier) => (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`rounded-xl border hairline p-7 flex flex-col ${
|
||||
tier.variant === 'premium' ? 'bg-slate-900 text-white shadow-sm' : 'bg-white text-slate-900'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className={`mono text-[11px] uppercase tracking-[0.18em] ${tier.variant === 'premium' ? 'text-white/70' : 'text-slate-500'}`}>
|
||||
{tier.name}
|
||||
</div>
|
||||
<div className="mt-3 text-3xl font-extrabold">{tier.price}</div>
|
||||
<p className={`mt-3 text-sm leading-relaxed ${tier.variant === 'premium' ? 'text-white/80' : 'text-slate-700'}`}>
|
||||
{tier.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className={`flex-1 space-y-3 text-sm ${tier.variant === 'premium' ? 'text-white/85' : 'text-slate-700'}`}>
|
||||
{tier.features.map((f, i) => (
|
||||
<li key={i} className="flex items-start gap-3">
|
||||
<span className={`${tier.variant === 'premium' ? 'text-white' : 'text-red-ink'} mt-0.5 font-bold`}>✓</span>
|
||||
<span>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{(() => {
|
||||
const enabled = (tier.enabled !== false) && !!tier.actionUrl;
|
||||
const isMailto = (tier.actionUrl || '').startsWith('mailto');
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!enabled}
|
||||
onClick={() => {
|
||||
if (!enabled || !tier.actionUrl) return;
|
||||
window.open(tier.actionUrl, isMailto ? '_self' : '_blank', isMailto ? undefined : 'noopener,noreferrer');
|
||||
}}
|
||||
className={`w-full rounded-md py-3 text-sm font-semibold transition-colors ${
|
||||
!enabled
|
||||
? 'bg-slate-200 text-slate-500 cursor-not-allowed'
|
||||
: tier.variant === 'premium'
|
||||
? 'bg-white text-slate-900 hover:bg-slate-100'
|
||||
: 'bg-slate-900 text-white hover:bg-slate-800'
|
||||
}`}
|
||||
title={!enabled ? (tier.disabledHint || 'Not available yet') : undefined}
|
||||
>
|
||||
{tier.cta}
|
||||
</button>
|
||||
|
||||
{!enabled && tier.disabledHint ? (
|
||||
<div className="mt-2 text-xs text-slate-500 leading-relaxed">{tier.disabledHint}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
75
site/red-team-shadow-dossiers/components/RoastGenerator.tsx
Normal file
75
site/red-team-shadow-dossiers/components/RoastGenerator.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import React, { useState } from 'react';
|
||||
import { generateRoast } from '../services/roastClient';
|
||||
import { ENABLE_ROAST_GENERATOR } from '../lib/flags';
|
||||
|
||||
export const RoastGenerator: React.FC = () => {
|
||||
if (!ENABLE_ROAST_GENERATOR) return null;
|
||||
const [input, setInput] = useState('');
|
||||
const [output, setOutput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCritique = async () => {
|
||||
if (!input.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await generateRoast(input);
|
||||
setOutput(result);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="review" className="py-16 md:py-20 px-6 bg-[rgba(247,245,240,0.6)] border-t hairline">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Quick critique</div>
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">Preliminary “truth audit” (AI-assisted)</h2>
|
||||
<p className="text-slate-700 mt-3 max-w-3xl">
|
||||
Paste a short excerpt from a policy, whitepaper, control description, or vendor claim. This produces a rough critique to help you
|
||||
decide whether a custom dossier is worthwhile. Do not paste secrets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="rounded-xl border hairline bg-white p-6">
|
||||
<label className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500">Input</label>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
className="mt-3 w-full h-48 rounded-lg border hairline bg-slate-50 p-4 text-sm focus:outline-none focus:ring-2 focus:ring-[rgba(185,28,28,0.25)] resize-none"
|
||||
placeholder="Paste an excerpt here…"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCritique}
|
||||
disabled={loading}
|
||||
className="mt-4 w-full rounded-md bg-slate-900 text-white py-3 text-sm font-semibold hover:bg-slate-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? 'Generating…' : 'Generate critique'}
|
||||
</button>
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
Output is best-effort commentary. For a real engagement, request availability via email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border hairline bg-white p-6 relative">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500">Output</span>
|
||||
<a href="mailto:dave@infrafabric.io" className="text-xs font-semibold underline hover:no-underline">Request custom dossier</a>
|
||||
</div>
|
||||
|
||||
<div className="h-52 md:h-[19.5rem] overflow-auto rounded-lg border hairline bg-slate-50 p-4 text-sm leading-relaxed text-slate-800">
|
||||
{output ? output : <span className="text-slate-400">No output yet.</span>}
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="absolute inset-0 bg-white/60 backdrop-blur-sm flex items-center justify-center rounded-xl">
|
||||
<div className="w-8 h-8 border-2 border-slate-900 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
123
site/red-team-shadow-dossiers/constants.tsx
Normal file
123
site/red-team-shadow-dossiers/constants.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
import { Dossier, PricingTier } from './types';
|
||||
|
||||
const coverSvg = (title: string, id: string, classification: string) => {
|
||||
// Inline SVG cover to avoid stock-photo vibe and keep distribution simple.
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1000" viewBox="0 0 800 1000">
|
||||
<rect width="800" height="1000" fill="#f7f5f0"/>
|
||||
<rect x="50" y="60" width="700" height="880" fill="#ffffff" stroke="#0f172a" stroke-opacity="0.12"/>
|
||||
<rect x="50" y="60" width="700" height="70" fill="#0b0d10"/>
|
||||
<text x="80" y="105" font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial" font-size="22" fill="#ffffff" letter-spacing="1">
|
||||
INFRAFABRIC · RED TEAM
|
||||
</text>
|
||||
|
||||
<text x="80" y="205" font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial" font-size="46" font-weight="800" fill="#0f172a">
|
||||
Shadow Dossier
|
||||
</text>
|
||||
<text x="80" y="255" font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial" font-size="24" fill="#334155">
|
||||
${title.replace(/&/g, '&')}
|
||||
</text>
|
||||
|
||||
<rect x="80" y="320" width="250" height="34" rx="17" fill="#ffffff" stroke="#0f172a" stroke-opacity="0.18"/>
|
||||
<text x="98" y="343" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas" font-size="14" fill="#334155">
|
||||
CLASSIFICATION: ${classification}
|
||||
</text>
|
||||
|
||||
<text x="80" y="410" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas" font-size="14" fill="#64748b">
|
||||
DOSSIER_ID: ${id}
|
||||
</text>
|
||||
<line x1="80" y1="440" x2="720" y2="440" stroke="#0f172a" stroke-opacity="0.10"/>
|
||||
|
||||
<g opacity="0.35">
|
||||
<text x="80" y="520" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas" font-size="12" fill="#64748b">
|
||||
This document is satirical/parody and critical commentary.
|
||||
</text>
|
||||
<text x="80" y="545" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas" font-size="12" fill="#64748b">
|
||||
Verification artifacts (hashes, traces) are provided via the evidence index.
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<g opacity="0.18">
|
||||
<text x="420" y="920" font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial" font-size="120" font-weight="900" fill="#b91c1c"
|
||||
transform="rotate(-12 420 920)">SATIRE</text>
|
||||
</g>
|
||||
</svg>
|
||||
`.trim();
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
};
|
||||
|
||||
export const DOSSIERS: Dossier[] = [
|
||||
{
|
||||
id: 'snyk-mirror-01',
|
||||
title: 'The Snyk-Mirror Anomaly',
|
||||
leakDate: 'Dec 25, 2025',
|
||||
summary:
|
||||
'A satirical red-team dossier on compliance theater around AI code guardrails—where policy meets production and loses. Includes diagrams, failure modes, and a verifiability trail.',
|
||||
classification: 'EYES-ONLY',
|
||||
status: 'scheduled',
|
||||
previewImageUrl: coverSvg('The Snyk-Mirror Anomaly', 'snyk-mirror-01', 'EYES-ONLY'),
|
||||
|
||||
// These are paths (not live yet). Codex/server will publish them when ready.
|
||||
pdfPath: '/static/hosted/snyk-shadow-dossier.pdf',
|
||||
evidencePath: '/static/hosted/evidence/index.html',
|
||||
|
||||
// Optional: set once sales are live.
|
||||
// gumroadUrl: 'https://gumroad.com/l/your-product',
|
||||
contactEmail: 'redteam@infrafabric.io',
|
||||
|
||||
// Optional: add sha256/bytes once you compute them at build time.
|
||||
// sha256: '…',
|
||||
// bytes: 1234567,
|
||||
}
|
||||
];
|
||||
|
||||
export const PRICING_TIERS: PricingTier[] = [
|
||||
{
|
||||
name: 'Public Drop (Free)',
|
||||
price: '$0',
|
||||
variant: 'free',
|
||||
description: 'The public PDF drop. Satire/parody + critical commentary, published with a verification trail when live.',
|
||||
features: [
|
||||
'Full PDF drop (public)',
|
||||
'Diagrams & artifacts intact',
|
||||
'Evidence index link for verification',
|
||||
'No email required'
|
||||
],
|
||||
cta: 'Request early access',
|
||||
enabled: true,
|
||||
actionUrl: 'mailto:redteam@infrafabric.io?subject=Shadow%20Dossier%20Early%20Access',
|
||||
disabledHint: 'The public drop is not published yet.'
|
||||
},
|
||||
{
|
||||
name: 'Classified Edition (PWYW)',
|
||||
price: '$29+',
|
||||
variant: 'premium',
|
||||
description: 'Annotated PDF + sources (raw Markdown + Mermaid). Sales go live when the first drop publishes.',
|
||||
features: [
|
||||
'Annotated PDF (red‑ink marginalia)',
|
||||
'Raw Markdown source',
|
||||
'Mermaid diagram sources',
|
||||
'Personalized classification ID'
|
||||
],
|
||||
cta: 'Sales open soon',
|
||||
enabled: false,
|
||||
actionUrl: '',
|
||||
disabledHint: 'Gumroad link will be added at launch.'
|
||||
},
|
||||
{
|
||||
name: 'Custom Dossier (Service)',
|
||||
price: '$499',
|
||||
variant: 'enterprise',
|
||||
description: 'A bespoke pre‑mortem/critique of your governance or security material, delivered as a dossier.',
|
||||
features: [
|
||||
'Review of your deck/report/controls',
|
||||
'Findings + failure modes',
|
||||
'Actionable mitigation notes',
|
||||
'Delivery via email'
|
||||
],
|
||||
cta: 'Request availability',
|
||||
enabled: true,
|
||||
actionUrl: 'mailto:dave@infrafabric.io?subject=Custom%20Shadow%20Dossier%20Inquiry'
|
||||
}
|
||||
];
|
||||
54
site/red-team-shadow-dossiers/index.html
Normal file
54
site/red-team-shadow-dossiers/index.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>InfraFabric Red Team — Shadow Dossiers</title>
|
||||
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="description" content="InfraFabric Red Team Shadow Dossiers: satirical critical commentary with verifiable artifacts (hashes, traces, evidence index).">
|
||||
<meta property="og:title" content="InfraFabric Red Team — Shadow Dossiers">
|
||||
<meta property="og:description" content="Satirical commentary on governance & compliance theater, published with verifiable artifacts.">
|
||||
<meta property="og:image" content="/og-dossier.jpg">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@IF_Dave">
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--paper:#f7f5f0;
|
||||
--ink:#0f172a;
|
||||
--muted:#334155;
|
||||
--border:rgba(15,23,42,.12);
|
||||
--accent:#b91c1c; /* subdued red */
|
||||
}
|
||||
body{
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
.mono{ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.red-ink{ color: var(--accent); }
|
||||
.bg-red-ink{ background-color: var(--accent); }
|
||||
.border-red-ink{ border-color: var(--accent); }
|
||||
|
||||
/* subtle, serious defaults */
|
||||
a{ text-underline-offset: 3px; }
|
||||
.hairline{ border-color: var(--border); }
|
||||
.bg-paper{ background: var(--paper); }
|
||||
|
||||
/* keep scrollbars unobtrusive */
|
||||
::-webkit-scrollbar{ width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track{ background: transparent; }
|
||||
::-webkit-scrollbar-thumb{ background: rgba(15,23,42,.18); border-radius: 999px; border: 3px solid transparent; background-clip: padding-box; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
site/red-team-shadow-dossiers/index.tsx
Normal file
16
site/red-team-shadow-dossiers/index.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
2
site/red-team-shadow-dossiers/lib/flags.ts
Normal file
2
site/red-team-shadow-dossiers/lib/flags.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const ENABLE_ROAST_GENERATOR =
|
||||
((import.meta as any).env?.VITE_ENABLE_ROAST as string | undefined) === 'true';
|
||||
15
site/red-team-shadow-dossiers/lib/urls.ts
Normal file
15
site/red-team-shadow-dossiers/lib/urls.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export const getPublicBaseUrl = (): string => {
|
||||
const raw = (import.meta as any).env?.VITE_PUBLIC_BASE_URL as string | undefined;
|
||||
if (!raw) return '';
|
||||
return raw.replace(/\/$/, '');
|
||||
};
|
||||
|
||||
export const resolvePublicUrl = (path?: string): string | null => {
|
||||
if (!path) return null;
|
||||
// If already absolute, return as-is.
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
|
||||
const base = getPublicBaseUrl();
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
};
|
||||
5
site/red-team-shadow-dossiers/metadata.json
Normal file
5
site/red-team-shadow-dossiers/metadata.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "InfraFabric Red Team Shadow Dossiers",
|
||||
"description": "A high-stakes, satirical front-end for the InfraFabric Red Team leaks, featuring classified dossiers, custom roasts, and operational realism theater.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
2612
site/red-team-shadow-dossiers/package-lock.json
generated
Normal file
2612
site/red-team-shadow-dossiers/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
site/red-team-shadow-dossiers/package.json
Normal file
23
site/red-team-shadow-dossiers/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "infrafabric-red-team-shadow-dossiers",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node server/server.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
5
site/red-team-shadow-dossiers/public/favicon.svg
Normal file
5
site/red-team-shadow-dossiers/public/favicon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect x="2" y="2" width="60" height="60" rx="12" fill="#0b0d10"/>
|
||||
<path d="M18 18h28v6H36v22h-6V24H18v-6z" fill="#ffffff"/>
|
||||
<rect x="2" y="2" width="60" height="60" rx="12" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
BIN
site/red-team-shadow-dossiers/public/og-dossier.jpg
Normal file
BIN
site/red-team-shadow-dossiers/public/og-dossier.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
14
site/red-team-shadow-dossiers/scripts/hash.mjs
Normal file
14
site/red-team-shadow-dossiers/scripts/hash.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, statSync } from 'node:fs';
|
||||
|
||||
const path = process.argv[2];
|
||||
if (!path) {
|
||||
console.error('Usage: node scripts/hash.mjs /absolute/or/relative/path/to/file.pdf');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const buf = readFileSync(path);
|
||||
const sha256 = createHash('sha256').update(buf).digest('hex');
|
||||
const bytes = statSync(path).size;
|
||||
|
||||
console.log(JSON.stringify({ sha256, bytes }, null, 2));
|
||||
93
site/red-team-shadow-dossiers/server/server.mjs
Normal file
93
site/red-team-shadow-dossiers/server/server.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
import express from "express";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const distDir = path.join(projectRoot, "dist");
|
||||
const indexHtmlPath = path.join(distDir, "index.html");
|
||||
|
||||
function pickPhrases(input) {
|
||||
const text = String(input || "").replace(/\r\n?/g, "\n");
|
||||
const lines = text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 200);
|
||||
|
||||
const interesting = [];
|
||||
const needles = ["must", "should", "require", "required", "ensure", "enforce", "policy", "control", "audit", "compliance"];
|
||||
for (const line of lines) {
|
||||
const lower = line.toLowerCase();
|
||||
if (needles.some((n) => lower.includes(n))) interesting.push(line);
|
||||
if (interesting.length >= 4) break;
|
||||
}
|
||||
|
||||
if (interesting.length) return interesting;
|
||||
return lines.slice(0, 3);
|
||||
}
|
||||
|
||||
function generateRoastText(content) {
|
||||
const trimmed = String(content || "").trim();
|
||||
const phrases = pickPhrases(trimmed);
|
||||
|
||||
const bullets = phrases.map((p) => `- ${p.length > 120 ? `${p.slice(0, 117)}…` : p}`).join("\n");
|
||||
|
||||
return [
|
||||
"We love the ambition here and are directionally aligned with the idea of \"secure rollout\" as long as we define secure as \"documented\" and rollout as \"phased.\"",
|
||||
"",
|
||||
"Key risk: this reads like a control narrative optimized for sign-off, not for the Friday-afternoon pull request that actually ships the code.",
|
||||
"",
|
||||
"Observed control theater (excerpt):",
|
||||
bullets ? bullets : "- (no extractable claims detected)",
|
||||
"",
|
||||
"Recommendation: convert every \"should\" into an owner, a gate (PR/CI/access), and a stop condition. Otherwise this becomes an alignment session that reproduces itself indefinitely.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function main() {
|
||||
const port = Number(process.env.PORT || 8080);
|
||||
const app = express();
|
||||
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json({ limit: "256kb" }));
|
||||
|
||||
app.get("/healthz", (_req, res) => {
|
||||
res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
app.post("/api/roast", (req, res) => {
|
||||
const content = String(req.body?.content ?? "");
|
||||
if (!content.trim()) return res.status(400).json({ text: "Missing content" });
|
||||
if (content.length > 20_000) return res.status(413).json({ text: "Content too large" });
|
||||
return res.status(200).json({ text: generateRoastText(content) });
|
||||
});
|
||||
|
||||
if (fs.existsSync(distDir) && fs.existsSync(indexHtmlPath)) {
|
||||
app.use(express.static(distDir, { fallthrough: true }));
|
||||
|
||||
app.get("*", (_req, res) => {
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.status(200).sendFile(indexHtmlPath);
|
||||
});
|
||||
} else {
|
||||
app.get("*", (_req, res) => {
|
||||
res
|
||||
.status(503)
|
||||
.type("text/plain")
|
||||
.send("red-team site is not built yet. Run `npm install` then `npm run build`.");
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(port, "0.0.0.0", () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`red-team site listening on http://0.0.0.0:${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Express stub (optional)
|
||||
|
||||
If you prefer a small Node server, implement POST /api/roast and reverse-proxy behind your web server.
|
||||
|
||||
This is a skeleton only; do not ship without auth/rate limiting.
|
||||
|
||||
16
site/red-team-shadow-dossiers/server_stubs/express/server.ts
Normal file
16
site/red-team-shadow-dossiers/server_stubs/express/server.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '64kb' }));
|
||||
|
||||
app.post('/api/roast', async (req, res) => {
|
||||
const content = String(req.body?.content ?? '');
|
||||
if (!content.trim()) return res.status(400).json({ text: 'Missing content' });
|
||||
|
||||
// TODO: server-side provider call
|
||||
return res.status(501).json({ text: 'Not implemented' });
|
||||
});
|
||||
|
||||
app.listen(8080, () => {
|
||||
console.log('Server listening on http://localhost:8080');
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
const API_KEY = process.env.API_KEY || "";
|
||||
|
||||
export const generateRoast = async (content: string) => {
|
||||
const ai = new GoogleGenAI({ apiKey: API_KEY });
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: `You are Dave, a cynical, veteran Red Team operator for InfraFabric. You specialize in "Operational Realism" and loathe "Compliance Theater."
|
||||
A user has submitted a snippet of a corporate whitepaper or security report.
|
||||
Your task is to write a short, sharp, satirical "Shadow Dossier" roast of this content.
|
||||
Keep it brief (under 150 words). Use jargon like "audit trail graveyard", "vaporware shielding", "SOC2 cosplay", "compliance debt", and "vulnerability PR".
|
||||
Focus on how the provided text is essentially high-effort fiction designed to appease auditors while leaving production wide open.
|
||||
|
||||
User content: "${content}"`,
|
||||
config: {
|
||||
systemInstruction: "You are 'Dave' from InfraFabric Red Team. Your tone is cynical, sharp, professional but satirical. You are allergic to corporate buzzwords unless used in mockery.",
|
||||
temperature: 0.9,
|
||||
}
|
||||
});
|
||||
|
||||
return response.text || "Dave is currently busy burning another audit trail. Try again later.";
|
||||
} catch (error) {
|
||||
console.error("Roast error:", error);
|
||||
return "Transmission failed. The corporate firewalls are thicker than their logic. (Check your API configuration).";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
/**
|
||||
* POST /api/roast
|
||||
* Request: { content: string }
|
||||
* Response: { text: string }
|
||||
*
|
||||
* IMPORTANT: keep provider API keys server-side only.
|
||||
*/
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).json({ text: 'Method Not Allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const content = (req.body?.content ?? '').toString();
|
||||
if (!content.trim()) {
|
||||
res.status(400).json({ text: 'Missing content' });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO (Codex): Call provider here (Gemini/OpenAI/etc.) using server env vars.
|
||||
// Example:
|
||||
// const text = await generateRoastWithGemini(content);
|
||||
|
||||
res.status(501).json({ text: 'Not implemented: wire server-side LLM provider here.' });
|
||||
}
|
||||
24
site/red-team-shadow-dossiers/services/roastClient.ts
Normal file
24
site/red-team-shadow-dossiers/services/roastClient.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export interface RoastResponse {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client calls a server-side endpoint.
|
||||
* IMPORTANT: Do not put model API keys in the browser bundle.
|
||||
* Codex/server should implement POST /api/roast.
|
||||
*/
|
||||
export const generateRoast = async (content: string): Promise<string> => {
|
||||
const resp = await fetch('/api/roast', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
return 'Roast endpoint unavailable. (Server has not implemented /api/roast yet.)';
|
||||
}
|
||||
|
||||
const data = (await resp.json().catch(() => null)) as RoastResponse | null;
|
||||
if (!data?.text) return 'No output returned.';
|
||||
return data.text;
|
||||
};
|
||||
29
site/red-team-shadow-dossiers/tsconfig.json
Normal file
29
site/red-team-shadow-dossiers/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
44
site/red-team-shadow-dossiers/types.ts
Normal file
44
site/red-team-shadow-dossiers/types.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
export type DossierStatus = 'draft' | 'scheduled' | 'live';
|
||||
|
||||
export interface Dossier {
|
||||
id: string;
|
||||
title: string;
|
||||
leakDate: string; // display date
|
||||
summary: string;
|
||||
classification: 'UNCLASSIFIED' | 'CLASSIFIED' | 'EYES-ONLY';
|
||||
status: DossierStatus;
|
||||
|
||||
previewImageUrl: string; // cover/thumbnail (data URI recommended)
|
||||
|
||||
/**
|
||||
* Paths are preferred over absolute URLs for pre-launch staging.
|
||||
* When deployed, VITE_PUBLIC_BASE_URL can be set to prefix these paths.
|
||||
*/
|
||||
pdfPath?: string; // e.g. /static/hosted/snyk-shadow-dossier.pdf
|
||||
evidencePath?: string; // e.g. /static/hosted/evidence/index.html
|
||||
|
||||
/** Optional, for live deployments or external destinations (e.g. Gumroad). */
|
||||
gumroadUrl?: string;
|
||||
contactEmail?: string;
|
||||
|
||||
/** Optional verifiability fields; only render when populated with real values. */
|
||||
sha256?: string; // preferred hash
|
||||
bytes?: number; // file size
|
||||
}
|
||||
|
||||
export interface PricingTier {
|
||||
name: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
cta: string;
|
||||
variant: 'free' | 'premium' | 'enterprise';
|
||||
|
||||
/**
|
||||
* If actionUrl is empty or enabled=false, the CTA renders disabled.
|
||||
* Use this for pre-launch staging so the site does not feel like a mockup.
|
||||
*/
|
||||
actionUrl?: string;
|
||||
enabled?: boolean;
|
||||
disabledHint?: string;
|
||||
}
|
||||
18
site/red-team-shadow-dossiers/vite.config.ts
Normal file
18
site/red-team-shadow-dossiers/vite.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue