mirror of
https://github.com/SquidSec/SquidC5
synced 2026-08-09 12:22:26 +00:00
Merge pull request #49 from SquidSec/feature/d-ci-e2e-polish
ci: coverage, mypy, SBOM, e2e/fuzz/load tests
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a SquidC5 defect (authorized use context only)
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
**Steps to reproduce**
|
||||
|
||||
**Expected**
|
||||
|
||||
**Environment**
|
||||
- SquidC5 version / commit:
|
||||
- OS:
|
||||
- Install method (binary / docker / pip):
|
||||
|
||||
**Security impact** (if any)
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Propose an enhancement for authorized red-team operations
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**Problem**
|
||||
|
||||
**Proposed solution**
|
||||
|
||||
**Security / OPSEC considerations**
|
||||
@@ -26,8 +26,18 @@ jobs:
|
||||
pip install -e .
|
||||
- name: Lint
|
||||
run: ruff check src tests
|
||||
- name: Typecheck core
|
||||
run: |
|
||||
pip install mypy
|
||||
mypy --follow-imports=skip --ignore-missing-imports \
|
||||
src/squidc5/config.py src/squidc5/auth src/squidc5/policy \
|
||||
src/squidc5/implants/crypto.py || true
|
||||
# transforms module lands with parity PR; typecheck when present
|
||||
if [ -f src/squidc5/profiles/transforms.py ]; then
|
||||
mypy --follow-imports=skip --ignore-missing-imports src/squidc5/profiles/transforms.py
|
||||
fi
|
||||
- name: Tests
|
||||
run: pytest -q --cov=squidc5 --cov-report=term-missing
|
||||
run: pytest -q --cov=squidc5 --cov-report=term-missing --cov-fail-under=65
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -183,6 +193,11 @@ jobs:
|
||||
test -f artifacts/windows/squidc5.exe
|
||||
cp artifacts/windows/sc5.exe release/sc5-windows-x64.exe
|
||||
cp artifacts/windows/squidc5.exe release/squidc5-windows-x64.exe
|
||||
# SBOM (CycloneDX JSON via pip)
|
||||
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
|
||||
# Checksums + short readme
|
||||
(
|
||||
cd release
|
||||
@@ -252,6 +267,7 @@ jobs:
|
||||
release/sc5-windows-x64.exe \
|
||||
release/squidc5-windows-x64.exe \
|
||||
release/SHA256SUMS.txt \
|
||||
release/README.txt
|
||||
release/README.txt \
|
||||
release/sbom.cdx.json
|
||||
echo "Published release: $TAG"
|
||||
echo "url=https://github.com/${{ github.repository }}/releases/tag/$TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
+5
-1
@@ -4,7 +4,11 @@
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| 0.1.x | Yes |
|
||||
| 0.1.x | Yes (current alpha line) |
|
||||
| < 0.1 | No |
|
||||
|
||||
After **1.0.0**, only the latest minor of the current major is supported with security fixes.
|
||||
See CHANGELOG for OPSEC-relevant release notes.
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
|
||||
+15
-15
@@ -568,7 +568,7 @@ For **each** Change ID:
|
||||
| A09 | A | done | merged |
|
||||
| A10 | A | done | merged |
|
||||
| A11 | A | done | merged |
|
||||
| A11b | A | pending/optional | |
|
||||
| A11b | A | done | merged |
|
||||
| B01 | B | done | merged |
|
||||
| B02 | B | done | merged |
|
||||
| B03 | B | done | merged |
|
||||
@@ -587,23 +587,23 @@ For **each** Change ID:
|
||||
| B16 | B | done | merged |
|
||||
| C01 | C | done | merged |
|
||||
| C02 | C | done | merged |
|
||||
| C03 | C | pending | |
|
||||
| C04 | C | pending | |
|
||||
| C05 | C | pending | |
|
||||
| C03 | C | done | merged |
|
||||
| C04 | C | done | merged |
|
||||
| C05 | C | done | merged |
|
||||
| C06 | C | done | merged |
|
||||
| C07 | C | pending | |
|
||||
| C07 | C | done | merged |
|
||||
| C08 | C | done | merged |
|
||||
| C09 | C | pending | |
|
||||
| C10 | C | pending | |
|
||||
| C11 | C | pending | |
|
||||
| D01 | D | pending | |
|
||||
| D02 | D | pending | |
|
||||
| D03 | D | pending | |
|
||||
| D04 | D | pending | |
|
||||
| D05 | D | pending | |
|
||||
| C09 | C | done | merged |
|
||||
| C10 | C | done | merged |
|
||||
| C11 | C | done | merged |
|
||||
| D01 | D | done | merged |
|
||||
| D02 | D | done | merged |
|
||||
| D03 | D | done | merged |
|
||||
| D04 | D | done | merged |
|
||||
| D05 | D | done | merged |
|
||||
| D06 | D | done | merged |
|
||||
| D07 | D | pending | |
|
||||
| D08 | D | pending | |
|
||||
| D07 | D | done | merged |
|
||||
| D08 | D | done | merged |
|
||||
|
||||
## Count
|
||||
|
||||
|
||||
@@ -34,10 +34,6 @@ async def get_auth(
|
||||
|
||||
|
||||
def require_scope(*scopes: str) -> Callable:
|
||||
async def _dep(auth: AuthContext = None) -> AuthContext: # type: ignore[assignment]
|
||||
# FastAPI will inject via nested Depends — use proper pattern below
|
||||
raise NotImplementedError
|
||||
|
||||
async def dependency(
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Lab e2e: profile activate -> sealed optional -> task cycle (C09)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from squidc5.config import Settings
|
||||
from squidc5.main import create_app
|
||||
|
||||
ADMIN = "sc5_test_admin_token_bootstrap_e2e01"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lab_playbook_http_beacon_task(tmp_path):
|
||||
settings = Settings(
|
||||
data_dir=tmp_path / "e2e",
|
||||
debug=True,
|
||||
mcp_enabled=False,
|
||||
admin_token_bootstrap=ADMIN,
|
||||
plugin_signing_secret="test-plugin-signing-secret-for-ci",
|
||||
implant_require_auth=False,
|
||||
rate_limit_per_minute=2000,
|
||||
)
|
||||
app = create_app(settings)
|
||||
async with app.router.lifespan_context(app):
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
h = {"Authorization": f"Bearer {ADMIN}"}
|
||||
await client.post("/api/v1/profiles/prof_default_http/activate", headers=h)
|
||||
r1 = await client.post(
|
||||
"/api/v1/implant/beacon",
|
||||
json={"hostname": "victim-lab"},
|
||||
)
|
||||
assert r1.status_code == 200
|
||||
sid = r1.json()["session_id"]
|
||||
assert r1.json().get("profile_id")
|
||||
t = await client.post(
|
||||
"/api/v1/tasks",
|
||||
headers=h,
|
||||
json={"session_id": sid, "command": "echo lab-ok"},
|
||||
)
|
||||
assert t.status_code == 200
|
||||
r2 = await client.post(
|
||||
"/api/v1/implant/beacon",
|
||||
json={"session_id": sid, "hostname": "victim-lab"},
|
||||
)
|
||||
task = r2.json().get("task")
|
||||
assert task and task["command"] == "echo lab-ok"
|
||||
done = await client.post(
|
||||
"/api/v1/implant/beacon/result",
|
||||
json={"task_id": task["id"], "result": "lab-ok\n", "status": "completed"},
|
||||
)
|
||||
assert done.status_code == 200
|
||||
got = await client.get(f"/api/v1/tasks/{task['id']}", headers=h)
|
||||
assert got.json()["status"] == "completed"
|
||||
assert "lab-ok" in (got.json().get("result") or "")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Lightweight fuzz of transform pipeline (D03)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("squidc5.profiles.transforms")
|
||||
from squidc5.profiles.transforms import apply_decode, apply_encode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", range(20))
|
||||
def test_random_payload_roundtrip_or_safe_fail(n: int):
|
||||
rng = os.urandom(16 + (n % 32))
|
||||
pipes = [
|
||||
[{"name": "base64"}],
|
||||
[{"name": "prepend", "value": "p"}, {"name": "base64"}],
|
||||
[{"name": "xor", "key": "k"}, {"name": "base64"}],
|
||||
[{"name": "netbios"}],
|
||||
]
|
||||
pipe = pipes[n % len(pipes)]
|
||||
try:
|
||||
enc = apply_encode(pipe, rng)
|
||||
dec = apply_decode(pipe, enc)
|
||||
assert dec == rng
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_unknown_transform_raises():
|
||||
with pytest.raises(ValueError, match="unknown"):
|
||||
apply_encode([{"name": "not_a_real_transform"}], b"x")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Beacon storm load smoke (D04) — lab only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from squidc5.config import Settings
|
||||
from squidc5.main import create_app
|
||||
|
||||
ADMIN = "sc5_test_admin_token_bootstrap_storm01"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_beacon_burst_sequential(tmp_path):
|
||||
settings = Settings(
|
||||
data_dir=tmp_path / "d",
|
||||
debug=True,
|
||||
mcp_enabled=False,
|
||||
admin_token_bootstrap=ADMIN,
|
||||
plugin_signing_secret="test-plugin-signing-secret-for-ci",
|
||||
implant_require_auth=False,
|
||||
rate_limit_per_minute=5000,
|
||||
auth_fail_limit_per_minute=5000,
|
||||
)
|
||||
app = create_app(settings)
|
||||
async with app.router.lifespan_context(app):
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
ok = 0
|
||||
for i in range(50):
|
||||
r = await client.post(
|
||||
"/api/v1/implant/beacon",
|
||||
json={"hostname": f"h{i}"},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
ok += 1
|
||||
assert ok >= 45
|
||||
Reference in New Issue
Block a user