# Donjon Platform v7.0 - Air-Gap Deployment Guide

Deploy Donjon on classified networks, SCIF environments, OT/ICS systems, and any network without internet connectivity. The platform is designed to operate fully offline with no degradation in scanning, compliance mapping, or reporting capabilities.

---

## Overview

Air-gapped deployment follows a three-phase process:

1. **Prepare** on a connected machine (download intel, create bundles)
2. **Transfer** via USB drive or data diode
3. **Deploy** on the air-gapped target

```
Connected Machine          Transfer Media          Air-Gapped Network
+------------------+       +-----------+       +--------------------+
| update-intel.py  | ----> |           | ----> | copy data/ DBs     |
|  populates the   |       | USB Drive |       |   into place       |
|  data/ intel DBs |       | or Diode  |       | donjon-launcher    |
+------------------+       +-----------+       +--------------------+
```

---

## Phase 1: Pre-Deployment (Connected Machine)

Perform these steps on a machine with internet access. This only needs to happen once for initial deployment, then periodically for intel updates.

### Step 1: Install the Platform

```bash
# Linux
git clone https://github.com/DonjonSec/donjon-platform.git
cd donjon-platform
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Windows
git clone https://github.com/DonjonSec/donjon-platform.git
cd donjon-platform
python -m venv venv
.\venv\Scripts\activate
pip install -r requirements.txt
```

### Step 2: Download Threat Intelligence

Download all 7 intelligence sources (803,000+ entries):

```bash
# Full download with NVD API key (recommended, ~30-60 minutes)
NVD_API_KEY=your-key python3 bin/update-intel.py --full

# Or without API key (~2-4 hours)
python3 bin/update-intel.py --full
```

This populates:

| Database | Contents | Typical Size |
|---|---|---|
| `data/vuln_intel.db` | 318,000+ NVD CVEs with CVSS scores | ~500 MB |
| `data/kev_catalog.json` | 1,500+ CISA Known Exploited Vulnerabilities | ~2 MB |
| `data/epss_cache.db` | 314,000+ EPSS exploit probability scores | ~50 MB |
| `data/intel_feeds.db` | ExploitDB, OSV, GitHub Advisories, MITRE ATT&CK, abuse.ch | ~200 MB |

### Step 3: Verify Intel Completeness

```bash
python3 bin/update-intel.py --status
```

Expected output:

```
  CISA KEV:
    Entries: 1,513
    Stale: False

  EPSS:
    Cached scores: 314,949
    Stale: False

  NVD (vuln_intel.db):
    CVEs cached: 318,225
    DB size: 487.3 MB

  Intel Feeds (intel_feeds.db):
    exploitdb: 30,215
    osv_vulns: 45,000+
    github_advisories: 12,000+
    cisa_alerts: 850+
    mitre_attack: 1,200+
    threat_indicators: 41,000+
```

### Step 4: Stage the Intel for Transfer

`update-intel.py` writes all intelligence into SQLite databases under `data/`. **These files are the intel payload** — you transfer them directly. There is no separate "bundle then import" step; the databases simply travel inside `data/` when you copy the platform directory (Phase 1 Step 5).

| File | Contents |
|---|---|
| `data/vuln_db/vuln_intel.db` | NVD CVEs with CVSS scores (the large file, ~600 MB+) |
| `data/intel_feeds.db` | CISA KEV, EPSS, ExploitDB, OSV, GitHub Advisories, MITRE ATT&CK, abuse.ch |

Record a SHA-256 of each database so you can verify integrity after transfer:

```bash
# Linux
sha256sum data/vuln_db/vuln_intel.db data/intel_feeds.db

# Windows (PowerShell)
Get-FileHash data\vuln_db\vuln_intel.db, data\intel_feeds.db -Algorithm SHA256
```

**Record both hashes.** You will use them to verify integrity after transfer.

> Optional convenience — `bin/bundle-intel.py` only packs *loose* JSON/JSONL feed exports found under `data/intel/` into a single `intel.jsonl` tarball; it does **not** contain the databases above. Real syntax is a positional mode + output path (no subcommands, no flags):
> `python3 bin/bundle-intel.py full donjon-intel-feeds.tar.gz` (modes: `full`, `incremental`, `minimal`). It prints `{"bundle_path": "...", "record_count": N}`. To use it on the target, extract the tarball back into `data/intel/` — there is no `import` command.

### Step 5: Prepare the USB Drive

