Wraps the gaps surfaced in the "what is not implemented" audit so the
fleet really is shippable end-to-end. Verified live on the Pi:
- cis490-shipper --ping → HTTP 200 through Caddy + mTLS via the
new wg-pki client CA leaf
- real episode dir → tar+zstd → PUT → HTTP 201 stored
- re-ship same bytes → 200 (idempotent)
- re-ship different bytes under same id → 409 (conflict)
Changes:
orchestrator/episode.py
- EpisodeConfig.revert_at_start / revert_at_end (Tier 0+ snapshot/
revert per docs/architecture.md). When set + qmp_socket present,
EpisodeRunner issues loadvm <snapshot_name> and emits
snapshot_revert / snapshot_revert_failed events on the same
monotonic clock as everything else.
collectors/qmp.py
- savevm() / loadvm() helpers using human-monitor-command, plus a
test against the fake QMP server.
exploits/workloads.py
- chunked_real_binary_upload() returns a ChunkedUpload plan: 8 KiB
base64 chunks (~6 KiB binary each) so msfrpc never sees a buffer-
busting payload. Includes a finalize step that sha256-verifies on
the guest before exec.
- real_binary_workload() now wraps the chunked plan for backwards
compat with single-shot callers.
exploits/driver.py
- Tier-4 dispatch walks the chunked plan in MSFExploitDriver:
each chunk is a separate session_shell_write; finalize verifies;
exec only runs on sha-ok. New events: real_binary_upload_begin,
real_binary_verify, real_binary_aborted.
etc/cis490-orchestrator.service
- Reads /etc/cis490/lab-host.env (FLEET_HOST_ID + optional BRIDGE).
- Grants AmbientCapabilities CAP_NET_RAW (tcpdump for source 4) +
CAP_SYS_ADMIN + CAP_PERFMON (perf for source 3) so collectors
work under hardening.
scripts/install-lab-host.sh
- Writes /etc/cis490/lab-host.env on first install with FLEET_HOST_ID
defaulting to `hostname -s`.
- Best-effort: fetches the Alpine baseline qcow2 (sha512-pinned) and
builds cidata.iso with the in-guest agent embedded; symlinks both
into /opt/cis490/vm/images/ so launchers find them.
scripts/fetch-alpine-baseline.sh
- Idempotent fetcher for the Alpine 3.21 cloud-init nocloud qcow2
matching the sha512 in docs/sources.md.
tools/plot_envelope.py
- Rebuilt to render whatever telemetry the episode dir contains:
proc → QMP block ops → perf IPC/miss-rate → bridge pkts/SYNs →
guest agent load/mem. Missing sources are silently skipped.
tools/index_reader.py
- cis490-index CLI: filter receiver's index.jsonl by host / sample
/ time range, sort, count-by group. Closest thing to a query
interface until we stand up Postgres/Timescale.
samples/README.md
- Rewritten to match the new manifest schema, the kind=real vs mimic
split, the per-(host, slot, ep) selection mechanic, and the
chunked-upload safety story.
Tests: 106 pass (was 102). New cases:
- test_qmp.py — savevm + loadvm (HMP wrapper + error path)
- test_tier4.py — chunked plan splitting, sha-pinned finalize,
end-to-end driver walks all chunks + verify + exec via the fake
msfrpc client
Closes the "what is not implemented" punch list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 lines
1.7 KiB
Bash
Executable file
62 lines
1.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Fetch the Alpine 3.21 NoCloud cloud-init image used as the Tier-1/2
|
|
# baseline guest. Convert to qcow2 if necessary; verify sha512 against
|
|
# the value pinned in docs/sources.md.
|
|
#
|
|
# Usage:
|
|
# scripts/fetch-alpine-baseline.sh <out_path>
|
|
#
|
|
# Examples:
|
|
# scripts/fetch-alpine-baseline.sh vm/images/alpine-baseline.qcow2
|
|
# sudo scripts/fetch-alpine-baseline.sh /var/lib/cis490/vm/images/alpine-baseline.qcow2
|
|
#
|
|
# Idempotent — re-runs check the destination and short-circuit if the
|
|
# checksum already matches.
|
|
|
|
set -euo pipefail
|
|
|
|
OUT="${1:-}"
|
|
if [[ -z "$OUT" ]]; then
|
|
echo "usage: $0 <out_path>" >&2
|
|
exit 2
|
|
fi
|
|
|
|
URL="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/cloud/nocloud_alpine-3.21.0-x86_64-bios-cloudinit-r0.qcow2"
|
|
SHA512="bb509092cda3548c11bc48a2168ce950d654b50db006e98939c06a5d86487f4e53cbb7954fafbba9ab5c8098008a9f304421ffc3397b0bc1d87b6aa309239b98"
|
|
|
|
log() { printf '[fetch-alpine] %s\n' "$*" >&2; }
|
|
|
|
if [[ -f "$OUT" ]]; then
|
|
actual="$(sha512sum "$OUT" | awk '{print $1}')"
|
|
if [[ "$actual" == "$SHA512" ]]; then
|
|
log "$OUT already present and verified"
|
|
exit 0
|
|
fi
|
|
log "$OUT exists but checksum differs — refetching"
|
|
rm -f "$OUT"
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$OUT")"
|
|
TMP="$OUT.partial"
|
|
trap 'rm -f "$TMP"' EXIT
|
|
|
|
log "downloading $URL"
|
|
if command -v curl >/dev/null; then
|
|
curl -fL --retry 3 --retry-delay 5 -o "$TMP" "$URL"
|
|
elif command -v wget >/dev/null; then
|
|
wget -O "$TMP" "$URL"
|
|
else
|
|
log "neither curl nor wget on PATH"
|
|
exit 1
|
|
fi
|
|
|
|
log "verifying sha512"
|
|
actual="$(sha512sum "$TMP" | awk '{print $1}')"
|
|
if [[ "$actual" != "$SHA512" ]]; then
|
|
log "sha512 mismatch: expected $SHA512, got $actual"
|
|
exit 1
|
|
fi
|
|
|
|
mv "$TMP" "$OUT"
|
|
trap - EXIT
|
|
log "wrote $OUT ($(stat -c%s "$OUT") bytes)"
|