diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9d843d1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: Documentation + +# Build the Zensical site under website/ and deploy to GitHub Pages. +# Requires: Settings → Pages → Source = "GitHub Actions". +on: + push: + branches: + - main + paths: + - "website/**" + # changelog.md snippet-includes the repo-root CHANGELOG.md, so a + # changelog-only commit must also rebuild the site. + - "CHANGELOG.md" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress one. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: website + steps: + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.x" + # Pin the version (parity with zizmor==/actionlint checksum pins in ci.yml): + # this job holds pages:write + id-token:write, so a yanked/compromised + # zensical release must not silently change the build. + - run: pip install zensical==0.0.50 + - run: zensical build --clean + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 + with: + path: website/site + - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 + id: deployment diff --git a/.gitignore b/.gitignore index 485145c..8ac2981 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ htmlcov/ settings.json # Cached standalone Tailwind binary (downloaded by scripts/vendor_assets.py) frontend/.bin/ + +# Zensical docs site build output + incremental cache (website/, #243) +website/site/ +website/.cache/ diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..3519a5c --- /dev/null +++ b/website/README.md @@ -0,0 +1,24 @@ +# Iceberg documentation site + +The public docs site — built with [Zensical](https://zensical.org/) (the +Material-based static site generator) from `docs/` + `zensical.toml`, styled to +the shared Iceberg design system (`docs/stylesheets/iceberg.css`, self-hosted +fonts). + +Deployed to GitHub Pages by `.github/workflows/docs.yml` on every push to +`main` touching `website/**` (Settings → Pages → Source = "GitHub Actions"). +The build pins `zensical==0.0.50` — bump deliberately, in step with the +sibling IcebergTTX/EBS sites. + +Preview locally: + +```bash +pip install zensical==0.0.50 +cd website +zensical serve # http://localhost:8000, live reload +zensical build --clean # writes site/ (gitignored) +``` + +Screenshots under `docs/assets/` are copies of the README set in +`../docs/images/` — regenerate those first (`docs/screenshots/README.md`), +then re-copy the subset used here. diff --git a/website/docs/assets/dashboard.png b/website/docs/assets/dashboard.png new file mode 100644 index 0000000..e11bbcb Binary files /dev/null and b/website/docs/assets/dashboard.png differ diff --git a/website/docs/assets/favicon.png b/website/docs/assets/favicon.png new file mode 100644 index 0000000..fa471ee Binary files /dev/null and b/website/docs/assets/favicon.png differ diff --git a/website/docs/assets/feed-reader.png b/website/docs/assets/feed-reader.png new file mode 100644 index 0000000..a8217bb Binary files /dev/null and b/website/docs/assets/feed-reader.png differ diff --git a/website/docs/assets/feed.png b/website/docs/assets/feed.png new file mode 100644 index 0000000..78cc77c Binary files /dev/null and b/website/docs/assets/feed.png differ diff --git a/website/docs/assets/icebergai-horizontal-light.svg b/website/docs/assets/icebergai-horizontal-light.svg new file mode 100644 index 0000000..892d482 --- /dev/null +++ b/website/docs/assets/icebergai-horizontal-light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + IcebergAIAI-native security tooling \ No newline at end of file diff --git a/website/docs/assets/icebergai-horizontal.svg b/website/docs/assets/icebergai-horizontal.svg new file mode 100644 index 0000000..626aa53 --- /dev/null +++ b/website/docs/assets/icebergai-horizontal.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + IcebergAIAI-native security tooling \ No newline at end of file diff --git a/website/docs/assets/icebergai-icon.svg b/website/docs/assets/icebergai-icon.svg new file mode 100644 index 0000000..4f12e59 --- /dev/null +++ b/website/docs/assets/icebergai-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/docs/assets/icebergai-mark-light.svg b/website/docs/assets/icebergai-mark-light.svg new file mode 100644 index 0000000..35aabc7 --- /dev/null +++ b/website/docs/assets/icebergai-mark-light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/docs/assets/icebergai-mark.svg b/website/docs/assets/icebergai-mark.svg new file mode 100644 index 0000000..41a7de7 --- /dev/null +++ b/website/docs/assets/icebergai-mark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/docs/assets/pdf-sample.png b/website/docs/assets/pdf-sample.png new file mode 100644 index 0000000..a460549 Binary files /dev/null and b/website/docs/assets/pdf-sample.png differ diff --git a/website/docs/assets/report-editor.png b/website/docs/assets/report-editor.png new file mode 100644 index 0000000..0fad987 Binary files /dev/null and b/website/docs/assets/report-editor.png differ diff --git a/website/docs/assets/report-view.png b/website/docs/assets/report-view.png new file mode 100644 index 0000000..a1bc977 Binary files /dev/null and b/website/docs/assets/report-view.png differ diff --git a/website/docs/assets/tasking-board.png b/website/docs/assets/tasking-board.png new file mode 100644 index 0000000..05b1fe6 Binary files /dev/null and b/website/docs/assets/tasking-board.png differ diff --git a/website/docs/changelog.md b/website/docs/changelog.md new file mode 100644 index 0000000..d1e6657 --- /dev/null +++ b/website/docs/changelog.md @@ -0,0 +1,6 @@ +--- +title: Changelog +icon: material/history +--- + +--8<-- "CHANGELOG.md" diff --git a/website/docs/deployment.md b/website/docs/deployment.md new file mode 100644 index 0000000..ecedb95 --- /dev/null +++ b/website/docs/deployment.md @@ -0,0 +1,109 @@ +--- +title: Deployment +icon: material/rocket-launch-outline +--- + +# Deployment + +Iceberg is a single FastAPI application serving both the JSON API (`/api/*`) +and the server-rendered portal, with PostgreSQL as the production datastore. +Three supported shapes: + +## Local development + +Zero dependencies beyond Python ≥ 3.14 — SQLite and on-disk working +directories: + +```bash +uv sync --extra dev +cp .env.example .env # adjust as needed +uv run uvicorn iceberg.main:app --reload +``` + +Open and use the **dev login** +(`ICEBERG_DEV_AUTH=true`, the default outside prod) — no IdP required. +Install the [`typst`](https://github.com/typst/typst) binary for PDF +rendering. + +!!! note "SQLite is dev/test only" + The app refuses to boot on a SQLite URL when `ICEBERG_ENVIRONMENT=prod`, + and the Docker image ships no SQLite fallback — a container can't + silently run without PostgreSQL. + +## Docker Compose (single host) + +```bash +docker compose up # https://localhost via Caddy + PostgreSQL +ICEBERG_DOMAIN=intel.example.com docker compose up # automatic Let's Encrypt TLS +``` + +The default Compose stack runs four services on an internal network: + +- **caddy** — the TLS front end and only public entry point (`:80`/`:443`, + HTTP/3). Automatic HTTPS: a local-CA certificate for the default + `localhost`, Let's Encrypt for a real domain. Runs non-root with all + capabilities dropped except `NET_BIND_SERVICE`, on a read-only rootfs. +- **iceberg** — the app, publishing plain HTTP on loopback only + (`127.0.0.1:8000`) as a local/debug side door. Started with + `--proxy-headers`; `X-Forwarded-*` is trusted only from the Compose + network, so audit logs record the real client IP. +- **postgres** — the datastore, on a named volume. +- **redis** — shared rate-limit buckets across uvicorn workers. + +For production Compose, set `ICEBERG_ENVIRONMENT=prod` (Secure cookies + +HSTS), a real `ICEBERG_SECRET_KEY`, `ICEBERG_AUTO_MIGRATE=false`, and +non-default `POSTGRES_*` credentials. + +## Kubernetes + +Manifests live under +[`deploy/k8s/`](https://github.com/IcebergAI/IcebergCTI/tree/main/deploy/k8s): +an Ingress terminates TLS to a ClusterIP Service and a single-replica +Deployment (`Recreate`, non-root, read-only rootfs, dropped capabilities). A +migrate Job runs `alembic upgrade head` out of band; config comes from a +ConfigMap and secrets from a Secret. PostgreSQL is a managed instance or the +optional StatefulSet; uploads and rendered PDFs live on a `ReadWriteOnce` +`/data` PVC — the reason replicas stay at 1 until shared storage lands. + +Pin the published image by immutable digest in production +(`deploy/k8s/release.sh`). + +## Releases + +A release is a git tag: pushing `v*` to `main` builds and pushes +`ghcr.io/icebergai/icebergcti` with an SBOM and SLSA provenance, attests the +provenance to the registry, signs the image keylessly with cosign, and cuts +the GitHub Release. See +[`docs/RELEASING.md`](https://github.com/IcebergAI/IcebergCTI/blob/main/docs/RELEASING.md). + +## Backup and restore + +Persistent state lives in two places — **PostgreSQL** (reports, requirements, +tags, audit events, settings) and the **`/data` volume** (attachments, +figures, rendered PDFs). Back up both while writers are stopped: + +```bash +docker compose stop iceberg +docker compose exec postgres pg_dump -U iceberg -d iceberg -Fc > iceberg-$(date +%F).dump +docker run --rm -v iceberg_iceberg-data:/data -v "$PWD":/out busybox \ + tar cf /out/iceberg-data-$(date +%F).tar -C /data . +docker compose start iceberg +``` + +Restore keeps the app stopped, restores PostgreSQL, clears the working +directories before extracting the archive, applies migrations, and runs +`iceberg-verify-files` before restart. PDFs regenerate; attachments and +figures are original material. + +## Configuration + +All settings are environment variables with the `ICEBERG_` prefix (see +[`.env.example`](https://github.com/IcebergAI/IcebergCTI/blob/main/.env.example)). +Secrets are **env-only** — provider client secrets, API keys and tokens are +never stored in the database. Admins get a read-only **effective +configuration** view at `/admin/config` showing every resolved value, its +provenance, and secret redaction, plus a settings hub at `/admin` with +per-subsystem status tiles. + +Production guards refuse to boot on a SQLite URL, a weak +`ICEBERG_SECRET_KEY`, or a wildcard `FORWARDED_ALLOW_IPS`. diff --git a/website/docs/fonts/archivo-400-normal-latin-ext.woff2 b/website/docs/fonts/archivo-400-normal-latin-ext.woff2 new file mode 100644 index 0000000..2e544c2 Binary files /dev/null and b/website/docs/fonts/archivo-400-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/archivo-400-normal-latin.woff2 b/website/docs/fonts/archivo-400-normal-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/website/docs/fonts/archivo-400-normal-latin.woff2 differ diff --git a/website/docs/fonts/archivo-500-normal-latin-ext.woff2 b/website/docs/fonts/archivo-500-normal-latin-ext.woff2 new file mode 100644 index 0000000..2e544c2 Binary files /dev/null and b/website/docs/fonts/archivo-500-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/archivo-500-normal-latin.woff2 b/website/docs/fonts/archivo-500-normal-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/website/docs/fonts/archivo-500-normal-latin.woff2 differ diff --git a/website/docs/fonts/archivo-600-normal-latin-ext.woff2 b/website/docs/fonts/archivo-600-normal-latin-ext.woff2 new file mode 100644 index 0000000..2e544c2 Binary files /dev/null and b/website/docs/fonts/archivo-600-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/archivo-600-normal-latin.woff2 b/website/docs/fonts/archivo-600-normal-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/website/docs/fonts/archivo-600-normal-latin.woff2 differ diff --git a/website/docs/fonts/archivo-700-normal-latin-ext.woff2 b/website/docs/fonts/archivo-700-normal-latin-ext.woff2 new file mode 100644 index 0000000..2e544c2 Binary files /dev/null and b/website/docs/fonts/archivo-700-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/archivo-700-normal-latin.woff2 b/website/docs/fonts/archivo-700-normal-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/website/docs/fonts/archivo-700-normal-latin.woff2 differ diff --git a/website/docs/fonts/archivo-800-normal-latin-ext.woff2 b/website/docs/fonts/archivo-800-normal-latin-ext.woff2 new file mode 100644 index 0000000..2e544c2 Binary files /dev/null and b/website/docs/fonts/archivo-800-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/archivo-800-normal-latin.woff2 b/website/docs/fonts/archivo-800-normal-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/website/docs/fonts/archivo-800-normal-latin.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2 differ diff --git a/website/docs/fonts/spectral-400-italic-latin-ext.woff2 b/website/docs/fonts/spectral-400-italic-latin-ext.woff2 new file mode 100644 index 0000000..5295b22 Binary files /dev/null and b/website/docs/fonts/spectral-400-italic-latin-ext.woff2 differ diff --git a/website/docs/fonts/spectral-400-italic-latin.woff2 b/website/docs/fonts/spectral-400-italic-latin.woff2 new file mode 100644 index 0000000..1e0f383 Binary files /dev/null and b/website/docs/fonts/spectral-400-italic-latin.woff2 differ diff --git a/website/docs/fonts/spectral-400-normal-latin-ext.woff2 b/website/docs/fonts/spectral-400-normal-latin-ext.woff2 new file mode 100644 index 0000000..78c4c00 Binary files /dev/null and b/website/docs/fonts/spectral-400-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/spectral-400-normal-latin.woff2 b/website/docs/fonts/spectral-400-normal-latin.woff2 new file mode 100644 index 0000000..71700f8 Binary files /dev/null and b/website/docs/fonts/spectral-400-normal-latin.woff2 differ diff --git a/website/docs/fonts/spectral-500-normal-latin-ext.woff2 b/website/docs/fonts/spectral-500-normal-latin-ext.woff2 new file mode 100644 index 0000000..0dbd536 Binary files /dev/null and b/website/docs/fonts/spectral-500-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/spectral-500-normal-latin.woff2 b/website/docs/fonts/spectral-500-normal-latin.woff2 new file mode 100644 index 0000000..440edbb Binary files /dev/null and b/website/docs/fonts/spectral-500-normal-latin.woff2 differ diff --git a/website/docs/fonts/spectral-600-normal-latin-ext.woff2 b/website/docs/fonts/spectral-600-normal-latin-ext.woff2 new file mode 100644 index 0000000..16fb98f Binary files /dev/null and b/website/docs/fonts/spectral-600-normal-latin-ext.woff2 differ diff --git a/website/docs/fonts/spectral-600-normal-latin.woff2 b/website/docs/fonts/spectral-600-normal-latin.woff2 new file mode 100644 index 0000000..3f56161 Binary files /dev/null and b/website/docs/fonts/spectral-600-normal-latin.woff2 differ diff --git a/website/docs/index.md b/website/docs/index.md new file mode 100644 index 0000000..7839a73 --- /dev/null +++ b/website/docs/index.md @@ -0,0 +1,113 @@ +--- +title: Iceberg +hide: + - navigation + - toc +--- + +
+ +Iceberg + +

