# Donjon Intel Pipeline — Scheduler (deploy-ready, NOT deployed)

Turns the founder-manual "build a bundle when I remember" step into a
repeatable, signed, monitored system running on **CT100** (factory-core,
192.0.2.110), publishing to the single release channel
(**releases.donjonsec.com**, served from the OCI Always Free host
129.213.159.23).

```
update-intel (refresh DBs)  ->  snapshot (manifest)  ->  sign (Ed25519+ML-DSA-65)  ->  publish (OCI)
        |                            |                          |                         |
   bin/update-intel.py        bin/intel-pipeline.py      keys/donjon-private-*     scp/rclone to OCI
        --all                       build                  (mounted at runtime)     (publish-prep stages it)
```

The product verifies the signed manifest on load
(`lib/intel_snapshot.py` -> `VulnDatabase.get_snapshot_status()`), enforcing
the freshness SLA (snapshot <= 7d; KEV <= 48h; NVD <= 24h). Missing / stale /
tampered intel is reported `trusted=false, stale=true` — never silently green.

---

## Cadence

| When | What | Why |
|------|------|-----|
| Daily 03:17 UTC | `update-intel --all --snapshot --sign` | NVD<=24h SLA needs a daily refresh |
| On failure | systemd `OnFailure=` -> notify unit | a stale feed is a product-quality incident |

A weekly-only cadence cannot satisfy the NVD<=24h / KEV<=48h thresholds the
product enforces, so this runs **daily**. The published bundle is what
air-gapped customers pull weekly; the daily run keeps the connected snapshot
current and the signature fresh.

---

## Files in this directory

| File | Purpose |
|------|---------|
| `donjon-intel-pipeline.service` | oneshot unit: update -> snapshot -> sign |
| `donjon-intel-pipeline.timer`   | daily schedule with randomized delay |
| `donjon-intel-pipeline-notify@.service` | `OnFailure` hook (logs + optional webhook) |
| `intel-pipeline.cron`           | cron fallback for hosts without systemd |
| `run-pipeline.sh`               | the wrapped command (keys, logging, exit codes) |

---

## Install on CT100 (requires Cris — gated)

These steps need credentials / a privileged shell and are **NOT** run by the
build agent (see HARD RULES). Run on CT100:

```bash
# 1. Mount the private signing keys read-only (canonical: /opt/donjonsec-keys)
#    NEVER generate new keys — that orphans every paid license.
export DONJON_INTEL_CLASSICAL_KEY=/opt/donjonsec-keys/donjon-private-classical.pem
export DONJON_INTEL_PQC_KEY=/opt/donjonsec-keys/donjon-private-pqc.bin

# 2. Copy units into place and template the paths
sudo cp donjon-intel-pipeline.* /etc/systemd/system/
sudo cp donjon-intel-pipeline-notify@.service /etc/systemd/system/

# 3. Drop run-pipeline.sh where the unit expects it
sudo install -m 0750 run-pipeline.sh /opt/darkfactory/bin/run-intel-pipeline.sh

# 4. Provide the signing keys to the unit WITHOUT plaintext on disk in the unit:
#    use a systemd EnvironmentFile with 0400 perms, or a credentials mount.
sudo install -m 0400 /dev/stdin /etc/donjon/intel-pipeline.env <<EOF
DONJON_INTEL_CLASSICAL_KEY=/opt/donjonsec-keys/donjon-private-classical.pem
DONJON_INTEL_PQC_KEY=/opt/donjonsec-keys/donjon-private-pqc.bin
DONJON_HOME=/opt/donjon-platform
NVD_API_KEY=...        # from reference_nvd_api_key
EOF

# 5. Enable
sudo systemctl daemon-reload
sudo systemctl enable --now donjon-intel-pipeline.timer
systemctl list-timers donjon-intel-pipeline.timer
```

## Monitoring

```bash
systemctl status donjon-intel-pipeline.service
journalctl -u donjon-intel-pipeline.service --since today
# Product-side health (what customers see):
python bin/update-intel.py --check      # exit 1 if stale
python bin/intel-pipeline.py verify --manifest data/intel-snapshot-manifest.json --data-dir data
```

`--check` and the product's `get_snapshot_status()` read the SAME thresholds,
so the dashboard and the cron alert tell one story.

## What still needs Cris (gated production steps)

1. Mount the **real** private keys on CT100 (`/opt/donjonsec-keys`) and the
   `EnvironmentFile` — no key material is committed to the repo.
2. Stand up the **publish target** on OCI (`releases.donjonsec.com/intel/`)
   and the upload credential (scp/rclone/Forgejo release token).
3. `systemctl enable --now` the timer (live scheduler enablement).
4. Wire the `OnFailure` notify hook to the real alert channel.
