Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
# solid-meme
# Secure Data Destruction USB Orchestrator

This repository contains a bootable USB orchestrator for secure data destruction with forensic-grade evidence logging. It relies on trusted, external erasure utilities and focuses on orchestration, isolation, and reporting.

## Goals
- Bootable, locked-down Linux that auto-launches the orchestrator UI
- Per-batch collection isolation on a Windows-readable (exFAT) logs partition
- Hardware diagnostics captured as structured JSON before erasure
- Handoff to trusted erasure tools (e.g., `nwipe`, vendor SSD/NVMe utilities)
- Post-erase verification and immutable evidence records

## Project Layout
- `orchestrator/` – Python application modules (diagnostics, logging, UI shell)
- `schemas/` – JSON Schemas for manifests and machine records
- `docs/` – Build, boot, and operational guidance
- `system/` – Systemd units and autologin configuration snippets for the live OS

## Getting Started (development laptop)
1) Build a minimal Linux rootfs or reuse an existing live image (e.g., Debian live base).
2) Partition a 64GB USB stick:
- **Partition 1** (Linux, bootable): ext4, labeled `LINUX_OS`.
- **Partition 2** (logs): exFAT, labeled `EVIDENCE_LOGS`.
3) Copy this repo onto Partition 1 under `/opt/orchestrator`.
4) Install runtime dependencies into the live image:
- Python 3 (stdlib only), `tk`, `lsblk`, `lscpu`, `lspci`, `dmidecode`, `iproute2`
- Trusted erasure tools you plan to expose (e.g., `nwipe`, `nvme-cli`, `blkdiscard`)
5) Install the provided systemd service and getty override from `system/` (see `docs/BOOT.md`).
6) Boot from USB. The UI should launch fullscreen without offering a shell.

## Safety Constraints
- No custom overwrite algorithms; only orchestrates trusted tools.
- Never auto-wipes: operator must confirm each run.
- Refuses to proceed if the logs partition is missing or unwritable.
- Collection switching is explicit; writing outside the active collection is treated as a hard failure.
- No network assumed; all tooling must be present on the USB.

## Running the orchestrator (test harness)
```
python -m orchestrator.main 2024-05-ACME nwipe OperatorName
```
This mounts the `EVIDENCE_LOGS` partition, creates a new collection folder, displays detected disks and summary diagnostics, and waits for confirmation before launching the selected erasure tool.

## Documentation
- `docs/BOOT.md` – USB layout, boot configuration, autologin, and service wiring
- `docs/SCHEMAS.md` – JSON schema references and logging expectations
- `docs/WORKFLOW.md` – Operator flow, failure handling, and evidence artifacts

## Notes
- The UI is intentionally minimal but locked to fullscreen to avoid shell escape.
- Failure states are loud and block progress (missing tools, missing logs partition, detection failures).
32 changes: 32 additions & 0 deletions docs/BOOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Boot & Autostart Configuration

## USB Partition Layout (required)
- **Partition 1 (Linux OS)**: ext4, label `LINUX_OS`, bootable, contains minimal rootfs and orchestrator app under `/opt/orchestrator`.
- **Partition 2 (Logs)**: exFAT, label `EVIDENCE_LOGS`, Windows-readable. Only logs/evidence live here.

## Kernel & Bootloader
- Use a minimal, read-only oriented Linux (e.g., Debian live with overlay). Disable GRUB menu and boot straight to the default entry.
- Kernel params to consider: `quiet splash`, `net.ifnames=0`, `rd.systemd.show_status=0` for clean boot; disable network via `systemd` unit (see below).

## Autologin & Shell Lockdown
- Configure `[email protected]` override to auto-login a dedicated user (e.g., `operator`) without a shell prompt. See `system/[email protected]/override.conf`.
- The user's shell should be `/usr/sbin/nologin` or a wrapper that immediately starts the orchestrator.
- Disable TTY switching if possible (e.g., with `chvt` restrictions) and remove additional virtual consoles.

## Orchestrator Autostart
- Install `system/orchestrator.service` to `/etc/systemd/system/`.
- Enable at build time: `systemctl enable orchestrator.service`.
- Service runs `/usr/bin/python3 -m orchestrator.main <collection_id> <tool> [operator]`. For unattended boot, wrap this in `/usr/local/bin/orchestrator-launch` that prompts for collection/operator via Tk.

## Network Hardening
- Disable networking by default: mask `NetworkManager.service` or `systemd-networkd.service` unless explicitly needed.
- Remove wireless supplicants. No package managers should be exposed in the UI.

