diff --git a/Makefile b/Makefile index 4e3c2324..817d400d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help demo test lint ci clean bench-leakage bench-multi-negatives bench-baseline bench-hidden-active bench-cluster-split bench-expert-ablation bench-expert-ablation-500 bench-selectivity bench-feature-decomp bench-gate bench-easy-baseline bench-charge-matched bench-order-dependent bench-precision-at-k bench-per-family bench-simulation-ablation bench-simulation-ablation-within-amp simulation-gate bench-strategy-compare regenerate-all generate phase3 pilot validate-scoring validate-scoring-phase3 validate-scoring-strict external-pilot pilot-confident presynth-qc gold-standard diversity synthesis-order novelty-broad external-consensus questionnaire gate-check ip-report benchmark-card wave0-5-gate-check wave0-5-novelty-audit wave0-5-novelty-audit-v2 wave0-5-panel wave0-5-evidence wave0-5-fill-external wave0-5b-generate wave0-5b-filter recalibration-engine recalibration-engine-dry-run validate-policy-version generate-synthetic-lab-results bump-policy-version generate-review-packet failed-candidate-report safe-publication-filter classify-negative-informativeness validate-rejection-events negative-result-dashboard calibration-audit calibration-audit-example calibration-overfit-check result-quality-filter synthetic-result-policy-check calibration-decision-checklist calibration-rollback-plan simulation-registry validate-simulation-result-schema simulation-baseline-check adapter-gate-check simulation-provenance simulation-ensemble-check simulation-ci-report simulation-deprecation-check simulation-scope-check simulation-evidence-packet artifact-version candidate-manifest artifact-changelog integration-check adapter-author-check license-check artifact-compat-check contribution-check adoption-scorecard release-gate-check decision-log release-request-check coi-check rotation-plan-check rotation-plan-check +.PHONY: help demo test lint ci clean bench-leakage bench-multi-negatives bench-baseline bench-hidden-active bench-cluster-split bench-expert-ablation bench-expert-ablation-500 bench-selectivity bench-feature-decomp bench-gate bench-easy-baseline bench-charge-matched bench-order-dependent bench-precision-at-k bench-per-family bench-simulation-ablation bench-simulation-ablation-within-amp simulation-gate bench-strategy-compare regenerate-all generate phase3 pilot validate-scoring validate-scoring-phase3 validate-scoring-strict external-pilot pilot-confident presynth-qc gold-standard diversity synthesis-order novelty-broad external-consensus questionnaire gate-check ip-report benchmark-card wave0-5-gate-check wave0-5-novelty-audit wave0-5-novelty-audit-v2 wave0-5-panel wave0-5-evidence wave0-5-fill-external wave0-5b-generate wave0-5b-filter recalibration-engine recalibration-engine-dry-run validate-policy-version generate-synthetic-lab-results bump-policy-version generate-review-packet failed-candidate-report safe-publication-filter classify-negative-informativeness validate-rejection-events negative-result-dashboard calibration-audit calibration-audit-example calibration-overfit-check result-quality-filter synthetic-result-policy-check calibration-decision-checklist calibration-rollback-plan simulation-registry validate-simulation-result-schema simulation-baseline-check adapter-gate-check simulation-provenance simulation-ensemble-check simulation-ci-report simulation-deprecation-check simulation-scope-check simulation-evidence-packet artifact-version candidate-manifest artifact-changelog integration-check adapter-author-check license-check artifact-compat-check contribution-check adoption-scorecard release-gate-check decision-log release-request-check coi-check rotation-plan-check security-report-check PYTHON := $(shell [ -f .venv/bin/python ] && echo .venv/bin/python || echo python3) PYTEST := $(shell [ -f .venv/bin/pytest ] && echo .venv/bin/pytest || echo pytest) @@ -860,3 +860,8 @@ rotation-plan-check: .venv/bin/openamp-foundry rotation-plan-check \ --plan-json '{"entries":[{"github_handle":"maintainer","role":"primary_maintainer","backup_handle":"backup-maintainer","responsibilities":["releases","safety-policy","final-approvals"],"status":"active"},{"github_handle":"backup-maintainer","role":"secondary_maintainer","backup_handle":"maintainer","responsibilities":["day-to-day-reviews","issue-triage"],"status":"active"}]}' @echo "Rotation plan check complete." + +security-report-check: + .venv/bin/openamp-foundry security-report-check \ + --report-json '{"report_id":"SEC-2026-001","severity":"medium","category":"dependency_vulnerability","description":"CVE-2026-1234 in requests library","affected_version":"v0.7.3","reporter_handle":"anonymous","report_date":"2026-07-09","status":"acknowledged"}' + @echo "Security report check complete." diff --git a/docs/evidence/METRICS_CURRENT.md b/docs/evidence/METRICS_CURRENT.md index cef922a1..06898ae2 100644 --- a/docs/evidence/METRICS_CURRENT.md +++ b/docs/evidence/METRICS_CURRENT.md @@ -5,7 +5,8 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak > **Purpose:** One authoritative table of current pipeline metrics. If any doc disagrees > with this file, this file wins. Updated whenever benchmark/benchmark config changes. > -> **Last updated:** 2026-07-09 (Phase J J5 — maintainer rotation plan — v0.7.3) +> **Last updated:** 2026-07-09 (Phase J J6 — security policy — v0.7.4) +> **New in v0.7.4:** Security policy (J6 — Loop 114) — `docs/governance/SECURITY_POLICY.md` with private vulnerability reporting process, response timeline (48h acknowledgment, 30d patch), severity classification (critical/high/medium/low), 5 vulnerability categories (code_vulnerability, secret_leakage, dependency_vulnerability, safety_guardrail_bypass, dual_use_risk), out-of-scope items, disclosure process. `src/openamp_foundry/governance/security_policy.py` with `VulnerabilityReport` dataclass (9 fields: report_id, severity, category, description, affected_version, reporter_handle, report_date, status, dry_lab_only), `SecurityReportValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_SEVERITY_LEVELS` (4: critical, high, medium, low), `VALID_VULNERABILITY_CATEGORIES` (5: code_vulnerability, secret_leakage, dependency_vulnerability, safety_guardrail_bypass, dual_use_risk), `VALID_REPORT_STATUSES` (6: received, acknowledged, under_review, patched, disclosed, not_applicable), `validate_vulnerability_report()` (9 checks: report_id SEC- prefix, valid severity, valid category, non-empty description, non-empty affected_version, non-empty reporter_handle, YYYY-MM-DD date, valid status, dry_lab_only must be True; critical+received warning, safety_guardrail_bypass warning), `validate_report_dict()` (dict input with 8 required fields guard). CLI (`openamp-foundry security-report-check`) with `--report-json`, `--format text|json`. `make security-report-check` target. 18 tests. Dry-lab only. Private vulnerability reporting now has a validated structure and documented process. > **New in v0.7.3:** Maintainer rotation plan (J5 — Loop 113) — `docs/governance/MAINTAINER_ROTATION_PLAN.md` with purpose, current maintainers table (3 entries covering primary, secondary, external_advisor), role definitions (4 roles: primary_maintainer, secondary_maintainer, external_advisor, contributor), bus-factor assessment (target >=2 for every critical function, CI maintenance and domain expertise currently at 1), rotation schedule (every 6 months), onboarding and offboarding checklists, linked policies. `src/openamp_foundry/governance/maintainer_rotation.py` with `MaintainerEntry` dataclass (6 fields: github_handle, role, backup_handle, responsibilities, status, dry_lab_only), `RotationPlanValidationResult` dataclass (7 fields: passed, errors, warnings, maintainer_count, critical_role_coverage, bus_factor_sufficient, dry_lab_only), `VALID_ROLES` (4: primary_maintainer, secondary_maintainer, external_advisor, contributor), `CRITICAL_ROLES` (2: primary_maintainer, secondary_maintainer), `VALID_STATUSES` (4: active, on_leave, emeritus, departing), `validate_maintainer_entry()` (6 checks), `validate_rotation_plan()` (aggregates entry validation + bus-factor coverage: missing critical role is error, single coverage is warning), `validate_rotation_plan_dict()` (dict input with missing-entries-fields guard). CLI (`openamp-foundry rotation-plan-check`) with `--plan-json`, `--format text|json`. `make rotation-plan-check` target. 21 tests. Dry-lab only. Project durability is now machine-validated. > **New in v0.7.2:** COI disclosure template (J4 — Loop 112) — `docs/governance/COI_DISCLOSURE_TEMPLATE.md` with structured COI disclosure template (purpose, fill-in-the-blank template with 10 fields: Disclosure ID, disclosure type (reviewer|contributor|maintainer|external_advisor), subject, related artifact, relationship type (financial|institutional|competitive|personal|none), description (conditional), date, recusal required, reviewer, review status (pending|acknowledged|resolved); when to disclose section covering financial/institutional/competitive/personal relationships; process with machine validation and escalation; linked policies). `src/openamp_foundry/governance/coi_disclosure.py` with `COIDisclosure` dataclass (10 fields), `COIValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_DISCLOSURE_TYPES` (4: contributor, external_advisor, maintainer, reviewer), `VALID_RELATIONSHIP_TYPES` (5: competitive, financial, institutional, none, personal), `VALID_REVIEW_STATUSES` (3: acknowledged, pending, resolved), `validate_coi_disclosure()` (10 checks including disclosure_id COI- prefix, valid disclosure_type, non-empty subject/related_artifact, valid relationship_type, description required unless none, YYYY-MM-DD date, non-empty reviewer, valid review_status, dry_lab_only must be True; financial without recusal warning), `validate_coi_dict()` (dict input with 10 required fields guard, missing fields returns passed=False early). CLI (`openamp-foundry coi-check`) with `--disclosure-json`, `--format text|json`. `make coi-check` target. 20 tests. Dry-lab only. COI disclosures now have a validated structure that builds institutional trust. > **New in v0.7.1:** Release request template (J3 — Loop 111) — `docs/governance/RELEASE_REQUEST_TEMPLATE.md` with structured release request template (purpose, fill-in-the-blank template with 17 fields: Release ID, release type, artifact ID/version, requestor name/institution, request date, evidence level 1-6, dry_lab_only, safety_review_status, benchmark_summary, known_limitations, intended_use, data_license, human_reviewer, review_class, approval_status; review criteria with 8 checks; process with classes A-D timelines and escalation path). `src/openamp_foundry/governance/release_request.py` with `ReleaseRequest` dataclass (17 fields), `ReleaseRequestValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_RELEASE_TYPES` (5), `VALID_SAFETY_STATUSES` (3), `VALID_INTENDED_USES` (4), `VALID_APPROVAL_STATUSES` (4), `VALID_REVIEW_CLASSES` (4), `validate_release_request()` (17 checks including dry_lab_only+evidence_level>4 error, public+safety_pending error, model+review_class warning), `validate_request_dict()` (dict input with missing-fields guard). CLI (`openamp-foundry release-request-check`) with `--request-json`, `--format text|json`. `make release-request-check` target. 25 tests. **3516 total.** Formal release requests now have a validated structure before entering human review. @@ -101,7 +102,7 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak > **New in v0.5.31:** Added dipeptide-order features for sequence-order awareness. `dipeptide_order_score` achieves AUROC 0.7861 on AMP-vs-scrambled discrimination — the strongest order-dependent feature in the pipeline. Only 7/31 features survive scrambling (amphipathicity/helix-wheel + dipeptide). All composition features are purely position-independent (exactly 0.5000 AUROC on scrambled test). > **New in v0.5.30:** Easy baseline benchmark added — charge density alone (AUROC 0.8166) outperforms the full pipeline ensemble (0.7792) on AMP-vs-Swiss-Prot-decoy discrimination. Honest finding documented: expected because pipeline optimizes for safety, not raw discrimination. > **New in v0.5.29:** Expanded benchmark to 500 AMPs + 500 composition-matched decoys (n=1000). AUROC 0.7792 (CI₉₅: 0.7505–0.8065) confirms signal generalizes. Cluster-aware CI: 0.746–0.8102. Representative AUROC: 0.778. Standard benchmark (n=191) retained for backward comparison. -> **Pipeline version:** v0.7.3 (J5 — maintainer rotation plan) +> **Pipeline version:** v0.7.4 (J6 — security policy) > **Branch:** main --- diff --git a/docs/governance/SECURITY_POLICY.md b/docs/governance/SECURITY_POLICY.md new file mode 100644 index 00000000..0e7e2d1a --- /dev/null +++ b/docs/governance/SECURITY_POLICY.md @@ -0,0 +1,93 @@ +# Security Policy + +## Purpose + +This document describes how to report security vulnerabilities in the OpenAMP Foundry project privately, what the project maintains as its security boundary, and how reports are handled. + +**Scope:** Computational code, Python library, CLI, schemas, documentation. This project is dry-lab only — there are no wet-lab protocols, clinical systems, or patient data. + +--- + +## Reporting a Vulnerability + +**Do NOT file a public GitHub issue for security vulnerabilities.** + +Instead, report privately via one of these channels (in priority order): + +1. **GitHub Private Vulnerability Reporting** — use the "Report a vulnerability" button on the GitHub Security tab. +2. **Maintainer email** — send to the primary maintainer (see MAINTAINER_ROTATION_PLAN.md for the current handle; find their contact via their GitHub profile). + +Include in your report: +- Description of the vulnerability +- Steps to reproduce (minimal example preferred) +- Affected version(s) or commit hash +- Potential impact assessment +- Whether you have a proposed fix + +--- + +## What Counts as a Security Issue + +| Category | In scope | Examples | +|---|---|---| +| Code vulnerabilities | Yes | Command injection via CLI args, path traversal, insecure deserialization | +| Secret leakage | Yes | API keys, tokens, or credentials committed to the repo | +| Dependency vulnerabilities | Yes | Known CVEs in pinned dependencies that affect users | +| Overclaiming / false biological claims | Yes | Code that removes dry-lab-only guardrails or inflates evidence level | +| Dual-use safety safeguards | Yes | Weakening toxicity, hemolysis, or novelty checks without review | +| Missing features / performance | No | Feature requests and performance issues belong in public issues | +| Documentation inaccuracies | No | Minor doc fixes belong in public PRs | + +--- + +## Response Timeline + +| Step | Target | +|---|---| +| Acknowledgment | Within 48 hours of receipt | +| Initial assessment | Within 5 business days | +| Patch or mitigation | Within 30 days for confirmed high/critical issues | +| Public disclosure | After patch is available and reporter is notified | + +If we cannot meet these timelines, we will communicate proactively. + +--- + +## Severity Classification + +| Severity | Description | +|---|---| +| Critical | Remote code execution, secret exposure, dry-lab boundary bypass | +| High | Privilege escalation, path traversal, dependency with known CVE | +| Medium | Denial of service (local CLI only), insecure default config | +| Low | Minor info disclosure, defense-in-depth improvements | + +--- + +## Out of Scope + +- Theoretical vulnerabilities without a reproducible proof of concept +- Issues that require physical access to the researcher's machine +- Social engineering attacks on maintainers +- Issues in dependencies where no fix is available upstream + +--- + +## Process + +1. Reporter files private report. +2. Maintainer acknowledges within 48 hours. +3. Maintainer assesses severity and determines whether fix is needed. +4. If confirmed: patch is developed on a private branch, reviewed, and merged. +5. A new release is cut with the fix. +6. Reporter is notified and given 7 days to review the fix before public disclosure. +7. Public disclosure is made via GitHub Security Advisory. + +--- + +## Linked Policies + +- [GOVERNANCE.md](../../GOVERNANCE.md) +- [SAFETY.md](../../SAFETY.md) +- [RESPONSIBLE_USE.md](../../RESPONSIBLE_USE.md) +- [MAINTAINER_ROTATION_PLAN.md](MAINTAINER_ROTATION_PLAN.md) diff --git a/docs/research/ROADMAP.md b/docs/research/ROADMAP.md index 10971ddd..b0cdc766 100644 --- a/docs/research/ROADMAP.md +++ b/docs/research/ROADMAP.md @@ -1,5 +1,96 @@ # Roadmap +## v0.7.4 — Loop 114: Phase J J6 — Security Policy ✓ (2026-07-09) + +`docs/governance/SECURITY_POLICY.md` with private vulnerability reporting +process, response timeline (48h acknowledgment, 30d patch), severity +classification (critical/high/medium/low), 5 vulnerability categories +(code_vulnerability, secret_leakage, dependency_vulnerability, +safety_guardrail_bypass, dual_use_risk), out-of-scope items, disclosure +process. + +`src/openamp_foundry/governance/security_policy.py` with +`VulnerabilityReport` dataclass (9 fields: report_id, severity, category, +description, affected_version, reporter_handle, report_date, status, +dry_lab_only), `SecurityReportValidationResult` dataclass (6 fields: +report_id, severity, passed, errors, warnings, dry_lab_only=True), +`VALID_SEVERITY_LEVELS` (4: critical, high, medium, low), +`VALID_VULNERABILITY_CATEGORIES` (5: code_vulnerability, secret_leakage, +dependency_vulnerability, safety_guardrail_bypass, dual_use_risk), +`VALID_REPORT_STATUSES` (6: received, acknowledged, under_review, patched, +disclosed, not_applicable), `validate_vulnerability_report()` (9 checks: +report_id SEC- prefix, valid severity, valid category, non-empty +description, non-empty affected_version, non-empty reporter_handle, +YYYY-MM-DD date, valid status, dry_lab_only must be True; critical+received +warning, safety_guardrail_bypass warning), `validate_report_dict()` (dict +input with 8 required fields guard, missing fields returns passed=False +early). + +CLI (`openamp-foundry security-report-check`) with `--report-json` +(required), `--format text|json`. Handler `_run_security_report_check` in +reports.py. + +`make security-report-check` target. 18 tests. **3575 total.** + +Private vulnerability reporting now has a validated structure and +documented process — security reporters have a clear channel and the +project has a documented response process. + +Changes: +- `docs/governance/SECURITY_POLICY.md` (J6) — Private vulnerability + reporting process with response timeline, severity classification + (critical/high/medium/low), 5 vulnerability categories, out-of-scope + items, disclosure process. +- `src/openamp_foundry/governance/security_policy.py` (J6) — Core module + with `VulnerabilityReport` (9 fields), `SecurityReportValidationResult` + (6 fields, dry_lab_only=True), `VALID_SEVERITY_LEVELS` (4), + `VALID_VULNERABILITY_CATEGORIES` (5), `VALID_REPORT_STATUSES` (6), + `validate_vulnerability_report()` (9 checks with critical+received + warning and safety_guardrail_bypass warning), + `validate_report_dict()` (dict input with 8 required fields guard). +- `tests/governance/test_security_policy.py` (J6) — 18 tests covering: + valid report passes, report_id not SEC- fails, empty report_id fails, + invalid severity fails, invalid category fails, empty description fails, + empty affected_version fails, empty reporter_handle fails, invalid date + fails, invalid status fails, dry_lab_only=False fails, critical+received + warns, safety_guardrail_bypass warns, validate_report_dict passes, + validate_report_dict missing fields fails, all results dry_lab_only=True, + VALID_SEVERITY_LEVELS has 4, VALID_VULNERABILITY_CATEGORIES has 5. +- `src/openamp_foundry/cli/main.py` (J6) — Registered `security-report-check` + subcommand with `--report-json`, `--format` flags. Added import and + dispatch. +- `src/openamp_foundry/cli/commands/reports.py` (J6) — Added + `_run_security_report_check()` CLI handler with JSON parsing, + `validate_report_dict()` call, text and JSON output, exit code 3 on + validation failure. +- `Makefile` (J6) — Added `security-report-check` target with demo + invocation using dependency_vulnerability severity medium. Added to + `.PHONY`. +- `docs/evidence/METRICS_CURRENT.md` (J6) — v0.7.4 J6 changelog. Pipeline + version: v0.7.4. Test count: 3575. +- `tests/test_test_count_regression.py` — baseline updated to 3575. + +Honest boundaries: +- Security policy validation checks structural and policy requirements + only. It does not verify that the vulnerability actually exists, that + the reporter has accurately described it, or that the severity + assessment is correct. +- `dry_lab_only: true` is a const field on all dataclasses — security + reports are governance artifacts, not biological findings. +- The validator checks that the report_date is in YYYY-MM-DD format but + does not verify that the date is reasonable (e.g. not in the future). +- Critical severity with received status produces a warning but does not + fail validation — the maintainer may have good reasons for delayed + acknowledgment, but the warning ensures it is visible. +- Safety guardrail bypass reports always produce a warning to ensure + immediate maintainer attention, regardless of other validation status. +- The security policy defines a process and timeline but does not + guarantee that maintainers will actually meet those timelines. +- The policy covers code vulnerabilities, secret leakage, dependency + vulnerabilities, safety guardrail bypass, and dual-use risks. It does + not cover theoretical vulnerabilities without a reproducible PoC, + social engineering, or upstream dependency issues without fixes. + ## v0.7.3 — Loop 113: Phase J J5 — Maintainer Rotation Plan ✓ (2026-07-09) `docs/governance/MAINTAINER_ROTATION_PLAN.md` with maintainer rotation and diff --git a/src/openamp_foundry/cli/commands/reports.py b/src/openamp_foundry/cli/commands/reports.py index b0125c0b..1fda1713 100644 --- a/src/openamp_foundry/cli/commands/reports.py +++ b/src/openamp_foundry/cli/commands/reports.py @@ -2647,3 +2647,37 @@ def _run_rotation_plan_check(args: argparse.Namespace) -> int: print("governance check, not a legal determination.") return 0 if result.passed else 3 + + +def _run_security_report_check(args: argparse.Namespace) -> int: + """Validate a security vulnerability report.""" + import json as _json + from openamp_foundry.governance.security_policy import validate_report_dict + + try: + d = _json.loads(args.report_json) + except _json.JSONDecodeError as exc: + print(_json.dumps({"status": "error", "error": f"Invalid JSON: {exc}"})) + return 2 + + if not isinstance(d, dict): + print(_json.dumps({"status": "error", "error": "--report-json must be a JSON object"})) + return 2 + + result = validate_report_dict(d) + output_format = getattr(args, "format", "text") + + if output_format == "json": + import dataclasses + print(_json.dumps(dataclasses.asdict(result), indent=2)) + else: + status = "PASS" if result.passed else "FAIL" + print(f"Security report {result.report_id} ({result.severity}): {status}") + for e in result.errors: + print(f" ERROR: {e}") + for w in result.warnings: + print(f" WARN: {w}") + if result.passed: + print(" Report validated. Ready for maintainer review.") + + return 0 if result.passed else 3 diff --git a/src/openamp_foundry/cli/main.py b/src/openamp_foundry/cli/main.py index e2b17fbc..c52cc486 100644 --- a/src/openamp_foundry/cli/main.py +++ b/src/openamp_foundry/cli/main.py @@ -12,6 +12,7 @@ from openamp_foundry.cli.commands.reports import _run_release_request_check from openamp_foundry.cli.commands.reports import _run_coi_check from openamp_foundry.cli.commands.reports import _run_rotation_plan_check +from openamp_foundry.cli.commands.reports import _run_security_report_check import argparse import json @@ -1924,6 +1925,24 @@ def build_parser() -> argparse.ArgumentParser: help="Output format (default: text).", ) + # ── Security report check (Phase J J6) ────────────────────────── + src = sub.add_parser( + "security-report-check", + help=( + "Validate a security vulnerability report before the " + "formal review queue. Dry-lab only." + ), + ) + src.add_argument( + "--report-json", type=str, required=True, + help="JSON string of a VulnerabilityReport dict (required).", + ) + src.add_argument( + "--format", type=str, default="text", + choices=["text", "json"], + help="Output format (default: text).", + ) + # ── Simulation-result confidence interval report (H7) ──────────── sicr = sub.add_parser( "simulation-ci-report", @@ -2177,6 +2196,9 @@ def main(argv: list[str] | None = None) -> int: if args.command == "rotation-plan-check": return _run_rotation_plan_check(args) + if args.command == "security-report-check": + return _run_security_report_check(args) + parser.error("unknown command") return 2 diff --git a/src/openamp_foundry/governance/security_policy.py b/src/openamp_foundry/governance/security_policy.py new file mode 100644 index 00000000..cad8efd1 --- /dev/null +++ b/src/openamp_foundry/governance/security_policy.py @@ -0,0 +1,112 @@ +"""Security policy validator — validates security vulnerability reports. + +Ensures all required fields are present before a vulnerability report +enters the formal review queue. +Dry-lab only. +""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +VALID_SEVERITY_LEVELS: set[str] = { + "critical", "high", "medium", "low" +} + +VALID_REPORT_STATUSES: set[str] = { + "received", "acknowledged", "under_review", "patched", "disclosed", "not_applicable" +} + +VALID_VULNERABILITY_CATEGORIES: set[str] = { + "code_vulnerability", "secret_leakage", "dependency_vulnerability", + "safety_guardrail_bypass", "dual_use_risk" +} + + +@dataclass +class VulnerabilityReport: + report_id: str # e.g. "SEC-2026-001" + severity: str # from VALID_SEVERITY_LEVELS + category: str # from VALID_VULNERABILITY_CATEGORIES + description: str # must not be empty + affected_version: str # must not be empty + reporter_handle: str # GitHub handle or "anonymous" + report_date: str # YYYY-MM-DD + status: str # from VALID_REPORT_STATUSES + dry_lab_only: bool = True + + +@dataclass +class SecurityReportValidationResult: + report_id: str + severity: str + passed: bool + errors: list + warnings: list + dry_lab_only: bool = True + + +def validate_vulnerability_report(r: VulnerabilityReport) -> SecurityReportValidationResult: + errors: list = [] + warnings: list = [] + + if not r.report_id or not r.report_id.startswith("SEC-"): + errors.append("report_id must not be empty and must start with 'SEC-'") + if r.severity not in VALID_SEVERITY_LEVELS: + errors.append(f"severity={r.severity!r} not in {sorted(VALID_SEVERITY_LEVELS)}") + if r.category not in VALID_VULNERABILITY_CATEGORIES: + errors.append(f"category={r.category!r} not in {sorted(VALID_VULNERABILITY_CATEGORIES)}") + if not r.description: + errors.append("description must not be empty") + if not r.affected_version: + errors.append("affected_version must not be empty") + if not r.reporter_handle: + errors.append("reporter_handle must not be empty") + if not r.report_date or len(r.report_date) != 10 or r.report_date[4] != "-": + errors.append("report_date must be in YYYY-MM-DD format") + if r.status not in VALID_REPORT_STATUSES: + errors.append(f"status={r.status!r} not in {sorted(VALID_REPORT_STATUSES)}") + if not r.dry_lab_only: + errors.append("dry_lab_only must be True") + + if r.severity == "critical" and r.status == "received": + warnings.append("critical severity report is still 'received' — acknowledge within 48 hours") + if r.category == "safety_guardrail_bypass": + warnings.append("safety guardrail bypass reports require immediate maintainer review") + + return SecurityReportValidationResult( + report_id=r.report_id or "", + severity=r.severity, + passed=len(errors) == 0, + errors=errors, + warnings=warnings, + dry_lab_only=True, + ) + + +def validate_report_dict(d: dict) -> SecurityReportValidationResult: + required = [ + "report_id", "severity", "category", "description", + "affected_version", "reporter_handle", "report_date", "status", + ] + missing = [f for f in required if f not in d] + if missing: + return SecurityReportValidationResult( + report_id=d.get("report_id", ""), + severity=d.get("severity", ""), + passed=False, + errors=[f"Missing required fields: {missing}"], + warnings=[], + dry_lab_only=True, + ) + r = VulnerabilityReport( + report_id=d.get("report_id", ""), + severity=d.get("severity", ""), + category=d.get("category", ""), + description=d.get("description", ""), + affected_version=d.get("affected_version", ""), + reporter_handle=d.get("reporter_handle", ""), + report_date=d.get("report_date", ""), + status=d.get("status", ""), + dry_lab_only=d.get("dry_lab_only", True), + ) + return validate_vulnerability_report(r) diff --git a/tests/governance/test_security_policy.py b/tests/governance/test_security_policy.py new file mode 100644 index 00000000..1b97b869 --- /dev/null +++ b/tests/governance/test_security_policy.py @@ -0,0 +1,150 @@ +"""Tests for security_policy.py — Phase J J6.""" +import pytest +from openamp_foundry.governance.security_policy import ( + VulnerabilityReport, + SecurityReportValidationResult, + VALID_SEVERITY_LEVELS, + VALID_VULNERABILITY_CATEGORIES, + validate_vulnerability_report, + validate_report_dict, +) + + +def _valid_report(**overrides) -> VulnerabilityReport: + defaults = dict( + report_id="SEC-2026-001", + severity="high", + category="code_vulnerability", + description="Path traversal via --output flag allows reading arbitrary files", + affected_version="v0.7.3", + reporter_handle="security-researcher", + report_date="2026-07-09", + status="acknowledged", + dry_lab_only=True, + ) + defaults.update(overrides) + return VulnerabilityReport(**defaults) + + +def test_valid_report_passes(): + result = validate_vulnerability_report(_valid_report()) + assert result.passed + assert result.errors == [] + + +def test_report_id_not_starting_with_SEC_fails(): + result = validate_vulnerability_report(_valid_report(report_id="BUG-2026-001")) + assert not result.passed + assert any("SEC-" in e for e in result.errors) + + +def test_empty_report_id_fails(): + result = validate_vulnerability_report(_valid_report(report_id="")) + assert not result.passed + assert any("report_id" in e for e in result.errors) + + +def test_invalid_severity_fails(): + result = validate_vulnerability_report(_valid_report(severity="extreme")) + assert not result.passed + assert any("severity" in e for e in result.errors) + + +def test_invalid_category_fails(): + result = validate_vulnerability_report(_valid_report(category="phishing")) + assert not result.passed + assert any("category" in e for e in result.errors) + + +def test_empty_description_fails(): + result = validate_vulnerability_report(_valid_report(description="")) + assert not result.passed + assert any("description" in e for e in result.errors) + + +def test_empty_affected_version_fails(): + result = validate_vulnerability_report(_valid_report(affected_version="")) + assert not result.passed + assert any("affected_version" in e for e in result.errors) + + +def test_empty_reporter_handle_fails(): + result = validate_vulnerability_report(_valid_report(reporter_handle="")) + assert not result.passed + assert any("reporter_handle" in e for e in result.errors) + + +def test_invalid_date_format_fails(): + result = validate_vulnerability_report(_valid_report(report_date="09-07-2026")) + assert not result.passed + assert any("report_date" in e for e in result.errors) + + +def test_invalid_status_fails(): + result = validate_vulnerability_report(_valid_report(status="wontfix")) + assert not result.passed + assert any("status" in e for e in result.errors) + + +def test_dry_lab_only_false_fails(): + result = validate_vulnerability_report(_valid_report(dry_lab_only=False)) + assert not result.passed + assert any("dry_lab_only" in e for e in result.errors) + + +def test_critical_severity_received_produces_warning(): + result = validate_vulnerability_report(_valid_report(severity="critical", status="received")) + assert result.passed + assert any("critical" in w for w in result.warnings) + + +def test_safety_guardrail_bypass_produces_warning(): + result = validate_vulnerability_report(_valid_report(category="safety_guardrail_bypass")) + assert result.passed + assert any("safety guardrail" in w for w in result.warnings) + + +def test_validate_report_dict_passes_with_valid_dict(): + d = dict( + report_id="SEC-2026-001", + severity="medium", + category="dependency_vulnerability", + description="CVE-2026-1234 in requests library", + affected_version="v0.7.3", + reporter_handle="anonymous", + report_date="2026-07-09", + status="under_review", + ) + result = validate_report_dict(d) + assert result.passed + assert result.dry_lab_only is True + + +def test_validate_report_dict_fails_with_missing_fields(): + result = validate_report_dict({"report_id": "SEC-2026-001"}) + assert not result.passed + assert any("Missing" in e for e in result.errors) + + +def test_all_results_have_dry_lab_only_true(): + result = validate_vulnerability_report(_valid_report()) + assert result.dry_lab_only is True + result2 = validate_vulnerability_report(_valid_report(dry_lab_only=False)) + assert result2.dry_lab_only is True + + +def test_valid_severity_levels_has_4_entries(): + assert len(VALID_SEVERITY_LEVELS) == 4 + assert "critical" in VALID_SEVERITY_LEVELS + assert "high" in VALID_SEVERITY_LEVELS + assert "medium" in VALID_SEVERITY_LEVELS + assert "low" in VALID_SEVERITY_LEVELS + + +def test_valid_vulnerability_categories_has_5_entries(): + assert len(VALID_VULNERABILITY_CATEGORIES) == 5 + assert "code_vulnerability" in VALID_VULNERABILITY_CATEGORIES + assert "secret_leakage" in VALID_VULNERABILITY_CATEGORIES + assert "dependency_vulnerability" in VALID_VULNERABILITY_CATEGORIES + assert "safety_guardrail_bypass" in VALID_VULNERABILITY_CATEGORIES + assert "dual_use_risk" in VALID_VULNERABILITY_CATEGORIES diff --git a/tests/test_test_count_regression.py b/tests/test_test_count_regression.py index 2626c45a..2201e0c1 100644 --- a/tests/test_test_count_regression.py +++ b/tests/test_test_count_regression.py @@ -2,11 +2,11 @@ import subprocess import sys -# Baseline: 3557 tests as of Loop 113 (2026-07-09) -- Phase J J5 (maintainer rotation plan) +# Baseline: 3575 tests as of Loop 114 (2026-07-09) -- Phase J J6 (security policy) # Note: 3 pre-existing collection errors in test_benchmark_calibration.py, # test_benchmark_charge_distribution.py, test_benchmark_cheap_enemies.py. # Raise threshold if legitimate tests are removed; lower threshold is okay. -BASELINE = 3557 +BASELINE = 3575 TOLERANCE = 0.05 # allow 5% fluctuation