```bash
# Linux
DEST=/media/USB_DRIVE/donjon
mkdir -p $DEST
cp -r donjon-platform/ $DEST/donjon-platform/   # includes data/ with the intel DBs

# Windows
set DEST=E:\donjon
mkdir %DEST%
xcopy /E /I donjon-platform\ %DEST%\donjon-platform\
```

Copying the platform directory carries the intel databases under `data/` with it. USB drive contents:

```
USB_DRIVE/
  donjon/
    donjon-platform/                   # Full platform (code, config, tools)
      data/vuln_db/vuln_intel.db       #   NVD CVEs (rides along in data/)
      data/intel_feeds.db              #   KEV / EPSS / ExploitDB / OSV / ...
```

---

## Phase 2: Transfer

### Standard USB Transfer

1. Physically transport the USB drive to the air-gapped environment
2. Follow your organization's media sanitization and import procedures
3. Scan the USB with the air-gapped network's malware scanning tools per policy

### Data Diode Transfer

For one-way data diode environments:

1. Place the `donjon-intel-bundle.tar.gz` and `donjon-platform/` directory in the diode's outbound queue
2. Verify transfer completion on the receiving side
3. The platform directory can be transferred as a tar archive for easier handling:

```bash
# On connected side
tar czf donjon-platform-full.tar.gz donjon-platform/

# On air-gapped side after transfer
tar xzf donjon-platform-full.tar.gz
```

---

## Phase 3: Deploy on Air-Gapped Target

### Step 1: Verify Transfer Integrity

```bash
# Linux
sha256sum /media/USB_DRIVE/donjon/donjon-platform/data/vuln_db/vuln_intel.db \
          /media/USB_DRIVE/donjon/donjon-platform/data/intel_feeds.db

# Windows (PowerShell)
Get-FileHash E:\donjon\donjon-platform\data\vuln_db\vuln_intel.db, E:\donjon\donjon-platform\data\intel_feeds.db -Algorithm SHA256
```

Compare each hash against the hashes recorded during Phase 1 Step 4. If they do not match, do not proceed -- a database may be corrupted or tampered with.

### Step 2: Copy to Target System

```bash
# Linux
cp -r /media/USB_DRIVE/donjon/donjon-platform/ /opt/donjon/

# Windows
xcopy /E /I E:\donjon\donjon-platform\ C:\donjon\donjon-platform\
```

This brings the intel databases (`data/vuln_db/vuln_intel.db`, `data/intel_feeds.db`) onto the target along with the platform.

### Step 3: Install Python Dependencies (Offline)

If the air-gapped system does not have pip packages pre-installed, include them on the USB:

**On the connected machine (preparation):**

```bash
# Download wheels for offline install
pip download -r requirements.txt -d /media/USB_DRIVE/donjon/wheels/
```

**On the air-gapped system:**

```bash
# Linux
cd /opt/donjon/donjon-platform
python3 -m venv venv
source venv/bin/activate
pip install --no-index --find-links=/media/USB_DRIVE/donjon/wheels/ -r requirements.txt

# Windows
cd C:\donjon\donjon-platform
python -m venv venv
.\venv\Scripts\activate
pip install --no-index --find-links=E:\donjon\wheels\ -r requirements.txt
```

### Step 4: Verify the Intel Is in Place

There is **no import command.** The intel databases were copied into `data/` with the platform in Step 2, and the platform reads them in place. Confirm they are present and populated:

```bash
# Linux
cd /opt/donjon/donjon-platform
python3 bin/update-intel.py --status

# Windows
cd C:\donjon\donjon-platform
python bin\update-intel.py --status
```

You should see non-zero CVE / KEV / EPSS / feed counts matching what you recorded in Phase 1 Step 3. If the databases are missing (e.g. you transferred the code without its `data/` directory), copy them in manually and re-run `--status`:

```bash
# Linux
mkdir -p data/vuln_db
cp /media/USB_DRIVE/donjon/donjon-platform/data/vuln_db/vuln_intel.db data/vuln_db/
cp /media/USB_DRIVE/donjon/donjon-platform/data/intel_feeds.db data/

# Windows
xcopy E:\donjon\donjon-platform\data\vuln_db\vuln_intel.db data\vuln_db\
copy E:\donjon\donjon-platform\data\intel_feeds.db data\
```

### Step 5: Run Your First Scan

```bash
# Linux
python3 bin/donjon-launcher quick

# Windows
python bin\donjon-launcher quick
```

The platform automatically detects air-gap mode (no internet connectivity) and:

