diff --git a/.github/workflows/coverage_security_guard.yml b/.github/workflows/coverage_security_guard.yml new file mode 100644 index 00000000..22df0898 --- /dev/null +++ b/.github/workflows/coverage_security_guard.yml @@ -0,0 +1,33 @@ +name: PR Coverage & Security Guard (ELUSoC_2026) + +on: + pull_request: + branches: [ main ] + +jobs: + coverage_security_guard: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[dev] + + - name: Run Bandit Security Scan + run: | + bandit -r agentwatch/ -ll + + - name: Run Tests and Measure Coverage + run: | + pytest --cov=agentwatch/ tests/ + + - name: Verify Minimum Coverage Thresholds + run: | + python scripts/verify_coverage.py diff --git a/docs/security_ci.md b/docs/security_ci.md new file mode 100644 index 00000000..0d30e541 --- /dev/null +++ b/docs/security_ci.md @@ -0,0 +1,13 @@ +# Automated CI Code Quality and Security Guard (ELUSoC_2026) + +## Overview +To maintain security compliance and avoid regression, the repository implements a PR linting workflow. + +## Steps Performed +1. **Security Vulnerability Scan**: Invokes `Bandit` to inspect files for security issues. +2. **Coverage Gate**: Validates that test suites execute and code coverage remains above the minimum threshold (default: 70%). + +## Files Involved +- `.github/workflows/coverage_security_guard.yml` - CI runner setup. +- `scripts/verify_coverage.py` - Coverage limit checker. +- `tests/test_coverage_guard.py` - Unit test verifying threshold limits. diff --git a/pyproject.toml b/pyproject.toml index 100c2d63..f03cff69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ dev = [ "pytest-cov>=5.0.0", "mypy>=1.11.0", "ruff>=0.6.0", + "bandit>=1.7.0", "httpx>=0.28.0", "opentelemetry-sdk>=1.27.0", ] diff --git a/scripts/verify_coverage.py b/scripts/verify_coverage.py new file mode 100644 index 00000000..09d162cf --- /dev/null +++ b/scripts/verify_coverage.py @@ -0,0 +1,35 @@ +""" +ELUSoC_2026 - Test Coverage Verification Script. +Ensures overall code coverage does not drop below required project thresholds. +""" + +from __future__ import annotations + +import json +import subprocess +import sys + + +def main() -> None: + # Run coverage report in JSON format + subprocess.run(["coverage", "json"], check=True) + + with open("coverage.json", "r") as f: + data = json.load(f) + + percent = data["totals"]["percent_covered"] + required_threshold = 70.0 + + print(f"Current Code Coverage: {percent:.2f}%") + print(f"Required Coverage Threshold: {required_threshold}%") + + if percent < required_threshold: + print("Code coverage is below the required threshold! PR blocked.") + sys.exit(1) + + print("Code coverage thresholds verified successfully.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/test_coverage_guard.py b/tests/test_coverage_guard.py new file mode 100644 index 00000000..59f936a1 --- /dev/null +++ b/tests/test_coverage_guard.py @@ -0,0 +1,35 @@ +"""Unit tests for verify_coverage.py script.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import pytest + + +def test_coverage_script_passes(tmp_path, monkeypatch): + # Mock coverage.json file + json_path = tmp_path / "coverage.json" + dummy_data = { + "totals": { + "percent_covered": 75.5 + } + } + with open(json_path, "w") as f: + json.dump(dummy_data, f) + + monkeypatch.chdir(tmp_path) + # Monkeypatch subprocess to mock the raw coverage call + monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: None) + + # Ensure root path is in sys.path + root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + if root_path not in sys.path: + sys.path.insert(0, root_path) + + from scripts.verify_coverage import main + with pytest.raises(SystemExit) as excinfo: + main() + assert excinfo.value.code == 0