Cyber threat intelligence

+ +Collect. Author. Disseminate. + +

+Iceberg is a CTI platform built around the intelligence cycle: analysts collect +sources into topic **notebooks**, author finished **intelligence products** in +markdown, and **disseminate** them to stakeholders matched by intelligence level +and TLP — with the whole loop closed by stakeholder feedback. +

+ +
+[The intelligence cycle](workflow.md){ .md-button .md-button--primary } +[Deploy it](deployment.md){ .md-button } +[View on GitHub](https://github.com/IcebergAI/IcebergCTI){ .md-button } +
+ +
+ +## Why Iceberg + +Most CTI tooling is an indicator database with a report feature bolted on. +Iceberg inverts that: it is a **finished-intelligence platform** — the +authoritative IOC store stays external (MISP) — built for the analysts who +write, the reviewers who gate, and the stakeholders who consume. Reports carry +an intelligence level (Strategic / Tactical / Operational), a TLP marking, +Admiralty-graded sources, and structured analytic techniques, and they move +through a review workflow to a frozen, immutable publication. + +![The analyst dashboard](assets/dashboard.png) + +
+ +- :material-notebook-outline: __Notebook-based collection__ + + --- + + Open a topic notebook, gather sources, notes, and uploaded attachments; + stage light-touch IOCs; apply the Diamond Model and ACH with live inline + embeds. + + [:octicons-arrow-right-24: Collection](workflow.md#collect) + +- :material-file-document-edit-outline: __Author finished products__ + + --- + + A full-height markdown editor with live preview, source citations, + requirement traceability, taxonomy tagging, and branded Typst PDF + rendering. + + [:octicons-arrow-right-24: Authoring](workflow.md#author) + +- :material-send-check-outline: __Dissemination that closes the loop__ + + --- + + On publish, reports match stakeholders by preferred intel level + TLP into + a personal feed with email notification; feedback and RFI satisfaction + flow back to analysts. + + [:octicons-arrow-right-24: Dissemination](workflow.md#disseminate) + +- :material-clipboard-list-outline: __Requirements → tasking__ + + --- + + Stakeholders record PIRs, GIRs and RFIs; analysts work them from a + tasking board with coverage gaps and traceability from requirement to + published product. + + [:octicons-arrow-right-24: Tasking](workflow.md#task) + +- :material-shield-lock-outline: __Governed by design__ + + --- + + Multi-provider OIDC SSO, role-based access, TLP-gated egress for AI, MISP + and dissemination, a structured audit log with SIEM forwarding, and + hardened outbound fetching. + + [:octicons-arrow-right-24: Security](security.md) + +- :material-connection: __Plays well with your stack__ + + --- + + MISP push, read-only TAXII 2.1 serving, TAXII/MISP pull, RSS ingestion, + ATT&CK Navigator layers, governed AI assist, publication webhooks, and a + global outbound proxy. + + [:octicons-arrow-right-24: Integrations](integrations.md) + +
+ +## The product, not the pipeline + +Everything converges on the finished report: TLP and intelligence-level +markings, numbered citations, an indicators appendix, inline Diamond/ACH/figure +embeds, and one-click PDF products typeset with Typst. + +![A published intelligence product rendered to PDF](assets/pdf-sample.png) diff --git a/website/docs/integrations.md b/website/docs/integrations.md new file mode 100644 index 0000000..0a6aab3 --- /dev/null +++ b/website/docs/integrations.md @@ -0,0 +1,76 @@ +--- +title: Integrations +icon: material/connection +--- + +# Integrations + +Iceberg is API-first and designed to sit inside an existing security stack. +Every outbound HTTP call honours the **global outbound proxy** configured at +`/admin/proxy` (system / direct / explicit with no-proxy exclusions). +Publication-driven delivery — email notifications, publication webhooks — and +RSS polling ride a durable **outbox job queue** drained by the +`iceberg-worker` process, enqueued in the same transaction as their cause, +with lease, retry and backoff; interactive calls (the MISP push, AI assist, +writer-triggered TAXII/MISP pulls) run synchronously and report their result +directly. + +## MISP (outbound push) + +Iceberg stages **light-touch IOCs** as notebook entities; the authoritative +indicator store stays external. A writer pushes a report's **cited +indicators to MISP as one event** — lifecycle-gated (approved or published +reports only; published reports push from their frozen snapshot), idempotent +(re-push updates the same event), failure-isolated, and authenticated with an +env-only API key. Each indicator's TLP rides along as a per-attribute tag; +indicators above `ICEBERG_MISP_MAX_TLP` prompt the writer to confirm before +leaving the org. Configured at `/admin/misp`. + +## TAXII 2.1 (serving and pull) + +- **Serving** — published reports are available read-only in TAXII + 2.1-shaped collections under `/api/taxii2/`, as STIX 2.1 `report` objects + with their cited indicators. +- **Pull** — writers trigger a TAXII 2.1 (or MISP) pull into a notebook, + landing objects as sources and staged IOCs behind the SSRF guard. + +## RSS / Atom ingestion + +Admins configure external feeds at `/admin/feeds`; articles poll into a +writer-only **feed reader** where an analyst sends an article to a notebook +as an auto-graded source. Fetching is opt-in, timeout- and byte-bounded, +failure-isolated, and content is sanitised; feed URLs are admin-only — the +SSRF containment boundary. + +## ATT&CK + +An offline **MITRE Enterprise ATT&CK** bundle import +(`iceberg-import-attack`) creates technique tags. Techniques tagged across +reports drive a coverage heatmap and downloadable **ATT&CK Navigator +layers** — per report and per actor/malware/campaign entity. + +## Governed AI assist + +Optional AI-assist endpoints (summarise a source, suggest IOC candidates, +draft tradecraft phrasing) run behind an admin-configured provider +(`/admin/ai`) with an env-only key. Source content above +`ICEBERG_AI_MAX_TLP` never leaves for the provider; AI-suggested indicators +are normalised (refanged, type-constrained) and always land as *staged* +entities for analyst review, never directly in a product. + +## Email, webhooks and SIEM + +- **Email** — stakeholders get a notification when a product lands in their + feed (SMTP, optional). +- **Publication webhook** — a signed webhook fires on publish for + tag-subscription integrations, configured at `/admin/webhook` with an + env-only token. +- **SIEM** — the structured audit log forwards via stdout/file, syslog, or + HTTP event collector (see [Security](security.md#audit-and-siem)). + +## API + +The whole portal is backed by a JSON API under `/api/*`, authenticated with +Bearer JWTs — everything an analyst can do in the UI, a pipeline can do +against the API. Interactive docs (`/docs`, `/redoc`) are enabled outside +production. diff --git a/website/docs/security.md b/website/docs/security.md new file mode 100644 index 0000000..6ef5a11 --- /dev/null +++ b/website/docs/security.md @@ -0,0 +1,88 @@ +--- +title: Security +icon: material/shield-lock-outline +--- + +# Security + +Iceberg is built for security-conscious environments: SSO-only authentication, +least-privilege roles, marking-gated egress, and an audit trail designed for a +SIEM. + +## Authentication + +Authentication is **multi-provider OIDC** — Microsoft Entra, Authentik, +Auth0 and Okta, simultaneously — admin-configured at `/admin/oidc` (env +seeds it). After login Iceberg mints its own short-lived **JWT**, sent as a +Bearer header by API clients or carried in a session cookie by the portal; a +per-user `token_version` supports logout and revocation, and the JWT and +session cookie are signed with **purpose-separated keys** derived from +`ICEBERG_SECRET_KEY`. + +Users are keyed on the immutable `(auth_provider, issuer, sub)` triple — +email is non-identifying, so the same person under two IdPs is two accounts, +and a spoofed issuer can't inherit another provider's users. A per-provider +`role_map` maps IdP groups to roles, defaulting to least-privilege +STAKEHOLDER; a missing or unverified email is rejected. Per-provider client +secrets are **env-only**. + +A **dev-login bypass** (`ICEBERG_DEV_AUTH=true`) issues a role JWT without an +IdP for local development; it is disabled in prod. + +## Roles + +| Role | Capability | +|---|---| +| `ADMIN` | Everything, plus admin configuration surfaces | +| `ANALYST` | Collect, author, publish, push to MISP | +| `REVIEWER` | As analyst, plus the review gate | +| `STAKEHOLDER` | Read-only: feed, published reports, own requirements, feedback | + +Notebook access is **role-wide** for writers, not owner-scoped — `owner_id` +is provenance only; the one owner/admin gate is deleting a whole notebook. + +## TLP as an egress gate + +TLP is a display and dissemination-routing marking — never an in-portal read +gate. Where it *does* gate is at the boundary: content above the configured +ceiling cannot leave through **AI assist** (`ICEBERG_AI_MAX_TLP`), +**dissemination** (`ICEBERG_DISSEMINATION_MAX_TLP`), or the **MISP push** +(`ICEBERG_MISP_MAX_TLP`). + +## Hardening + +- **CSRF** — a same-origin middleware rejects cookie-authenticated + state-changing requests whose `Origin`/`Referer` mismatch the host; + Bearer and anonymous requests are exempt; logout is POST-only. +- **Strict CSP** — `script-src 'self'` with no `unsafe-inline`/`unsafe-eval`: + a vendored CSP build of Alpine, SRI-pinned self-hosted assets, no CDN + contact at runtime. +- **SSRF guard** — the one server-side fetcher (RSS and writer-triggered + TAXII/MISP pull) is http(s)-only, rejects private/loopback addresses, + bounds bytes and timeouts, and re-validates every redirect hop. +- **Upload validation** — MIME whitelist, size caps, and magic-byte content + sniffing. +- **Rate limiting** and an oversized-body rejection before anything buffers; + security response headers set by the app itself. +- **Immutable publications** — stakeholder, PDF and MISP output serve from a + frozen publication snapshot; an optimistic-lock version column turns a + stale write into a 409. +- **Prod guards** — boot refuses SQLite, a weak secret key, or wildcard + proxy trust in production. + +## Audit and SIEM + +Security-relevant events are captured to a **structured-JSON audit log** +(OWASP application-logging shape) with the real client IP, and can be +forwarded to a SIEM via stdout/file, syslog, or an HTTP event collector — +configured at `/admin/audit`. The audit detail JSON never carries secrets or +PII payloads. + +## Supply chain + +CI runs ruff, bandit, vulture, mypy, pip-audit, CodeQL SAST, a Trivy image +scan, and a zizmor + actionlint audit of the workflows themselves; all +GitHub Actions are SHA-pinned. Release images ship an SBOM and SLSA +provenance and are cosign-signed. See +[`SECURITY.md`](https://github.com/IcebergAI/IcebergCTI/blob/main/SECURITY.md) +for the vulnerability disclosure policy. diff --git a/website/docs/stylesheets/fonts.css b/website/docs/stylesheets/fonts.css new file mode 100644 index 0000000..879119f --- /dev/null +++ b/website/docs/stylesheets/fonts.css @@ -0,0 +1,275 @@ +/* Docs-site copy of the app's font sheet — derived from static/css/vendor/fonts.css + with the /static/fonts/ URLs rewritten to ../fonts/ (Zensical serves the woff2 + files from website/docs/fonts/). The app sheet is the source of truth; + font = false in zensical.toml disables Google Fonts so these are the only + faces loaded. */ + +/* Archivo 400 normal latin-ext */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-400-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Archivo 400 normal latin */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-400-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Archivo 500 normal latin-ext */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-500-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Archivo 500 normal latin */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-500-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Archivo 600 normal latin-ext */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-600-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Archivo 600 normal latin */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-600-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Archivo 700 normal latin-ext */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-700-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Archivo 700 normal latin */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-700-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Archivo 800 normal latin-ext */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 800; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-800-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Archivo 800 normal latin */ +@font-face { + font-family: 'Archivo'; + font-style: normal; + font-weight: 800; + font-stretch: 100%; + font-display: swap; + src: url(../fonts/archivo-800-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* JetBrains Mono 400 normal latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/jetbrains-mono-400-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* JetBrains Mono 400 normal latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/jetbrains-mono-400-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* JetBrains Mono 500 normal latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/jetbrains-mono-500-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* JetBrains Mono 500 normal latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/jetbrains-mono-500-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* JetBrains Mono 600 normal latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/jetbrains-mono-600-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* JetBrains Mono 600 normal latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/jetbrains-mono-600-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* JetBrains Mono 700 normal latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/jetbrains-mono-700-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* JetBrains Mono 700 normal latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/jetbrains-mono-700-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Spectral 400 italic latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(../fonts/spectral-400-italic-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Spectral 400 italic latin */ +@font-face { + font-family: 'Spectral'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(../fonts/spectral-400-italic-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Spectral 400 normal latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/spectral-400-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Spectral 400 normal latin */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/spectral-400-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Spectral 500 normal latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/spectral-500-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Spectral 500 normal latin */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/spectral-500-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Spectral 600 normal latin-ext */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/spectral-600-normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Spectral 600 normal latin */ +@font-face { + font-family: 'Spectral'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/spectral-600-normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/website/docs/stylesheets/iceberg.css b/website/docs/stylesheets/iceberg.css new file mode 100644 index 0000000..9b6eec7 --- /dev/null +++ b/website/docs/stylesheets/iceberg.css @@ -0,0 +1,306 @@ +/* ========================================================================== + * IcebergCTI docs — Iceberg design system mapped onto the Material theme. + * + * Strategy: define the Iceberg oklch tokens per colour scheme, then point + * Material's --md-* variables at them so the whole theme recolours. The + * header is the app's dark command-center "rail" in BOTH schemes; links and + * interactive accents are the fixed glacial-cyan. + * + * Token source of truth: src/iceberg/static/css/iceberg.css. Kept in step with + * the sibling IcebergTTX/EBS docs sheets so the family sites read as one system. + * ========================================================================== */ + +/* ---- Fonts (self-hosted; @font-face rules live in fonts.css) ------------- */ +:root { + --md-text-font: "Archivo"; + --md-code-font: "JetBrains Mono"; +} + +/* ========================================================================== + * LIGHT scheme + * ========================================================================== */ +[data-md-color-scheme="default"] { + /* Iceberg tokens (light) */ + --paper: oklch(0.984 0.006 240); + --surface: oklch(1 0 0); + --surface-2: oklch(0.972 0.008 240); + --surface-ink: oklch(0.22 0.026 256); + --ink: oklch(0.26 0.026 262); + --ink-soft: oklch(0.44 0.022 260); + --muted: oklch(0.58 0.017 258); + --faint: oklch(0.70 0.012 258); + --line: oklch(0.915 0.008 248); + --line-strong: oklch(0.855 0.012 248); + + --accent: oklch(0.66 0.118 226); + --accent-ink: oklch(0.50 0.115 234); + --accent-deep: oklch(0.42 0.10 238); + --accent-soft: oklch(0.955 0.028 226); + --accent-line: oklch(0.86 0.055 226); + --accent-glow: oklch(0.66 0.118 226 / 0.16); + + --rail: oklch(0.225 0.024 256); + --rail-2: oklch(0.285 0.026 256); + --rail-line: oklch(0.34 0.022 256); + --rail-ink: oklch(0.80 0.02 250); + + /* Material mapping */ + --md-default-bg-color: var(--paper); + --md-default-bg-color--light: color-mix(in oklch, var(--paper), transparent 30%); + --md-default-fg-color: var(--ink); + --md-default-fg-color--light: var(--ink-soft); + --md-default-fg-color--lighter: var(--muted); + --md-default-fg-color--lightest: var(--line); + + --md-primary-fg-color: var(--rail); /* header background */ + --md-primary-fg-color--light: var(--rail-2); + --md-primary-fg-color--dark: var(--surface-ink); + --md-primary-bg-color: oklch(0.97 0.01 250); + --md-primary-bg-color--light: var(--rail-ink); + + --md-accent-fg-color: var(--accent-ink); + --md-accent-fg-color--transparent: var(--accent-glow); + + --md-typeset-a-color: var(--accent-ink); + --md-typeset-mark-color: var(--accent-glow); + + --md-code-bg-color: var(--surface-2); + --md-code-fg-color: var(--ink); + + --md-footer-bg-color: var(--surface-ink); + --md-footer-bg-color--dark: oklch(0.15 0.02 256); + --md-footer-fg-color: oklch(0.82 0.02 250); + --md-footer-fg-color--light: oklch(0.62 0.018 252); + --md-footer-fg-color--lighter: oklch(0.50 0.016 254); +} + +/* ========================================================================== + * DARK (slate) scheme + * ========================================================================== */ +[data-md-color-scheme="slate"] { + --paper: oklch(0.185 0.02 256); + --surface: oklch(0.23 0.022 256); + --surface-2: oklch(0.27 0.024 256); + --surface-ink: oklch(0.15 0.02 256); + --ink: oklch(0.93 0.012 250); + --ink-soft: oklch(0.80 0.015 252); + --muted: oklch(0.66 0.016 255); + --faint: oklch(0.55 0.014 256); + --line: oklch(0.32 0.018 256); + --line-strong: oklch(0.40 0.022 256); + + --accent: oklch(0.72 0.12 226); + --accent-ink: oklch(0.80 0.11 226); + --accent-deep: oklch(0.86 0.085 226); + --accent-soft: oklch(0.30 0.05 232); + --accent-line: oklch(0.44 0.07 230); + --accent-glow: oklch(0.72 0.12 226 / 0.22); + + --rail: oklch(0.16 0.018 256); + --rail-2: oklch(0.235 0.022 256); + --rail-line: oklch(0.30 0.02 256); + --rail-ink: oklch(0.82 0.02 250); + + --md-default-bg-color: var(--paper); + --md-default-bg-color--light: color-mix(in oklch, var(--paper), transparent 30%); + --md-default-fg-color: var(--ink); + --md-default-fg-color--light: var(--ink-soft); + --md-default-fg-color--lighter: var(--muted); + --md-default-fg-color--lightest: var(--line); + + --md-primary-fg-color: var(--rail); + --md-primary-fg-color--light: var(--rail-2); + --md-primary-fg-color--dark: var(--surface-ink); + --md-primary-bg-color: var(--ink); + --md-primary-bg-color--light: var(--rail-ink); + + --md-accent-fg-color: var(--accent-ink); + --md-accent-fg-color--transparent: var(--accent-glow); + + --md-typeset-a-color: var(--accent-ink); + --md-typeset-mark-color: var(--accent-glow); + + --md-code-bg-color: var(--surface-ink); + --md-code-fg-color: var(--ink); + + --md-footer-bg-color: var(--surface-ink); + --md-footer-bg-color--dark: oklch(0.12 0.018 256); + --md-footer-fg-color: var(--rail-ink); + --md-footer-fg-color--light: var(--muted); + --md-footer-fg-color--lighter: var(--faint); +} + +/* ========================================================================== + * Chrome + * ========================================================================== */ + +/* 3px glacial-cyan brandbar strip pinned above the (dark) header */ +.md-header { + border-top: 3px solid var(--accent); + box-shadow: 0 1px 0 var(--rail-line); +} +.md-tabs { + background-color: var(--md-primary-fg-color--dark); +} + +/* Search field: darker well on the dark header */ +.md-search__form { + background-color: color-mix(in oklch, var(--rail-2), transparent 10%); +} +.md-search__form:hover { + background-color: var(--rail-2); +} + +/* Links: cyan, with an accent underline on hover */ +.md-typeset a { + text-decoration-color: var(--accent-line); +} +.md-typeset a:hover { + color: var(--accent); +} + +/* ========================================================================== + * Typography — Spectral (serif) for the editorial layer + * ========================================================================== */ +.md-typeset h1 { + font-family: "Spectral", Georgia, serif; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); +} +.md-typeset blockquote { + font-family: "Spectral", Georgia, serif; + border-left: 3px solid var(--accent); + color: var(--ink-soft); +} + +/* Mono eyebrow / small-caps kicker used on pages */ +.md-typeset .eyebrow { + font-family: "JetBrains Mono", monospace; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-ink); +} + +/* Inline code + code blocks pick up token surfaces */ +.md-typeset code { + border: 1px solid var(--line); + border-radius: 6px; +} + +/* ========================================================================== + * Hero (landing page) — dark command-center panel + * ========================================================================== */ +.iceberg-hero { + margin: -1rem 0 2.5rem; + padding: 3rem 2.5rem 3.25rem; + border-radius: 14px; + border: 1px solid var(--rail-line); + background: + radial-gradient(120% 140% at 100% 0%, oklch(0.34 0.06 232 / 0.55), transparent 55%), + linear-gradient(160deg, var(--rail-2), var(--surface-ink)); + color: oklch(0.93 0.012 250); + box-shadow: 0 20px 48px -24px oklch(0.15 0.03 256 / 0.7); +} +.iceberg-hero img.hero-wordmark { + height: 3.1rem; + width: auto; + margin: 0 0 1.25rem; +} +.iceberg-hero .eyebrow { + color: var(--accent-deep); + margin-bottom: 0.75rem; +} +.iceberg-hero p.tagline { + font-family: "Spectral", Georgia, serif; + font-size: 1.28rem; + line-height: 1.5; + max-width: 44ch; + color: oklch(0.86 0.015 252); + margin: 0.25rem 0 1.75rem; +} +.iceberg-hero .hero-cta { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +/* Primary/ghost buttons (Material .md-button) tuned to the cyan accent */ +.md-typeset .md-button { + border-radius: 8px; + border-color: var(--accent-line); + color: var(--accent-deep); + transition: background-color .15s, color .15s, border-color .15s; +} +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + background-color: var(--accent); + border-color: var(--accent); + color: oklch(0.16 0.02 256); +} +.md-typeset .md-button--primary { + background-color: var(--accent); + border-color: var(--accent); + color: oklch(0.16 0.02 256); +} +.md-typeset .md-button--primary:hover { + background-color: var(--accent-ink); + border-color: var(--accent-ink); + color: oklch(0.98 0.01 250); +} +/* The hero panel is dark in BOTH schemes, so its buttons use fixed colours. + * The theme's plain .md-button carries a --line background, which is + * near-white in the light scheme — the light hero text would vanish on it. */ +.iceberg-hero .md-button { + background-color: oklch(1 0 0 / 0.08); + border-color: color-mix(in oklch, var(--accent), transparent 40%); + color: oklch(0.9 0.02 232); +} +.iceberg-hero .md-button--primary { + background-color: oklch(0.66 0.118 226); + border-color: oklch(0.66 0.118 226); + color: oklch(0.16 0.02 256); +} +.iceberg-hero .md-button--primary:focus, +.iceberg-hero .md-button--primary:hover { + background-color: oklch(0.78 0.10 226); + border-color: oklch(0.78 0.10 226); + color: oklch(0.16 0.02 256); +} + +/* ========================================================================== + * Grid cards — surface panels with an accent hover + * ========================================================================== */ +.md-typeset .grid.cards > ul > li, +.md-typeset .grid > .card { + border: 1px solid var(--line); + border-radius: 12px; + background-color: var(--surface); + box-shadow: 0 1px 2px oklch(0.45 0.03 256 / 0.05); + transition: border-color .15s, box-shadow .15s, transform .15s; +} +.md-typeset .grid.cards > ul > li:hover, +.md-typeset .grid > .card:hover { + border-color: var(--accent-line); + box-shadow: 0 10px 26px -14px oklch(0.4 0.04 256 / 0.35); + transform: translateY(-2px); +} +.md-typeset .grid.cards > ul > li > hr { + border-bottom-color: var(--line); +} + +/* Screenshots: framed with the token line + soft lift */ +.md-typeset img.shot { + border: 1px solid var(--line); + border-radius: 10px; + box-shadow: 0 12px 32px -18px oklch(0.4 0.04 256 / 0.4); +} + +/* Admonitions: accent the "note/info" flavour to cyan */ +.md-typeset .admonition.note, +.md-typeset .admonition.info, +.md-typeset details.note, +.md-typeset details.info { + border-left-color: var(--accent); +} diff --git a/website/docs/workflow.md b/website/docs/workflow.md new file mode 100644 index 0000000..fcaa4cb --- /dev/null +++ b/website/docs/workflow.md @@ -0,0 +1,114 @@ +--- +title: The intelligence cycle +icon: material/refresh +--- + +# The intelligence cycle + +Iceberg follows the traditional intelligence cycle — direction, collection, +analysis, production, dissemination, feedback — with a screen for each phase +and traceability across all of them. + +## Direction: requirements and tasking { #task } + +Stakeholders (read-only users) record their own intelligence requirements, +typed by how they direct work: + +| Type | Meaning | Shape | +|---|---|---| +| **PIR** | Priority intelligence requirement | Decision-tied, time-bound, carries priority | +| **GIR** | General intelligence requirement | Standing collection interest | +| **RFI** | Request for information | Ad-hoc question, satisfied by a specific product | + +Requirements aggregate into a **tasking board** grouped by status. Ordering +blends urgency and kind — a PIR is floored to at least High priority so it +leads standing work, but a genuine Critical item still tops its column. A PIR +coverage panel flags PIRs with no linked report or notebook (collection gaps) +and those past their review-by date. + +![The tasking board](assets/tasking-board.png) + +## Collection: notebooks { #collect } + +Analysts open a topic **notebook** and gather material into it: + +- **Sources** — links and references, graded with an offline Admiralty/NATO + reliability heuristic; manual sources default to TLP:AMBER. +- **Notes** — free-form analyst notes. +- **Attachments** — uploaded reference files (MIME-whitelisted, size-capped, + magic-byte validated). +- **Light-touch IOCs** — indicators staged as notebook entities for later + citation and MISP push. Iceberg is *not* an IOC store; the authoritative + store stays external. + +Collection is fed from outside too: an admin-configured **RSS/Atom reader** +lets analysts send an article to a notebook as an auto-graded source, and +writers can pull from **TAXII 2.1** and **MISP** servers directly into +notebook sources and IOCs. + +![The analyst feed reader](assets/feed-reader.png) + +## Analysis: structured techniques + +Inside a notebook, analysts apply structured analytic techniques whose +artefacts embed live into reports: + +- **Diamond Model** of intrusion analysis diagrams +- **Analysis of Competing Hypotheses (ACH)** matrices +- **Figures** — uploaded images with captions + +Inline-embed tokens (`[[diamond:ID]]`, `[[ach:ID]]`, `[[figure:ID]]`, and a +bare `[[attack]]` coverage matrix) resolve in the live preview, the published +web view, and the PDF. Tokens are notebook-scoped — a cross-notebook or +unknown id degrades to an "unavailable" notice rather than leaking content. + +## Production: authoring and review { #author } + +One or more **reports** (intelligence products) are authored from a notebook's +material in a full-height three-pane editor: markdown, live preview, and a +docked panel for citations, tags, requirement links, and rendering. + +![The report editor](assets/report-editor.png) + +Reports carry: + +- an **intelligence level** — Strategic, Tactical, or Operational — used for + dissemination matching; +- a **TLP marking** — a display and routing marking, never an in-portal read + gate; +- **citations** of notebook sources and attachments, rendered as numbered + references; +- **cited indicators**, rendered as an Indicators appendix; +- **taxonomy tags** — threat actor, campaign, malware, ATT&CK technique, + sector, topic — driving faceted search, entity profiles, and an ATT&CK + coverage heatmap; +- an advisory **estimative-language lint** (tradecraft nudges, never a block). + +Products render on demand to **branded PDFs** via Typst in three formats — +full assessment, executive brief, one-pager — independent of intel level. + +Reports move Draft → In review → Approved → **Published**. Publishing freezes +an immutable **publication snapshot**: stakeholder views, PDFs and MISP pushes +all serve from the snapshot, never from live notebook material. + +![A published report](assets/report-view.png) + +## Dissemination: the stakeholder feed { #disseminate } + +On publish, a report is matched to stakeholders by their **preferred +intelligence level** and **TLP ceiling** and delivered to a personal feed, +with optional email notification and tag-subscription webhooks. Each feed item +explains *why* it was delivered — an answered RFI and a preference match are +reported separately. + +![The stakeholder intelligence feed](assets/feed.png) + +## Feedback: closing the loop + +On a delivered product, a stakeholder leaves **feedback**: a usefulness +rating, an optional **RFI-satisfaction verdict** against one of their own +requirements, and a comment. A *Met* verdict from the owning stakeholder +auto-advances that requirement to Satisfied — closing the cycle. Feedback +surfaces on the report for authors and on the requirement for analysts, and +rolls up into a writer-only **program maturity dashboard** of CTI-CMM-style +health indicators. diff --git a/website/zensical.toml b/website/zensical.toml new file mode 100644 index 0000000..aa3a2aa --- /dev/null +++ b/website/zensical.toml @@ -0,0 +1,128 @@ +# IcebergCTI documentation site — Zensical (Material-based) configuration. +# Styled to the shared Iceberg design system via docs/stylesheets/iceberg.css. + +[project] +site_name = "Iceberg" +site_description = "A cyber threat intelligence platform for collecting intelligence, authoring finished products, and disseminating them to stakeholders." +site_author = "IcebergAI" +site_url = "https://icebergai.github.io/IcebergCTI/" + +repo_url = "https://github.com/IcebergAI/IcebergCTI" +repo_name = "IcebergAI/IcebergCTI" + +copyright = """ +Copyright © 2026 IcebergAI · License +""" + +# Self-hosted fonts first, then the Iceberg token map + component overrides. +extra_css = [ + "stylesheets/fonts.css", + "stylesheets/iceberg.css", +] + +nav = [ + { "Home" = "index.md" }, + { "The intelligence cycle" = "workflow.md" }, + { "Deployment" = "deployment.md" }, + { "Security" = "security.md" }, + { "Integrations" = "integrations.md" }, + { "Changelog" = "changelog.md" }, +] + +# ---------------------------------------------------------------------------- +# Theme +# ---------------------------------------------------------------------------- +[project.theme] +name = "material" +logo = "assets/icebergai-mark.svg" # brand chip, reads on the dark header +favicon = "assets/favicon.png" +font = false # disable Google Fonts; we self-host (see fonts.css) + +features = [ + "announce.dismiss", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.follow", +] + +# Light / dark toggle mirroring the app's dual theme. primary/accent = "custom" +# so the actual colours come from the --md-* overrides in iceberg.css. +[[project.theme.palette]] +scheme = "default" +primary = "custom" +accent = "custom" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +primary = "custom" +accent = "custom" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/IcebergAI/IcebergCTI" + +# ---------------------------------------------------------------------------- +# Markdown extensions (carried over from the Zensical default template) +# ---------------------------------------------------------------------------- +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.arithmatex] +generic = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.magiclink] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +# Pull the repo-root CHANGELOG.md into changelog.md via `--8<-- "CHANGELOG.md"` +# (base_path reaches out of website/ to the repo root; check_paths fails the +# build if the file moves rather than silently emitting an empty page). +[project.markdown_extensions.pymdownx.snippets] +base_path = [".."] +check_paths = true +[project.markdown_extensions.pymdownx.superfences] +# NOTE: the mermaid fence is configured for parity with the sibling IcebergTTX/EBS +# sites, but no page currently uses one — the theme lazy-loads mermaid.min.js from +# **unpkg.com**, and every other asset on this site is self-hosted. Adding a +# ```mermaid fence silently reintroduces a third-party CDN dependency at view +# time; render the diagram as a table or an inline SVG instead. +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]