Initial commit

This commit is contained in:
Miguel Jacq 2026-01-02 15:23:46 +11:00
commit 85ecaf2014
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
14 changed files with 1558 additions and 0 deletions

167
src/assets/css/site.css Normal file
View file

@ -0,0 +1,167 @@
:root{
--cspresso-mint:#2DD4BF;
--cspresso-violet:#A78BFA;
--cspresso-ink:#0b1220;
--cspresso-ink-2:#111827;
--cspresso-cream:#F6F0E7;
--bs-link-color: var(--cspresso-mint);
--bs-link-hover-color: var(--cspresso-violet);
--bs-link-color-rgb: 45, 212, 191;
--bs-link-hover-color-rgb: 167, 139, 250;
--bs-nav-pills-link-active-bg: rgba(45,212,191,0.18);
--bs-nav-pills-link-active-color: var(--cspresso-ink);
}
html{scroll-behavior:smooth;}
body{
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
color: var(--cspresso-ink);
}
.navbar{
backdrop-filter: blur(10px);
}
.navbar-toggler-icon{
background-image: url("../img/hamburger.svg");
}
.brand-mark{width:84px;height:84px;}
.hero{
background:
radial-gradient(900px 600px at 15% 15%, rgba(45,212,191,0.28), rgba(45,212,191,0.00) 60%),
radial-gradient(900px 600px at 85% 35%, rgba(167,139,250,0.26), rgba(167,139,250,0.00) 60%),
linear-gradient(180deg, rgba(11,18,32,0.02), rgba(11,18,32,0.00));
}
.hero .lead{
color: rgba(15,23,42,0.78);
}
.hero-card{
background: rgba(255,255,255,0.70);
border: 1px solid rgba(15,23,42,0.08);
box-shadow: 0 18px 48px rgba(15,23,42,0.10);
border-radius: 1.25rem;
}
.kicker{
display: inline-flex;
align-items: center;
gap: .5rem;
font-weight: 600;
font-size: .95rem;
padding: .45rem .75rem;
border-radius: 999px;
background: rgba(45,212,191,0.12);
border: 1px solid rgba(45,212,191,0.25);
color: var(--cspresso-ink);
}
.section-title{
letter-spacing: -0.02em;
}
.icon-pill{
width: 42px;
height: 42px;
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(45,212,191,0.12);
border: 1px solid rgba(45,212,191,0.25);
color: var(--cspresso-ink);
}
.feature-card{
border: 1px solid rgba(15,23,42,0.08);
border-radius: 1.25rem;
box-shadow: 0 12px 30px rgba(15,23,42,0.06);
}
.terminal{
background: #0b1220;
color: #e5e7eb;
border-radius: 1rem;
padding: 1.25rem;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 18px 52px rgba(11,18,32,0.35);
}
.terminal .prompt{ color: #93c5fd; }
.terminal code{ color: #e5e7eb; }
.codeblock{
position: relative;
}
.copy-btn{
position: absolute;
top: .75rem;
right: .75rem;
}
.badge-soft{
background: rgba(15,23,42,0.06);
border: 1px solid rgba(15,23,42,0.10);
color: rgba(15,23,42,0.85);
}
.callout{
border: 1px solid rgba(15,23,42,0.10);
border-radius: 1rem;
background: rgba(255,255,255,0.78);
}
footer{
background:
radial-gradient(900px 300px at 20% 0%, rgba(45,212,191,0.12), rgba(45,212,191,0.00) 60%),
radial-gradient(900px 300px at 80% 0%, rgba(167,139,250,0.10), rgba(167,139,250,0.00) 60%),
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-top: 1px solid rgba(15,23,42,0.06);
}
.smallprint{ color: rgba(15,23,42,0.65); }
/* Make anchor scrolling nicer under sticky nav */
.scroll-mt-nav{ scroll-margin-top: 90px; }
a{ text-decoration-color: rgba(45,212,191,0.55); }
a:hover{ text-decoration-color: rgba(167,139,250,0.85); }
/* Quickstart pills */
.nav-pills .nav-link{
border: 1px solid rgba(45,212,191,0.25);
color: var(--cspresso-ink);
background: rgba(45,212,191,0.10);
border-radius: 999px;
}
.nav-pills .nav-link:hover{
background: rgba(167,139,250,0.10);
border-color: rgba(45,212,191,0.55);
}
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link{
background: rgba(45,212,191,0.18);
color: var(--cspresso-ink);
border-color: rgba(15,23,42,0.18);
}
.chip{
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .25rem .55rem;
border-radius: 999px;
font-size: .85rem;
background: rgba(15,23,42,0.06);
border: 1px solid rgba(15,23,42,0.10);
color: rgba(15,23,42,0.88);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="g" x1="40" y1="30" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop stop-color="#2DD4BF"/>
<stop offset="1" stop-color="#A78BFA"/>
</linearGradient>
<linearGradient id="m" x1="60" y1="70" x2="160" y2="170" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B1220"/>
<stop offset="1" stop-color="#111827"/>
</linearGradient>
</defs>
<circle cx="110" cy="110" r="96" fill="url(#m)" stroke="url(#g)" stroke-width="6"/>
<path d="M78 64c-10 10-2 18 5 24s15 14 5 26" stroke="url(#g)" stroke-width="6" stroke-linecap="round"/>
<path d="M110 58c-10 10-2 18 5 24s15 14 5 26" stroke="url(#g)" stroke-width="6" stroke-linecap="round" opacity=".9"/>
<path d="M142 64c-10 10-2 18 5 24s15 14 5 26" stroke="url(#g)" stroke-width="6" stroke-linecap="round" opacity=".8"/>
<path d="M72 104h76c0 35-13 56-38 56s-38-21-38-56Z" fill="#F6F0E7" opacity=".96"/>
<path d="M72 104h76" stroke="#F6F0E7" stroke-width="6" stroke-linecap="round"/>
<path d="M148 112h10c10 0 18 8 18 18s-8 18-18 18h-12" stroke="#F6F0E7" stroke-width="8" stroke-linecap="round"/>
<rect x="86" y="122" width="48" height="24" rx="8" fill="#0B1220" opacity=".92"/>
<text x="110" y="139" text-anchor="middle"
font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
font-size="12" fill="#F6F0E7">'self'</text>
<path d="M68 166c14 10 28 14 42 14s28-4 42-14" stroke="#F6F0E7" stroke-width="7" stroke-linecap="round" opacity=".9"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

107
src/assets/img/enroll.svg Normal file
View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="720" viewBox="0 0 720 720" role="img" aria-label="enroll logo">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#F7D58A"/>
<stop offset="1" stop-color="#E8B35E"/>
</linearGradient>
<linearGradient id="crumbGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#FFE7BD"/>
<stop offset="1" stop-color="#E6A44A"/>
</linearGradient>
<linearGradient id="shineGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#FFF2D6" stop-opacity="0.85"/>
<stop offset="1" stop-color="#FFF2D6" stop-opacity="0"/>
</linearGradient>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur"/>
<feOffset dx="0" dy="10" result="off"/>
<feColorMatrix in="off" type="matrix"
values="0 0 0 0 0.15 0 0 0 0 0.08 0 0 0 0 0.02 0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style>
.stroke { stroke:#5A3415; stroke-width:10; stroke-linecap:round; stroke-linejoin:round; }
.thin { stroke:#5A3415; stroke-width:7; stroke-linecap:round; stroke-linejoin:round; }
.text-dark { fill:#5A3415; font-family: ui-rounded, "Arial Rounded MT Bold", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; font-weight: 800; }
.text-light { fill:#8A5A2D; font-family: ui-rounded, "Arial Rounded MT Bold", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; font-weight: 800; }
.sesame { fill:#F8E6C8; opacity:0.92; }
.crumb { fill:#E9B56B; opacity:0.30; }
</style>
</defs>
<!-- NOTE: background removed for transparency -->
<!-- Roll group -->
<g filter="url(#softShadow)" transform="translate(-120,-70)">
<path class="stroke" fill="url(#crumbGrad)"
d="M220 450
C220 320, 320 235, 480 235
C640 235, 740 320, 740 450
C740 565, 640 610, 480 610
C320 610, 220 565, 220 450 Z"/>
<path d="M300 320
C340 285, 410 265, 480 265
C550 265, 620 285, 660 320
C610 305, 545 298, 480 298
C415 298, 350 305, 300 320 Z"
fill="url(#shineGrad)"/>
<g class="thin" opacity="0.85" fill="none">
<path d="M330 320 C350 285, 400 270, 430 315"/>
<path d="M420 305 C450 268, 510 268, 540 305"/>
<path d="M530 315 C560 270, 610 285, 630 320"/>
</g>
<g>
<ellipse class="sesame" cx="335" cy="368" rx="10" ry="6"/>
<ellipse class="sesame" cx="625" cy="368" rx="10" ry="6"/>
<ellipse class="sesame" cx="385" cy="345" rx="10" ry="6"/>
<ellipse class="sesame" cx="575" cy="345" rx="10" ry="6"/>
<ellipse class="sesame" cx="400" cy="330" rx="10" ry="6"/>
<ellipse class="sesame" cx="560" cy="330" rx="10" ry="6"/>
</g>
<g>
<circle class="crumb" cx="310" cy="430" r="6"/>
<circle class="crumb" cx="650" cy="430" r="6"/>
<circle class="crumb" cx="350" cy="470" r="5"/>
<circle class="crumb" cx="610" cy="470" r="5"/>
<circle class="crumb" cx="420" cy="520" r="6"/>
<circle class="crumb" cx="540" cy="520" r="6"/>
</g>
<g>
<circle cx="445" cy="365" r="12" fill="#5A3415"/>
<circle cx="515" cy="365" r="12" fill="#5A3415"/>
<path d="M445 405 C468 430, 492 430, 515 405"
fill="none" stroke="#5A3415" stroke-width="12" stroke-linecap="round"/>
</g>
<g transform="translate(330,470)">
<rect x="0" y="0" width="300" height="92" rx="26" fill="#F7C879" class="thin"/>
<g class="thin" fill="none" opacity="0.95">
<rect x="18" y="16" width="190" height="24" rx="11" fill="#FFE7BD"/>
<rect x="18" y="50" width="190" height="24" rx="11" fill="#FFE7BD"/>
<path d="M18 82 H232"/>
</g>
<g>
<circle cx="245" cy="28" r="6.5" fill="#5A3415"/>
<circle cx="268" cy="28" r="6.5" fill="#5A3415"/>
<circle cx="291" cy="28" r="6.5" fill="#5A3415"/>
<rect x="238" y="52" width="56" height="22" rx="9" fill="#FFE7BD" stroke="#5A3415" stroke-width="7"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path stroke="#5A3415" stroke-opacity="0.85" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"
d="M4 7h22M4 15h22M4 23h22"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

60
src/assets/js/site.js Normal file
View file

@ -0,0 +1,60 @@
(function(){
// Copy-to-clipboard for code blocks
function setupCopyButtons(){
document.querySelectorAll('[data-copy-target]').forEach(function(btn){
btn.addEventListener('click', async function(){
var sel = btn.getAttribute('data-copy-target');
var el = document.querySelector(sel);
if(!el) return;
var text = el.innerText || el.textContent || '';
try{
await navigator.clipboard.writeText(text.trim());
var old = btn.innerHTML;
btn.innerHTML = 'Copied';
btn.classList.add('btn-success');
btn.classList.remove('btn-outline-secondary');
setTimeout(function(){
btn.innerHTML = old;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
}, 1200);
}catch(e){
// Fallback
var ta = document.createElement('textarea');
ta.value = text.trim();
document.body.appendChild(ta);
ta.select();
try{ document.execCommand('copy'); }catch(_){}
document.body.removeChild(ta);
}
});
});
}
// Asciinema embed helper:
// Put <div class="asciicast" data-asciinema-id="12345"></div>
// Or provide a self-hosted player by swapping the script URL.
function setupAsciinema(){
document.querySelectorAll('.asciicast[data-asciinema-id]').forEach(function(el){
var id = el.getAttribute('data-asciinema-id');
if(!id || id === 'REPLACE_ME'){
el.innerHTML = '<div class="alert alert-warning mb-0">Add your asciinema id here: <code>data-asciinema-id</code>.</div>';
return;
}
// Avoid injecting twice
if(document.getElementById('asciinema-embed-'+id)) return;
var s = document.createElement('script');
s.src = 'https://asciinema.org/a/' + encodeURIComponent(id) + '.js';
s.id = 'asciinema-embed-'+id;
s.async = true;
// The script replaces a placeholder div with the player if it's directly after it.
el.appendChild(s);
});
}
document.addEventListener('DOMContentLoaded', function(){
setupCopyButtons();
setupAsciinema();
});
})();

244
src/docs.html Normal file
View file

@ -0,0 +1,244 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso Docs</title>
<meta name="description" content="Docs for cspresso: crawl a site with headless Chromium and generate a draft Content-Security-Policy; evaluate candidates in Report-Only mode.">
<link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/site.css" rel="stylesheet">
<meta property="og:title" content="cspresso Docs">
<meta property="og:description" content="Docs for cspresso: crawl a site with headless Chromium and generate a draft Content-Security-Policy; evaluate candidates in Report-Only mode.">
<meta property="og:type" content="website">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light" style="backdrop-filter: blur(8px);">
<div class="container py-1">
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<span>cspresso</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link active fw-semibold" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="recipes.html">Recipes</a></li>
<li class="nav-item"><a class="nav-link" href="evaluate.html">Evaluate</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-3">
<div class="sticky-lg-top" style="top: 90px;">
<div class="fw-semibold mb-2">On this page</div>
<div class="list-group small">
<a class="list-group-item list-group-item-action" href="#install">Install</a>
<a class="list-group-item list-group-item-action" href="#run">Run</a>
<a class="list-group-item list-group-item-action" href="#output">Output</a>
<a class="list-group-item list-group-item-action" href="#inline">Inline scripts &amp; styles</a>
<a class="list-group-item list-group-item-action" href="#evaluate">Evaluate (Report-Only)</a>
<a class="list-group-item list-group-item-action" href="#flags">Flags</a>
</div>
<div class="mt-3 small text-secondary">
Prefer canonical docs? See the <a class="link-secondary" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">README</a>.
</div>
</div>
</div>
<div class="col-lg-9">
<header class="mb-4">
<div class="kicker mb-2"><i class="bi bi-journal-text"></i> Docs</div>
<h1 class="display-6 fw-800 mb-2">Usage</h1>
<p class="text-secondary mb-0">
cspresso crawls up to <code>--max-pages</code> same-origin pages in Chromium, observes what loads, and emits a draft CSP.
</p>
</header>
<section id="install" class="mb-5">
<h2 class="section-title fw-bold">Install</h2>
<div class="row g-3">
<div class="col-lg-7">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docinst"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="docinst"># Recommended
pipx install cspresso
# Or plain pip (use a venv)
pip install cspresso
# An AppImage is also available on the
# git repo Releases page.
</code></pre>
</div>
</div>
<div class="col-lg-5">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Python + Playwright</div>
<div class="text-muted">
You need Python 3.10+ and Playwrights Chromium. cspresso can auto-install Chromium if missing.
</div>
<hr>
<div class="small text-secondary">
Verify Releases artifacts with the mig5 key:
<a class="link-secondary" href="https://mig5.net/static/mig5.asc" target="_blank" rel="noreferrer">https://mig5.net/static/mig5.asc</a>
(fingerprint <code>00AE817C24A10C2540461A9C1D7CDE0234DB458D</code>).
</div>
</div>
</div>
</div>
</section>
<section id="run" class="mb-5">
<h2 class="section-title fw-bold">Run</h2>
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docrun"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="docrun">cspresso https://example.com --max-pages 10</code></pre>
</div>
<div class="mt-3 text-secondary">
The crawl stays on the same origin (it follows internal links) and will wait for <code>networkidle</code> plus a small “settle”
delay to catch late fetches.
</div>
</section>
<section id="output" class="mb-5">
<h2 class="section-title fw-bold">Output</h2>
<p class="text-secondary">
Default output prints visited URLs as comments and then the proposed header line.
Use <span class="chip">--json</span> for machine-readable output.
</p>
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docjson"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="docjson">cspresso https://example.com --json</code></pre>
</div>
</section>
<section id="inline" class="mb-5">
<h2 class="section-title fw-bold">Inline scripts &amp; styles</h2>
<div class="callout p-4">
<div class="fw-semibold mb-2">Why this is hard</div>
<div class="text-muted">
If the page uses nonces, you must generate a new nonce per HTML response and inject it into both the CSP header and the HTML tags.
Hashes only work when inline content is stable byte-for-byte. For <code>style="..."</code> and <code>on*</code> attributes,
browsers require <code>'unsafe-hashes'</code> for hashes to apply.<br/>Not to worry, cspresso will detect these and generate the hashes
in its response, offering them as <code>style-src-attr</code> options or with <code>unsafe-hashes</code> for older browsers.
</div>
</div>
</section>
<section id="evaluate" class="mb-5">
<h2 class="section-title fw-bold">Evaluate (Report-Only)</h2>
<p class="text-secondary">
Provide a CSP string to <span class="chip">--evaluate</span> and cspresso will inject it as
<code>Content-Security-Policy-Report-Only</code> on HTML responses during the crawl. If any violations are detected, it exits with code <code>1</code>.
</p>
<p class="text-secondary">This is a great way of testing a cspresso-brewed CSP before actually adding it to your site.</p>
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#doceval"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="doceval">cspresso https://example.com \
--bypass-csp \
--evaluate "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net;" \
--json</code></pre>
</div>
<div class="mt-3 small text-secondary">
Recommendation: use <code>--bypass-csp</code> with <code>--evaluate</code> so existing CSP enforcement doesnt change the pages behaviour during testing.
</div>
</section>
<section id="flags">
<h2 class="section-title fw-bold">Flags</h2>
<p class="text-secondary">A condensed reference of the most-used options:</p>
<div class="row g-3">
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Crawling</div>
<div class="text-muted"><span class="chip">--max-pages</span> <span class="chip">--timeout-ms</span> <span class="chip">--settle-ms</span> <span class="chip">--headed</span></div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Policy shaping</div>
<div class="text-muted"><span class="chip">--include-sourcemaps</span> <span class="chip">--upgrade-insecure-requests</span> <span class="chip">--allow-blob</span> <span class="chip">--unsafe-eval</span></div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Playwright</div>
<div class="text-muted"><span class="chip">--browsers-path</span> <span class="chip">--no-install</span> <span class="chip">--with-deps</span></div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Evaluation</div>
<div class="text-muted"><span class="chip">--bypass-csp</span> <span class="chip">--evaluate</span> <span class="chip">--ignore-non-html</span> <span class="chip">--json</span></div>
</div></div>
</div>
</section>
</div>
</div>
</div>
</main>
<footer class="py-5 mt-5">
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<div class="fw-bold">cspresso (a mig5 project)</div>
</div>
<div class="smallprint">
Crawl a site with headless Chromium, observe what loads, and emit a draft Content-Security-Policy.
Review and tighten before enforcing.
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Pages</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="recipes.html" class="link-secondary text-decoration-none">Recipes</a>
<a href="evaluate.html" class="link-secondary text-decoration-none">Evaluate</a>
<a href="security.html" class="link-secondary text-decoration-none">Security</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-wrap justify-content-between gap-2 small text-secondary">
<div>© 2026 <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
</body>
</html>

189
src/evaluate.html Normal file
View file

@ -0,0 +1,189 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso Evaluate</title>
<meta name="description" content="Evaluate a candidate Content-Security-Policy by injecting it as CSP Report-Only and failing if violations are detected.">
<link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/site.css" rel="stylesheet">
<meta property="og:title" content="cspresso Evaluate">
<meta property="og:description" content="Evaluate a candidate Content-Security-Policy by injecting it as CSP Report-Only and failing if violations are detected.">
<meta property="og:type" content="website">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light" style="backdrop-filter: blur(8px);">
<div class="container py-1">
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<span>cspresso</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="recipes.html">Recipes</a></li>
<li class="nav-item"><a class="nav-link active fw-semibold" href="evaluate.html">Evaluate</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="py-5">
<div class="container">
<header class="mb-4">
<div class="kicker mb-2"><i class="bi bi-shield-check"></i> Evaluate</div>
<h1 class="display-6 fw-800 mb-2">Test a CSP before you enforce it</h1>
<p class="text-secondary mb-0">
Use <span class="chip">--evaluate</span> to inject a candidate policy as <code>Content-Security-Policy-Report-Only</code>,
collect violations, and fail the run if anything would break.
</p>
</header>
<section class="mb-5">
<div class="row g-3">
<div class="col-lg-7">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ev1"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="ev1">cspresso https://example.com \
--bypass-csp \
--evaluate "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net;" \
--json</code></pre>
</div>
</div>
<div class="col-lg-5">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Exit codes</div>
<div class="text-muted">
<div><code>0</code> → no ReportOnly violations detected</div>
<div><code>1</code> → violations detected (ideal for CI gates)</div>
</div>
<hr>
<div class="small text-secondary">
Tip: keep your CSP string quoted; it usually contains spaces and semicolons.
</div>
</div>
</div>
</div>
</section>
<section class="mb-5">
<h2 class="section-title fw-bold">Why <code>--bypass-csp</code> matters</h2>
<p class="text-secondary">
If the target site already sets an enforcing CSP, it can block loads and change runtime behaviour.
That can hide potential violations in your candidate policy. Using <code>--bypass-csp</code> strips existing CSP headers
on HTML responses during the crawl.
</p>
<div class="callout p-4">
<div class="fw-semibold mb-2">Safety note</div>
<div class="text-muted">
Bypassing CSP means youre letting the page execute without those protections. Run evaluation only on sites you trust,
or in a sandboxed environment.
</div>
</div>
</section>
<section class="mb-5">
<h2 class="section-title fw-bold">CI example (GitHub Actions)</h2>
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#gha"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="gha">- name: Evaluate CSP
run: |
pipx install cspresso
cspresso https://example.com \
--bypass-csp \
--evaluate "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net;" \
--json</code></pre>
</div>
<div class="mt-3 small text-secondary">
If you cache Playwright browsers, set <code>--browsers-path</code> to a persistent directory.
</div>
</section>
<section>
<h2 class="section-title fw-bold">Troubleshooting</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Sourcemaps causing connect-src noise</div>
<div class="text-muted">
DevTools often fetches sourcemaps even when headless browsing doesnt. If you want to model those requests,
use <code>--include-sourcemaps</code> to add sourcemap origins to <code>connect-src</code>.
</div>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Non-HTML crawled resources</div>
<div class="text-muted">
If your site has downloadable files on the same origin, consider <code>--ignore-non-html</code> to avoid edge cases
like browser “word-wrap” injected styles affecting hashes.
</div>
</div>
</div>
</div>
</section>
</div>
</main>
<footer class="py-5 mt-5">
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<div class="fw-bold">cspresso (a mig5 project)</div>
</div>
<div class="smallprint">
Crawl a site with headless Chromium, observe what loads, and emit a draft Content-Security-Policy.
Review and tighten before enforcing.
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Pages</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="recipes.html" class="link-secondary text-decoration-none">Recipes</a>
<a href="evaluate.html" class="link-secondary text-decoration-none">Evaluate</a>
<a href="security.html" class="link-secondary text-decoration-none">Security</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-wrap justify-content-between gap-2 small text-secondary">
<div>© 2026 <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
</body>
</html>

13
src/examples.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso - Recipes</title>
<meta http-equiv="refresh" content="0; url=recipes.html">
<link rel="canonical" href="recipes.html">
</head>
<body>
<p>Redirecting to <a href="recipes.html">recipes.html</a></p>
</body>
</html>

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

397
src/index.html Normal file
View file

@ -0,0 +1,397 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso - Brew a Content Security Policy</title>
<meta name="description" content="cspresso crawls a site with headless Chromium (Playwright) and emits a draft Content-Security-Policy based on what loads.">
<link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/site.css" rel="stylesheet">
<meta property="og:title" content="cspresso - Brew a Content Security Policy">
<meta property="og:description" content="cspresso crawls a site with headless Chromium (Playwright) and emits a draft Content-Security-Policy based on what loads.">
<meta property="og:type" content="website">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light" style="backdrop-filter: blur(8px);">
<div class="container py-1">
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<span>cspresso</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="recipes.html">Recipes</a></li>
<li class="nav-item"><a class="nav-link" href="evaluate.html">Evaluate</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
</li>
</ul>
</div>
</div>
</nav>
<main>
<header class="hero py-5">
<div class="container py-4">
<div class="row align-items-center g-4">
<div class="col-lg-6">
<div class="kicker mb-3"><i class="bi bi-cup-hot"></i> Brew a Content Security Policy</div>
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Turn real page loads into a CSP you can ship.</h1>
<p class="lead mb-4">
cspresso crawls up to <em>N</em> sameorigin pages with headless Chromium (Playwright), watches the assets that load,
and emits a <strong>draft</strong> <code>Content-Security-Policy</code> header.
</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-dark btn-lg" href="#quickstart"><i class="bi bi-rocket-takeoff"></i> Quickstart</a>
<a class="btn btn-outline-dark btn-lg" href="docs.html"><i class="bi bi-journal-text"></i> Docs</a>
<a class="btn btn-outline-secondary btn-lg" href="evaluate.html"><i class="bi bi-shield-check"></i> Evaluate</a>
<a class="btn btn-outline-secondary btn-lg" href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer"><i class="bi bi-box-seam"></i> PyPI</a>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="chip">--json</span>
<span class="chip">--evaluate</span>
<span class="chip">--bypass-csp</span>
<span class="chip">--include-sourcemaps</span>
</div>
</div>
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#heroCode"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="heroCode">pipx install cspresso
cspresso https://example.com --max-pages 10
# visited: https://example.com/
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; ...;</code></pre>
</div>
<div class="mt-3 small text-secondary">
Remember: its only a starting point: crawls may not hit every flow, and inline hashing/nonces require care.
</div>
</div>
</div>
</div>
</header>
<section id="quickstart" class="py-5 border-top">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-4">
<h2 class="section-title fw-bold mb-2">Quickstart</h2>
</div>
<div class="col-lg-12">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs1"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0">
<code id="qs1">
cspresso https://mig5.net \
--ignore-non-html \
--max-pages 10
</code>
</pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">What youll get</div>
<div class="text-muted">
A header line you can paste into your vhost, or parseable info with <code>--json</code>
</div>
<hr>
<div class="small text-secondary">
Tip: if an existing CSP might block loads during analysis, add <code>--bypass-csp</code>.
</div>
</div>
</div>
<div class="col-12">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs2"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs2"># Evaluate a candidate CSP (Report-Only) and fail CI on violations
cspresso https://mig5.net \
--bypass-csp \
--evaluate "default-src 'self'; img-src 'none';" \
--json
{
[...]
"violations": [
{
"console": true,
"disposition": "report",
"documentURI": "https://mig5.net/",
"text": "Loading the image 'https://mig5.net/logo.svg' violates the following Content Security Policy directive: \"img-src 'none'\". The policy is report-only, so the violation has been logged but no further action has been taken.",
"type": "info"
},
{
"console": true,
"disposition": "report",
"documentURI": "https://mig5.net/static/mig5.asc",
"text": "Applying inline style violates the following Content Security Policy directive 'default-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-4Su6mBWzEIFnH4pAGMOuaeBrstwJN4Z3pq/s1Kn4/KQ='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback. The policy is report-only, so the violation has been logged but no further action has been taken.",
"type": "info"
}
]
}
# exit code: 1 if violations detected</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5" id="how">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<h2 class="section-title display-6 fw-bold mb-2">How it works</h2>
<p class="text-muted mb-0">
cspresso lets the browser do the hard part: execute the page, watch what it loads, and distill origins into directives.
</p>
</div>
<div class="col-lg-7">
<div class="row g-3">
<div class="col-md-6">
<div class="card feature-card h-100"><div class="card-body p-4">
<div class="icon-pill mb-3"><i class="bi bi-globe2"></i></div>
<div class="fw-semibold mb-1">Crawl</div>
<div class="text-muted">Visit up to <code>--max-pages</code> same-origin pages and let the apps JS run.</div>
</div></div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100"><div class="card-body p-4">
<div class="icon-pill mb-3"><i class="bi bi-activity"></i></div>
<div class="fw-semibold mb-1">Observe</div>
<div class="text-muted">Track scripts, styles, images, fonts, frames, and “connect-like” requests.</div>
</div></div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100"><div class="card-body p-4">
<div class="icon-pill mb-3"><i class="bi bi-shield-lock"></i></div>
<div class="fw-semibold mb-1">Draft a CSP</div>
<div class="text-muted">Emit a baseline policy plus observed origins per directive.</div>
</div></div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100"><div class="card-body p-4">
<div class="icon-pill mb-3"><i class="bi bi-check2-square"></i></div>
<div class="fw-semibold mb-1">Evaluate</div>
<div class="text-muted">Inject a candidate as ReportOnly and capture violations with an exit code for CI.</div>
</div></div>
</div>
</div>
<div class="mt-3 small text-secondary">
Inline script/style is tricky: nonces must be generated per response, and hashes must match bytes exactly.
cspresso reports what it sees, but you should review and tighten before enforcing.
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="flags">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Popular flags</h2>
<p class="text-muted mb-0">A few options that tend to matter in real deployments.</p>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--bypass-csp</span></div>
<div class="text-muted">Strip existing CSP response headers so they dont block discovery or evaluation.</div>
</div></div></div>
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--evaluate</span></div>
<div class="text-muted">Inject a candidate policy as ReportOnly and exit 1 if any violations are detected.</div>
</div></div></div>
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--include-sourcemaps</span></div>
<div class="text-muted">Heuristically discover sourcemap origins and add them to <code>connect-src</code>.</div>
</div></div></div>
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--upgrade-insecure-requests</span></div>
<div class="text-muted">Emit <code>upgrade-insecure-requests</code> in the proposed policy.</div>
</div></div></div>
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--browsers-path</span></div>
<div class="text-muted">Control where Playwright installs Chromium (handy for AppImage/CI caches).</div>
</div></div></div>
<div class="col-md-6 col-lg-4"><div class="card feature-card h-100"><div class="card-body p-4">
<div class="fw-semibold mb-2"><span class="chip">--json</span></div>
<div class="text-muted">Machine-readable output: CSP, visited URLs, notes, and evaluation violations.</div>
</div></div></div>
</div>
</div>
</section>
<section class="py-5" id="install">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Install</h2>
<p class="text-muted mb-0">pipx, pip, Poetry, or a standalone AppImage from Releases.</p>
</div>
<div class="col-lg-4 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer"><i class="bi bi-download"></i> Releases</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="installTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="inst1-tab" data-bs-toggle="pill" data-bs-target="#inst1" type="button" role="tab">pipx / pip</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="inst2-tab" data-bs-toggle="pill" data-bs-target="#inst2" type="button" role="tab">Poetry</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="inst3-tab" data-bs-toggle="pill" data-bs-target="#inst3" type="button" role="tab">AppImage</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="inst1" role="tabpanel" aria-labelledby="inst1-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst1code"># Recommended
pipx install cspresso
# Or plain pip (use a venv)
pip install cspresso</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Playwright browsers</div>
<div class="text-muted">
cspresso can auto-install Chromium for Playwright if it isnt present. By default it installs into <code>./.pw-browsers</code>
for deterministic builds and easy CI caching.
</div>
<hr>
<div class="small text-secondary">
Override with <code>--browsers-path</code> or <code>PLAYWRIGHT_BROWSERS_PATH</code>.
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="inst2" role="tabpanel" aria-labelledby="inst2-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst2code">poetry add cspresso</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Linux deps</div>
<div class="text-muted">
If Chromium wont start due to missing libraries, try <code>--with-deps</code> (may require elevated privileges).
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="inst3" role="tabpanel" aria-labelledby="inst3-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst3code">chmod +x cspresso.AppImage
./cspresso.AppImage https://example.com \
--browsers-path "$HOME/.cache/cspresso/pw-browsers"</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Tip</div>
<div class="text-muted">
AppImages mount read-only - use <code>--browsers-path</code> to install browsers into a writable cache directory.
</div>
<hr>
<div class="small text-secondary">
Verify releases with the mig5 GPG key (fingerprint <code>00AE817C24A10C2540461A9C1D7CDE0234DB458D</code>).
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="py-5 mt-5">
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<div class="fw-bold">cspresso (a mig5 project)</div>
</div>
<div class="smallprint">
Crawl a site with headless Chromium, observe what loads, and emit a draft Content-Security-Policy.
Review and tighten before enforcing.
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Pages</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="recipes.html" class="link-secondary text-decoration-none">Recipes</a>
<a href="evaluate.html" class="link-secondary text-decoration-none">Evaluate</a>
<a href="security.html" class="link-secondary text-decoration-none">Security</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-wrap justify-content-between gap-2 small text-secondary">
<div>© 2026 <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
</body>
</html>

174
src/recipes.html Normal file
View file

@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso Recipes</title>
<meta name="description" content="cspresso recipes: common commands for crawling, debugging, sourcemaps, AppImage usage, and CI evaluation.">
<link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/site.css" rel="stylesheet">
<meta property="og:title" content="cspresso Recipes">
<meta property="og:description" content="cspresso recipes: common commands for crawling, debugging, sourcemaps, AppImage usage, and CI evaluation.">
<meta property="og:type" content="website">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light" style="backdrop-filter: blur(8px);">
<div class="container py-1">
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<span>cspresso</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link active fw-semibold" href="recipes.html">Recipes</a></li>
<li class="nav-item"><a class="nav-link" href="evaluate.html">Evaluate</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="py-5">
<div class="container">
<header class="mb-4">
<div class="kicker mb-2"><i class="bi bi-lightning-charge"></i> Recipes</div>
<h1 class="display-6 fw-800 mb-2">Practical workflows</h1>
<p class="text-secondary mb-0">A handful of commands that cover most real-world cspresso usage.</p>
</header>
<div class="row g-4">
<div class="col-lg-6">
<h2 class="section-title fw-bold">Draft a CSP</h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r1"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r1">cspresso https://example.com --max-pages 10</code></pre>
</div>
<div class="text-secondary">
Start here, then audit the output. Crawls wont cover every flow (auth-only pages, conditional loads, A/B tests, etc.).
</div>
</div>
<div class="col-lg-6">
<h2 class="section-title fw-bold">Headed debugging</h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r2"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r2">cspresso https://example.com --headed --settle-ms 2500</code></pre>
</div>
<div class="text-secondary">
Useful when the site does delayed loads or you want to visually confirm whats happening during the crawl.
</div>
</div>
<div class="col-lg-6">
<h2 class="section-title fw-bold">Sourcemaps &amp; <code>connect-src</code></h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r3"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r3">cspresso https://example.com --include-sourcemaps</code></pre>
</div>
<div class="text-secondary">
If browsers/devtools fetch <code>*.map</code> files from a CDN, this helps make sure the CDN origin lands in <code>connect-src</code>.
</div>
</div>
<div class="col-lg-6">
<h2 class="section-title fw-bold">Upgrade insecure requests</h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r4"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r4">cspresso https://example.com --upgrade-insecure-requests</code></pre>
</div>
<div class="text-secondary">
Handy during migrations when you still have a few stray HTTP URLs.
</div>
</div>
<div class="col-lg-6">
<h2 class="section-title fw-bold">AppImage (writable browser cache)</h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r5"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r5">./cspresso.AppImage https://example.com \
--browsers-path "$HOME/.cache/cspresso/pw-browsers"</code></pre>
</div>
<div class="text-secondary">
AppImages mount read-only. Set <code>--browsers-path</code> so Playwright can install Chromium into a writable directory.
</div>
</div>
<div class="col-lg-6">
<h2 class="section-title fw-bold">CI gate with <code>--evaluate</code></h2>
<div class="codeblock mb-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#r6"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="r6">cspresso https://example.com \
--bypass-csp \
--evaluate "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net;" \
--json</code></pre>
</div>
<div class="text-secondary">
Exits <code>1</code> if the candidate policy would be violated - great for PR checks.
</div>
</div>
</div>
</div>
</main>
<footer class="py-5 mt-5">
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<div class="fw-bold">cspresso (a mig5 project)</div>
</div>
<div class="smallprint">
Crawl a site with headless Chromium, observe what loads, and emit a draft Content-Security-Policy.
Review and tighten before enforcing.
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Pages</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="recipes.html" class="link-secondary text-decoration-none">Recipes</a>
<a href="evaluate.html" class="link-secondary text-decoration-none">Evaluate</a>
<a href="security.html" class="link-secondary text-decoration-none">Security</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-wrap justify-content-between gap-2 small text-secondary">
<div>© 2026 <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
</body>
</html>

162
src/security.html Normal file
View file

@ -0,0 +1,162 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cspresso Security</title>
<meta name="description" content="Security notes for cspresso: it runs a real browser; what bypassing existing CSP means; and how to use it safely.">
<link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/site.css" rel="stylesheet">
<meta property="og:title" content="cspresso Security">
<meta property="og:description" content="Security notes for cspresso: it runs a real browser; what bypassing existing CSP means; and how to use it safely.">
<meta property="og:type" content="website">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light" style="backdrop-filter: blur(8px);">
<div class="container py-1">
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<span>cspresso</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="recipes.html">Recipes</a></li>
<li class="nav-item"><a class="nav-link" href="evaluate.html">Evaluate</a></li>
<li class="nav-item"><a class="nav-link active fw-semibold" href="security.html">Security</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="py-5">
<div class="container">
<header class="mb-4">
<div class="kicker mb-2"><i class="bi bi-shield-lock"></i> Security</div>
<h1 class="display-6 fw-800 mb-2">Security notes</h1>
<p class="text-secondary mb-0">
cspresso runs a real browser. Thats the point - and also the main safety consideration.
</p>
</header>
<div class="row g-12">
<div class="col-lg-12">
<section class="mb-12">
<h2 class="section-title fw-bold">What cspresso does</h2>
<p class="text-secondary">
cspresso launches Chromium via Playwright and loads your target pages. The sites JavaScript and CSS execute like a normal browser session.
Network requests are observed to build a draft CSP, and (optionally) a candidate policy is injected as ReportOnly to capture violations.
</p>
</section>
<section class="mb-12">
<h2 class="section-title fw-bold">About <code>--bypass-csp</code></h2>
<div class="callout p-4">
<div class="fw-semibold mb-2">It can change risk</div>
<div class="text-muted">
Bypassing CSP strips existing CSP headers on HTML responses. This option is provided in order to avoid the outcome of the rendering negatively influencing what cspresso thinks a good CSP should be.
If a site is compromised, CSP might have been limiting what injected scripts could do (that's the whole point of a CSP!); bypassing removes that layer.
</div>
<hr>
<div class="small text-secondary">
Recommendation: only use <code>--bypass-csp</code> on sites you trust, or run cspresso inside a sandboxed environment (VM/container).
</div>
</div>
</section>
<section class="mb-12">
<h2 class="section-title fw-bold">Data handling</h2>
<p class="text-secondary">
cspressos primary output is a policy string and metadata (visited URLs, notes, and - in evaluation mode - detected violations).
Treat the output as sensitive if your site URLs or CSP reveal internal endpoints.
</p>
</section>
<section>
<h2 class="section-title fw-bold">Hardening tips</h2>
<div class="row g-3">
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Prefer CI / disposable environments</div>
<div class="text-muted">Running in CI makes it easy to isolate and to cache Chromium via <code>--browsers-path</code>.</div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Limit crawl scope</div>
<div class="text-muted">Keep <code>--max-pages</code> small and start from a stable landing page to reduce surprises.</div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Review before enforcing</div>
<div class="text-muted">cspresso emits a draft. Tighten directives (especially <code>script-src</code>/<code>connect-src</code>) and consider nonces.</div>
</div></div>
<div class="col-md-6"><div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Verify releases</div>
<div class="text-muted">mig5 key: <a class="link-secondary" href="https://mig5.net/static/mig5.asc" target="_blank" rel="noreferrer">https://mig5.net/static/mig5.asc</a><br>Fingerprint: <code>00AE817C24A10C2540461A9C1D7CDE0234DB458D</code></div>
</div></div>
</div>
</section>
</div>
</div>
</div>
</main>
<footer class="py-5 mt-5">
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/cspresso.svg" alt="cspresso">
<div class="fw-bold">cspresso (a mig5 project)</div>
</div>
<div class="smallprint">
Crawl a site with headless Chromium, observe what loads, and emit a draft Content-Security-Policy.
Review and tighten before enforcing.
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Pages</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="recipes.html" class="link-secondary text-decoration-none">Recipes</a>
<a href="evaluate.html" class="link-secondary text-decoration-none">Evaluate</a>
<a href="security.html" class="link-secondary text-decoration-none">Security</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/cspresso" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/cspresso/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/cspresso/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-wrap justify-content-between gap-2 small text-secondary">
<div>© 2026 <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
</body>
</html>