Files
breakpilot-compliance/backend-compliance/compliance/db/isms_audit_repository.py
Sharang Parnerkar 482e8574ad refactor(backend/db): split repository.py + isms_repository.py per-aggregate
Phase 1 Step 5 of PHASE1_RUNBOOK.md.

compliance/db/repository.py (1547 LOC) decomposed into seven sibling
per-aggregate repository modules:

  regulation_repository.py     (268) — Regulation + Requirement
  control_repository.py        (291) — Control + ControlMapping
  evidence_repository.py       (143)
  risk_repository.py           (148)
  audit_export_repository.py   (110)
  service_module_repository.py (247)
  audit_session_repository.py  (478) — AuditSession + AuditSignOff

compliance/db/isms_repository.py (838 LOC) decomposed into two
sub-aggregate modules mirroring the models split:

  isms_governance_repository.py (354) — Scope, Policy, Objective, SoA
  isms_audit_repository.py      (499) — Finding, CAPA, Review, Internal Audit,
                                         Trail, Readiness

Both original files become thin re-export shims (37 and 25 LOC
respectively) so every existing import continues to work unchanged.
New code SHOULD import from the aggregate module directly.

All new sibling files under the 500-line hard cap; largest is
isms_audit_repository.py at 499 (on the edge; when Phase 1 Step 4
router->service extraction lands, the audit_session repo may split
further if growth exceeds 500).

Verified:
  - 173/173 pytest compliance/tests/ tests/contracts/ pass
  - OpenAPI 360 paths / 484 operations unchanged
  - All repo files under 500 LOC

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:08:39 +02:00

500 lines
17 KiB
Python

