mirror of
https://github.com/SquidSec/SquidC5
synced 2026-08-09 12:22:26 +00:00
Comment stripping mis-indented cache: keys so GitHub never ran CI jobs, leaving required checks stuck on Expected.
410 lines
16 KiB
YAML
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 }}
|