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