## Logs Partition Mount
- The service uses label `EVIDENCE_LOGS` and mounts it to `/mnt/evidence_logs` (configurable in `config.py`).
- If missing or unwritable, the UI will surface a blocking error and refuse to continue.

## Image Build Notes
- Bake all required binaries into the initramfs/rootfs: `python3`, `tk`, `lsblk`, `lscpu`, `lspci`, `dmidecode`, `ip`, `upower`, and the erasure tools you plan to support (`nwipe`, `nvme`, `blkdiscard`).
- Keep the OS read-only where practical; store transient state under `/run/orchestrator`.
- Verify `EVIDENCE_LOGS` survives reboots and is still readable on Windows.
10 changes: 10 additions & 0 deletions docs/SCHEMAS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# JSON Schemas

## Collection Manifest (`schemas/collection_manifest.schema.json`)
- Captures collection metadata and per-machine summary entries.
- Enforced on write to catch malformed updates.

## Machine Record (`schemas/machine_record.schema.json`)
- Immutable per-machine evidence file containing diagnostics, storage inventory, erasure tool used, timestamps, and result.

Both schemas are referenced in `orchestrator/config.py` for validation or offline tooling. They are kept alongside the application to avoid network lookups.
33 changes: 33 additions & 0 deletions docs/WORKFLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Operator Workflow

## Collection setup (once per batch)
1. Operator enters Collection ID (or accepts auto `YYYY-MM-DD_CLIENT` suggestion) and optional operator name.
2. Application mounts `EVIDENCE_LOGS` to `/mnt/evidence_logs` and creates:
- `Collections/<COLLECTION_ID>/manifest.json`
- `Collections/<COLLECTION_ID>/summary.csv`
- `Collections/<COLLECTION_ID>/machines/`
3. A lock file (`.collection.lock`) is placed to prevent accidental mixing.

If any step fails (partition missing, unwritable, existing collection), the UI blocks and surfaces the error.

## Per-machine flow
1. Detect storage devices via `lsblk -O -J`; render them for operator review.
2. Run diagnostics (CPU, GPU, RAM from `lscpu`/`lspci`, network MACs via `ip -json link`, system info via `dmidecode`, boot mode detection).
3. Store diagnostics in memory; show summarized view in UI.
4. Operator must click **Confirm erase**. No auto-wipe.
5. Orchestrator launches the selected trusted erasure tool (e.g., `nwipe`, `blkdiscard`, `nvme format`) with the chosen devices.
6. On return, post-erase verification probes each device for remaining filesystems/partitions.
7. A machine record JSON is written and `summary.csv` row appended. Results: `PASS`, `FAIL`, or `OPERATOR_ABORTED`.

## Evidence artifacts
- **`manifest.json`**: collection metadata and per-machine summary entries.
- **`machines/<ID>.json`**: detailed record with diagnostics, storage inventory, tool used, timestamps, result, optional operator/notes.
- **`summary.csv`**: quick view for client delivery.

## Failure handling (hard stops)
- Logs partition missing or not exFAT.
- Collection directory cannot be created or lock is missing (potential mixing risk).
- Required diagnostic binaries missing (`lsblk`, `lscpu`, `lspci`, `dmidecode`, `ip`).
- No internal storage detected.

Failures are both displayed and logged; the operator must resolve before proceeding.
1 change: 1 addition & 0 deletions orchestrator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Secure data destruction orchestrator package."""
110 changes: 110 additions & 0 deletions orchestrator/collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Collection isolation and lifecycle management."""
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from . import config
from .logger import CollectionLogger, SummaryWriter


class CollectionError(RuntimeError):
"""Raised when collection lifecycle fails."""


@dataclass
class CollectionContext:
collection_id: str
root: Path
manifest_path: Path
summary_path: Path
machines_dir: Path
lock_file: Path


class CollectionManager:
"""Creates and validates isolated collections within the logs partition."""

def __init__(self, logs_root: Path) -> None:
self.logs_root = logs_root

def _ensure_partition(self) -> Path:
if not self.logs_root.exists():
raise CollectionError("Logs partition is not mounted or missing")
if not self.logs_root.is_dir() or not (self.logs_root / config.COLLECTIONS_DIRNAME).parent.exists():
raise CollectionError("Logs location is invalid or not writable")
return self.logs_root

def create_collection(self, collection_id: str) -> CollectionContext:
base = self._ensure_partition() / config.COLLECTIONS_DIRNAME
base.mkdir(parents=True, exist_ok=True)
collection_dir = base / collection_id
if collection_dir.exists():
raise CollectionError(f"Collection '{collection_id}' already exists; refuse to mix data")
collection_dir.mkdir(parents=True, exist_ok=False)
machines_dir = collection_dir / config.MACHINES_DIRNAME
machines_dir.mkdir(parents=True, exist_ok=True)

