Skip to content

fix(ci): extract Extension-Kit artifacts from image, no cloning in CI #99

fix(ci): extract Extension-Kit artifacts from image, no cloning in CI

fix(ci): extract Extension-Kit artifacts from image, no cloning in CI #99

Workflow file for this run

name: Integration Tests
# Runs on a GitHub-hosted windows-latest runner.
# WSL2 runs three containers via Docker daemon:
# - extension-kit builder: clones source into /tmp/ext-src, runs make all
# - adaptixc2 detached server, --network host, built ext-kit + patched profile mounted
# - testing-kit runs feature validation + integration tests
# 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'
ADAPTIXC2_IMAGE: ghcr.io/tgjls/adaptixc2:latest
EXTENSION_KIT_IMAGE: ghcr.io/tgjls/extension-kit:latest
TESTING_KIT_IMAGE: ghcr.io/tgjls/testing-kit:latest
jobs:
integration-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Pass CI variables into WSL
shell: powershell
run: echo "WSLENV=CI_USER/u:CI_PASS/u:CI_AGENT_PATH/u" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# ── Windows: CI user, OpenSSH, tmp directory ─────────────────────────────
- 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
additional-packages: docker.io iproute2 curl
- name: Start Docker daemon in WSL
shell: wsl-bash {0}
run: |
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy || true
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true
sudo dockerd > /dev/null 2>&1 &
echo "Waiting for Docker..."
for i in $(seq 1 30); do
sudo docker info >/dev/null 2>&1 && { echo "Docker ready."; break; }
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: CI config ────────────────────────────────────────────────────────
- 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
# ── WSL: Pull images ──────────────────────────────────────────────────────
- name: Pull CI images
shell: wsl-bash {0}
run: |
sudo docker pull ghcr.io/tgjls/adaptixc2:latest
sudo docker pull ghcr.io/tgjls/extension-kit:latest
sudo docker pull ghcr.io/tgjls/testing-kit:latest
# ── WSL: Extract Extension-Kit artifacts ─────────────────────────────────
- name: Extract Extension-Kit from image
shell: wsl-bash {0}
run: |
CID=$(sudo docker create ghcr.io/tgjls/extension-kit:latest)
sudo docker cp "$CID":/ext /tmp/ext-src
sudo docker rm "$CID"
# ── WSL: Prepare AdaptixC2 profile ────────────────────────────────────────
- name: Extract and patch profile.yaml to enable Extension-Kit
shell: wsl-bash {0}
run: |
# Extract profile without running the ENTRYPOINT
CID=$(sudo docker create ghcr.io/tgjls/adaptixc2:latest)
sudo docker cp "$CID":/app/profile.yaml /tmp/ci_profile.yaml
sudo docker rm "$CID"
# Uncomment the Extension-Kit axscript line
sed -i 's|# - "Extension-Kit/extension-kit.axs"| - "Extension-Kit/extension-kit.axs"|' /tmp/ci_profile.yaml
# ── WSL: Start AdaptixC2 server ───────────────────────────────────────────
- name: Start AdaptixC2 server
shell: wsl-bash {0}
run: |
sudo docker run -d --name adaptixc2 \
--network host \
-v /tmp/ext-src:/app/Extension-Kit:ro \
-v /tmp/ci_profile.yaml:/app/profile.yaml:ro \
ghcr.io/tgjls/adaptixc2:latest
echo "Waiting for AdaptixC2 (port 4321)..."
for i in $(seq 1 60); do
(exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null && { echo "Server ready."; break; }
sleep 1
done
(exec 3<>/dev/tcp/127.0.0.1/4321) 2>/dev/null || {
echo "=== AdaptixC2 server failed to start within 60s ==="
sudo docker logs adaptixc2
exit 1
}
# ── WSL: Tests ────────────────────────────────────────────────────────────
- name: Feature validation
shell: wsl-bash {0}
run: |
sudo docker run --rm \
ghcr.io/tgjls/testing-kit:latest --help 2>&1 | \
grep -q -- "-o" && echo "✓ --output flag available" || \
{ echo "✗ --output flag missing"; exit 1; }
- name: Integration 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/.github/ci/tasks.yaml":/tasks.yaml:ro \
-v /tmp/ci_config.yaml:/tmp/ci_config.yaml:ro \
-v ~/.ssh/ci_key:/root/.ssh/ci_key:ro \
ghcr.io/tgjls/testing-kit:latest \
-c /tmp/ci_config.yaml -t /tasks.yaml
# ── Cleanup ───────────────────────────────────────────────────────────────
- name: Print AdaptixC2 server logs
if: always()
shell: wsl-bash {0}
run: sudo docker logs adaptixc2 2>&1 || true
- name: Teardown
if: always()
shell: wsl-bash {0}
run: |
sudo docker rm -f adaptixc2 2>/dev/null || true
rm -f ~/.ssh/ci_key ~/.ssh/ci_key.pub