- Disables cloud scanning (AWS/Azure/GCP)
- Defaults AI to the template backend (works offline, no LLM required). For local LLM analysis, point Donjon at an in-enclave model and set `DONJON_OFFLINE=1` -- see [AI Backend in Air-Gap Mode](#ai-backend-in-air-gap-mode) below
- Uses local SQLite for all data storage
- Relies on the imported intel bundle for CVE/KEV/EPSS data

---

## Updating Intel Periodically

Threat intelligence goes stale. Establish a regular update cadence:

| Frequency | Use Case |
|---|---|
| Weekly | High-security environments, active threat hunting |
| Monthly | Standard compliance environments |
| Quarterly | Stable environments with low change rate |

### Update Procedure

**On the connected machine:**

```bash
# Refresh all sources in place (NVD + KEV + EPSS + feeds)
python3 bin/update-intel.py --all
```

This merges new data into the databases under `data/` on the connected machine.

**Transfer to the air-gapped system** following the same USB/diode process: copy the refreshed `data/vuln_db/vuln_intel.db` and `data/intel_feeds.db` over the existing files on the target, then re-run `python3 bin/update-intel.py --status` to confirm. Replacing the database files updates the target to the new intel snapshot.

### Checking Freshness

Run this on the air-gapped system to check if intel needs updating:

```bash
python3 bin/update-intel.py --check
```

Exit code 0 means all data is fresh. Exit code 1 means at least one source is stale. Thresholds: NVD >24h, KEV >48h, feeds >72h.

---

## Security Considerations

### Transfer Media

- Use dedicated, encrypted USB drives for Donjon transfers
- Sanitize USB drives before and after each transfer per your organization's policy
- Label drives clearly with classification level and contents
- Maintain a chain-of-custody log for media entering classified environments

### Platform Security

- The platform stores scan results and evidence in `data/evidence/evidence.db` -- this database contains sensitive vulnerability information about your network. Protect it accordingly
- In portable/USB mode, no credentials are written to the drive (cloud credentials, API keys are held in memory only)
- The license file (`data/license.json`) is cryptographically signed with dual ML-DSA-65 (post-quantum) + Ed25519 signatures. Tampering with the license file causes validation failure
- All scanning requires explicit user initiation -- there are no automatic outbound connections

### Data Classification

Consider the classification level of scan results. Donjon findings reveal:

- Network topology and open services
- Software versions and known vulnerabilities
- Configuration weaknesses
- Compliance posture gaps

Treat exported reports at the same classification level as the network being scanned.

### Integrity Verification

Verify platform integrity after transfer:

```bash
# Generate integrity manifest (first run on trusted copy)
python3 -c "from lib.integrity import generate_manifest; generate_manifest(['lib.licensing', 'lib.compliance', 'lib.evidence', 'scanners.base'])"

# Verify on air-gapped system (compares module hashes against manifest)
python3 -c "from lib.integrity import verify_manifest; verify_manifest()"
```

### AI Backend in Air-Gap Mode

AI analysis runs **fully local** in an air-gapped deployment. Donjon never ships or hosts a model and never phones home for inference -- you point it at a model running on your own hardware, inside the enclave. AI analysis is a Pro feature; Community Edition uses the `template` backend.

The default `template` AI backend works fully offline using Python-based analysis templates. No LLM is required and it is always available. For LLM-powered analysis inside the air gap, point Donjon at a local model server.

#### Enforce offline mode: `DONJON_OFFLINE=1`

Set the `DONJON_OFFLINE` environment variable on the air-gapped host. This is a hard guard, enforced in `lib/ai_engine.py`, that makes public network egress impossible:

```bash
# Linux/macOS
export DONJON_OFFLINE=1

# Windows (PowerShell)
$env:DONJON_OFFLINE = "1"
```

When `DONJON_OFFLINE` is set:

- **All cloud backends are blocked** (Anthropic, OpenAI, Gemini, StepFun, Bedrock, Mistral, Cohere, xAI, DeepSeek, Together, Groq, Fireworks, Azure, OpenRouter). They are not offered by the setup wizard or the dashboard, and any attempt to use one falls back to `template`.
- **Local backends (`ollama`, `custom`, `generic`) are allowed only if their endpoint is loopback or RFC1918-private** (`localhost`, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, or a `.local`/`.internal`/`.lan`/`.home`/`.corp`/`.intra` host). A public hostname or IP is refused -- the check is DNS-free, so the host never even resolves an external name.
- **`_call_ai` refuses a remote backend** even if one were somehow configured: defence in depth, no silent egress.

The platform also auto-detects air-gap operation (no internet) at launch and defaults AI to `template`; `DONJON_OFFLINE=1` makes that guarantee explicit and unconditional.

#### Option A -- Local Ollama (recommended)

1. Install Ollama on the air-gapped system (the installer does not need internet).
2. Transfer model weights via USB. Either copy `~/.ollama/models` from a connected machine, or run `python bin/setup-ai.py` on a connected machine to pull models, then copy that directory to the target.
3. Configure Donjon to use the local Ollama instance:

```yaml
# config/active/config.yaml
ai:
  provider: 'ollama'
  ollama_url: 'http://localhost:11434'
  model: 'qwen2.5-coder:14b'
  sanitize_external: false     # not needed for a local LLM
```

Or run the air-gap-aware wizard (it offers only local backends when `DONJON_OFFLINE=1`):

```bash
DONJON_OFFLINE=1 python bin/setup-ai.py --wizard
```

#### Option B -- llama.cpp / vLLM / LM Studio via the `custom` backend

Any server that exposes an **OpenAI-compatible** `/v1/chat/completions` endpoint works with the `custom` backend -- `llama.cpp` (`llama-server`), `vLLM` (`--api`), `LM Studio` (local server), `LiteLLM`, or a private gateway:

```yaml
ai:
  provider: 'custom'
  api_url: 'http://127.0.0.1:8000/v1/chat/completions'   # must be loopback/private
  # api_key: ''         # only if your local server enforces one
  model: 'your-local-model'
  sanitize_external: false
```

```bash
# Non-interactive equivalent
DONJON_OFFLINE=1 python bin/setup-ai.py --backend custom \
    --api-url http://127.0.0.1:8000/v1/chat/completions --model-name your-local-model
```

#### Option C -- Any other HTTP model server via the `generic` backend

If your local inference server is **not** OpenAI-compatible, use the `generic` universal adapter. You supply a `request_template` (JSON with a `{prompt}` placeholder, optional `{system}`) and a `response_path` that extracts the answer:

```yaml
ai:
  provider: 'generic'
  api_url: 'http://127.0.0.1:9000/generate'   # must be loopback/private
  request_template: '{"inputs": "{prompt}", "parameters": {"max_new_tokens": 2048}}'
  response_path: 'generated_text'              # or e.g. choices[0].message.content
  # headers: { 'X-Api-Key': 'token' }          # optional
  sanitize_external: false
```

```bash
# Non-interactive equivalent (e.g. a Text-Generation-Inference server)
DONJON_OFFLINE=1 python bin/setup-ai.py --backend generic \
    --api-url http://127.0.0.1:9000/generate \
    --request-template '{"inputs": "{prompt}"}' \
    --response-path generated_text
```

> In `DONJON_OFFLINE` mode the wizard and the engine both refuse any `api_url` that is not loopback/private, so a misconfiguration cannot cause data to leave the enclave.

---

## Quick Reference

| Task | Command |
|---|---|
| Download all intel | `python3 bin/update-intel.py --full` |
| Transfer intel (offline) | Copy `data/vuln_db/vuln_intel.db` + `data/intel_feeds.db` into the target's `data/` |
| Verify intel in place | `python3 bin/update-intel.py --status` |
| Pack loose feed files (optional) | `python3 bin/bundle-intel.py full OUTPUT.tar.gz` (modes: `full`/`incremental`/`minimal`) |
| Check intel freshness | `python3 bin/update-intel.py --check` |
| Show intel status | `python3 bin/update-intel.py --status` |
| Quick scan | `python3 bin/donjon-launcher quick` |
| Start web dashboard | `python3 bin/start-server.py` |
| Export results | `python3 bin/donjon-launcher` > Compliance & Reports > Export |

---

## Troubleshooting

**"NVD cache is empty" after import:**
The intel bundle contains feed data but NVD CVE data is stored in `data/vuln_intel.db`. Ensure this file was included on the USB drive alongside the bundle. The NVD database is a separate SQLite file, not part of the tar.gz bundle.

**"Module not found" errors:**
Python dependencies were not installed. Follow Step 3 in Phase 3 to install from offline wheels.

**Scan finds no hosts:**
Ensure nmap is installed on the air-gapped system. Without nmap, the platform uses TCP connect scanning which may miss hosts behind firewalls. Install nmap from your organization's approved software repository.

**Low vulnerability detection count:**
Ensure `data/intel_feeds.db` was copied to the target alongside `data/vuln_db/vuln_intel.db`. Without the feeds database the platform has CVE data but lacks exploit cross-references from ExploitDB, OSV, and the other intel feeds.