manifest_path = collection_dir / config.MANIFEST_FILENAME
summary_path = collection_dir / config.SUMMARY_FILENAME
lock_file = collection_dir / config.LOCK_FILENAME
lock_file.write_text("active", encoding="utf-8")

# seed manifest
manifest = {
"collection_id": collection_id,
"version": config.APP_VERSION,
"machines": [],
}
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
summary_path.write_text("machine_id,device_count,result,start_ts,end_ts\n", encoding="utf-8")

return CollectionContext(
collection_id=collection_id,
root=collection_dir,
manifest_path=manifest_path,
summary_path=summary_path,
machines_dir=machines_dir,
lock_file=lock_file,
)

def load_collection(self, collection_id: str) -> CollectionContext:
base = self._ensure_partition() / config.COLLECTIONS_DIRNAME
collection_dir = base / collection_id
if not collection_dir.exists():
raise CollectionError(f"Collection '{collection_id}' not found")
if not (collection_dir / config.LOCK_FILENAME).exists():
raise CollectionError("Collection lock missing; abort to avoid mixing")

return CollectionContext(
collection_id=collection_id,
root=collection_dir,
manifest_path=collection_dir / config.MANIFEST_FILENAME,
summary_path=collection_dir / config.SUMMARY_FILENAME,
machines_dir=collection_dir / config.MACHINES_DIRNAME,
lock_file=collection_dir / config.LOCK_FILENAME,
)

def close_collection(self, context: CollectionContext) -> None:
if context.lock_file.exists():
context.lock_file.unlink()

def new_machine_logger(self, context: CollectionContext, machine_id: str) -> tuple[CollectionLogger, SummaryWriter]:
machine_json = context.machines_dir / f"{machine_id}.json"
manifest = context.manifest_path
summary = context.summary_path
return CollectionLogger(manifest, machine_json), SummaryWriter(summary)


def next_sequence(collection_dir: Path) -> str:
existing = sorted(collection_dir.glob("*.json"))
if not existing:
return "0001"
last = existing[-1].stem.split("_")[0]
try:
value = int(last)
except ValueError:
value = len(existing)
return f"{value + 1:04d}"
65 changes: 65 additions & 0 deletions orchestrator/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Configuration constants for the orchestrator."""
from __future__ import annotations

from pathlib import Path


# Partition labels
LOGS_PARTITION_LABEL = "EVIDENCE_LOGS"

# Directory structure within the logs partition
COLLECTIONS_DIRNAME = "Collections"
MANIFEST_FILENAME = "manifest.json"
SUMMARY_FILENAME = "summary.csv"
MACHINES_DIRNAME = "machines"
LOCK_FILENAME = ".collection.lock"

# UI defaults
UI_TITLE = "Secure Erasure Orchestrator"
UI_BG = "#101010"
UI_FG = "#e8e8e8"
UI_WARN = "#ff8800"
UI_ERROR = "#ff3b30"
UI_SUCCESS = "#45d483"
UI_FONT = ("Roboto", 14)

# Diagnostics commands (must exist in the rootfs)
REQUIRED_BINARIES = [
"lsblk",
"lscpu",
"lsusb",
"lspci",
"dmidecode",
]

# Paths used when running inside the live USB OS
MOUNT_BASE = Path("/mnt")
DEFAULT_LOGS_MOUNTPOINT = MOUNT_BASE / "evidence_logs"
DEFAULT_RUNTIME_STATE = Path("/run/orchestrator")

# Erasure tools we allow to invoke (must be installed separately)
SUPPORTED_ERASURE_TOOLS = {
"nwipe": {
"display": "nwipe (DoD 5220.22-M, verify)",
"command": ["/usr/bin/nwipe"],
"requires_tty": True,
},
"blkdiscard": {
"display": "blkdiscard (SSD/NVMe secure discard)",
"command": ["/usr/bin/blkdiscard", "--force"],
"requires_tty": False,
},
"nvme_format": {
"display": "nvme format (secure erase)",
"command": ["/usr/bin/nvme", "format", "--ses=1"],
"requires_tty": False,
},
}

# JSON schema locations (relative to project root)
SCHEMAS_ROOT = Path(__file__).resolve().parent.parent / "schemas"
COLLECTION_MANIFEST_SCHEMA = SCHEMAS_ROOT / "collection_manifest.schema.json"
MACHINE_RECORD_SCHEMA = SCHEMAS_ROOT / "machine_record.schema.json"

# Misc
APP_VERSION = "0.1.0"
Loading