# Target VM build specs (PIPELINE.md §4.2 / §4.13)

Every Tier-3 module in `manifest.toml` `[catalog]` MUST land its session
against a target VM that this directory defines. Targets are NOT
fetched from third-party blob stores (no Metasploitable2 from
SourceForge — that was the §3 evidence); they are built locally from
declarative specs, sha256-pinned, and re-verified at every release.

## Layout

```
vm/targets/<name>/
├── spec.toml      ← what this target promises (verified at load time)
├── build.sh       ← declarative build steps; produces $OUT_PATH
└── verify.sh      ← boots the produced image, asserts every promise
```

## Adding a target

1. Create `vm/targets/<name>/`.

2. Write `spec.toml`. Every field is required; containment fields all
   have ONE safe value (no knobs):

   ```toml
   name = "<name>"
   description = "<short prose>"
   base_image = "<e.g. alpine-3.21-virt>"

   [promises]
   cve = "CVE-YYYY-NNNN"
   service_name = "samba"           # what the module catalog talks to
   service_port = 445
   service_proto = "tcp"
   vulnerable_software = "samba"    # the actual vulnerable component
   vulnerable_version = "3.0.20"

   [containment]
   upstream_egress = false           # MUST
   shared_filesystem = false         # MUST
   unprivileged_qemu = true          # MUST
   fresh_snapshot_per_episode = true # MUST
   ```

3. Write `build.sh`. The orchestrator invokes it with
   `OUT_PATH=<staging>.qcow2` and `BASE_IMAGE_NAME=<base>`. The script
   should produce a valid qcow2 at `$OUT_PATH` and exit 0.

4. Write `verify.sh`. The orchestrator invokes it with
   `IMAGE_PATH=<staging>.qcow2` and `EXPECTED_*` env vars matching
   spec.promises. Boot the image in a containment-correct
   configuration (see "Verification harness" below), wait for the
   service to come up, assert the promised port + version. Exit 0
   only if every promise verifies.

5. Run the build:

   ```sh
   sudo python tools/build_target.py <name>
   ```

   On success the script prints the sha256 + the manifest.toml
   stanza to add. Build artifacts go to `/var/lib/cis490/vm/images/`
   by default.

6. Operator amends `manifest.toml`:

   ```toml
   [[targets.images]]
   image_name = "<name>"
   sha256 = "<from build_target output>"
   build_script = "vm/targets/<name>/build.sh"
   ```

   This is a substantive amendment per §16 — operator sign-off
   required. Lands in the same merge as any modules that depend on
   the target.

## Verification harness

`verify.sh` MUST boot the image with the §4.13 containment posture:

  * `-netdev user,...,restrict=on` — no upstream egress
  * No `-virtfs` / `-fsdev` / `-9pfs` host-shared mounts
  * Run QEMU as the unprivileged service user (no `sudo qemu-system-*`)
  * `snapshot=on` so the build artifact isn't mutated by verification

A `tests/test_containment.py` regression asserts every spec on disk
declares the correct containment posture. A spec that asserts
weakened containment is a containment regression and `load_target_spec`
rejects it before build_target.py even invokes build.sh.

## Why this exists

Targets we don't build, we don't trust. PIPELINE.md §3 surfaced 0/67
session_open against the SourceForge Metasploitable2 image — and we
couldn't even tell whether that was a payload bug, a hostfwd bug, a
SLIRP timing race, or just the image being modified somewhere along
the supply chain. With locally-built declarative targets:

  * The vulnerable service is verified up at the promised port +
    version BEFORE the image is admitted.
  * The image's sha256 is recorded in manifest.toml; tampering is
    visible.
  * Build is reproducible: same spec.toml + same build.sh on a fresh
    base produces the same image.

This is non-negotiable per §4.2 / §4.3. Tier-3 modules that target
unverified images stay out of the catalog.
