feat(pipeline): G2 Compliance Commit Ledger — code↔control audit trail
New table: compliance_commits (commit hash, affected controls, risk level)
New API:
POST /v1/compliance-commits (SDK registers commit + impact)
GET /v1/compliance-commits (list with filters)
GET /v1/compliance-commits/by-control/{id} (all commits for a control)
GET /v1/compliance-commits/stats (dashboard)
GET /v1/compliance-commits/{id} (detail)
GIN index on affected_control_ids for fast @> containment queries.
454 tests pass, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from api.dependency_routes import router as dependency_router
|
|||||||
from api.master_control_routes import router as master_control_router
|
from api.master_control_routes import router as master_control_router
|
||||||
from api.decision_trace_routes import router as decision_trace_router
|
from api.decision_trace_routes import router as decision_trace_router
|
||||||
from api.decision_trace_routes import full_trace_router
|
from api.decision_trace_routes import full_trace_router
|
||||||
|
from api.compliance_commit_routes import router as compliance_commit_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(generator_router)
|
router.include_router(generator_router)
|
||||||
@@ -16,3 +17,4 @@ router.include_router(dependency_router)
|
|||||||
router.include_router(master_control_router)
|
router.include_router(master_control_router)
|
||||||
router.include_router(decision_trace_router)
|
router.include_router(decision_trace_router)
|
||||||
router.include_router(full_trace_router)
|
router.include_router(full_trace_router)
|
||||||
|
router.include_router(compliance_commit_router)
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""Compliance Commit Ledger API — G2.
|
||||||
|
|
||||||
|
Tracks code commits and their compliance impact. SDK reports each commit
|
||||||
|
with affected controls, building an audit trail for code↔compliance mapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from db.session import SessionLocal
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1/compliance-commits", tags=["compliance-commits"])
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCommitRequest(BaseModel):
|
||||||
|
tenant_id: str
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
commit_hash: str
|
||||||
|
commit_message: Optional[str] = None
|
||||||
|
commit_author: Optional[str] = None
|
||||||
|
commit_date: Optional[str] = None
|
||||||
|
branch: Optional[str] = None
|
||||||
|
repo_url: Optional[str] = None
|
||||||
|
affected_control_ids: list[str] = []
|
||||||
|
affected_files: list[str] = []
|
||||||
|
risk_level: str = "low"
|
||||||
|
analysis_summary: Optional[str] = None
|
||||||
|
analysis_metadata: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def register_commit(req: CreateCommitRequest):
|
||||||
|
"""Register a code commit with its compliance impact."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
cid = str(uuid.uuid4())
|
||||||
|
db.execute(text("""
|
||||||
|
INSERT INTO compliance_commits
|
||||||
|
(id, tenant_id, project_id, commit_hash, commit_message,
|
||||||
|
commit_author, commit_date, branch, repo_url,
|
||||||
|
affected_control_ids, affected_files,
|
||||||
|
risk_level, analysis_summary, analysis_metadata)
|
||||||
|
VALUES
|
||||||
|
(CAST(:id AS uuid), CAST(:tenant_id AS uuid), :project_id,
|
||||||
|
:commit_hash, :commit_message, :commit_author,
|
||||||
|
:commit_date, :branch, :repo_url,
|
||||||
|
CAST(:control_ids AS jsonb), CAST(:files AS jsonb),
|
||||||
|
:risk_level, :analysis_summary, CAST(:metadata AS jsonb))
|
||||||
|
"""), {
|
||||||
|
"id": cid,
|
||||||
|
"tenant_id": req.tenant_id,
|
||||||
|
"project_id": req.project_id,
|
||||||
|
"commit_hash": req.commit_hash,
|
||||||
|
"commit_message": req.commit_message,
|
||||||
|
"commit_author": req.commit_author,
|
||||||
|
"commit_date": req.commit_date,
|
||||||
|
"branch": req.branch,
|
||||||
|
"repo_url": req.repo_url,
|
||||||
|
"control_ids": json.dumps(req.affected_control_ids),
|
||||||
|
"files": json.dumps(req.affected_files),
|
||||||
|
"risk_level": req.risk_level,
|
||||||
|
"analysis_summary": req.analysis_summary,
|
||||||
|
"metadata": json.dumps(req.analysis_metadata),
|
||||||
|
})
|
||||||
|
db.commit()
|
||||||
|
return {
|
||||||
|
"id": cid,
|
||||||
|
"status": "registered",
|
||||||
|
"affected_controls": len(req.affected_control_ids),
|
||||||
|
"risk_level": req.risk_level,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_commits(
|
||||||
|
tenant_id: Optional[str] = None,
|
||||||
|
control_id: Optional[str] = None,
|
||||||
|
risk_level: Optional[str] = None,
|
||||||
|
branch: Optional[str] = None,
|
||||||
|
since: Optional[str] = None,
|
||||||
|
limit: int = Query(50, ge=1, le=500),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
):
|
||||||
|
"""List compliance commits with filters."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
clauses = []
|
||||||
|
params: dict = {"limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
if tenant_id:
|
||||||
|
clauses.append("tenant_id = CAST(:tenant_id AS uuid)")
|
||||||
|
params["tenant_id"] = tenant_id
|
||||||
|
if control_id:
|
||||||
|
clauses.append("affected_control_ids @> CAST(:cid_json AS jsonb)")
|
||||||
|
params["cid_json"] = json.dumps([control_id])
|
||||||
|
if risk_level:
|
||||||
|
clauses.append("risk_level = :risk")
|
||||||
|
params["risk"] = risk_level
|
||||||
|
if branch:
|
||||||
|
clauses.append("branch = :branch")
|
||||||
|
params["branch"] = branch
|
||||||
|
if since:
|
||||||
|
clauses.append("commit_date >= CAST(:since AS timestamptz)")
|
||||||
|
params["since"] = since
|
||||||
|
|
||||||
|
where = "WHERE " + " AND ".join(clauses) if clauses else ""
|
||||||
|
|
||||||
|
rows = db.execute(text(f"""
|
||||||
|
SELECT id, commit_hash, commit_message, commit_author, commit_date,
|
||||||
|
branch, affected_control_ids, affected_files, risk_level
|
||||||
|
FROM compliance_commits
|
||||||
|
{where}
|
||||||
|
ORDER BY commit_date DESC NULLS LAST
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
"""), params).fetchall()
|
||||||
|
|
||||||
|
total = db.execute(text(f"""
|
||||||
|
SELECT count(*) FROM compliance_commits {where}
|
||||||
|
"""), params).scalar()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": total,
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"id": str(r[0]),
|
||||||
|
"commit_hash": r[1],
|
||||||
|
"message": r[2],
|
||||||
|
"author": r[3],
|
||||||
|
"date": str(r[4]) if r[4] else None,
|
||||||
|
"branch": r[5],
|
||||||
|
"affected_control_ids": r[6],
|
||||||
|
"affected_files": r[7],
|
||||||
|
"risk_level": r[8],
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def commit_stats(tenant_id: Optional[str] = None):
|
||||||
|
"""Dashboard stats for compliance commits."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
tf = ""
|
||||||
|
params: dict = {}
|
||||||
|
if tenant_id:
|
||||||
|
tf = "WHERE tenant_id = CAST(:tid AS uuid)"
|
||||||
|
params["tid"] = tenant_id
|
||||||
|
|
||||||
|
risk = db.execute(text(f"""
|
||||||
|
SELECT risk_level, count(*) FROM compliance_commits {tf}
|
||||||
|
GROUP BY risk_level
|
||||||
|
"""), params).fetchall()
|
||||||
|
|
||||||
|
recent = db.execute(text(f"""
|
||||||
|
SELECT count(*) FROM compliance_commits
|
||||||
|
{tf + ' AND' if tf else 'WHERE'} commit_date > NOW() - interval '7 days'
|
||||||
|
"""), params).scalar()
|
||||||
|
|
||||||
|
total = sum(r[1] for r in risk)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_commits": total,
|
||||||
|
"last_7_days": recent,
|
||||||
|
"by_risk_level": {r[0]: r[1] for r in risk},
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-control/{control_id}")
|
||||||
|
async def commits_by_control(
|
||||||
|
control_id: str,
|
||||||
|
limit: int = Query(50, ge=1, le=200),
|
||||||
|
):
|
||||||
|
"""Get all commits that affect a specific control."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
rows = db.execute(text("""
|
||||||
|
SELECT id, commit_hash, commit_message, commit_author, commit_date,
|
||||||
|
branch, repo_url, affected_files, risk_level
|
||||||
|
FROM compliance_commits
|
||||||
|
WHERE affected_control_ids @> CAST(:cid_json AS jsonb)
|
||||||
|
ORDER BY commit_date DESC NULLS LAST
|
||||||
|
LIMIT :limit
|
||||||
|
"""), {
|
||||||
|
"cid_json": json.dumps([control_id]),
|
||||||
|
"limit": limit,
|
||||||
|
}).fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"control_id": control_id,
|
||||||
|
"total_commits": len(rows),
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"id": str(r[0]),
|
||||||
|
"commit_hash": r[1],
|
||||||
|
"message": r[2],
|
||||||
|
"author": r[3],
|
||||||
|
"date": str(r[4]) if r[4] else None,
|
||||||
|
"branch": r[5],
|
||||||
|
"repo_url": r[6],
|
||||||
|
"affected_files": r[7],
|
||||||
|
"risk_level": r[8],
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{commit_id}")
|
||||||
|
async def get_commit(commit_id: str):
|
||||||
|
"""Get details of a single compliance commit."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
row = db.execute(text("""
|
||||||
|
SELECT * FROM compliance_commits WHERE id = CAST(:id AS uuid)
|
||||||
|
"""), {"id": commit_id}).fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Commit not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(row.id),
|
||||||
|
"tenant_id": str(row.tenant_id),
|
||||||
|
"project_id": str(row.project_id) if row.project_id else None,
|
||||||
|
"commit_hash": row.commit_hash,
|
||||||
|
"commit_message": row.commit_message,
|
||||||
|
"commit_author": row.commit_author,
|
||||||
|
"commit_date": str(row.commit_date) if row.commit_date else None,
|
||||||
|
"branch": row.branch,
|
||||||
|
"repo_url": row.repo_url,
|
||||||
|
"affected_control_ids": row.affected_control_ids,
|
||||||
|
"affected_files": row.affected_files,
|
||||||
|
"risk_level": row.risk_level,
|
||||||
|
"analysis_summary": row.analysis_summary,
|
||||||
|
"analysis_metadata": row.analysis_metadata,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- Migration 007: Compliance Commit Ledger (G2)
|
||||||
|
-- Schema: compliance
|
||||||
|
-- Run: ssh macmini "docker exec -i bp-core-postgres psql -U breakpilot -d breakpilot_db" < control-pipeline/migrations/007_compliance_commits.sql
|
||||||
|
|
||||||
|
SET search_path TO compliance, public;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS compliance_commits (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
project_id UUID,
|
||||||
|
|
||||||
|
-- Git Info
|
||||||
|
commit_hash VARCHAR(64) NOT NULL,
|
||||||
|
commit_message TEXT,
|
||||||
|
commit_author VARCHAR(200),
|
||||||
|
commit_date TIMESTAMPTZ,
|
||||||
|
branch VARCHAR(200),
|
||||||
|
repo_url TEXT,
|
||||||
|
|
||||||
|
-- Affected Controls
|
||||||
|
affected_control_ids JSONB NOT NULL DEFAULT '[]',
|
||||||
|
affected_files JSONB DEFAULT '[]',
|
||||||
|
|
||||||
|
-- Analysis
|
||||||
|
risk_level VARCHAR(20) DEFAULT 'low'
|
||||||
|
CHECK (risk_level IN ('low', 'medium', 'high', 'critical')),
|
||||||
|
analysis_summary TEXT,
|
||||||
|
analysis_metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_tenant ON compliance_commits(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_hash ON compliance_commits(commit_hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_date ON compliance_commits(commit_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_risk ON compliance_commits(risk_level);
|
||||||
|
-- GIN index for JSONB array containment queries (@>)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_control_ids ON compliance_commits USING GIN (affected_control_ids);
|
||||||
Reference in New Issue
Block a user