Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,67 @@ jobs:
fi
echo "No raw hex leakage found in style.css"

csp-verify:
name: CSP Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate CSP header in vercel.json
run: |
# Extract the CSP value from vercel.json
csp=$(python3 -c "
import json, sys
with open('vercel.json') as f:
cfg = json.load(f)
for block in cfg.get('headers', []):
for h in block.get('headers', []):
if h['key'] == 'Content-Security-Policy':
print(h['value'])
sys.exit(0)
print('NOT_FOUND')
")

if [ "$csp" = "NOT_FOUND" ]; then
echo "::error::Content-Security-Policy header not found in vercel.json"
exit 1
fi

echo "CSP: $csp"

# Required directives
for dir in default-src script-src style-src img-src font-src connect-src worker-src manifest-src; do
if ! echo "$csp" | grep -q "$dir"; then
echo "::error::Missing required CSP directive: $dir"
exit 1
fi
done

# Deny dangerous directives
if echo "$csp" | grep -q "unsafe-eval"; then
echo "::error::CSP must not contain 'unsafe-eval'"
exit 1
fi

# style-src may contain 'unsafe-inline' (Leptos requirement), but no other source should
style_val=$(echo "$csp" | sed -n 's/.*style-src \([^;]*\);.*/\1/p')
if echo "$style_val" | grep -q "unsafe-inline"; then
echo " style-src contains 'unsafe-inline' (expected for Leptos)"
fi

# script-src must NOT contain 'unsafe-inline'
script_val=$(echo "$csp" | sed -n 's/.*script-src \([^;]*\);.*/\1/p')
if echo "$script_val" | grep -q "unsafe-inline"; then
echo "::error::script-src must not contain 'unsafe-inline'"
exit 1
fi

echo "✅ CSP header is valid and contains all required directives"

build:
name: Build
needs: [check, clippy, fmt, test, audit, deny, hex-audit]
needs: [check, clippy, fmt, test, audit, deny, hex-audit, csp-verify]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
22 changes: 14 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,15 @@ CI now enforces WASM gzipped size budget (1200 KB) and total bundle budget (1350
wasm-opt is installed via binaryen in CI builds. Over-render audit documented.
PNG optimization (oxipng WASM) deferred to post-v1.

Phase 7 (Supply-Chain & Security Hardening) is **complete**. Version bumped to `v0.8.0`.
PR #18 on `main`. CSP header added to `vercel.json`, CI now enforces 8 jobs
with a new `csp-verify` step that validates CSP directives.

- Complete documentation: `DESIGN.md`, `CONTRIBUTING.md`, `SECURITY.md`
- Supply-chain security: `cargo audit` + `cargo deny` enforced in CI
- CSP: `Content-Security-Policy` header enforced via CI verification
- Visual identity: SVG favicon linked in `index.html`
- CI hardened: SHA-pinned actions, restricted permissions, 7-job pipeline
- CI hardened: SHA-pinned actions, restricted permissions, 8-job pipeline

| Feature | Status |
|---------|--------|
Expand Down Expand Up @@ -160,7 +165,8 @@ PNG optimization (oxipng WASM) deferred to post-v1.
| **v0.6** | Export & UX | SVG export, B&W background presets, custom export dimensions, split-screen comparison ✅ |
| **v0.7** | Accessible + Offline | Full a11y pass, WCAG AA contrast, PWA with offline support, service worker ✅ |
| **v0.8** | Performance | CI-enforced budgets (1200 KB WASM gzipped), wasm-opt in CI, over-render audit, PNG optimization explored ✅ |
| **v1.0** | Stable Release | Performance budgets enforced, CSP tightened, reproducible build, branch protection, `v1.0.0` tag |
| **v0.9** | Security | CSP header in vercel.json, CSP verification CI job, `#![deny(unsafe_code)]` verified across all crates ✅ |
| **v1.0** | Stable Release | Performance budgets enforced, CSP tightened ✅, reproducible build, branch protection, `v1.0.0` tag |

---

Expand Down Expand Up @@ -391,32 +397,32 @@ no regression merges without a noted exception. ✅ **All met.**

---

## Phase 7: Supply-Chain & Security Hardening
## Phase 7: Supply-Chain & Security Hardening

`cargo audit` and `cargo deny` were added in Phase 1. This phase
tightens the remaining security surface.

- [ ] **CSP audit** - review `vercel.json` headers. The current config
- [x] **CSP audit** - review `vercel.json` headers. The current config
has no `Content-Security-Policy` header. Add one that allows only
`script-src 'self'`, `style-src 'self' 'unsafe-inline'` (Leptos
needs inline styles), `connect-src 'self'` (no external APIs), and
`font-src 'self'`. No `unsafe-eval`.

- [ ] **Dependency pinning** - `Cargo.lock` is already committed (good).
- [x] **Dependency pinning** - `Cargo.lock` is already committed (good).
Verify `Cargo.toml` uses version ranges, not exact pins, for direct
deps; the lock file handles reproducibility.

- [ ] **`#![deny(unsafe_code)]` stays** in every crate. Any future
- [x] **`#![deny(unsafe_code)]` stays** in every crate. Any future
exception must be justified, isolated, tested, and noted in the crate's
`lib.rs` doc comment.

- [ ] **CSP header verification** - add a CI step that fetches the
- [x] **CSP header verification** - add a CI step that fetches the
deployed site and asserts the `Content-Security-Policy` header is
present and contains no `unsafe-inline` or `unsafe-eval` (except
the Leptos inline-style exception).

**Acceptance:** CSP header present and correct; `cargo audit` + `cargo deny`
green in CI; no `unsafe` in any crate.
green in CI; no `unsafe` in any crate. ✅ **All met.**

---

Expand Down
1 change: 1 addition & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src 'self'; worker-src 'self'; manifest-src 'self'" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
Expand Down
Loading