diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index ffec63d..8bfef4d 100644
--- a/.github/workflows/main-ci.yml
+++ b/.github/workflows/main-ci.yml
@@ -56,7 +56,8 @@ jobs:
ci/check_loldrivers_hash_only.py \
ci/check_mock_services_loopback.py \
ci/check_no_real_rmm_license.py \
- ci/check_no_suspicious_pth.py; do
+ ci/check_no_suspicious_pth.py \
+ ci/check_snowflake_report_integrity.py; do
name=$(basename "$check" .py)
if python3 "$check"; then
echo "- ✅ \`${name}\`" >> $GITHUB_STEP_SUMMARY
@@ -276,6 +277,13 @@ jobs:
mkdir -p _site/dashboard/assets
cp reports/databricks-apps-assessment/assets/*.svg _site/dashboard/assets/ 2>/dev/null || true
+ # Copy Snowflake assessment (static HTML/CSS — no build step needed).
+ # Explicit allowlist so future *.md / notes / TODOs stay out of _site.
+ mkdir -p _site/snowflake/assets
+ cp reports/snowflake-platform-assessment/*.html _site/snowflake/
+ cp reports/snowflake-platform-assessment/assets/*.css _site/snowflake/assets/
+ test -f _site/snowflake/index.html || { echo "assemble: snowflake/index.html missing"; exit 1; }
+
# Copy CVE README as an index
cp cves/README.md _site/cve-index.md 2>/dev/null || true
diff --git a/CLAUDE.md b/CLAUDE.md
index 0b703a3..0f56f9e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -64,6 +64,16 @@ The report at `reports/databricks-apps-assessment/` is a concatenated Streamlit
- Build: `python build.py` — Check: `python build.py --check`
- All imports belong in `_00_header.py` only; no cross-imports between `src/` files
+## Snowflake Report
+
+The report at `reports/snowflake-platform-assessment/` is a set of linked static HTML pages (no build step).
+
+- Edit pages directly: `index.html`, `threat-landscape.html`, `cve-inventory.html`, `attack-chains.html`, `detection.html`, `recommendations.html`
+- Shared styles: `assets/style.css`
+- Serve locally: `python -m http.server 8080` from the report directory, then open `http://localhost:8080/`
+- The CI pipeline copies the directory as-is to `_site/snowflake/`
+- Working notes / findings: `docs/analysis/snowflake-platform-attack-surface-2026.md`
+
---
## Index
@@ -166,6 +176,7 @@ The report at `reports/databricks-apps-assessment/` is a concatenated Streamlit
→ [docs/analysis/firmware-landscape-2026/README.md](docs/analysis/firmware-landscape-2026/README.md) — Hydroph0bia, LogoFAIL successors, UEFI cert expiry
→ [docs/analysis/apple-mie-impact.md](docs/analysis/apple-mie-impact.md) — Apple Memory Integrity Enforcement
→ [docs/analysis/vishing-2026-market.md](docs/analysis/vishing-2026-market.md) — deepfake vishing economics + healthcare targeting
+→ [docs/analysis/snowflake-platform-attack-surface-2026.md](docs/analysis/snowflake-platform-attack-surface-2026.md) — CVE inventory, UNC5537 analysis, Cortex AI/Native Apps/SPCS attack surface, chains A–E, detection gaps
### Research Docs — Methodology
→ [docs/methodology/callstack-spoofing.md](docs/methodology/callstack-spoofing.md)
@@ -202,3 +213,4 @@ The report at `reports/databricks-apps-assessment/` is a concatenated Streamlit
### Reports
→ [reports/databricks-apps-assessment/](reports/databricks-apps-assessment/) — Streamlit report (see build notes above)
+→ [reports/snowflake-platform-assessment/](reports/snowflake-platform-assessment/) — Static HTML report; findings at [docs/analysis/snowflake-platform-attack-surface-2026.md](docs/analysis/snowflake-platform-attack-surface-2026.md)
diff --git a/ci/check_snowflake_report_integrity.py b/ci/check_snowflake_report_integrity.py
new file mode 100755
index 0000000..b97be74
--- /dev/null
+++ b/ci/check_snowflake_report_integrity.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""CI gate: Snowflake report HTML pages must share an identical nav block
+and every internal href must resolve to a sibling file."""
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+REPORT_DIR = ROOT / "reports" / "snowflake-platform-assessment"
+
+NAV_RE = re.compile(r"", re.DOTALL)
+HREF_RE = re.compile(r'href="([^"#?]+)(?:[#?][^"]*)?"')
+
+
+def collect_pages():
+ return sorted(REPORT_DIR.glob("*.html"))
+
+
+def check_nav_parity(pages):
+ navs = {}
+ for p in pages:
+ m = NAV_RE.search(p.read_text())
+ if not m:
+ return [f"{p.relative_to(ROOT)}: no