"""
ISMS repositories — extracted from compliance/db/isms_repository.py.
Phase 1 Step 5: split per sub-aggregate. Re-exported from
``compliance.db.isms_repository`` for backwards compatibility.
"""
import uuid
from datetime import datetime, date, timezone
from typing import List, Optional, Dict, Any, Tuple
from sqlalchemy.orm import Session as DBSession
from compliance.db.models import (
ISMSScopeDB, ISMSPolicyDB, SecurityObjectiveDB,
StatementOfApplicabilityDB, AuditFindingDB, CorrectiveActionDB,
ManagementReviewDB, InternalAuditDB, AuditTrailDB, ISMSReadinessCheckDB,
ApprovalStatusEnum, FindingTypeEnum, FindingStatusEnum, CAPATypeEnum,
)
class AuditFindingRepository:
"""Repository for Audit Findings (Major/Minor/OFI)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
finding_type: FindingTypeEnum,
title: str,
description: str,
auditor: str,
iso_chapter: Optional[str] = None,
annex_a_control: Optional[str] = None,
objective_evidence: Optional[str] = None,
owner: Optional[str] = None,
due_date: Optional[date] = None,
internal_audit_id: Optional[str] = None,
) -> AuditFindingDB:
"""Create a new audit finding."""
# Generate finding ID
year = date.today().year
existing_count = self.db.query(AuditFindingDB).filter(
AuditFindingDB.finding_id.like(f"FIND-{year}-%")
).count()
finding_id = f"FIND-{year}-{existing_count + 1:03d}"
finding = AuditFindingDB(
id=str(uuid.uuid4()),
finding_id=finding_id,
finding_type=finding_type,
iso_chapter=iso_chapter,
annex_a_control=annex_a_control,
title=title,
description=description,
objective_evidence=objective_evidence,
owner=owner,
auditor=auditor,
due_date=due_date,
internal_audit_id=internal_audit_id,
status=FindingStatusEnum.OPEN,
)
self.db.add(finding)
self.db.commit()
self.db.refresh(finding)
return finding
def get_by_id(self, finding_id: str) -> Optional[AuditFindingDB]:
"""Get finding by UUID or finding_id."""
return self.db.query(AuditFindingDB).filter(
(AuditFindingDB.id == finding_id) | (AuditFindingDB.finding_id == finding_id)
).first()
def get_all(
self,
finding_type: Optional[FindingTypeEnum] = None,
status: Optional[FindingStatusEnum] = None,
internal_audit_id: Optional[str] = None,
) -> List[AuditFindingDB]:
"""Get all findings with optional filters."""
query = self.db.query(AuditFindingDB)
if finding_type:
query = query.filter(AuditFindingDB.finding_type == finding_type)
if status:
query = query.filter(AuditFindingDB.status == status)
if internal_audit_id:
query = query.filter(AuditFindingDB.internal_audit_id == internal_audit_id)
return query.order_by(AuditFindingDB.identified_date.desc()).all()
def get_open_majors(self) -> List[AuditFindingDB]:
"""Get all open major findings (blocking certification)."""
return self.db.query(AuditFindingDB).filter(
AuditFindingDB.finding_type == FindingTypeEnum.MAJOR,
AuditFindingDB.status != FindingStatusEnum.CLOSED
).all()
def get_statistics(self) -> Dict[str, Any]:
"""Get finding statistics."""
findings = self.get_all()
return {
"total": len(findings),
"major": sum(1 for f in findings if f.finding_type == FindingTypeEnum.MAJOR),
"minor": sum(1 for f in findings if f.finding_type == FindingTypeEnum.MINOR),
"ofi": sum(1 for f in findings if f.finding_type == FindingTypeEnum.OFI),
"positive": sum(1 for f in findings if f.finding_type == FindingTypeEnum.POSITIVE),
"open": sum(1 for f in findings if f.status != FindingStatusEnum.CLOSED),
"closed": sum(1 for f in findings if f.status == FindingStatusEnum.CLOSED),
"blocking_certification": sum(
1 for f in findings
if f.finding_type == FindingTypeEnum.MAJOR and f.status != FindingStatusEnum.CLOSED
),
}
def close(
self,
finding_id: str,
closed_by: str,
closure_notes: str,
verification_method: Optional[str] = None,
verification_evidence: Optional[str] = None,
) -> Optional[AuditFindingDB]:
"""Close a finding after verification."""
finding = self.get_by_id(finding_id)
if not finding:
return None
finding.status = FindingStatusEnum.CLOSED
finding.closed_date = date.today()
finding.closed_by = closed_by
finding.closure_notes = closure_notes
finding.verification_method = verification_method
finding.verification_evidence = verification_evidence
finding.verified_by = closed_by
finding.verified_at = datetime.now(timezone.utc)
self.db.commit()
self.db.refresh(finding)
return finding
class CorrectiveActionRepository:
"""Repository for Corrective/Preventive Actions (CAPA)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
finding_id: str,
capa_type: CAPATypeEnum,
title: str,
description: str,
assigned_to: str,
planned_completion: date,
expected_outcome: Optional[str] = None,
effectiveness_criteria: Optional[str] = None,
) -> CorrectiveActionDB:
"""Create a new CAPA."""
# Generate CAPA ID
year = date.today().year
existing_count = self.db.query(CorrectiveActionDB).filter(
CorrectiveActionDB.capa_id.like(f"CAPA-{year}-%")
).count()
capa_id = f"CAPA-{year}-{existing_count + 1:03d}"
capa = CorrectiveActionDB(
id=str(uuid.uuid4()),
capa_id=capa_id,
finding_id=finding_id,
capa_type=capa_type,
title=title,
description=description,
expected_outcome=expected_outcome,
assigned_to=assigned_to,
planned_completion=planned_completion,
effectiveness_criteria=effectiveness_criteria,
status="planned",
)
self.db.add(capa)
# Update finding status
finding = self.db.query(AuditFindingDB).filter(AuditFindingDB.id == finding_id).first()
if finding:
finding.status = FindingStatusEnum.CORRECTIVE_ACTION_PENDING
self.db.commit()
self.db.refresh(capa)
return capa
def get_by_id(self, capa_id: str) -> Optional[CorrectiveActionDB]:
"""Get CAPA by UUID or capa_id."""
return self.db.query(CorrectiveActionDB).filter(
(CorrectiveActionDB.id == capa_id) | (CorrectiveActionDB.capa_id == capa_id)
).first()
def get_by_finding(self, finding_id: str) -> List[CorrectiveActionDB]:
"""Get all CAPAs for a finding."""
return self.db.query(CorrectiveActionDB).filter(
CorrectiveActionDB.finding_id == finding_id
).order_by(CorrectiveActionDB.planned_completion).all()
def verify(
self,
capa_id: str,
verified_by: str,
is_effective: bool,
effectiveness_notes: Optional[str] = None,
) -> Optional[CorrectiveActionDB]:
"""Verify a completed CAPA."""
capa = self.get_by_id(capa_id)
if not capa:
return None
capa.effectiveness_verified = is_effective
capa.effectiveness_verification_date = date.today()
capa.effectiveness_notes = effectiveness_notes
capa.status = "verified" if is_effective else "completed"
# If verified, check if all CAPAs for finding are verified
if is_effective:
finding = self.db.query(AuditFindingDB).filter(
AuditFindingDB.id == capa.finding_id
).first()
if finding:
unverified = self.db.query(CorrectiveActionDB).filter(
CorrectiveActionDB.finding_id == finding.id,
CorrectiveActionDB.id != capa.id,
CorrectiveActionDB.status != "verified"
).count()
if unverified == 0:
finding.status = FindingStatusEnum.VERIFICATION_PENDING
self.db.commit()
self.db.refresh(capa)
return capa
class ManagementReviewRepository:
"""Repository for Management Reviews (ISO 27001 Chapter 9.3)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
title: str,
review_date: date,
chairperson: str,
review_period_start: Optional[date] = None,
review_period_end: Optional[date] = None,
) -> ManagementReviewDB:
"""Create a new management review."""
# Generate review ID
year = review_date.year
quarter = (review_date.month - 1) // 3 + 1
review_id = f"MR-{year}-Q{quarter}"
# Check for duplicate
existing = self.db.query(ManagementReviewDB).filter(
ManagementReviewDB.review_id == review_id
).first()
if existing:
review_id = f"{review_id}-{str(uuid.uuid4())[:4]}"
review = ManagementReviewDB(
id=str(uuid.uuid4()),
review_id=review_id,
title=title,
review_date=review_date,
review_period_start=review_period_start,
review_period_end=review_period_end,
chairperson=chairperson,
status="draft",
)
self.db.add(review)
self.db.commit()
self.db.refresh(review)
return review
def get_by_id(self, review_id: str) -> Optional[ManagementReviewDB]:
"""Get review by UUID or review_id."""
return self.db.query(ManagementReviewDB).filter(
(ManagementReviewDB.id == review_id) | (ManagementReviewDB.review_id == review_id)
).first()
def get_latest_approved(self) -> Optional[ManagementReviewDB]:
"""Get the most recent approved management review."""
return self.db.query(ManagementReviewDB).filter(
ManagementReviewDB.status == "approved"
).order_by(ManagementReviewDB.review_date.desc()).first()
def approve(
self,
review_id: str,
approved_by: str,
next_review_date: date,
minutes_document_path: Optional[str] = None,
) -> Optional[ManagementReviewDB]:
"""Approve a management review."""
review = self.get_by_id(review_id)
if not review:
return None
review.status = "approved"
review.approved_by = approved_by
review.approved_at = datetime.now(timezone.utc)
review.next_review_date = next_review_date
review.minutes_document_path = minutes_document_path
self.db.commit()
self.db.refresh(review)
return review
class InternalAuditRepository:
"""Repository for Internal Audits (ISO 27001 Chapter 9.2)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
title: str,
audit_type: str,
planned_date: date,
lead_auditor: str,
scope_description: Optional[str] = None,
iso_chapters_covered: Optional[List[str]] = None,
annex_a_controls_covered: Optional[List[str]] = None,
) -> InternalAuditDB:
"""Create a new internal audit."""
# Generate audit ID
year = planned_date.year
existing_count = self.db.query(InternalAuditDB).filter(
InternalAuditDB.audit_id.like(f"IA-{year}-%")
).count()
audit_id = f"IA-{year}-{existing_count + 1:03d}"
audit = InternalAuditDB(
id=str(uuid.uuid4()),
audit_id=audit_id,
title=title,
audit_type=audit_type,
scope_description=scope_description,
iso_chapters_covered=iso_chapters_covered,
annex_a_controls_covered=annex_a_controls_covered,
planned_date=planned_date,
lead_auditor=lead_auditor,
status="planned",
)
self.db.add(audit)
self.db.commit()
self.db.refresh(audit)
return audit
def get_by_id(self, audit_id: str) -> Optional[InternalAuditDB]:
"""Get audit by UUID or audit_id."""
return self.db.query(InternalAuditDB).filter(
(InternalAuditDB.id == audit_id) | (InternalAuditDB.audit_id == audit_id)
).first()
def get_latest_completed(self) -> Optional[InternalAuditDB]:
"""Get the most recent completed internal audit."""
return self.db.query(InternalAuditDB).filter(
InternalAuditDB.status == "completed"
).order_by(InternalAuditDB.actual_end_date.desc()).first()
def complete(
self,
audit_id: str,
audit_conclusion: str,
overall_assessment: str,
follow_up_audit_required: bool = False,
) -> Optional[InternalAuditDB]:
"""Complete an internal audit."""
audit = self.get_by_id(audit_id)
if not audit:
return None
audit.status = "completed"
audit.actual_end_date = date.today()
audit.report_date = date.today()
audit.audit_conclusion = audit_conclusion
audit.overall_assessment = overall_assessment
audit.follow_up_audit_required = follow_up_audit_required
self.db.commit()
self.db.refresh(audit)
return audit
class AuditTrailRepository:
"""Repository for Audit Trail entries."""
def __init__(self, db: DBSession):
self.db = db
def log(
self,
entity_type: str,
entity_id: str,
entity_name: str,
action: str,
performed_by: str,
field_changed: Optional[str] = None,
old_value: Optional[str] = None,
new_value: Optional[str] = None,
change_summary: Optional[str] = None,
) -> AuditTrailDB:
"""Log an audit trail entry."""
import hashlib
entry = AuditTrailDB(
id=str(uuid.uuid4()),
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
action=action,
field_changed=field_changed,
old_value=old_value,
new_value=new_value,
change_summary=change_summary,
performed_by=performed_by,
performed_at=datetime.now(timezone.utc),
checksum=hashlib.sha256(
f"{entity_type}|{entity_id}|{action}|{performed_by}".encode()
).hexdigest(),
)
self.db.add(entry)
self.db.commit()
self.db.refresh(entry)
return entry
def get_by_entity(
self,
entity_type: str,
entity_id: str,
limit: int = 100,
) -> List[AuditTrailDB]:
"""Get audit trail for a specific entity."""
return self.db.query(AuditTrailDB).filter(
AuditTrailDB.entity_type == entity_type,
AuditTrailDB.entity_id == entity_id
).order_by(AuditTrailDB.performed_at.desc()).limit(limit).all()
def get_paginated(
self,
page: int = 1,
page_size: int = 50,
entity_type: Optional[str] = None,
entity_id: Optional[str] = None,
performed_by: Optional[str] = None,
action: Optional[str] = None,
) -> Tuple[List[AuditTrailDB], int]:
"""Get paginated audit trail with filters."""
query = self.db.query(AuditTrailDB)
if entity_type:
query = query.filter(AuditTrailDB.entity_type == entity_type)
if entity_id:
query = query.filter(AuditTrailDB.entity_id == entity_id)
if performed_by:
query = query.filter(AuditTrailDB.performed_by == performed_by)
if action:
query = query.filter(AuditTrailDB.action == action)
total = query.count()
entries = query.order_by(AuditTrailDB.performed_at.desc()).offset(
(page - 1) * page_size
).limit(page_size).all()
return entries, total
class ISMSReadinessCheckRepository:
"""Repository for ISMS Readiness Check results."""
def __init__(self, db: DBSession):
self.db = db
def save(self, check: ISMSReadinessCheckDB) -> ISMSReadinessCheckDB:
"""Save a readiness check result."""
self.db.add(check)
self.db.commit()
self.db.refresh(check)
return check
def get_latest(self) -> Optional[ISMSReadinessCheckDB]:
"""Get the most recent readiness check."""
return self.db.query(ISMSReadinessCheckDB).order_by(
ISMSReadinessCheckDB.check_date.desc()
).first()
def get_history(self, limit: int = 10) -> List[ISMSReadinessCheckDB]:
"""Get readiness check history."""
return self.db.query(ISMSReadinessCheckDB).order_by(
ISMSReadinessCheckDB.check_date.desc()
).limit(limit).all()