Skip to content

fix(28): CR-01/CR-02/WR-03/WR-05 harden run.py agent handling #93

fix(28): CR-01/CR-02/WR-03/WR-05 harden run.py agent handling

fix(28): CR-01/CR-02/WR-03/WR-05 harden run.py agent handling #93

Workflow file for this run

name: Integration Tests
# Runs on a GitHub-hosted windows-latest runner.
# WSL2 is initialized and runs Docker daemon natively.
# The custom published Docker image runs with --network host passthrough.
# SSH delivery goes from Docker(WSL) → Windows.
# Beacon callbacks go from Windows → Docker(WSL).
on:
push:
pull_request:
workflow_dispatch:
env:
CI_USER: ci_runner
CI_PASS: Ci_Test_Pass1!
CI_AGENT_DIR: 'C:\ci'
CI_AGENT_PATH: 'C:\ci\agent.exe'
CI_CONTAINER: 'ghcr.io/thegr3atjosh/adaptix-prebuilt:latest'
jobs:
integration-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Pass CI variables into WSL
shell: powershell
run: echo "WSLENV=CI_USER/u:CI_PASS/u:CI_AGENT_PATH/u:CI_CONTAINER/u" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# ── Windows: CI user, OpenSSH, tmp directory ────────────────────────────
# C:\ci (agent drop dir) and Defender/firewall config are handled by the
# SSH preamble after connecting — these steps only do what must exist
# before SSH is available.
- name: Create CI user
shell: powershell
run: |
$pass = ConvertTo-SecureString $env:CI_PASS -AsPlainText -Force
if (-not (Get-LocalUser $env:CI_USER -ErrorAction SilentlyContinue)) {
New-LocalUser $env:CI_USER -Password $pass -PasswordNeverExpires
Add-LocalGroupMember -Group Administrators -Member $env:CI_USER
} else {
Set-LocalUser $env:CI_USER -Password $pass
}
- name: Start OpenSSH Server with password auth
shell: powershell
run: |
$cap = Get-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
if ($cap.State -ne 'Installed') {
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
}
Set-Service sshd -StartupType Automatic
Start-Service sshd
$cfg = "$env:ProgramData\ssh\sshd_config"
(Get-Content $cfg) `
-replace '^#?PasswordAuthentication\s+\w+', 'PasswordAuthentication yes' |
Set-Content $cfg
Restart-Service sshd
- name: Create tmp directory for SSH key transfer
shell: powershell
run: New-Item -ItemType Directory -Force -Path C:\tmp | Out-Null
# ── WSL: Ubuntu + Docker ────────────────────────────────────────────────
- uses: Vampire/setup-wsl@v7
with:
distribution: Ubuntu-24.04
# Install Docker inside WSL
additional-packages: docker.io iproute2 curl
- name: Start Docker daemon in WSL
shell: wsl-bash {0}
run: |
# WSL compatibility for Docker
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy || true
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true
# Start dockerd in the background
sudo dockerd > /dev/null 2>&1 &
echo "Waiting for Docker to start..."
for i in $(seq 1 30); do
if sudo docker info >/dev/null 2>&1; then
echo "Docker started successfully."
break
fi
sleep 1
done
sudo docker info >/dev/null 2>&1 || { echo "Docker failed to start"; exit 1; }
# ── WSL: SSH setup ───────────────────────────────────────────────────────
- name: Generate SSH keypair
shell: wsl-bash {0}
run: |
ssh-keygen -t ed25519 -N "" -f ~/.ssh/ci_key
cp ~/.ssh/ci_key.pub /mnt/c/tmp/ci_key.pub
- name: Install SSH public key on Windows
shell: powershell
run: |
$authFile = "$env:ProgramData\ssh\administrators_authorized_keys"
New-Item -Force -ItemType File -Path $authFile | Out-Null
Get-Content C:\tmp\ci_key.pub | Add-Content $authFile
icacls $authFile /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
# ── WSL: config + Execution ──────────────────────────────────────────────
- name: Write CI config
shell: wsl-bash {0}
run: |
WSL_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1)
WSL_GW=$(ip route show default 2>/dev/null | awk 'NR==1{print $3}')
WINDOWS_IP=$(cmd.exe /c ipconfig 2>/dev/null | tr -d '\r' | awk '/vEthernet.*WSL/{f=1} f && /IPv4 Address/{print $NF; exit}')
if [ -z "$WINDOWS_IP" ] || ip addr show 2>/dev/null | grep -qF "$WINDOWS_IP"; then
CALLBACK_HOST="127.0.0.1"
SSH_HOST="127.0.0.1"
echo "=== WSL2 mirrored mode: WSL_IP=$WSL_IP WINDOWS_IP=${WINDOWS_IP:-none}, using localhost ==="
else
CALLBACK_HOST="$WSL_IP"
SSH_HOST="${WSL_GW:-$WINDOWS_IP}"
echo "=== WSL2 NAT mode: WSL_IP=$WSL_IP WSL_GW=$WSL_GW WINDOWS_IP=$WINDOWS_IP, SSH→$SSH_HOST ==="
fi
cat > /tmp/ci_config.yaml << EOF
server:
url: https://127.0.0.1:4321
endpoint: /endpoint
operator:
name: ci
password: pass
setup:
project: ci
agent_output: /tmp/ci_agent.exe
listener:
name: ci_http
type: BeaconHTTP
config:
host_bind: "0.0.0.0"
port_bind: 8080
callback_addresses:
- "$CALLBACK_HOST:8080"
http_method: POST
uri:
- /beacon
user_agent:
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
hb_header: "X-Beacon-Id"
encrypt_key: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
ssl: false
page-payload: '{"status":"ok","data":"<<<PAYLOAD_DATA>>>","metrics":"sync"}'
agent:
agent: beacon
listener: ci_http
listener_type: BeaconHTTP
config:
arch: x64
format: Exe
sleep: "0s"
jitter: 0
ssh:
host: "$SSH_HOST"
username: "$CI_USER"
key_path: /root/.ssh/ci_key
source_path: /tmp/ci_agent.exe
agent_path: '$CI_AGENT_PATH'
terminate: true
preamble:
- 'New-Item -ItemType Directory -Force -Path C:\ci | Out-Null'
- 'Set-MpPreference -DisableRealtimeMonitoring \$true'
- 'Add-MpPreference -ExclusionPath C:\ci'
- 'New-NetFirewallRule -DisplayName CI_C2_8080 -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow -Profile Any | Out-Null'
EOF
- name: Pull Adaptix Container
shell: wsl-bash {0}
run: sudo docker pull "$CI_CONTAINER"
- name: Run CI Container (Server + Tests)
shell: wsl-bash {0}
run: |
WIN_WS=$(cmd.exe /c "echo %GITHUB_WORKSPACE%" 2>/dev/null | tr -d '\r')
WSL_WS=$(wslpath "$WIN_WS")
sudo docker run --rm --network host \
-v "$WSL_WS:/workspace" \
-v ~/.ssh/ci_key:/root/.ssh/ci_key:ro \
-v /tmp/ci_config.yaml:/tmp/ci_config.yaml:ro \
"$CI_CONTAINER" \
bash -c '
export PATH="/usr/local/bin:/root/.local/bin:$PATH"
uv tool install /workspace --reinstall
# ── Feature validation (no server required) ──────────────────
echo "=== Feature validation ==="
adaptix-testing --help 2>&1 | grep -q -- "-o" && \
echo "✓ --output flag available" || \
{ echo "✗ --output flag missing from CLI"; exit 1; }
echo "=== Feature validation passed ==="
# ── Server startup ───────────────────────────────────────────
echo "Generating required TLS certificate..."
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout /tmp/adaptixc2/dist/server.rsa.key \
-out /tmp/adaptixc2/dist/server.rsa.crt \
-days 1 -subj "/CN=ci" 2>/dev/null
echo "Starting AdaptixC2 Server..."
cd /tmp/adaptixc2/dist
./adaptixserver -profile profile.yaml > /tmp/adaptixserver.log 2>&1 &
SERVER_PID=$!
# Wait up to 60s for the C2 to boot up fully
for i in $(seq 1 60); do
(exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null && break
sleep 1
done
(exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null || {
echo "=== AdaptixC2 server failed to start within 60s ==="
cat /tmp/adaptixserver.log
exit 1
}
# ── Integration tests (exercises --output and preamble) ──────
echo "AdaptixC2 Ready! Running integration tests..."
adaptix-testing -c /tmp/ci_config.yaml -t /workspace/.github/ci/tasks.yaml \
-o /tmp/ci-results.txt
TEST_EXIT_CODE=$?
echo "=== Test results ==="
cat /tmp/ci-results.txt
# Verify output file has a summary on success
if [ $TEST_EXIT_CODE -eq 0 ]; then
grep -q "All tasks passed" /tmp/ci-results.txt && \
echo "✓ Output file contains expected summary" || \
{ echo "✗ Output file missing success summary"; exit 1; }
fi
kill $SERVER_PID 2>/dev/null
exit $TEST_EXIT_CODE
'
# ── Cleanup ──────────────────────────────────────────────────────────────
- name: Remove SSH key
if: always()
shell: wsl-bash {0}
run: rm -f ~/.ssh/ci_key ~/.ssh/ci_key.pub