Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@ jobs:
run: |
FAIL=0
echo "### Repo Hygiene" >> $GITHUB_STEP_SUMMARY
for check in check_detection_pairing check_no_committed_drivers check_no_real_tenants; do
if python3 tools/ci/${check}.py; then
echo "- ✅ \`${check}\`" >> $GITHUB_STEP_SUMMARY
for check in tools/ci/check_detection_pairing.py \
tools/ci/check_no_committed_drivers.py \
tools/ci/check_no_real_tenants.py \
ci/check_aitm_loopback_only.py \
ci/check_kernel_lpe_harness.py \
ci/check_loldrivers_hash_only.py \
ci/check_mock_services_loopback.py \
ci/check_no_real_rmm_license.py \
ci/check_no_suspicious_pth.py; do
name=$(basename "$check" .py)
if python3 "$check"; then
echo "- ✅ \`${name}\`" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ \`${check}\`" >> $GITHUB_STEP_SUMMARY
echo "- ❌ \`${name}\`" >> $GITHUB_STEP_SUMMARY
FAIL=1
fi
done
Expand Down Expand Up @@ -98,6 +107,7 @@ jobs:
tools/edr-silencing/callback-integrity/tests/ \
tools/browser-native-postex/tests/ \
tools/bofs/tests/ \
tools/idol/tests/ \
-v --tb=short 2>&1 | tee /tmp/pytest-output.txt
PASSED=$(grep -c "PASSED" /tmp/pytest-output.txt || true)
FAILED=$(grep -c "FAILED" /tmp/pytest-output.txt || true)
Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,20 @@ jobs:
run: |
FAIL=0
echo "### Repo Hygiene" >> $GITHUB_STEP_SUMMARY
for check in check_detection_pairing check_no_committed_drivers check_no_real_tenants; do
if python3 tools/ci/${check}.py; then
echo "- ✅ \`${check}\`" >> $GITHUB_STEP_SUMMARY
for check in tools/ci/check_detection_pairing.py \
tools/ci/check_no_committed_drivers.py \
tools/ci/check_no_real_tenants.py \
ci/check_aitm_loopback_only.py \
ci/check_kernel_lpe_harness.py \
ci/check_loldrivers_hash_only.py \
ci/check_mock_services_loopback.py \
ci/check_no_real_rmm_license.py \
ci/check_no_suspicious_pth.py; do
name=$(basename "$check" .py)
if python3 "$check"; then
echo "- ✅ \`${name}\`" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ \`${check}\`" >> $GITHUB_STEP_SUMMARY
echo "- ❌ \`${name}\`" >> $GITHUB_STEP_SUMMARY
FAIL=1
fi
done
Expand Down Expand Up @@ -109,6 +118,7 @@ jobs:
tools/edr-silencing/callback-integrity/tests/ \
tools/browser-native-postex/tests/ \
tools/bofs/tests/ \
tools/idol/tests/ \
--tb=short 2>&1 | tee /tmp/pytest-output.txt
PASSED=$(grep -c "PASSED" /tmp/pytest-output.txt || true)
FAILED=$(grep -c "FAILED" /tmp/pytest-output.txt || true)
Expand Down
25 changes: 12 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ lab-up:
@echo " Dashboard: python3 tools/dashboard/dashboard_cli.py --c2 http://127.0.0.1:8443"
@echo " K8s post-ex: make lab-k8s-up (requires: kind, kubectl)"
@echo ""
@echo "Network is fully isolated (internal: true). No internet access."
@echo "Inter-service traffic stays on lab-internal (no external gateway)."
@echo "Host port publication is via lab-bridge; ContainmentGuard enforces"
@echo "loopback/RFC1918 at the tool layer."

## Stop and destroy the lab (removes containers, networks, volumes)
lab-down:
Expand Down Expand Up @@ -228,22 +230,19 @@ lab-sccm-down:
@echo "Mock SCCM stopped."

## ── Mock Azure Arc lab (v5) ──────────────────────────────────────────────────
## Extends mock-imds (port 9200) with Azure Arc MSI endpoint for arc_pivot.py demos
## mock-imds (started by `make lab-up`) already serves /metadata/identity/oauth2/token
## for Azure Arc MSI simulation. This target ensures it is running.

## Start the Azure Arc IMDS extension (adds /metadata/identity endpoint to mock-imds)
## Ensure mock-imds is up; the Arc MSI endpoint is served unconditionally
lab-arc-up:
@echo "Starting Azure Arc MSI pivot lab..."
@echo " This lab reuses mock-imds (port 9200) with Arc MSI endpoint enabled."
docker build -t mock-imds-arc infra/lab/mock-imds/ \
--build-arg ARC_MSI=1 2>/dev/null || \
EXPLOIT_LAB_ACTIVE=1 python3 infra/lab/mock-imds/mock_imds.py --arc-msi &
@echo "Mock IMDS (Azure Arc mode): http://127.0.0.1:9200"
@echo "Starting mock-imds (Azure Arc MSI endpoint is served by default)..."
$(COMPOSE) up -d mock-imds
@echo "Mock IMDS: http://127.0.0.1:9200"
@echo " Arc MSI token: GET http://127.0.0.1:9200/metadata/identity/oauth2/token?api-version=2020-06-01"
@echo " Arc pivot: EXPLOIT_LAB_ACTIVE=1 ENTRA_LAB_TENANT_ID=lab-tenant-001 \\"
@echo " python tools/lateral-movement/azure-arc/arc_pivot.py"

## Stop the Azure Arc IMDS extension
## Stop the mock-imds container
lab-arc-down:
pkill -f mock_imds.py || true
docker stop mock-imds-arc 2>/dev/null && docker rm mock-imds-arc 2>/dev/null || true
@echo "Azure Arc lab stopped."
$(COMPOSE) stop mock-imds || true
@echo "mock-imds stopped."
23 changes: 22 additions & 1 deletion docker-compose.lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services:
- "127.0.0.1:8443:8443" # Operator API - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock OAuth Server ──────────────────────────────────────────────────
Expand All @@ -44,6 +45,7 @@ services:
- "127.0.0.1:8090:8090" # OAuth mock - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock Graph API ─────────────────────────────────────────────────────
Expand All @@ -63,6 +65,7 @@ services:
- c2-server
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock Slack API ─────────────────────────────────────────────────────
Expand All @@ -82,6 +85,7 @@ services:
- c2-server
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Beacon 1 ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -143,6 +147,7 @@ services:
- c2-server
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Target App 1 ──────────────────────────────────────────────────────
Expand All @@ -160,6 +165,7 @@ services:
- "127.0.0.1:8501:8501" # Streamlit UI - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Target App 2 ──────────────────────────────────────────────────────
Expand All @@ -177,6 +183,7 @@ services:
- "127.0.0.1:8502:8501" # Mapped to different host port
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock Entra IdP ────────────────────────────────────────────────────────
Expand All @@ -198,6 +205,7 @@ services:
- "127.0.0.1:9100:9100" # Entra IdP mock - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock IMDS ─────────────────────────────────────────────────────────────
Expand All @@ -220,6 +228,7 @@ services:
- "127.0.0.1:9200:9200" # IMDS mock - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Mock SCCM ─────────────────────────────────────────────────────────────────
Expand All @@ -237,6 +246,7 @@ services:
- "127.0.0.1:9600:9600" # Mock SCCM API - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

# ── Vulnerable Lab App (XSS delivery demo) ────────────────────────────────
Expand All @@ -254,9 +264,20 @@ services:
- "127.0.0.1:8503:8503" # Vulnerable app - loopback only
networks:
- lab-internal
- lab-bridge
restart: unless-stopped

networks:
# Internal-only network — no external gateway. Inter-service traffic only.
# Beacons attach to this only so they cannot egress.
lab-internal:
driver: bridge
internal: true # NO external gateway - completely isolated
internal: true

# Bridge network for services that publish host ports (operator API, mocks,
# target apps). `internal: false` is required for docker port publication
# to work. ContainmentGuard still enforces RFC1918/loopback at the tool
# layer, so reachable-from-host does not mean reachable-from-internet for
# the tools themselves.
lab-bridge:
driver: bridge
2 changes: 1 addition & 1 deletion infra/docker/Dockerfile.beacon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12-slim
FROM python:3.14-slim

RUN pip install --no-cache-dir requests

Expand Down
8 changes: 4 additions & 4 deletions infra/docker/Dockerfile.c2-server
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM python:3.12-slim
FROM python:3.14-slim

RUN pip install --no-cache-dir flask requests
RUN pip install --no-cache-dir flask requests cryptography pyjwt

# Non-root user
RUN useradd -m -s /bin/bash operator
# Non-root user (operator group pre-exists in debian-slim as system gid 37)
RUN useradd -m -s /bin/bash -g operator operator
USER operator
WORKDIR /home/operator

Expand Down
6 changes: 3 additions & 3 deletions infra/docker/Dockerfile.exploit-server
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM python:3.12-slim
FROM python:3.14-slim

RUN pip install --no-cache-dir flask requests

# Non-root user
RUN useradd -m -s /bin/bash operator
# Non-root user (operator group pre-exists in debian-slim as system gid 37)
RUN useradd -m -s /bin/bash -g operator operator
USER operator
WORKDIR /home/operator

Expand Down
5 changes: 2 additions & 3 deletions infra/docker/Dockerfile.mock-entra
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
FROM python:3.12-slim
FROM python:3.14-slim

WORKDIR /app

COPY infra/lab/mock-entra/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir 'flask>=3.0' 'pyjwt>=2.8' 'cryptography>=42.0'

COPY infra/lab/mock-entra/server.py .
COPY infra/lab/mock-entra/lab_ca_policy.json .
Expand Down
5 changes: 2 additions & 3 deletions infra/docker/Dockerfile.mock-graph
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
FROM python:3.12-slim
FROM python:3.14-slim
WORKDIR /app
COPY infra/lab/mock-graph/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir 'flask>=3.0' 'requests>=2.31'
COPY infra/lab/mock-graph/app.py .
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
Expand Down
5 changes: 2 additions & 3 deletions infra/docker/Dockerfile.mock-imds
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
FROM python:3.12-slim
FROM python:3.14-slim

WORKDIR /app

COPY infra/lab/mock-imds/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir 'flask>=3.0'

COPY infra/lab/mock-imds/server.py .

Expand Down
5 changes: 2 additions & 3 deletions infra/docker/Dockerfile.mock-oauth
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
FROM python:3.12-slim
FROM python:3.14-slim
WORKDIR /app
COPY infra/lab/mock-oauth/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir 'flask>=3.0'
COPY infra/lab/mock-oauth/app.py .
ENV PYTHONUNBUFFERED=1
EXPOSE 8090
Expand Down
5 changes: 2 additions & 3 deletions infra/docker/Dockerfile.mock-slack
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
FROM python:3.12-slim
FROM python:3.14-slim
WORKDIR /app
COPY infra/lab/mock-slack/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir 'flask>=3.0' 'requests>=2.31'
COPY infra/lab/mock-slack/app.py .
ENV PYTHONUNBUFFERED=1
EXPOSE 8081
Expand Down
2 changes: 1 addition & 1 deletion infra/docker/Dockerfile.target-app
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12-slim
FROM python:3.14-slim

RUN pip install --no-cache-dir streamlit

Expand Down
11 changes: 7 additions & 4 deletions tools/idol/01-credential-harvest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def age_str(mtime: float) -> str:
return f"{int(delta/86400)}d ago"


def scan_credential_paths():
def _credential_targets():
home = Path.home()
targets = [
return [
# (description, glob pattern or exact path)
("AWS credentials", str(home / ".aws" / "credentials")),
("AWS config", str(home / ".aws" / "config")),
Expand All @@ -67,6 +67,9 @@ def scan_credential_paths():
("Env files (home)", str(home / "**" / ".env")),
]


def scan_credential_paths():
targets = _credential_targets()
found_files = []

for desc, pattern in targets:
Expand Down Expand Up @@ -131,7 +134,7 @@ def main():
print(f"{CYAN} AGENT-ENHANCED: What changes with an LLM at runtime{RESET}")
print(f"{CYAN}{'═'*62}{RESET}\n")

print(f" {BOLD}The scan above checks {len(targets)} hardcoded paths.{RESET}")
print(f" {BOLD}The scan above checks {len(_credential_targets())} hardcoded paths.{RESET}")
print(f" {BOLD}An LLM-driven virus replaces the path list with comprehension.{RESET}\n")

print(f" {YELLOW}1. Semantic file understanding{RESET}")
Expand Down Expand Up @@ -163,7 +166,7 @@ def main():
print(f" {DIM} because it discovers credentials through comprehension, not rules.{RESET}")
print(f" {DIM} You cannot write a detection signature for 'understanding.'{RESET}\n")

print(f" {RED}{BOLD} Static: scan 13 paths → miss everything else{RESET}")
print(f" {RED}{BOLD} Static: scan {len(_credential_targets())} paths → miss everything else{RESET}")
print(f" {RED}{BOLD} Agent: read any file → understand what it contains{RESET}\n")

return len(results), total_kb, elapsed
Expand Down
6 changes: 6 additions & 0 deletions tools/lib/containment.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ def assert_loopback(self, host: str):
"only loopback and Docker bridge IPs are allowed."
)

# 0.0.0.0 (all interfaces) is permitted only inside docker: the lab
# compose network is internal:true and host port mappings are bound
# to 127.0.0.1, so all reachable interfaces are loopback or RFC1918.
if addr == IPv4Address("0.0.0.0") and self.in_docker():
return

if not any(addr in net for net in ALLOWED_NETWORKS):
raise ContainmentError(
f"[{self.tool_name}] Address {addr} is outside allowed networks "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

services:
mock-browser-target:
image: python:3.12-slim
image: python:3.14-slim
container_name: mock-browser-target
# Bind to loopback on the host — never expose externally
ports:
Expand Down
Loading