Files
SquidSec-SquidC5/.github/workflows/ci.yml
T
Mr. The Plague 0fce0dcd48 fix(ci): repair broken workflow YAML indentation
Comment stripping mis-indented cache: keys so GitHub never ran CI jobs,
leaving required checks stuck on Expected.
2026-08-04 11:43:54 -04:00

410 lines
16 KiB
YAML

name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
# Public repo CI runs on GitHub-hosted runners (ubuntu-latest / windows-latest).
# Org self-hosted runners disallow public repos (RCE / LAN risk).
# SECURITY: same-repo PRs only for jobs that could touch secrets / heavy build.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
# PRs: 3.12 only (faster). master/main push: 3.11 + 3.12 matrix.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.12"]') || fromJSON('["3.11", "3.12"]') }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
cache: ""
- name: Install dependencies
run: |
set -euo pipefail
python -c "import sys; print(sys.executable, sys.version)"
python -m pip install --upgrade pip
# Same interpreter as tests
python -m pip install -r requirements-dev.txt
python -m pip install -e .
- name: Lint
run: python -m ruff check src tests
- name: Typecheck core
run: |
set -euo pipefail
# mypy is already in requirements-dev.txt — do not reinstall every run
python -m mypy --follow-imports=skip --ignore-missing-imports \
src/squidc5/config.py src/squidc5/auth src/squidc5/policy \
src/squidc5/implants/crypto.py || true
if [ -f src/squidc5/profiles/transforms.py ]; then
python -m mypy --follow-imports=skip --ignore-missing-imports src/squidc5/profiles/transforms.py
fi
- name: Tests
run: |
set -euo pipefail
# Isolate pytest temp / avoid leftover listeners on shared runners
export TMPDIR="${RUNNER_TEMP:-/tmp}/sc5-pytest-$$"
mkdir -p "$TMPDIR"
python -m pytest -q --cov=squidc5 --cov-report=term-missing --cov-fail-under=65
native-agent:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25"
- name: Test + build sc5beacon matrix
working-directory: agents/sc5beacon
run: |
set -euo pipefail
go mod tidy
go test -count=1 ./...
mkdir -p ../../dist
go build -ldflags='-s -w' -o ../../dist/sc5beacon-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o ../../dist/sc5beacon-linux-arm64 .
GOOS=windows GOARCH=amd64 go build -ldflags='-s -w' -o ../../dist/sc5beacon-windows-amd64.exe .
GOOS=darwin GOARCH=arm64 go build -ldflags='-s -w' -o ../../dist/sc5beacon-darwin-arm64 .
ls -la ../../dist/sc5beacon*
- name: Lab soak smoke (config + sysinfo task path)
working-directory: agents/sc5beacon
run: |
set -euo pipefail
go test -count=1 -run 'TestLoadConfig|TestCOFF|TestSimulate' -v ./...
set +e
../../dist/sc5beacon-linux-amd64 2>/tmp/sc5beacon_err_$$.txt
code=$?
set -e
test "$code" -ne 0
grep -qi config /tmp/sc5beacon_err_$$.txt || grep -qi psk /tmp/sc5beacon_err_$$.txt || grep -qi required /tmp/sc5beacon_err_$$.txt
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
continue-on-error: true
with:
name: sc5beacon-native
path: dist/sc5beacon*
retention-days: 3
if-no-files-found: warn
docker:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Build image
run: docker build -t "squidc5:ci-${{ github.run_id }}" .
- name: Run container smoke test
run: |
set -euo pipefail
NAME="squidc5-ci-${{ github.run_id }}-$$"
# Ephemeral host port
HOST_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" -p "127.0.0.1:${HOST_PORT}:8443" "squidc5:ci-${{ github.run_id }}"
echo "container=$NAME host_port=$HOST_PORT"
ok=0
for i in $(seq 1 40); do
if curl -skf "https://127.0.0.1:${HOST_PORT}/api/v1/health"; then
echo
echo "healthy"
ok=1
break
fi
sleep 2
done
if [ "$ok" != "1" ]; then
docker logs "$NAME" || true
docker rm -f "$NAME" || true
exit 1
fi
docker rm -f "$NAME" || true
- name: Cleanup image
if: always()
run: |
docker rm -f "squidc5-ci-${{ github.run_id }}" 2>/dev/null || true
docker rmi "squidc5:ci-${{ github.run_id }}" 2>/dev/null || true
security:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: ""
- name: Install pip-audit
run: |
set -euo pipefail
python -m pip install --upgrade pip
python -m pip install pip-audit
- name: Audit dependencies
run: |
set -euo pipefail
for i in 1 2 3; do
# setup-python puts this env's scripts on PATH after python -m pip install
pip-audit -r requirements.txt && exit 0
echo "pip-audit attempt $i failed; retrying..."
sleep $((i * 5))
done
exit 1
# Standalone operator CLI + server binaries (no venv required).
binaries:
name: binaries (${{ matrix.artifact }})
needs: test
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
artifact: linux-x64
is_windows: false
- runner: windows-latest
artifact: windows-x64
is_windows: true
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: ""
- name: Install package + PyInstaller (Linux)
if: matrix.is_windows == false
run: |
set -euo pipefail
# Always use the same interpreter as the build (setup-python tool cache)
python -c "import sys; print(sys.executable)"
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -e ".[binaries]"
python -m pip show pyinstaller
- name: Install package + PyInstaller (Windows)
if: matrix.is_windows == true
shell: pwsh
run: |
# Always install via `python -m pip` so PyInstaller matches this interpreter.
python -c "import sys; print(sys.executable)"
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -e ".[binaries]"
python -m pip show pyinstaller
python -c "import PyInstaller; print('PyInstaller', PyInstaller.__version__)"
- name: Build binaries (Linux)
if: matrix.is_windows == false
run: python packaging/build_binaries.py
- name: Build binaries (Windows)
if: matrix.is_windows == true
shell: pwsh
run: |
python -c "import sys; print(sys.executable); import PyInstaller; print(PyInstaller.__version__)"
python packaging/build_binaries.py
- name: Smoke-test CLI (Linux)
if: matrix.is_windows == false
run: |
set -euo pipefail
./dist/binaries/sc5 --help
./dist/binaries/sc5 --url http://127.0.0.1:9 health || true
- name: Smoke-test CLI (Windows)
if: matrix.is_windows == true
shell: pwsh
run: |
./dist/binaries/sc5.exe --help
./dist/binaries/sc5.exe --url http://127.0.0.1:9 health; exit 0
- name: Smoke-test server (Linux)
if: matrix.is_windows == false
run: |
set -euo pipefail
export SQUIDC5_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
export SQUIDC5_DATA_DIR="${RUNNER_TEMP:-/tmp}/sc5-bin-smoke-$$"
mkdir -p "$SQUIDC5_DATA_DIR"
echo "smoke port=$SQUIDC5_PORT data=$SQUIDC5_DATA_DIR"
./dist/binaries/squidc5 &
pid=$!
trap 'kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true' EXIT
for i in $(seq 1 50); do
if curl -skf "https://127.0.0.1:${SQUIDC5_PORT}/api/v1/health"; then
echo
# Avoid curl|head SIGPIPE (exit 23) under pipefail
curl -skf -o /dev/null -w "ops_http=%{http_code}\n" "https://127.0.0.1:${SQUIDC5_PORT}/ops"
exit 0
fi
sleep 1
done
echo "server failed to become healthy on port $SQUIDC5_PORT"
exit 1
- name: Upload binaries
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
continue-on-error: true
with:
name: squidc5-${{ matrix.artifact }}
path: |
dist/binaries/sc5
dist/binaries/sc5.exe
dist/binaries/squidc5
dist/binaries/squidc5.exe
dist/binaries/README.txt
dist/binaries/*/
if-no-files-found: error
retention-days: 3
# Publish GitHub Release after tests. Builds Linux here (avoids Actions artifact quota).
release:
name: github-release
needs: [test]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: ""
- name: Build Linux binaries
shell: bash
run: |
set -euo pipefail
python -c "import sys; print(sys.executable)"
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -e ".[binaries]"
python -m pip show pyinstaller
python packaging/build_binaries.py
./dist/binaries/sc5 --help
export SQUIDC5_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
export SQUIDC5_DATA_DIR="${RUNNER_TEMP:-/tmp}/sc5-rel-smoke-$$"
mkdir -p "$SQUIDC5_DATA_DIR"
./dist/binaries/squidc5 &
pid=$!
trap 'kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true' EXIT
for i in $(seq 1 50); do
curl -skf "https://127.0.0.1:${SQUIDC5_PORT}/api/v1/health" && break
sleep 1
done
curl -skf "https://127.0.0.1:${SQUIDC5_PORT}/api/v1/health"
- name: Try Windows artifacts (optional)
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: squidc5-windows-x64
path: artifacts/windows
- name: Stage release assets
shell: bash
run: |
set -euo pipefail
mkdir -p release
test -f dist/binaries/sc5
test -f dist/binaries/squidc5
cp dist/binaries/sc5 release/sc5-linux-x64
cp dist/binaries/squidc5 release/squidc5-linux-x64
chmod +x release/sc5-linux-x64 release/squidc5-linux-x64
if [[ -f artifacts/windows/sc5.exe && -f artifacts/windows/squidc5.exe ]]; then
cp artifacts/windows/sc5.exe release/sc5-windows-x64.exe
cp artifacts/windows/squidc5.exe release/squidc5-windows-x64.exe
fi
python -m pip install --quiet cyclonedx-bom
pip install -q -r requirements.txt
cyclonedx-py requirements requirements.txt -o release/sbom.cdx.json || \
echo '{"bomFormat":"CycloneDX","specVersion":"1.5","components":[]}' > release/sbom.cdx.json
(
cd release
sha256sum * > SHA256SUMS.txt
)
cat > release/README.txt << 'EOF'
SquidC5 standalone binaries (no Python/venv required)
sc5-linux-x64 / sc5-windows-x64.exe Operator CLI
squidc5-linux-x64 / squidc5-windows-x64.exe C2 server
Server defaults: 0.0.0.0:8443 data in ./data/
Ops console: https://HOST:8443/ops (self-signed TLS by default)
Authorized use only.
EOF
ls -lah release/
- name: Release metadata
id: relmeta
run: |
set -euo pipefail
SHORT_SHA="${GITHUB_SHA::7}"
TAG="v0.1.${{ github.run_number }}-${SHORT_SHA}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "title=SquidC5 ${TAG}" >> "$GITHUB_OUTPUT"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
# pin commit for supply-chain (tag v2 -> 3bb1273)
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ steps.relmeta.outputs.tag }}
name: ${{ steps.relmeta.outputs.title }}
target_commitish: ${{ github.sha }}
make_latest: true
body: |
## SquidC5 release `${{ steps.relmeta.outputs.tag }}`
Built from `${{ github.ref_name }}` @ `${{ steps.relmeta.outputs.short_sha }}` after CI tests passed.
### Assets
| File | Platform | Role |
|------|----------|------|
| `sc5-linux-x64` | Linux x64 | Operator CLI |
| `squidc5-linux-x64` | Linux x64 | C2 server |
| `sc5-windows-x64.exe` | Windows x64 | Operator CLI (when available) |
| `squidc5-windows-x64.exe` | Windows x64 | C2 server (when available) |
| `SHA256SUMS.txt` | - | Checksums |
### Server (Linux)
```bash
chmod +x squidc5-linux-x64
./squidc5-linux-x64
# token: ./data/admin_token.txt
# console: https://HOST:8443/ops
```
Authorized red-team / pen-test use only.
files: |
release/sc5-linux-x64
release/squidc5-linux-x64
release/SHA256SUMS.txt
release/README.txt
release/sbom.cdx.json
release/sc5-windows-x64.exe
release/squidc5-windows-x64.exe
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}