Files
breakpilot-compliance/backend-compliance/compliance/db/isms_governance_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

355 lines
12 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 ISMSScopeRepository:
"""Repository for ISMS Scope (ISO 27001 Chapter 4.3)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
scope_statement: str,
created_by: str,
included_locations: Optional[List[str]] = None,
included_processes: Optional[List[str]] = None,
included_services: Optional[List[str]] = None,
excluded_items: Optional[List[str]] = None,
exclusion_justification: Optional[str] = None,
organizational_boundary: Optional[str] = None,
physical_boundary: Optional[str] = None,
technical_boundary: Optional[str] = None,
) -> ISMSScopeDB:
"""Create a new ISMS scope definition."""
# Supersede existing scopes
existing = self.db.query(ISMSScopeDB).filter(
ISMSScopeDB.status != ApprovalStatusEnum.SUPERSEDED
).all()
for s in existing:
s.status = ApprovalStatusEnum.SUPERSEDED
scope = ISMSScopeDB(
id=str(uuid.uuid4()),
scope_statement=scope_statement,
included_locations=included_locations,
included_processes=included_processes,
included_services=included_services,
excluded_items=excluded_items,
exclusion_justification=exclusion_justification,
organizational_boundary=organizational_boundary,
physical_boundary=physical_boundary,
technical_boundary=technical_boundary,
status=ApprovalStatusEnum.DRAFT,
created_by=created_by,
)
self.db.add(scope)
self.db.commit()
self.db.refresh(scope)
return scope
def get_current(self) -> Optional[ISMSScopeDB]:
"""Get the current (non-superseded) ISMS scope."""
return self.db.query(ISMSScopeDB).filter(
ISMSScopeDB.status != ApprovalStatusEnum.SUPERSEDED
).order_by(ISMSScopeDB.created_at.desc()).first()
def get_by_id(self, scope_id: str) -> Optional[ISMSScopeDB]:
"""Get scope by ID."""
return self.db.query(ISMSScopeDB).filter(ISMSScopeDB.id == scope_id).first()
def approve(
self,
scope_id: str,
approved_by: str,
effective_date: date,
review_date: date,
) -> Optional[ISMSScopeDB]:
"""Approve the ISMS scope."""
scope = self.get_by_id(scope_id)
if not scope:
return None
import hashlib
scope.status = ApprovalStatusEnum.APPROVED
scope.approved_by = approved_by
scope.approved_at = datetime.now(timezone.utc)
scope.effective_date = effective_date
scope.review_date = review_date
scope.approval_signature = hashlib.sha256(
f"{scope.scope_statement}|{approved_by}|{datetime.now(timezone.utc).isoformat()}".encode()
).hexdigest()
self.db.commit()
self.db.refresh(scope)
return scope
class ISMSPolicyRepository:
"""Repository for ISMS Policies (ISO 27001 Chapter 5.2)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
policy_id: str,
title: str,
policy_type: str,
authored_by: str,
description: Optional[str] = None,
policy_text: Optional[str] = None,
applies_to: Optional[List[str]] = None,
review_frequency_months: int = 12,
related_controls: Optional[List[str]] = None,
) -> ISMSPolicyDB:
"""Create a new ISMS policy."""
policy = ISMSPolicyDB(
id=str(uuid.uuid4()),
policy_id=policy_id,
title=title,
policy_type=policy_type,
description=description,
policy_text=policy_text,
applies_to=applies_to,
review_frequency_months=review_frequency_months,
related_controls=related_controls,
authored_by=authored_by,
status=ApprovalStatusEnum.DRAFT,
)
self.db.add(policy)
self.db.commit()
self.db.refresh(policy)
return policy
def get_by_id(self, policy_id: str) -> Optional[ISMSPolicyDB]:
"""Get policy by UUID or policy_id."""
return self.db.query(ISMSPolicyDB).filter(
(ISMSPolicyDB.id == policy_id) | (ISMSPolicyDB.policy_id == policy_id)
).first()
def get_all(
self,
policy_type: Optional[str] = None,
status: Optional[ApprovalStatusEnum] = None,
) -> List[ISMSPolicyDB]:
"""Get all policies with optional filters."""
query = self.db.query(ISMSPolicyDB)
if policy_type:
query = query.filter(ISMSPolicyDB.policy_type == policy_type)
if status:
query = query.filter(ISMSPolicyDB.status == status)
return query.order_by(ISMSPolicyDB.policy_id).all()
def get_master_policy(self) -> Optional[ISMSPolicyDB]:
"""Get the approved master ISMS policy."""
return self.db.query(ISMSPolicyDB).filter(
ISMSPolicyDB.policy_type == "master",
ISMSPolicyDB.status == ApprovalStatusEnum.APPROVED
).first()
def approve(
self,
policy_id: str,
approved_by: str,
reviewed_by: str,
effective_date: date,
) -> Optional[ISMSPolicyDB]:
"""Approve a policy."""
policy = self.get_by_id(policy_id)
if not policy:
return None
import hashlib
policy.status = ApprovalStatusEnum.APPROVED
policy.reviewed_by = reviewed_by
policy.approved_by = approved_by
policy.approved_at = datetime.now(timezone.utc)
policy.effective_date = effective_date
policy.next_review_date = date(
effective_date.year + (policy.review_frequency_months // 12),
effective_date.month,
effective_date.day
)
policy.approval_signature = hashlib.sha256(
f"{policy.policy_id}|{approved_by}|{datetime.now(timezone.utc).isoformat()}".encode()
).hexdigest()
self.db.commit()
self.db.refresh(policy)
return policy
class SecurityObjectiveRepository:
"""Repository for Security Objectives (ISO 27001 Chapter 6.2)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
objective_id: str,
title: str,
description: str,
category: str,
owner: str,
kpi_name: Optional[str] = None,
kpi_target: Optional[float] = None,
kpi_unit: Optional[str] = None,
target_date: Optional[date] = None,
related_controls: Optional[List[str]] = None,
) -> SecurityObjectiveDB:
"""Create a new security objective."""
objective = SecurityObjectiveDB(
id=str(uuid.uuid4()),
objective_id=objective_id,
title=title,
description=description,
category=category,
kpi_name=kpi_name,
kpi_target=kpi_target,
kpi_unit=kpi_unit,
owner=owner,
target_date=target_date,
related_controls=related_controls,
status="active",
)
self.db.add(objective)
self.db.commit()
self.db.refresh(objective)
return objective
def get_by_id(self, objective_id: str) -> Optional[SecurityObjectiveDB]:
"""Get objective by UUID or objective_id."""
return self.db.query(SecurityObjectiveDB).filter(
(SecurityObjectiveDB.id == objective_id) |
(SecurityObjectiveDB.objective_id == objective_id)
).first()
def get_all(
self,
category: Optional[str] = None,
status: Optional[str] = None,
) -> List[SecurityObjectiveDB]:
"""Get all objectives with optional filters."""
query = self.db.query(SecurityObjectiveDB)
if category:
query = query.filter(SecurityObjectiveDB.category == category)
if status:
query = query.filter(SecurityObjectiveDB.status == status)
return query.order_by(SecurityObjectiveDB.objective_id).all()
def update_progress(
self,
objective_id: str,
kpi_current: float,
) -> Optional[SecurityObjectiveDB]:
"""Update objective progress."""
objective = self.get_by_id(objective_id)
if not objective:
return None
objective.kpi_current = kpi_current
if objective.kpi_target:
objective.progress_percentage = min(100, (kpi_current / objective.kpi_target) * 100)
# Auto-mark as achieved if 100%
if objective.progress_percentage >= 100 and objective.status == "active":
objective.status = "achieved"
objective.achieved_date = date.today()
self.db.commit()
self.db.refresh(objective)
return objective
class StatementOfApplicabilityRepository:
"""Repository for Statement of Applicability (SoA)."""
def __init__(self, db: DBSession):
self.db = db
def create(
self,
annex_a_control: str,
annex_a_title: str,
annex_a_category: str,
is_applicable: bool = True,
applicability_justification: Optional[str] = None,
implementation_status: str = "planned",
breakpilot_control_ids: Optional[List[str]] = None,
) -> StatementOfApplicabilityDB:
"""Create a new SoA entry."""
entry = StatementOfApplicabilityDB(
id=str(uuid.uuid4()),
annex_a_control=annex_a_control,
annex_a_title=annex_a_title,
annex_a_category=annex_a_category,
is_applicable=is_applicable,
applicability_justification=applicability_justification,
implementation_status=implementation_status,
breakpilot_control_ids=breakpilot_control_ids or [],
)
self.db.add(entry)
self.db.commit()
self.db.refresh(entry)
return entry
def get_by_control(self, annex_a_control: str) -> Optional[StatementOfApplicabilityDB]:
"""Get SoA entry by Annex A control ID (e.g., 'A.5.1')."""
return self.db.query(StatementOfApplicabilityDB).filter(
StatementOfApplicabilityDB.annex_a_control == annex_a_control
).first()
def get_all(
self,
is_applicable: Optional[bool] = None,
implementation_status: Optional[str] = None,
category: Optional[str] = None,
) -> List[StatementOfApplicabilityDB]:
"""Get all SoA entries with optional filters."""
query = self.db.query(StatementOfApplicabilityDB)
if is_applicable is not None:
query = query.filter(StatementOfApplicabilityDB.is_applicable == is_applicable)
if implementation_status:
query = query.filter(StatementOfApplicabilityDB.implementation_status == implementation_status)
if category:
query = query.filter(StatementOfApplicabilityDB.annex_a_category == category)
return query.order_by(StatementOfApplicabilityDB.annex_a_control).all()
def get_statistics(self) -> Dict[str, Any]:
"""Get SoA statistics."""
entries = self.get_all()
total = len(entries)
applicable = sum(1 for e in entries if e.is_applicable)
implemented = sum(1 for e in entries if e.implementation_status == "implemented")
approved = sum(1 for e in entries if e.approved_at)
return {
"total": total,
"applicable": applicable,
"not_applicable": total - applicable,
"implemented": implemented,
"planned": sum(1 for e in entries if e.implementation_status == "planned"),
"approved": approved,
"pending_approval": total - approved,
"implementation_rate": round((implemented / applicable * 100) if applicable > 0 else 0, 1),
}