CIS490/tools/build_cidata.py
Maximus Gorog 7216ec09bd Tier 2: real Alpine VM, real workload, real envelope
End-to-end now drives a real KVM guest through the full XMRig-shaped
phase schedule with the workload running INSIDE the guest. Telemetry is
host-side /proc/<qemu_pid>; the load is busybox `yes` (sustained CPU
saturation) and `dd if=/dev/urandom` (disk burst on infecting), driven
over the serial console at every phase transition. The plotted envelope
shows clean idle → armed → infecting (disk spike) → infected_running
(100% CPU plateau) → dormant → re-entry → final clean.

Components:

  vm/launch_demo.sh              now boots Alpine 3.21 nocloud-cloudinit
                                 (Cirros 0.6.x's cirros-init blocks on the
                                 EC2 metadata service for ~17 min before
                                 falling through to NoCloud — abandoned).
                                 Mounts a cidata ISO as a second drive.

  tools/build_cidata.py          pure-Python NoCloud ISO builder (pycdlib).
                                 Sets root password and ssh_pwauth via
                                 runcmd so we don't depend on a specific
                                 cloud-init version's plain_text_passwd
                                 handling.

  tools/vm_serial.py             serial-console client (stdlib socket).
                                 Idempotent login (detects already-in-shell
                                 state), sentinel-bracketed run() that
                                 distinguishes shell output from the TTY
                                 echo of input by requiring a leading
                                 \r\n boundary on the marker.

  tools/vm_load_controller.py    in-guest load controller. set_phase()
                                 dispatches the per-phase shell command
                                 over the serial connection.

  tools/run_real_vm_demo.py      ties it all together: boot VM, wait for
                                 cloud-init runcmd, log in, run the
                                 EpisodeRunner with on_phase=controller,
                                 shut down VM.

Deps: paramiko, pycdlib added.

docs/sources.md updated with Alpine cloud image (sha512 pinned), and
the new Python deps.

README leads with the tier-2 plot now (real VM, real workload). The
previous synthetic plot is moved below with explicit "host-side mimic,
not a VM" labelling. Tier-2 status flipped to  in the tier table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:38:53 -06:00

112 lines
2.9 KiB
Python

"""Build a NoCloud cidata ISO for cloud-init.
Cirros 0.6.x — and most cloud images — look for a NoCloud datasource at
boot: an ISO9660 volume labeled ``cidata`` containing two files,
``user-data`` and ``meta-data``. We attach it as a second drive so
cloud-init proceeds without spending ~17 minutes timing out trying to
reach a non-existent metadata service.
This script is intentionally self-contained and uses only pycdlib (pure
Python) — no system mkisofs/xorriso/cloud-localds dependency.
Usage:
uv run python tools/build_cidata.py vm/images/cidata.iso
The defaults bake in the ``cirros`` user with the documented Cirros
password, enable SSH password auth (so future Metasploit-class images
work without changes), and set a hostname. Override via flags if needed.
"""
from __future__ import annotations
import argparse
import io
import sys
from pathlib import Path
import pycdlib
DEFAULT_USER_DATA = """\
#cloud-config
hostname: cis490
manage_etc_hosts: true
users:
- name: cis490
plain_text_passwd: cis490
lock_passwd: false
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/sh
ssh_pwauth: true
disable_root: false
chpasswd:
expire: false
list: |
root:cis490
cis490:cis490
runcmd:
- [ sh, -c, "echo CIS490_BOOT_OK > /tmp/.cis490-boot" ]
"""
DEFAULT_META_DATA = """\
instance-id: cis490-vm-001
local-hostname: cis490
"""
def build_cidata(out_path: Path, user_data: bytes, meta_data: bytes) -> None:
iso = pycdlib.PyCdlib()
# Joliet=3 + Rock Ridge so cloud-init reads filenames correctly on Linux.
iso.new(joliet=3, vol_ident="cidata", interchange_level=3, rock_ridge="1.09")
iso.add_fp(
io.BytesIO(user_data),
len(user_data),
iso_path="/USERDATA.;1",
rr_name="user-data",
joliet_path="/user-data",
)
iso.add_fp(
io.BytesIO(meta_data),
len(meta_data),
iso_path="/METADATA.;1",
rr_name="meta-data",
joliet_path="/meta-data",
)
iso.write(str(out_path))
iso.close()
def main() -> int:
parser = argparse.ArgumentParser(prog="build_cidata")
parser.add_argument("out_path", type=Path)
parser.add_argument(
"--user-data",
type=Path,
default=None,
help="path to a custom cloud-config user-data file",
)
parser.add_argument(
"--meta-data",
type=Path,
default=None,
help="path to a custom meta-data file",
)
args = parser.parse_args()
user_data = (
args.user_data.read_bytes() if args.user_data else DEFAULT_USER_DATA.encode()
)
meta_data = (
args.meta_data.read_bytes() if args.meta_data else DEFAULT_META_DATA.encode()
)
args.out_path.parent.mkdir(parents=True, exist_ok=True)
build_cidata(args.out_path, user_data, meta_data)
print(f"wrote {args.out_path} ({args.out_path.stat().st_size} bytes)")
return 0
if __name__ == "__main__":
sys.exit(main())