From ec26183e2e7160deae6930095851d2325c1fe1c3 Mon Sep 17 00:00:00 2001 From: kitos Date: Tue, 9 Jun 2026 16:40:14 +0200 Subject: [PATCH] refactor(pep8): enforce full PEP8 compliance across backend Python codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ruff.toml: select E/W/F/I/N rules, line-length=120, drop legacy ignores - Auto-fix: sort 82 import blocks (isort), remove 29 unused imports, strip 6 trailing-whitespace blank lines in docstrings - main.py: move setup_logging and settings imports to top (E402) - errors.py: noqa N818 on DDD exception names (96 call sites, safe) - intel_service.py: noqa N817 for universal ET alias - atomic/elastic/sigma import services: move _MAX_UNCOMPRESSED_SIZE and _MAX_ENTRIES to module level (N806) - compliance_import_service.py: move SAMPLE_CONTROLS / CIS_CONTROLS to module level; wrap long description strings (N806 + E501) - snapshot_service.py: move STATUS_ORDER dict to module level (N806) - sigma_import_service.py: remove dead dedup_key expression (F841) - threat_actor_import_service.py: remove dead stix_to_actor expression (F841) - data_source.py, seed_demo.py, campaign_scheduler_service.py, lolbas_import_service.py: wrap lines exceeding 120 chars (E501) - d3fend_import_service.py: per-file E501 ignore (data file with long strings) All 439 unit tests pass. ruff check app/ → All checks passed! Co-Authored-By: Claude Sonnet 4.6 --- backend/app/database.py | 2 +- backend/app/domain/errors.py | 6 +- .../ports/repositories/test_repository.py | 2 +- backend/app/domain/test_entity.py | 1 - .../persistence/mappers/technique_mapper.py | 1 - backend/app/jobs/mitre_sync_job.py | 14 +- backend/app/main.py | 72 ++-- backend/app/models/__init__.py | 38 +- backend/app/models/audit.py | 7 +- backend/app/models/campaign.py | 14 +- backend/app/models/compliance.py | 12 +- backend/app/models/coverage_snapshot.py | 11 +- backend/app/models/data_source.py | 8 +- backend/app/models/defensive_technique.py | 11 +- backend/app/models/detection_rule.py | 5 +- backend/app/models/evidence.py | 7 +- backend/app/models/intel.py | 5 +- backend/app/models/jira_link.py | 6 +- backend/app/models/notification.py | 3 +- backend/app/models/osint_item.py | 5 +- backend/app/models/scoring_config.py | 2 +- backend/app/models/technique.py | 7 +- backend/app/models/test.py | 16 +- backend/app/models/test_detection_result.py | 11 +- backend/app/models/test_template.py | 3 +- .../models/test_template_detection_rule.py | 3 +- backend/app/models/threat_actor.py | 14 +- backend/app/models/user.py | 5 +- backend/app/models/worklog.py | 5 +- backend/app/routers/auth.py | 17 +- backend/app/routers/campaigns.py | 32 +- backend/app/routers/compliance.py | 12 +- backend/app/routers/d3fend.py | 8 +- backend/app/routers/data_sources.py | 4 +- backend/app/routers/detection_rules.py | 9 +- backend/app/routers/evidence.py | 6 +- backend/app/routers/jira.py | 2 +- backend/app/routers/notifications.py | 6 +- backend/app/routers/operational_metrics.py | 3 +- backend/app/routers/osint.py | 6 +- backend/app/routers/professional_reports.py | 2 +- backend/app/routers/scores.py | 13 +- backend/app/routers/snapshots.py | 23 +- backend/app/routers/system.py | 8 +- backend/app/routers/techniques.py | 4 +- backend/app/routers/test_templates.py | 12 +- backend/app/routers/tests.py | 61 ++- backend/app/routers/users.py | 2 +- backend/app/routers/worklogs.py | 2 +- backend/app/schemas/__init__.py | 18 +- backend/app/schemas/technique.py | 1 - backend/app/schemas/test.py | 1 - backend/app/schemas/test_template.py | 1 - backend/app/schemas/user.py | 3 +- backend/app/seed_demo.py | 22 +- .../app/services/advanced_metrics_service.py | 2 +- backend/app/services/atomic_import_service.py | 10 +- .../app/services/caldera_import_service.py | 2 +- backend/app/services/campaign_crud_service.py | 9 +- .../services/campaign_scheduler_service.py | 12 +- backend/app/services/campaign_service.py | 8 +- .../app/services/compliance_import_service.py | 357 ++++++++++++------ backend/app/services/compliance_service.py | 3 +- backend/app/services/d3fend_import_service.py | 6 +- backend/app/services/data_source_service.py | 2 +- .../app/services/detection_rule_service.py | 5 +- .../app/services/elastic_import_service.py | 11 +- backend/app/services/heatmap_service.py | 2 - backend/app/services/intel_service.py | 2 +- backend/app/services/lolbas_import_service.py | 8 +- backend/app/services/mitre_sync_service.py | 2 +- backend/app/services/notification_service.py | 3 +- .../services/operational_metrics_service.py | 10 +- backend/app/services/report_engine.py | 4 +- .../app/services/report_generation_service.py | 5 +- backend/app/services/score_cache.py | 8 +- backend/app/services/scoring_service.py | 8 +- backend/app/services/sigma_import_service.py | 18 +- backend/app/services/snapshot_service.py | 24 +- backend/app/services/test_crud_service.py | 2 +- backend/app/services/test_workflow_service.py | 7 +- .../services/threat_actor_import_service.py | 7 +- backend/app/services/threat_actor_service.py | 3 +- backend/app/services/user_service.py | 6 +- backend/ruff.toml | 24 +- 85 files changed, 712 insertions(+), 432 deletions(-) diff --git a/backend/app/database.py b/backend/app/database.py index 0c6adb0..d84b999 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,5 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() diff --git a/backend/app/domain/errors.py b/backend/app/domain/errors.py index e23f0d5..761eff4 100644 --- a/backend/app/domain/errors.py +++ b/backend/app/domain/errors.py @@ -46,7 +46,7 @@ class DuplicateEntityError(DomainError): # ── State machine ──────────────────────────────────────────────────── -class InvalidStateTransition(DomainError): +class InvalidStateTransition(DomainError): # noqa: N818 — DDD term, renaming would break 96 call sites """A state-machine transition is not allowed.""" def __init__( @@ -67,7 +67,7 @@ class InvalidStateTransition(DomainError): # ── Business rules ──────────────────────────────────────────────────── -class BusinessRuleViolation(DomainError): +class BusinessRuleViolation(DomainError): # noqa: N818 — DDD term, renaming would break 96 call sites """An operation violates a business invariant.""" def __init__(self, message: str) -> None: @@ -89,7 +89,7 @@ class InvalidOperationError(BusinessRuleViolation): # ── Authorization ──────────────────────────────────────────────────── -class PermissionViolation(DomainError): +class PermissionViolation(DomainError): # noqa: N818 — DDD term, renaming would break 96 call sites """The user lacks permissions for an action.""" def __init__(self, message: str = "Insufficient permissions") -> None: diff --git a/backend/app/domain/ports/repositories/test_repository.py b/backend/app/domain/ports/repositories/test_repository.py index 79b6a26..a63289a 100644 --- a/backend/app/domain/ports/repositories/test_repository.py +++ b/backend/app/domain/ports/repositories/test_repository.py @@ -6,7 +6,7 @@ This is a domain contract — implementations live in infrastructure/. from __future__ import annotations import uuid -from typing import Protocol, runtime_checkable +from typing import Protocol from app.domain.enums import TestState diff --git a/backend/app/domain/test_entity.py b/backend/app/domain/test_entity.py index 182d64b..e91cdca 100644 --- a/backend/app/domain/test_entity.py +++ b/backend/app/domain/test_entity.py @@ -34,7 +34,6 @@ from app.domain.errors import ( InvalidStateTransition, ) - # ── Value objects ──────────────────────────────────────────────────── diff --git a/backend/app/infrastructure/persistence/mappers/technique_mapper.py b/backend/app/infrastructure/persistence/mappers/technique_mapper.py index 74cd588..42b6049 100644 --- a/backend/app/infrastructure/persistence/mappers/technique_mapper.py +++ b/backend/app/infrastructure/persistence/mappers/technique_mapper.py @@ -3,7 +3,6 @@ from __future__ import annotations from app.domain.entities.technique import TechniqueEntity -from app.domain.enums import TechniqueStatus class TechniqueMapper: diff --git a/backend/app/jobs/mitre_sync_job.py b/backend/app/jobs/mitre_sync_job.py index 45a9680..c97fd33 100644 --- a/backend/app/jobs/mitre_sync_job.py +++ b/backend/app/jobs/mitre_sync_job.py @@ -15,15 +15,15 @@ import logging from apscheduler.schedulers.background import BackgroundScheduler from app.database import SessionLocal -from app.services.mitre_sync_service import sync_mitre -from app.services.intel_service import scan_intel -from app.services.notification_service import cleanup_old_notifications -from app.services.snapshot_service import create_snapshot, cleanup_old_snapshots -from app.services.campaign_scheduler_service import check_and_run_recurring_campaigns from app.jobs.jira_sync_job import sync_all_jira_links -from app.services.osint_enrichment_service import enrich_all_techniques -from app.services.stale_detection_service import detect_stale_coverage from app.jobs.retention_job import run_retention_job +from app.services.campaign_scheduler_service import check_and_run_recurring_campaigns +from app.services.intel_service import scan_intel +from app.services.mitre_sync_service import sync_mitre +from app.services.notification_service import cleanup_old_notifications +from app.services.osint_enrichment_service import enrich_all_techniques +from app.services.snapshot_service import cleanup_old_snapshots, create_snapshot +from app.services.stale_detection_service import detect_stale_coverage logger = logging.getLogger(__name__) diff --git a/backend/app/main.py b/backend/app/main.py index c67b735..a0af92f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,55 +3,55 @@ import os from contextlib import asynccontextmanager from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from fastapi.exceptions import RequestValidationError from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy.exc import SQLAlchemyError -from app.routers import auth as auth_router -from app.routers import techniques as techniques_router -from app.routers import tests as tests_router -from app.routers import evidence as evidence_router -from app.routers import test_templates as test_templates_router -from app.routers import system as system_router -from app.routers import metrics as metrics_router -from app.routers import users as users_router -from app.routers import audit as audit_router -from app.routers import notifications as notifications_router -from app.routers import reports as reports_router -from app.routers import data_sources as data_sources_router -from app.routers import threat_actors as threat_actors_router -from app.routers import d3fend as d3fend_router -from app.routers import detection_rules as detection_rules_router -from app.routers import campaigns as campaigns_router -from app.routers import heatmap as heatmap_router -from app.routers import scores as scores_router -from app.routers import operational_metrics as operational_metrics_router -from app.routers import compliance as compliance_router -from app.routers import snapshots as snapshots_router -from app.routers import jira as jira_router -from app.routers import worklogs as worklogs_router -from app.routers import professional_reports as professional_reports_router -from app.routers import analytics as analytics_router -from app.routers import advanced_metrics as advanced_metrics_router -from app.routers import osint as osint_router +from app.config import settings as _settings from app.domain.errors import DomainError +from app.jobs.mitre_sync_job import scheduler, start_scheduler +from app.limiter import limiter +from app.logging_config import setup_logging from app.middleware.error_handler import domain_exception_handler from app.middleware.request_context import RequestContextMiddleware -from app.limiter import limiter +from app.routers import advanced_metrics as advanced_metrics_router +from app.routers import analytics as analytics_router +from app.routers import audit as audit_router +from app.routers import auth as auth_router +from app.routers import campaigns as campaigns_router +from app.routers import compliance as compliance_router +from app.routers import d3fend as d3fend_router +from app.routers import data_sources as data_sources_router +from app.routers import detection_rules as detection_rules_router +from app.routers import evidence as evidence_router +from app.routers import heatmap as heatmap_router +from app.routers import jira as jira_router +from app.routers import metrics as metrics_router +from app.routers import notifications as notifications_router +from app.routers import operational_metrics as operational_metrics_router +from app.routers import osint as osint_router +from app.routers import professional_reports as professional_reports_router +from app.routers import reports as reports_router +from app.routers import scores as scores_router +from app.routers import snapshots as snapshots_router +from app.routers import system as system_router +from app.routers import techniques as techniques_router +from app.routers import test_templates as test_templates_router +from app.routers import tests as tests_router +from app.routers import threat_actors as threat_actors_router +from app.routers import users as users_router +from app.routers import worklogs as worklogs_router from app.storage import ensure_bucket_exists -from app.jobs.mitre_sync_job import start_scheduler, scheduler + +# Configure structured logging before any module initialises its own logger +setup_logging() # ── Environment detection ───────────────────────────────────────────────── _IS_PRODUCTION = os.environ.get("AEGIS_ENV", "").lower() == "production" -# ── Logging ─────────────────────────────────────────────────────────────── -from app.logging_config import setup_logging - -setup_logging() - @asynccontextmanager async def lifespan(app: FastAPI): """Startup / shutdown logic.""" @@ -81,8 +81,6 @@ app.add_middleware(RequestContextMiddleware) app.add_exception_handler(DomainError, domain_exception_handler) # ── CORS ────────────────────────────────────────────────────────────────── -from app.config import settings as _settings - _cors_origins: list[str] = [ o.strip() for o in _settings.CORS_ORIGINS.split(",") if o.strip() ] diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 2780d6d..1de7c9e 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,26 +1,30 @@ # Import all models here so Alembic can detect them -from app.models.user import User -from app.models.technique import Technique -from app.models.test import Test -from app.models.test_template import TestTemplate +from app.models.audit import AuditLog +from app.models.campaign import Campaign, CampaignTest +from app.models.compliance import ( + ComplianceControl, + ComplianceControlMapping, + ComplianceFramework, +) +from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState +from app.models.data_source import DataSource +from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping +from app.models.detection_rule import DetectionRule +from app.models.enums import TeamSide, TechniqueStatus, TestResult, TestState from app.models.evidence import Evidence from app.models.intel import IntelItem -from app.models.audit import AuditLog -from app.models.notification import Notification -from app.models.data_source import DataSource -from app.models.detection_rule import DetectionRule -from app.models.threat_actor import ThreatActor, ThreatActorTechnique -from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping -from app.models.test_template_detection_rule import TestTemplateDetectionRule -from app.models.test_detection_result import TestDetectionResult -from app.models.campaign import Campaign, CampaignTest -from app.models.compliance import ComplianceFramework, ComplianceControl, ComplianceControlMapping -from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState from app.models.jira_link import JiraLink, JiraLinkEntityType, JiraSyncDirection -from app.models.worklog import Worklog +from app.models.notification import Notification from app.models.osint_item import OsintItem from app.models.scoring_config import ScoringConfig -from app.models.enums import TechniqueStatus, TestState, TestResult, TeamSide +from app.models.technique import Technique +from app.models.test import Test +from app.models.test_detection_result import TestDetectionResult +from app.models.test_template import TestTemplate +from app.models.test_template_detection_rule import TestTemplateDetectionRule +from app.models.threat_actor import ThreatActor, ThreatActorTechnique +from app.models.user import User +from app.models.worklog import Worklog __all__ = [ "User", "Technique", "Test", "TestTemplate", "Evidence", diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py index dda16b5..94bde89 100644 --- a/backend/app/models/audit.py +++ b/backend/app/models/audit.py @@ -1,6 +1,7 @@ import uuid -from sqlalchemy import Column, String, DateTime, ForeignKey, Index, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Column, DateTime, ForeignKey, Index, String, func +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base @@ -9,7 +10,7 @@ from app.database import Base class AuditLog(Base): """ Audit log model for tracking all system actions. - + Records user actions, entity changes, and system events for security auditing and compliance purposes. """ diff --git a/backend/app/models/campaign.py b/backend/app/models/campaign.py index 464972f..fdaaaef 100644 --- a/backend/app/models/campaign.py +++ b/backend/app/models/campaign.py @@ -5,11 +5,19 @@ enabling simulation of complete attack chains and APT emulations. """ import uuid + from sqlalchemy import ( - Column, String, Text, Integer, Boolean, DateTime, - ForeignKey, Index, func, + Boolean, + Column, + DateTime, + ForeignKey, + Index, + Integer, + String, + Text, + func, ) -from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base diff --git a/backend/app/models/compliance.py b/backend/app/models/compliance.py index 4ad2a8e..8db90e3 100644 --- a/backend/app/models/compliance.py +++ b/backend/app/models/compliance.py @@ -5,9 +5,17 @@ MITRE ATT&CK techniques, enabling compliance gap analysis. """ import uuid + from sqlalchemy import ( - Column, String, Text, Boolean, DateTime, - ForeignKey, Index, UniqueConstraint, func, + Boolean, + Column, + DateTime, + ForeignKey, + Index, + String, + Text, + UniqueConstraint, + func, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/coverage_snapshot.py b/backend/app/models/coverage_snapshot.py index 42fed60..6fe1463 100644 --- a/backend/app/models/coverage_snapshot.py +++ b/backend/app/models/coverage_snapshot.py @@ -6,9 +6,16 @@ per technique per snapshot) to avoid bloated JSONB fields. """ import uuid + from sqlalchemy import ( - Column, String, Float, Integer, DateTime, - ForeignKey, Index, func, + Column, + DateTime, + Float, + ForeignKey, + Index, + Integer, + String, + func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/data_source.py b/backend/app/models/data_source.py index 60e0da3..7aca2c4 100644 --- a/backend/app/models/data_source.py +++ b/backend/app/models/data_source.py @@ -1,8 +1,9 @@ """DataSource model — registry of external data sources for import.""" import uuid -from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Boolean, Column, DateTime, Index, String, Text, func +from sqlalchemy.dialects.postgresql import JSONB, UUID from app.database import Base @@ -20,7 +21,8 @@ class DataSource(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String, unique=True, nullable=False) # e.g. "atomic_red_team" display_name = Column(String, nullable=False) # e.g. "Atomic Red Team" - type = Column(String, nullable=False) # attack_procedure / detection_rule / threat_intel / defensive_technique + # Values: attack_procedure / detection_rule / threat_intel / defensive_technique + type = Column(String, nullable=False) url = Column(String, nullable=True) # URL base of repo/API description = Column(Text, nullable=True) is_enabled = Column(Boolean, default=True) diff --git a/backend/app/models/defensive_technique.py b/backend/app/models/defensive_technique.py index afff7b7..1414877 100644 --- a/backend/app/models/defensive_technique.py +++ b/backend/app/models/defensive_technique.py @@ -5,9 +5,16 @@ ATT&CK techniques, enabling recommended countermeasure lookups. """ import uuid + from sqlalchemy import ( - Column, String, Text, DateTime, - ForeignKey, Index, UniqueConstraint, func, + Column, + DateTime, + ForeignKey, + Index, + String, + Text, + UniqueConstraint, + func, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/detection_rule.py b/backend/app/models/detection_rule.py index c411415..8944201 100644 --- a/backend/app/models/detection_rule.py +++ b/backend/app/models/detection_rule.py @@ -1,8 +1,9 @@ """DetectionRule model — detection rules from multiple sources.""" import uuid -from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Boolean, Column, DateTime, Index, String, Text, func +from sqlalchemy.dialects.postgresql import JSONB, UUID from app.database import Base diff --git a/backend/app/models/evidence.py b/backend/app/models/evidence.py index 0f87db2..a754e77 100644 --- a/backend/app/models/evidence.py +++ b/backend/app/models/evidence.py @@ -1,5 +1,6 @@ import uuid -from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Enum, func + +from sqlalchemy import Column, DateTime, Enum, ForeignKey, String, Text, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship @@ -10,10 +11,10 @@ from app.models.enums import TeamSide class Evidence(Base): """ Evidence model for storing file metadata associated with tests. - + Files are stored in MinIO, and this model tracks the file location, integrity hash, and upload metadata. - + The ``team`` field distinguishes whether this evidence was uploaded by Red Team (attack evidence) or Blue Team (detection evidence). """ diff --git a/backend/app/models/intel.py b/backend/app/models/intel.py index 69056a2..5589ed0 100644 --- a/backend/app/models/intel.py +++ b/backend/app/models/intel.py @@ -1,5 +1,6 @@ import uuid -from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship @@ -9,7 +10,7 @@ from app.database import Base class IntelItem(Base): """ Intelligence item model for tracking threat intelligence related to techniques. - + Stores URLs and metadata from automated intel scans that may indicate new attack variations or detection bypasses for specific techniques. """ diff --git a/backend/app/models/jira_link.py b/backend/app/models/jira_link.py index 4f43728..e8f0481 100644 --- a/backend/app/models/jira_link.py +++ b/backend/app/models/jira_link.py @@ -2,8 +2,10 @@ import enum import uuid -from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, Index, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Column, DateTime, ForeignKey, Index, String, func +from sqlalchemy import Enum as SQLEnum +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base diff --git a/backend/app/models/notification.py b/backend/app/models/notification.py index 17e30a3..b58f337 100644 --- a/backend/app/models/notification.py +++ b/backend/app/models/notification.py @@ -1,7 +1,8 @@ """Notification model — in-app notifications for user actions.""" import uuid -from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index, func + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Index, String, Text, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/osint_item.py b/backend/app/models/osint_item.py index b8cea0a..c097c4e 100644 --- a/backend/app/models/osint_item.py +++ b/backend/app/models/osint_item.py @@ -1,8 +1,9 @@ """OSINT enrichment items — CVEs, blogs, PoCs, and advisories linked to techniques.""" import uuid -from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text, func +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base diff --git a/backend/app/models/scoring_config.py b/backend/app/models/scoring_config.py index ff8133d..4033f1c 100644 --- a/backend/app/models/scoring_config.py +++ b/backend/app/models/scoring_config.py @@ -2,7 +2,7 @@ import uuid -from sqlalchemy import Column, Float, DateTime, ForeignKey, func +from sqlalchemy import Column, DateTime, Float, ForeignKey, func from sqlalchemy.dialects.postgresql import UUID from app.database import Base diff --git a/backend/app/models/technique.py b/backend/app/models/technique.py index f2b9cb2..7c40eac 100644 --- a/backend/app/models/technique.py +++ b/backend/app/models/technique.py @@ -1,8 +1,7 @@ import uuid -from datetime import datetime -from sqlalchemy import Column, String, Text, Boolean, DateTime, Enum -from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy import Boolean, Column, DateTime, Enum, String, Text +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base @@ -12,7 +11,7 @@ from app.models.enums import TechniqueStatus class Technique(Base): """ MITRE ATT&CK Technique model. - + Represents an attack technique from the MITRE ATT&CK framework, including its coverage status and associated tests. """ diff --git a/backend/app/models/test.py b/backend/app/models/test.py index 2388495..9851db6 100644 --- a/backend/app/models/test.py +++ b/backend/app/models/test.py @@ -1,10 +1,22 @@ import uuid -from sqlalchemy import Column, String, Text, Boolean, Integer, DateTime, ForeignKey, Enum, Index, func + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Enum, + ForeignKey, + Index, + Integer, + String, + Text, + func, +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from app.database import Base -from app.models.enums import TestState, TestResult +from app.models.enums import TestResult, TestState class Test(Base): diff --git a/backend/app/models/test_detection_result.py b/backend/app/models/test_detection_result.py index 2897bbf..a002f3c 100644 --- a/backend/app/models/test_detection_result.py +++ b/backend/app/models/test_detection_result.py @@ -5,9 +5,16 @@ rule as triggered / not triggered / not applicable, along with notes. """ import uuid -from datetime import datetime -from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index, UniqueConstraint +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Index, + Text, + UniqueConstraint, +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/test_template.py b/backend/app/models/test_template.py index 262034b..7a4ccdc 100644 --- a/backend/app/models/test_template.py +++ b/backend/app/models/test_template.py @@ -1,7 +1,8 @@ """TestTemplate model — predefined test catalog entries.""" import uuid -from sqlalchemy import Column, String, Text, Boolean, DateTime, Index, func + +from sqlalchemy import Boolean, Column, DateTime, Index, String, Text, func from sqlalchemy.dialects.postgresql import UUID from app.database import Base diff --git a/backend/app/models/test_template_detection_rule.py b/backend/app/models/test_template_detection_rule.py index 8380821..89c4651 100644 --- a/backend/app/models/test_template_detection_rule.py +++ b/backend/app/models/test_template_detection_rule.py @@ -5,9 +5,8 @@ for a given test template / attack procedure. """ import uuid -from datetime import datetime -from sqlalchemy import Column, Boolean, ForeignKey, Index, UniqueConstraint +from sqlalchemy import Boolean, Column, ForeignKey, Index, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship diff --git a/backend/app/models/threat_actor.py b/backend/app/models/threat_actor.py index 5e1b6cd..d035c9e 100644 --- a/backend/app/models/threat_actor.py +++ b/backend/app/models/threat_actor.py @@ -5,11 +5,19 @@ techniques, imported from MITRE CTI (STIX 2.0). """ import uuid + from sqlalchemy import ( - Column, String, Text, Boolean, DateTime, - ForeignKey, Index, UniqueConstraint, func, + Boolean, + Column, + DateTime, + ForeignKey, + Index, + String, + Text, + UniqueConstraint, + func, ) -from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base diff --git a/backend/app/models/user.py b/backend/app/models/user.py index cb0324b..8630394 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,5 +1,6 @@ import uuid -from sqlalchemy import Column, String, Boolean, DateTime, func + +from sqlalchemy import Boolean, Column, DateTime, String, func from sqlalchemy.dialects.postgresql import UUID from app.database import Base @@ -8,7 +9,7 @@ from app.database import Base class User(Base): """ User model for authentication and authorization. - + Possible roles: - admin: Full system access - red_tech: Red team technician - can create and edit tests diff --git a/backend/app/models/worklog.py b/backend/app/models/worklog.py index 439b1d3..85fb2d1 100644 --- a/backend/app/models/worklog.py +++ b/backend/app/models/worklog.py @@ -1,8 +1,9 @@ """Worklog model — immutable internal time-tracking records.""" import uuid -from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Index, func -from sqlalchemy.dialects.postgresql import UUID, JSONB + +from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text, func +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from app.database import Base diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index b17584a..f8a6c59 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -11,11 +11,10 @@ import os from fastapi import APIRouter, Cookie, Depends, Request, Response from fastapi.security import OAuth2PasswordRequestForm +from jose import JWTError, jwt from sqlalchemy.orm import Session -from jose import jwt, JWTError - -from app.auth import create_access_token, blacklist_token, verify_password +from app.auth import blacklist_token, create_access_token, verify_password from app.config import settings from app.database import get_db from app.dependencies.auth import get_current_user @@ -24,13 +23,15 @@ from app.domain.unit_of_work import UnitOfWork from app.limiter import limiter from app.middleware.request_context import resolve_client_ip from app.models.user import User -from app.services.auth_service import ( - _DUMMY_HASH, - change_password as auth_change_password, -) -from app.services.audit_service import log_action from app.schemas.auth import TokenResponse, UserOut from app.schemas.user import PasswordChange +from app.services.audit_service import log_action +from app.services.auth_service import ( + _DUMMY_HASH, +) +from app.services.auth_service import ( + change_password as auth_change_password, +) router = APIRouter(prefix="/auth", tags=["auth"]) diff --git a/backend/app/routers/campaigns.py b/backend/app/routers/campaigns.py index 7eeeb61..9dc9d61 100644 --- a/backend/app/routers/campaigns.py +++ b/backend/app/routers/campaigns.py @@ -9,29 +9,51 @@ import uuid from typing import Optional from fastapi import APIRouter, Depends, Query -from sqlalchemy.orm import Session from pydantic import BaseModel, Field +from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role +from app.domain.unit_of_work import UnitOfWork from app.models.user import User -from app.services.campaign_service import generate_campaign_from_threat_actor +from app.services.audit_service import log_action +from app.services.campaign_crud_service import ( + activate_campaign as crud_activate, +) from app.services.campaign_crud_service import ( add_test_to_campaign as crud_add_test, - activate_campaign as crud_activate, +) +from app.services.campaign_crud_service import ( complete_campaign as crud_complete, +) +from app.services.campaign_crud_service import ( create_campaign as crud_create, +) +from app.services.campaign_crud_service import ( get_campaign_detail as crud_get_detail, +) +from app.services.campaign_crud_service import ( get_campaign_history as crud_get_history, +) +from app.services.campaign_crud_service import ( get_campaign_progress_data as crud_get_progress, +) +from app.services.campaign_crud_service import ( list_campaigns as crud_list, +) +from app.services.campaign_crud_service import ( remove_test_from_campaign as crud_remove_test, +) +from app.services.campaign_crud_service import ( schedule_campaign as crud_schedule, +) +from app.services.campaign_crud_service import ( serialize_campaign, +) +from app.services.campaign_crud_service import ( update_campaign as crud_update, ) -from app.domain.unit_of_work import UnitOfWork -from app.services.audit_service import log_action +from app.services.campaign_service import generate_campaign_from_threat_actor from app.services.notification_service import notify_role logger = logging.getLogger(__name__) diff --git a/backend/app/routers/compliance.py b/backend/app/routers/compliance.py index cd642ae..ead0a91 100644 --- a/backend/app/routers/compliance.py +++ b/backend/app/routers/compliance.py @@ -13,15 +13,15 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_role from app.models.user import User +from app.services.compliance_import_service import ( + import_cis_controls_v8_mappings, + import_nist_800_53_mappings, +) from app.services.compliance_service import ( - list_frameworks, - get_framework_status, build_framework_report_csv, get_framework_gaps, -) -from app.services.compliance_import_service import ( - import_nist_800_53_mappings, - import_cis_controls_v8_mappings, + get_framework_status, + list_frameworks, ) router = APIRouter(prefix="/compliance", tags=["compliance"]) diff --git a/backend/app/routers/d3fend.py b/backend/app/routers/d3fend.py index 4afa50a..1a61a9e 100644 --- a/backend/app/routers/d3fend.py +++ b/backend/app/routers/d3fend.py @@ -10,13 +10,15 @@ from app.database import get_db from app.dependencies.auth import get_current_user, require_role from app.models.user import User from app.services.d3fend_import_service import ( - import_d3fend_techniques, import_d3fend_mappings, + import_d3fend_techniques, +) +from app.services.d3fend_query_service import ( + get_defenses_for_attack_technique, + list_d3fend_tactics, ) from app.services.d3fend_query_service import ( list_defensive_techniques as list_defensive_techniques_svc, - list_d3fend_tactics, - get_defenses_for_attack_technique, ) logger = logging.getLogger(__name__) diff --git a/backend/app/routers/data_sources.py b/backend/app/routers/data_sources.py index 95af5cf..2a751f8 100644 --- a/backend/app/routers/data_sources.py +++ b/backend/app/routers/data_sources.py @@ -5,10 +5,11 @@ Provides a centralized panel for managing all external data sources including sync triggers, enable/disable toggles, and statistics. """ +from typing import Optional + from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session -from typing import Optional from app.database import get_db from app.dependencies.auth import require_role @@ -23,7 +24,6 @@ from app.services.data_source_service import ( update_source, ) - # --------------------------------------------------------------------------- # Pydantic schemas for request validation # --------------------------------------------------------------------------- diff --git a/backend/app/routers/detection_rules.py b/backend/app/routers/detection_rules.py index c847db1..f6235a7 100644 --- a/backend/app/routers/detection_rules.py +++ b/backend/app/routers/detection_rules.py @@ -14,17 +14,16 @@ from pydantic import BaseModel from sqlalchemy.orm import Session from app.database import get_db -from app.dependencies.auth import get_current_user, require_role, require_any_role +from app.dependencies.auth import get_current_user, require_any_role, require_role from app.models.user import User from app.services.detection_rule_service import ( - list_rules, - get_rules_for_template, auto_associate_rules, - get_rules_for_test, evaluate_rule, + get_rules_for_template, + get_rules_for_test, + list_rules, ) - # ── Pydantic schemas for request validation ──────────────────────────── diff --git a/backend/app/routers/evidence.py b/backend/app/routers/evidence.py index b46a2b2..956aad2 100644 --- a/backend/app/routers/evidence.py +++ b/backend/app/routers/evidence.py @@ -28,23 +28,23 @@ from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile, from sqlalchemy.orm import Session from app.database import get_db -from app.domain.unit_of_work import UnitOfWork from app.dependencies.auth import get_current_user +from app.domain.unit_of_work import UnitOfWork +from app.limiter import limiter from app.models.enums import TeamSide from app.models.evidence import Evidence from app.models.user import User from app.schemas.evidence import EvidenceOut from app.services.audit_service import log_action from app.services.evidence_service import ( + MAX_UPLOAD_SIZE, get_evidence_or_raise, get_test_or_raise, list_evidence_for_test, - MAX_UPLOAD_SIZE, validate_delete_permission, validate_file, validate_upload_permission, ) -from app.limiter import limiter from app.storage import get_presigned_url, upload_file router = APIRouter(tags=["evidence"]) diff --git a/backend/app/routers/jira.py b/backend/app/routers/jira.py index ce5be9a..4f19d18 100644 --- a/backend/app/routers/jira.py +++ b/backend/app/routers/jira.py @@ -17,7 +17,7 @@ from app.schemas.jira_schema import ( JiraLinkCreate, JiraLinkOut, ) -from app.services import jira_service, audit_service +from app.services import audit_service, jira_service logger = logging.getLogger(__name__) diff --git a/backend/app/routers/notifications.py b/backend/app/routers/notifications.py index da42c58..c60b8c7 100644 --- a/backend/app/routers/notifications.py +++ b/backend/app/routers/notifications.py @@ -19,10 +19,10 @@ from app.domain.unit_of_work import UnitOfWork from app.models.user import User from app.schemas.notification import NotificationOut, UnreadCountOut from app.services.notification_service import ( - list_notifications, - mark_as_read, - mark_all_as_read, get_unread_count, + list_notifications, + mark_all_as_read, + mark_as_read, ) router = APIRouter(prefix="/notifications", tags=["notifications"]) diff --git a/backend/app/routers/operational_metrics.py b/backend/app/routers/operational_metrics.py index 344a1ed..a874a2c 100644 --- a/backend/app/routers/operational_metrics.py +++ b/backend/app/routers/operational_metrics.py @@ -11,9 +11,8 @@ from app.database import get_db from app.dependencies.auth import get_current_user from app.models.user import User from app.services.operational_metrics_service import ( - get_all_operational_metrics, - get_operational_trend, get_metrics_by_team, + get_operational_trend, ) router = APIRouter(prefix="/metrics/operational", tags=["operational-metrics"]) diff --git a/backend/app/routers/osint.py b/backend/app/routers/osint.py index 97e0fff..9670f67 100644 --- a/backend/app/routers/osint.py +++ b/backend/app/routers/osint.py @@ -4,7 +4,7 @@ OSINT items (CVEs, advisories, etc.) linked to techniques. from uuid import UUID -from fastapi import APIRouter, Depends, Query, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlalchemy.orm import Session @@ -17,9 +17,11 @@ from app.services.osint_enrichment_service import ( get_osint_items_for_technique, get_osint_summary, get_technique_or_raise, - list_osint_items as service_list_osint_items, mark_osint_reviewed, ) +from app.services.osint_enrichment_service import ( + list_osint_items as service_list_osint_items, +) router = APIRouter(prefix="/osint", tags=["osint"]) diff --git a/backend/app/routers/professional_reports.py b/backend/app/routers/professional_reports.py index 053d80d..1414b1c 100644 --- a/backend/app/routers/professional_reports.py +++ b/backend/app/routers/professional_reports.py @@ -8,8 +8,8 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role -from app.models.user import User from app.limiter import limiter +from app.models.user import User from app.services import report_generation_service router = APIRouter(prefix="/reports/generate", tags=["professional-reports"]) diff --git a/backend/app/routers/scores.py b/backend/app/routers/scores.py index 8f8201a..4a2ae78 100644 --- a/backend/app/routers/scores.py +++ b/backend/app/routers/scores.py @@ -13,17 +13,16 @@ from app.database import get_db from app.dependencies.auth import get_current_user, require_role from app.domain.unit_of_work import UnitOfWork from app.models.user import User -from app.services.scoring_service import ( - score_technique_by_mitre_id, - score_actor_by_id, - calculate_tactic_score, - calculate_organization_score, - get_score_history, -) from app.services.scoring_config_service import ( get_weights_dict, update_scoring_weights, ) +from app.services.scoring_service import ( + calculate_tactic_score, + get_score_history, + score_actor_by_id, + score_technique_by_mitre_id, +) router = APIRouter(prefix="/scores", tags=["scores"]) diff --git a/backend/app/routers/snapshots.py b/backend/app/routers/snapshots.py index ad0a4a9..b956cfd 100644 --- a/backend/app/routers/snapshots.py +++ b/backend/app/routers/snapshots.py @@ -17,18 +17,19 @@ from app.dependencies.auth import get_current_user, require_any_role, require_ro from app.domain.errors import BusinessRuleViolation from app.domain.unit_of_work import UnitOfWork from app.models.user import User -from app.services.snapshot_service import ( - create_snapshot, - compare_snapshots, - cleanup_old_snapshots, - get_coverage_evolution, - serialize_snapshot_summary, - list_snapshots as list_snapshots_svc, - get_snapshot_or_raise, - get_snapshot_detail, - delete_snapshot, -) from app.services.audit_service import log_action +from app.services.snapshot_service import ( + compare_snapshots, + create_snapshot, + delete_snapshot, + get_coverage_evolution, + get_snapshot_detail, + get_snapshot_or_raise, + serialize_snapshot_summary, +) +from app.services.snapshot_service import ( + list_snapshots as list_snapshots_svc, +) logger = logging.getLogger(__name__) diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index ac1e1df..9f2922c 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -12,12 +12,12 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import require_role -from app.models.user import User -from app.services.mitre_sync_service import sync_mitre -from app.services.intel_service import scan_intel -from app.services.atomic_import_service import import_atomic_red_team from app.jobs.mitre_sync_job import scheduler from app.limiter import limiter +from app.models.user import User +from app.services.atomic_import_service import import_atomic_red_team +from app.services.intel_service import scan_intel +from app.services.mitre_sync_service import sync_mitre logger = logging.getLogger(__name__) diff --git a/backend/app/routers/techniques.py b/backend/app/routers/techniques.py index 9737ada..d04dd84 100644 --- a/backend/app/routers/techniques.py +++ b/backend/app/routers/techniques.py @@ -9,11 +9,11 @@ from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session from app.database import get_db -from app.dependencies.auth import get_current_user, require_role, require_any_role +from app.dependencies.auth import get_current_user, require_any_role, require_role from app.dependencies.repositories import get_technique_repository from app.domain.entities.technique import TechniqueEntity -from app.domain.errors import DuplicateEntityError, EntityNotFoundError from app.domain.enums import TechniqueStatus +from app.domain.errors import DuplicateEntityError, EntityNotFoundError from app.domain.unit_of_work import UnitOfWork from app.infrastructure.persistence.repositories.sa_technique_repository import ( SATechniqueRepository, diff --git a/backend/app/routers/test_templates.py b/backend/app/routers/test_templates.py index ae55756..4cbd5d3 100644 --- a/backend/app/routers/test_templates.py +++ b/backend/app/routers/test_templates.py @@ -40,13 +40,21 @@ from app.schemas.test_template import ( from app.services.audit_service import log_action from app.services.test_template_service import ( bulk_activate, - create_template as create_template_svc, get_template_or_raise, get_template_stats, - get_templates_by_technique as templates_by_technique, list_templates, soft_delete_template, +) +from app.services.test_template_service import ( + create_template as create_template_svc, +) +from app.services.test_template_service import ( + get_templates_by_technique as templates_by_technique, +) +from app.services.test_template_service import ( toggle_template_active as toggle_template_active_svc, +) +from app.services.test_template_service import ( update_template as update_template_svc, ) diff --git a/backend/app/routers/tests.py b/backend/app/routers/tests.py index 061f68a..39c1419 100644 --- a/backend/app/routers/tests.py +++ b/backend/app/routers/tests.py @@ -26,49 +26,84 @@ from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role, require_role -from app.domain.enums import DataClassification +from app.domain.unit_of_work import UnitOfWork from app.limiter import limiter from app.models.enums import TestState from app.models.user import User from app.schemas.test import ( + TestBlueUpdate, + TestBlueValidate, + TestClassificationUpdate, TestCreate, TestOut, - TestUpdate, TestRedUpdate, - TestBlueUpdate, TestRedValidate, - TestBlueValidate, TestRemediationUpdate, - TestClassificationUpdate, + TestUpdate, ) from app.schemas.test_template import TestTemplateInstantiate -from app.domain.unit_of_work import UnitOfWork from app.services.audit_service import log_action from app.services.status_service import recalculate_technique_status from app.services.test_crud_service import ( create_test as crud_create_test, +) +from app.services.test_crud_service import ( create_test_from_template as crud_create_from_template, +) +from app.services.test_crud_service import ( get_test_detail as crud_get_test_detail, +) +from app.services.test_crud_service import ( get_test_or_raise as crud_get_test_or_raise, +) +from app.services.test_crud_service import ( get_test_timeline as crud_get_test_timeline, +) +from app.services.test_crud_service import ( get_test_with_technique as crud_get_test_with_technique, +) +from app.services.test_crud_service import ( list_tests as crud_list_tests, +) +from app.services.test_crud_service import ( update_test as crud_update_test, +) +from app.services.test_crud_service import ( update_test_blue as crud_update_test_blue, +) +from app.services.test_crud_service import ( update_test_red as crud_update_test_red, ) from app.services.test_workflow_service import ( - start_execution as wf_start_execution, - submit_red_evidence as wf_submit_red, - submit_blue_evidence as wf_submit_blue, - validate_as_red_lead as wf_validate_red, - validate_as_blue_lead as wf_validate_blue, - reopen_test as wf_reopen, - handle_remediation_completed as wf_handle_remediation, get_retest_chain as wf_get_retest_chain, +) +from app.services.test_workflow_service import ( + handle_remediation_completed as wf_handle_remediation, +) +from app.services.test_workflow_service import ( pause_timer as wf_pause_timer, +) +from app.services.test_workflow_service import ( + reopen_test as wf_reopen, +) +from app.services.test_workflow_service import ( resume_timer as wf_resume_timer, ) +from app.services.test_workflow_service import ( + start_execution as wf_start_execution, +) +from app.services.test_workflow_service import ( + submit_blue_evidence as wf_submit_blue, +) +from app.services.test_workflow_service import ( + submit_red_evidence as wf_submit_red, +) +from app.services.test_workflow_service import ( + validate_as_blue_lead as wf_validate_blue, +) +from app.services.test_workflow_service import ( + validate_as_red_lead as wf_validate_red, +) router = APIRouter(prefix="/tests", tags=["tests"]) diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index c9e96cd..4119094 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -9,7 +9,7 @@ from app.database import get_db from app.dependencies.auth import require_role from app.domain.unit_of_work import UnitOfWork from app.models.user import User -from app.schemas.user import UserCreate, UserUpdate, UserOut +from app.schemas.user import UserCreate, UserOut, UserUpdate from app.services.audit_service import log_action from app.services.user_service import ( create_user, diff --git a/backend/app/routers/worklogs.py b/backend/app/routers/worklogs.py index b7a65d8..c27ecf8 100644 --- a/backend/app/routers/worklogs.py +++ b/backend/app/routers/worklogs.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Optional from uuid import UUID -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from sqlalchemy.orm import Session diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 62b98f1..7a38c2f 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,32 +1,28 @@ """Pydantic schemas — re-exported for convenient imports.""" from app.schemas.auth import LoginRequest, TokenResponse, UserOut - +from app.schemas.evidence import EvidenceOut, EvidenceUpload from app.schemas.technique import ( TechniqueCreate, TechniqueOut, TechniqueSummary, TechniqueUpdate, ) - from app.schemas.test import ( + TestBlueUpdate, + TestBlueValidate, TestCreate, TestOut, + TestRedUpdate, + TestRedValidate, TestUpdate, TestValidate, - TestRedUpdate, - TestBlueUpdate, - TestRedValidate, - TestBlueValidate, ) - -from app.schemas.evidence import EvidenceOut, EvidenceUpload - from app.schemas.test_template import ( - TestTemplateOut, TestTemplateCreate, - TestTemplateSummary, TestTemplateInstantiate, + TestTemplateOut, + TestTemplateSummary, ) __all__ = [ diff --git a/backend/app/schemas/technique.py b/backend/app/schemas/technique.py index 9cb8042..8a9eb2b 100644 --- a/backend/app/schemas/technique.py +++ b/backend/app/schemas/technique.py @@ -7,7 +7,6 @@ from pydantic import BaseModel, ConfigDict from app.models.enums import TechniqueStatus - # ── Create ────────────────────────────────────────────────────────── class TechniqueCreate(BaseModel): diff --git a/backend/app/schemas/test.py b/backend/app/schemas/test.py index c9369e8..8da1351 100644 --- a/backend/app/schemas/test.py +++ b/backend/app/schemas/test.py @@ -8,7 +8,6 @@ from pydantic import BaseModel, ConfigDict from app.domain.enums import DataClassification from app.models.enums import TestResult, TestState - # ── Create ────────────────────────────────────────────────────────── diff --git a/backend/app/schemas/test_template.py b/backend/app/schemas/test_template.py index 54a45e5..e2e46c9 100644 --- a/backend/app/schemas/test_template.py +++ b/backend/app/schemas/test_template.py @@ -5,7 +5,6 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict - # ── Full output ───────────────────────────────────────────────────── diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 7d89d61..1babcd8 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -4,8 +4,7 @@ import re import uuid from datetime import datetime -from pydantic import BaseModel, ConfigDict, EmailStr, field_validator - +from pydantic import BaseModel, ConfigDict, field_validator # ── Username policy ───────────────────────────────────────────────── diff --git a/backend/app/seed_demo.py b/backend/app/seed_demo.py index 4e18009..82d7968 100644 --- a/backend/app/seed_demo.py +++ b/backend/app/seed_demo.py @@ -18,14 +18,14 @@ from datetime import datetime, timedelta from app.auth import hash_password from app.database import SessionLocal -from app.models.user import User +from app.models.audit import AuditLog +from app.models.enums import TeamSide, TechniqueStatus, TestResult, TestState +from app.models.evidence import Evidence +from app.models.notification import Notification from app.models.technique import Technique from app.models.test import Test from app.models.test_template import TestTemplate -from app.models.evidence import Evidence -from app.models.audit import AuditLog -from app.models.notification import Notification -from app.models.enums import TechniqueStatus, TestState, TestResult, TeamSide +from app.models.user import User logger = logging.getLogger(__name__) @@ -214,7 +214,11 @@ def _seed_tests(db, users: list[User], techniques: list[Technique], count: int = name=f"Demo Test {i + 1} — {technique.name[:40]}", description=f"Automated demo test #{i + 1} for {technique.mitre_id}.", platform=random.choice(PLATFORMS), - procedure_text=f"Step 1: Prepare environment.\nStep 2: Execute {technique.mitre_id} procedure.\nStep 3: Observe results.", + procedure_text=( + f"Step 1: Prepare environment.\n" + f"Step 2: Execute {technique.mitre_id} procedure.\n" + f"Step 3: Observe results." + ), tool_used=random.choice(["powershell", "bash", "cmd", "python", "caldera", "metasploit"]), execution_date=datetime.utcnow() - timedelta(days=random.randint(0, 60)), created_by=creator.id, @@ -353,7 +357,11 @@ def _seed_templates(db, techniques: list[Technique], count: int = 10) -> None: description=f"Demo template: {name}. Targets {technique.mitre_id} ({technique.name}).", source="demo", source_url=None, - attack_procedure=f"1. Set up environment for {technique.mitre_id}.\n2. Execute the procedure.\n3. Record observations.", + attack_procedure=( + f"1. Set up environment for {technique.mitre_id}.\n" + "2. Execute the procedure.\n" + "3. Record observations." + ), expected_detection=f"SIEM should alert on {technique.mitre_id} indicators.", platform=random.choice(PLATFORMS), tool_suggested=random.choice(["powershell", "cmd", "bash", "python"]), diff --git a/backend/app/services/advanced_metrics_service.py b/backend/app/services/advanced_metrics_service.py index 31bb2f4..0ef7520 100644 --- a/backend/app/services/advanced_metrics_service.py +++ b/backend/app/services/advanced_metrics_service.py @@ -7,9 +7,9 @@ from datetime import datetime, timedelta from sqlalchemy import case, func from sqlalchemy.orm import Session +from app.models.enums import TestResult from app.models.technique import Technique from app.models.test import Test -from app.models.enums import TestResult def get_coverage_by_tactic(db: Session) -> list[dict]: diff --git a/backend/app/services/atomic_import_service.py b/backend/app/services/atomic_import_service.py index 9f88278..3f4ba0f 100644 --- a/backend/app/services/atomic_import_service.py +++ b/backend/app/services/atomic_import_service.py @@ -24,7 +24,6 @@ templates are identified by their ``atomic_test_id`` and simply skipped. import io import logging -import os import shutil import tempfile import zipfile @@ -54,6 +53,10 @@ _DOWNLOAD_TIMEOUT = 300 # Top-level directory name inside the ZIP _ZIP_ROOT_PREFIX = "atomic-red-team-master" +# Safety limits for ZIP extraction — prevent zip-bomb DoS +_MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB +_MAX_ENTRIES = 50_000 + # --------------------------------------------------------------------------- # Internal helpers @@ -77,11 +80,6 @@ def _safe_extract_zip(zip_bytes: bytes, dest: str) -> None: directory (path traversal / Zip Slip) or if the archive exceeds the safety limits. """ - # Maximum uncompressed size: 500 MB — prevents zip-bomb DoS - _MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 - # Maximum number of entries - _MAX_ENTRIES = 50_000 - dest_path = Path(dest).resolve() with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: diff --git a/backend/app/services/caldera_import_service.py b/backend/app/services/caldera_import_service.py index 5c64978..bd9c51a 100644 --- a/backend/app/services/caldera_import_service.py +++ b/backend/app/services/caldera_import_service.py @@ -33,8 +33,8 @@ import requests as _requests import yaml from sqlalchemy.orm import Session -from app.models.test_template import TestTemplate from app.models.data_source import DataSource +from app.models.test_template import TestTemplate from app.services.audit_service import log_action logger = logging.getLogger(__name__) diff --git a/backend/app/services/campaign_crud_service.py b/backend/app/services/campaign_crud_service.py index 34a503c..6c4cfdb 100644 --- a/backend/app/services/campaign_crud_service.py +++ b/backend/app/services/campaign_crud_service.py @@ -16,16 +16,15 @@ from app.domain.errors import ( PermissionViolation, ) from app.models.campaign import Campaign, CampaignTest -from app.models.test import Test from app.models.technique import Technique -from app.utils import escape_like +from app.models.test import Test +from app.services.campaign_scheduler_service import calculate_next_run from app.services.campaign_service import ( + TACTIC_TO_PHASE, get_campaign_progress, validate_no_circular_dependency, - TACTIC_TO_PHASE, ) -from app.services.campaign_scheduler_service import calculate_next_run - +from app.utils import escape_like # ── Serialization helpers ──────────────────────────────────────────────── diff --git a/backend/app/services/campaign_scheduler_service.py b/backend/app/services/campaign_scheduler_service.py index 17aa134..7f34ee3 100644 --- a/backend/app/services/campaign_scheduler_service.py +++ b/backend/app/services/campaign_scheduler_service.py @@ -5,17 +5,16 @@ fresh tests, and computing the next run date. """ import logging -import uuid from datetime import datetime, timedelta from sqlalchemy.orm import Session from app.models.campaign import Campaign, CampaignTest -from app.models.test import Test from app.models.enums import TestState -from app.services.notification_service import create_notification -from app.services.audit_service import log_action +from app.models.test import Test from app.models.user import User +from app.services.audit_service import log_action +from app.services.notification_service import create_notification logger = logging.getLogger(__name__) @@ -166,7 +165,10 @@ def check_and_run_recurring_campaigns(db: Session) -> int: user_id=campaign.created_by, type="recurring_campaign_run", title="Recurring campaign executed", - message=f'Campaign "{child.name}" was automatically created from recurring template "{campaign.name}".', + message=( + f'Campaign "{child.name}" was automatically created ' + f'from recurring template "{campaign.name}".' + ), entity_type="campaign", entity_id=child.id, ) diff --git a/backend/app/services/campaign_service.py b/backend/app/services/campaign_service.py index cfb4b02..01045f5 100644 --- a/backend/app/services/campaign_service.py +++ b/backend/app/services/campaign_service.py @@ -6,18 +6,16 @@ threat actors, and progress calculation. import logging import uuid -from datetime import datetime from sqlalchemy.orm import Session from app.domain.exceptions import EntityNotFoundError, InvalidOperationError -from app.models.campaign import Campaign, CampaignTest, KILL_CHAIN_PHASES +from app.models.campaign import Campaign, CampaignTest +from app.models.enums import TechniqueStatus, TestState +from app.models.technique import Technique from app.models.test import Test from app.models.test_template import TestTemplate -from app.models.technique import Technique from app.models.threat_actor import ThreatActor, ThreatActorTechnique -from app.models.enums import TechniqueStatus, TestState -from app.services.notification_service import create_notification from app.models.user import User logger = logging.getLogger(__name__) diff --git a/backend/app/services/compliance_import_service.py b/backend/app/services/compliance_import_service.py index 7dd0b41..b288814 100644 --- a/backend/app/services/compliance_import_service.py +++ b/backend/app/services/compliance_import_service.py @@ -6,22 +6,256 @@ ComplianceControl, and ComplianceControlMapping records. """ import logging -import json import re -from typing import Optional import requests from sqlalchemy.orm import Session from app.models.compliance import ( - ComplianceFramework, ComplianceControl, ComplianceControlMapping, + ComplianceFramework, ) from app.models.technique import Technique logger = logging.getLogger(__name__) +# ── Module-level control definitions (avoids N806 / uppercase-in-function) ──── + +_NIST_SAMPLE_CONTROLS = [ + { + "control_id": "AC-2", + "title": "Account Management", + "category": "Access Control", + "techniques": ["T1078", "T1136", "T1098", "T1087", "T1069"], + }, + { + "control_id": "AC-3", + "title": "Access Enforcement", + "category": "Access Control", + "techniques": ["T1078", "T1548", "T1134"], + }, + { + "control_id": "AC-4", + "title": "Information Flow Enforcement", + "category": "Access Control", + "techniques": ["T1048", "T1041", "T1572"], + }, + { + "control_id": "AC-6", + "title": "Least Privilege", + "category": "Access Control", + "techniques": ["T1078", "T1548", "T1134"], + }, + { + "control_id": "AU-2", + "title": "Event Logging", + "category": "Audit and Accountability", + "techniques": ["T1562", "T1070"], + }, + { + "control_id": "AU-6", + "title": "Audit Record Review", + "category": "Audit and Accountability", + "techniques": ["T1562", "T1070", "T1027"], + }, + { + "control_id": "CA-7", + "title": "Continuous Monitoring", + "category": "Assessment, Authorization, and Monitoring", + "techniques": ["T1059", "T1053"], + }, + { + "control_id": "CM-2", + "title": "Baseline Configuration", + "category": "Configuration Management", + "techniques": ["T1574", "T1546"], + }, + { + "control_id": "CM-6", + "title": "Configuration Settings", + "category": "Configuration Management", + "techniques": ["T1574", "T1546", "T1112"], + }, + { + "control_id": "CM-7", + "title": "Least Functionality", + "category": "Configuration Management", + "techniques": ["T1059", "T1218"], + }, + { + "control_id": "IA-2", + "title": "Identification and Authentication", + "category": "Identification and Authentication", + "techniques": ["T1078", "T1110"], + }, + { + "control_id": "IA-5", + "title": "Authenticator Management", + "category": "Identification and Authentication", + "techniques": ["T1078", "T1110", "T1003"], + }, + { + "control_id": "IR-4", + "title": "Incident Handling", + "category": "Incident Response", + "techniques": ["T1059", "T1547"], + }, + { + "control_id": "RA-5", + "title": "Vulnerability Monitoring and Scanning", + "category": "Risk Assessment", + "techniques": ["T1190", "T1203"], + }, + { + "control_id": "SC-7", + "title": "Boundary Protection", + "category": "System and Communications Protection", + "techniques": ["T1048", "T1041", "T1071"], + }, + { + "control_id": "SC-28", + "title": "Protection of Information at Rest", + "category": "System and Communications Protection", + "techniques": ["T1005", "T1114"], + }, + { + "control_id": "SI-3", + "title": "Malicious Code Protection", + "category": "System and Information Integrity", + "techniques": ["T1059", "T1204", "T1566"], + }, + { + "control_id": "SI-4", + "title": "System Monitoring", + "category": "System and Information Integrity", + "techniques": ["T1059", "T1053", "T1547"], + }, + { + "control_id": "SI-7", + "title": "Software, Firmware, and Information Integrity", + "category": "System and Information Integrity", + "techniques": ["T1195", "T1553"], + }, + { + "control_id": "PM-16", + "title": "Threat Awareness Program", + "category": "Program Management", + "techniques": ["T1566", "T1204"], + }, +] + +_CIS_CONTROLS = [ + { + "control_id": "CIS-1", + "title": "Inventory and Control of Enterprise Assets", + "category": "IG1 — Basic", + "techniques": ["T1595", "T1590", "T1018", "T1082"], + }, + { + "control_id": "CIS-2", + "title": "Inventory and Control of Software Assets", + "category": "IG1 — Basic", + "techniques": ["T1518", "T1072", "T1195"], + }, + { + "control_id": "CIS-3", + "title": "Data Protection", + "category": "IG1 — Basic", + "techniques": ["T1005", "T1114", "T1560", "T1048", "T1041"], + }, + { + "control_id": "CIS-4", + "title": "Secure Configuration of Enterprise Assets and Software", + "category": "IG1 — Basic", + "techniques": ["T1574", "T1546", "T1112", "T1543"], + }, + { + "control_id": "CIS-5", + "title": "Account Management", + "category": "IG1 — Basic", + "techniques": ["T1078", "T1136", "T1098", "T1087"], + }, + { + "control_id": "CIS-6", + "title": "Access Control Management", + "category": "IG1 — Basic", + "techniques": ["T1078", "T1548", "T1134", "T1021"], + }, + { + "control_id": "CIS-7", + "title": "Continuous Vulnerability Management", + "category": "IG2 — Foundational", + "techniques": ["T1190", "T1203", "T1068", "T1210"], + }, + { + "control_id": "CIS-8", + "title": "Audit Log Management", + "category": "IG2 — Foundational", + "techniques": ["T1562", "T1070", "T1059"], + }, + { + "control_id": "CIS-9", + "title": "Email and Web Browser Protections", + "category": "IG2 — Foundational", + "techniques": ["T1566", "T1204", "T1189", "T1598"], + }, + { + "control_id": "CIS-10", + "title": "Malware Defenses", + "category": "IG2 — Foundational", + "techniques": ["T1059", "T1204", "T1027", "T1140", "T1497"], + }, + { + "control_id": "CIS-11", + "title": "Data Recovery", + "category": "IG1 — Basic", + "techniques": ["T1486", "T1490", "T1561"], + }, + { + "control_id": "CIS-12", + "title": "Network Infrastructure Management", + "category": "IG2 — Foundational", + "techniques": ["T1557", "T1071", "T1572", "T1571"], + }, + { + "control_id": "CIS-13", + "title": "Network Monitoring and Defense", + "category": "IG2 — Foundational", + "techniques": ["T1071", "T1048", "T1041", "T1105", "T1572"], + }, + { + "control_id": "CIS-14", + "title": "Security Awareness and Skills Training", + "category": "IG1 — Basic", + "techniques": ["T1566", "T1204", "T1598"], + }, + { + "control_id": "CIS-15", + "title": "Service Provider Management", + "category": "IG2 — Foundational", + "techniques": ["T1199", "T1195"], + }, + { + "control_id": "CIS-16", + "title": "Application Software Security", + "category": "IG2 — Foundational", + "techniques": ["T1190", "T1059", "T1203"], + }, + { + "control_id": "CIS-17", + "title": "Incident Response Management", + "category": "IG2 — Foundational", + "techniques": ["T1059", "T1547", "T1053"], + }, + { + "control_id": "CIS-18", + "title": "Penetration Testing", + "category": "IG3 — Organizational", + "techniques": ["T1595", "T1046", "T1190", "T1059"], + }, +] + # URL for the NIST 800-53 Rev 5 to ATT&CK mapping # This is the JSON STIX bundle that contains the relationships NIST_MAPPING_URL = ( @@ -53,7 +287,11 @@ def import_nist_800_53_mappings(db: Session) -> dict: framework = ComplianceFramework( name="NIST 800-53 Rev 5", version="5", - description="National Institute of Standards and Technology Special Publication 800-53 Revision 5 — Security and Privacy Controls for Information Systems and Organizations", + description=( + "National Institute of Standards and Technology " + "Special Publication 800-53 Revision 5 — " + "Security and Privacy Controls for Information Systems and Organizations" + ), url="https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final", is_active=True, ) @@ -216,49 +454,6 @@ def _import_sample_nist_mappings(db: Session, framework: ComplianceFramework) -> This ensures the feature works even without network access. """ - SAMPLE_CONTROLS = [ - {"control_id": "AC-2", "title": "Account Management", "category": "Access Control", - "techniques": ["T1078", "T1136", "T1098", "T1087", "T1069"]}, - {"control_id": "AC-3", "title": "Access Enforcement", "category": "Access Control", - "techniques": ["T1078", "T1548", "T1134"]}, - {"control_id": "AC-4", "title": "Information Flow Enforcement", "category": "Access Control", - "techniques": ["T1048", "T1041", "T1572"]}, - {"control_id": "AC-6", "title": "Least Privilege", "category": "Access Control", - "techniques": ["T1078", "T1548", "T1134"]}, - {"control_id": "AU-2", "title": "Event Logging", "category": "Audit and Accountability", - "techniques": ["T1562", "T1070"]}, - {"control_id": "AU-6", "title": "Audit Record Review", "category": "Audit and Accountability", - "techniques": ["T1562", "T1070", "T1027"]}, - {"control_id": "CA-7", "title": "Continuous Monitoring", "category": "Assessment, Authorization, and Monitoring", - "techniques": ["T1059", "T1053"]}, - {"control_id": "CM-2", "title": "Baseline Configuration", "category": "Configuration Management", - "techniques": ["T1574", "T1546"]}, - {"control_id": "CM-6", "title": "Configuration Settings", "category": "Configuration Management", - "techniques": ["T1574", "T1546", "T1112"]}, - {"control_id": "CM-7", "title": "Least Functionality", "category": "Configuration Management", - "techniques": ["T1059", "T1218"]}, - {"control_id": "IA-2", "title": "Identification and Authentication", "category": "Identification and Authentication", - "techniques": ["T1078", "T1110"]}, - {"control_id": "IA-5", "title": "Authenticator Management", "category": "Identification and Authentication", - "techniques": ["T1078", "T1110", "T1003"]}, - {"control_id": "IR-4", "title": "Incident Handling", "category": "Incident Response", - "techniques": ["T1059", "T1547"]}, - {"control_id": "RA-5", "title": "Vulnerability Monitoring and Scanning", "category": "Risk Assessment", - "techniques": ["T1190", "T1203"]}, - {"control_id": "SC-7", "title": "Boundary Protection", "category": "System and Communications Protection", - "techniques": ["T1048", "T1041", "T1071"]}, - {"control_id": "SC-28", "title": "Protection of Information at Rest", "category": "System and Communications Protection", - "techniques": ["T1005", "T1114"]}, - {"control_id": "SI-3", "title": "Malicious Code Protection", "category": "System and Information Integrity", - "techniques": ["T1059", "T1204", "T1566"]}, - {"control_id": "SI-4", "title": "System Monitoring", "category": "System and Information Integrity", - "techniques": ["T1059", "T1053", "T1547"]}, - {"control_id": "SI-7", "title": "Software, Firmware, and Information Integrity", "category": "System and Information Integrity", - "techniques": ["T1195", "T1553"]}, - {"control_id": "PM-16", "title": "Threat Awareness Program", "category": "Program Management", - "techniques": ["T1566", "T1204"]}, - ] - # Build technique lookup all_techniques = {t.mitre_id: t for t in db.query(Technique).all()} @@ -276,7 +471,7 @@ def _import_sample_nist_mappings(db: Session, framework: ComplianceFramework) -> controls_created = 0 mappings_created = 0 - for sample in SAMPLE_CONTROLS: + for sample in _NIST_SAMPLE_CONTROLS: # Create or get control if sample["control_id"] in existing_controls: control = existing_controls[sample["control_id"]] @@ -348,8 +543,11 @@ def import_cis_controls_v8_mappings(db: Session) -> dict: framework = ComplianceFramework( name="CIS Controls v8", version="8", - description="Center for Internet Security Critical Security Controls Version 8 — " - "a prioritized set of 18 security safeguards organized by Implementation Groups (IG1, IG2, IG3).", + description=( + "Center for Internet Security Critical Security Controls Version 8 — " + "a prioritized set of 18 security safeguards " + "organized by Implementation Groups (IG1, IG2, IG3)." + ), url="https://www.cisecurity.org/controls/v8", is_active=True, ) @@ -360,62 +558,7 @@ def import_cis_controls_v8_mappings(db: Session) -> dict: logger.info("CIS Controls v8 framework already exists") # ── 2. Control definitions with ATT&CK mappings ─────────────── - CIS_CONTROLS = [ - {"control_id": "CIS-1", "title": "Inventory and Control of Enterprise Assets", - "category": "IG1 — Basic", - "techniques": ["T1595", "T1590", "T1018", "T1082"]}, - {"control_id": "CIS-2", "title": "Inventory and Control of Software Assets", - "category": "IG1 — Basic", - "techniques": ["T1518", "T1072", "T1195"]}, - {"control_id": "CIS-3", "title": "Data Protection", - "category": "IG1 — Basic", - "techniques": ["T1005", "T1114", "T1560", "T1048", "T1041"]}, - {"control_id": "CIS-4", "title": "Secure Configuration of Enterprise Assets and Software", - "category": "IG1 — Basic", - "techniques": ["T1574", "T1546", "T1112", "T1543"]}, - {"control_id": "CIS-5", "title": "Account Management", - "category": "IG1 — Basic", - "techniques": ["T1078", "T1136", "T1098", "T1087"]}, - {"control_id": "CIS-6", "title": "Access Control Management", - "category": "IG1 — Basic", - "techniques": ["T1078", "T1548", "T1134", "T1021"]}, - {"control_id": "CIS-7", "title": "Continuous Vulnerability Management", - "category": "IG2 — Foundational", - "techniques": ["T1190", "T1203", "T1068", "T1210"]}, - {"control_id": "CIS-8", "title": "Audit Log Management", - "category": "IG2 — Foundational", - "techniques": ["T1562", "T1070", "T1059"]}, - {"control_id": "CIS-9", "title": "Email and Web Browser Protections", - "category": "IG2 — Foundational", - "techniques": ["T1566", "T1204", "T1189", "T1598"]}, - {"control_id": "CIS-10", "title": "Malware Defenses", - "category": "IG2 — Foundational", - "techniques": ["T1059", "T1204", "T1027", "T1140", "T1497"]}, - {"control_id": "CIS-11", "title": "Data Recovery", - "category": "IG1 — Basic", - "techniques": ["T1486", "T1490", "T1561"]}, - {"control_id": "CIS-12", "title": "Network Infrastructure Management", - "category": "IG2 — Foundational", - "techniques": ["T1557", "T1071", "T1572", "T1571"]}, - {"control_id": "CIS-13", "title": "Network Monitoring and Defense", - "category": "IG2 — Foundational", - "techniques": ["T1071", "T1048", "T1041", "T1105", "T1572"]}, - {"control_id": "CIS-14", "title": "Security Awareness and Skills Training", - "category": "IG1 — Basic", - "techniques": ["T1566", "T1204", "T1598"]}, - {"control_id": "CIS-15", "title": "Service Provider Management", - "category": "IG2 — Foundational", - "techniques": ["T1199", "T1195"]}, - {"control_id": "CIS-16", "title": "Application Software Security", - "category": "IG2 — Foundational", - "techniques": ["T1190", "T1059", "T1203"]}, - {"control_id": "CIS-17", "title": "Incident Response Management", - "category": "IG2 — Foundational", - "techniques": ["T1059", "T1547", "T1053"]}, - {"control_id": "CIS-18", "title": "Penetration Testing", - "category": "IG3 — Organizational", - "techniques": ["T1595", "T1046", "T1190", "T1059"]}, - ] + # (defined at module level as _CIS_CONTROLS) # Build technique lookup all_techniques = {t.mitre_id: t for t in db.query(Technique).all()} @@ -439,7 +582,7 @@ def import_cis_controls_v8_mappings(db: Session) -> dict: controls_created = 0 mappings_created = 0 - for item in CIS_CONTROLS: + for item in _CIS_CONTROLS: if item["control_id"] in existing_controls: control = existing_controls[item["control_id"]] else: diff --git a/backend/app/services/compliance_service.py b/backend/app/services/compliance_service.py index 696231d..c1b39ef 100644 --- a/backend/app/services/compliance_service.py +++ b/backend/app/services/compliance_service.py @@ -16,16 +16,15 @@ from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError from app.models.compliance import ( - ComplianceFramework, ComplianceControl, ComplianceControlMapping, + ComplianceFramework, ) from app.models.technique import Technique from app.models.test_template import TestTemplate from app.models.threat_actor import ThreatActorTechnique from app.services.scoring_service import calculate_technique_score - # ── Helpers ─────────────────────────────────────────────────────────── diff --git a/backend/app/services/d3fend_import_service.py b/backend/app/services/d3fend_import_service.py index d0f2f91..28b0c45 100644 --- a/backend/app/services/d3fend_import_service.py +++ b/backend/app/services/d3fend_import_service.py @@ -7,14 +7,13 @@ Uses the D3FEND public API: """ import logging -import uuid from typing import Any import httpx from sqlalchemy.orm import Session -from app.models.technique import Technique from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping +from app.models.technique import Technique logger = logging.getLogger(__name__) @@ -405,9 +404,10 @@ def sync(db: Session) -> dict: Called by the Data Sources router when the user clicks Sync for D3FEND. Returns a flat summary dict suitable for ``last_sync_stats``. """ - from app.models.data_source import DataSource from datetime import datetime + from app.models.data_source import DataSource + tech_result = import_d3fend_techniques(db) mapping_result = import_d3fend_mappings(db) diff --git a/backend/app/services/data_source_service.py b/backend/app/services/data_source_service.py index 25ad0ca..8adad2e 100644 --- a/backend/app/services/data_source_service.py +++ b/backend/app/services/data_source_service.py @@ -164,8 +164,8 @@ def get_source_stats(db: Session, source_id: str) -> dict: if not ds: raise EntityNotFoundError("Data source", source_id) - from app.models.test_template import TestTemplate from app.models.detection_rule import DetectionRule + from app.models.test_template import TestTemplate template_count = 0 rule_count = 0 diff --git a/backend/app/services/detection_rule_service.py b/backend/app/services/detection_rule_service.py index 18c4d80..d73d643 100644 --- a/backend/app/services/detection_rule_service.py +++ b/backend/app/services/detection_rule_service.py @@ -15,14 +15,13 @@ from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError from app.models.detection_rule import DetectionRule +from app.models.technique import Technique from app.models.test import Test +from app.models.test_detection_result import TestDetectionResult from app.models.test_template import TestTemplate from app.models.test_template_detection_rule import TestTemplateDetectionRule -from app.models.test_detection_result import TestDetectionResult -from app.models.technique import Technique from app.utils import escape_like - # ── Public service functions ────────────────────────────────────────── diff --git a/backend/app/services/elastic_import_service.py b/backend/app/services/elastic_import_service.py index 75b2b59..c8bf87f 100644 --- a/backend/app/services/elastic_import_service.py +++ b/backend/app/services/elastic_import_service.py @@ -32,8 +32,8 @@ from pathlib import Path import requests as _requests from sqlalchemy.orm import Session -from app.models.detection_rule import DetectionRule from app.models.data_source import DataSource +from app.models.detection_rule import DetectionRule from app.services.audit_service import log_action logger = logging.getLogger(__name__) @@ -50,6 +50,10 @@ ELASTIC_ZIP_URL = ( _DOWNLOAD_TIMEOUT = 300 _ZIP_ROOT_PREFIX = "detection-rules-main" +# Safety limits for ZIP extraction — prevent zip-bomb DoS +_MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB +_MAX_ENTRIES = 50_000 + # Severity normalisation _SEVERITY_MAP = { "informational": "informational", @@ -82,11 +86,6 @@ def _safe_extract_zip(zip_bytes: bytes, dest: str) -> None: directory (path traversal / Zip Slip) or if the archive exceeds the safety limits. """ - # Maximum uncompressed size: 500 MB — prevents zip-bomb DoS - _MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 - # Maximum number of entries - _MAX_ENTRIES = 50_000 - dest_path = Path(dest).resolve() with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: diff --git a/backend/app/services/heatmap_service.py b/backend/app/services/heatmap_service.py index 1ac34cf..b194ba4 100644 --- a/backend/app/services/heatmap_service.py +++ b/backend/app/services/heatmap_service.py @@ -10,7 +10,6 @@ no ``db.commit()``. from __future__ import annotations import json -from typing import Optional from sqlalchemy import func, or_ from sqlalchemy.orm import Session @@ -18,7 +17,6 @@ from sqlalchemy.orm import Session from app.domain.errors import BusinessRuleViolation, EntityNotFoundError from app.models.campaign import Campaign, CampaignTest from app.models.detection_rule import DetectionRule -from app.models.defensive_technique import DefensiveTechniqueMapping from app.models.enums import TechniqueStatus, TestState from app.models.technique import Technique from app.models.test import Test diff --git a/backend/app/services/intel_service.py b/backend/app/services/intel_service.py index c456885..469a0e0 100644 --- a/backend/app/services/intel_service.py +++ b/backend/app/services/intel_service.py @@ -11,9 +11,9 @@ parser. No LLMs or paid APIs are used. import logging import re -import defusedxml.ElementTree as ET from datetime import datetime +import defusedxml.ElementTree as ET # noqa: N817 — ET is the universal stdlib alias for ElementTree import requests as _requests from sqlalchemy.orm import Session diff --git a/backend/app/services/lolbas_import_service.py b/backend/app/services/lolbas_import_service.py index 24ab76e..5605298 100644 --- a/backend/app/services/lolbas_import_service.py +++ b/backend/app/services/lolbas_import_service.py @@ -37,8 +37,8 @@ import requests as _requests import yaml from sqlalchemy.orm import Session -from app.models.test_template import TestTemplate from app.models.data_source import DataSource +from app.models.test_template import TestTemplate from app.services.audit_service import log_action logger = logging.getLogger(__name__) @@ -176,7 +176,11 @@ def _parse_lolbas(root_dir: Path) -> list[dict]: results.append({ "mitre_technique_id": mitre_id, "name": f"LOLBAS: {binary_name} — {usecase or cmd_description or mitre_id}"[:500], - "description": f"{description}\n\n{cmd_description}".strip()[:2000] if description else cmd_description[:2000] if cmd_description else None, + "description": ( + f"{description}\n\n{cmd_description}".strip()[:2000] + if description + else cmd_description[:2000] if cmd_description else None + ), "source": "lolbas", "platform": "windows", "tool_suggested": binary_name, diff --git a/backend/app/services/mitre_sync_service.py b/backend/app/services/mitre_sync_service.py index 73335a5..80c052b 100644 --- a/backend/app/services/mitre_sync_service.py +++ b/backend/app/services/mitre_sync_service.py @@ -13,8 +13,8 @@ import requests as _requests from sqlalchemy.orm import Session from taxii2client.v20 import Server as TaxiiServer -from app.models.technique import Technique from app.models.enums import TechniqueStatus +from app.models.technique import Technique from app.services.audit_service import log_action logger = logging.getLogger(__name__) diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 4838468..5be53e9 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -10,14 +10,13 @@ but do **not** commit. The caller is responsible for committing. import uuid from datetime import datetime, timedelta -from sqlalchemy.orm import Session from sqlalchemy import func +from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError from app.models.notification import Notification from app.models.user import User - # --------------------------------------------------------------------------- # Core CRUD # --------------------------------------------------------------------------- diff --git a/backend/app/services/operational_metrics_service.py b/backend/app/services/operational_metrics_service.py index 2b81f93..2be0619 100644 --- a/backend/app/services/operational_metrics_service.py +++ b/backend/app/services/operational_metrics_service.py @@ -6,14 +6,14 @@ Calculates security operations KPIs from test data and audit logs. from datetime import datetime, timedelta from typing import Optional -from sqlalchemy import func, case, and_, or_, extract +from sqlalchemy import func from sqlalchemy.orm import Session -from app.models.test import Test -from app.models.technique import Technique -from app.models.test_detection_result import TestDetectionResult from app.models.audit import AuditLog -from app.models.enums import TestState, TestResult +from app.models.enums import TestResult, TestState +from app.models.technique import Technique +from app.models.test import Test +from app.models.test_detection_result import TestDetectionResult def _safe_stats(values: list[float]) -> dict: diff --git a/backend/app/services/report_engine.py b/backend/app/services/report_engine.py index 4e31e30..16c7dc7 100644 --- a/backend/app/services/report_engine.py +++ b/backend/app/services/report_engine.py @@ -3,9 +3,9 @@ Uses WeasyPrint for PDF generation and docxtpl for DOCX. """ +import logging import os import uuid -import logging from datetime import datetime from jinja2 import Environment, FileSystemLoader @@ -34,7 +34,7 @@ class ReportEngine: def generate_pdf(self, template_name: str, context: dict) -> str: """Render HTML and convert to PDF with WeasyPrint.""" - from weasyprint import HTML, CSS + from weasyprint import CSS, HTML html_content = self.render_html(template_name, context) css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css") diff --git a/backend/app/services/report_generation_service.py b/backend/app/services/report_generation_service.py index 1c6ec64..66898f5 100644 --- a/backend/app/services/report_generation_service.py +++ b/backend/app/services/report_generation_service.py @@ -98,7 +98,7 @@ def generate_coverage_report( output_format: str = "pdf", ) -> str: """Generate an organization-wide MITRE ATT&CK coverage report.""" - from sqlalchemy import func, case + from sqlalchemy import case, func org_score = _safe_org_score(db) @@ -234,7 +234,8 @@ def generate_quarterly_summary( output_format: str = "pdf", ) -> str: """Quarterly summary — reuses executive metrics plus snapshot trend rows.""" - from sqlalchemy import case as sql_case, func + from sqlalchemy import case as sql_case + from sqlalchemy import func org_score = _safe_org_score(db) quarter_ago = datetime.utcnow() - timedelta(days=90) diff --git a/backend/app/services/score_cache.py b/backend/app/services/score_cache.py index 67218f7..7bfd296 100644 --- a/backend/app/services/score_cache.py +++ b/backend/app/services/score_cache.py @@ -58,13 +58,13 @@ def get_organization_score_cached(db): def get_operational_metrics_cached(db): """Cached wrapper around operational metrics (MTTD, MTTR, efficacy).""" from app.services.operational_metrics_service import ( - calculate_mttd, - calculate_mttr, - calculate_detection_efficacy, calculate_alert_fidelity, calculate_coverage_velocity, - calculate_validation_throughput, + calculate_detection_efficacy, + calculate_mttd, + calculate_mttr, calculate_rejection_rate, + calculate_validation_throughput, ) cached = get("op_metrics") diff --git a/backend/app/services/scoring_service.py b/backend/app/services/scoring_service.py index a3cf003..9b41289 100644 --- a/backend/app/services/scoring_service.py +++ b/backend/app/services/scoring_service.py @@ -10,19 +10,18 @@ never produce N+1 traffic. """ from datetime import datetime, timedelta, timezone -from typing import Optional from sqlalchemy import case, func from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError +from app.models.defensive_technique import DefensiveTechniqueMapping +from app.models.detection_rule import DetectionRule +from app.models.enums import TestResult, TestState from app.models.technique import Technique from app.models.test import Test -from app.models.detection_rule import DetectionRule from app.models.test_detection_result import TestDetectionResult -from app.models.defensive_technique import DefensiveTechniqueMapping from app.models.threat_actor import ThreatActor, ThreatActorTechnique -from app.models.enums import TestState, TestResult from app.services.scoring_config_service import get_scoring_weights _SEVERITY_FACTORS: dict[str, float] = { @@ -659,7 +658,6 @@ def get_score_history(db: Session, period: str = "90d") -> list: computing scores based on test dates within time windows. Returns a list of weekly data points. """ - from app.models.audit import AuditLog now = datetime.utcnow() if period == "30d": diff --git a/backend/app/services/sigma_import_service.py b/backend/app/services/sigma_import_service.py index 7872874..2ec3f37 100644 --- a/backend/app/services/sigma_import_service.py +++ b/backend/app/services/sigma_import_service.py @@ -35,8 +35,8 @@ import requests as _requests import yaml from sqlalchemy.orm import Session -from app.models.detection_rule import DetectionRule from app.models.data_source import DataSource +from app.models.detection_rule import DetectionRule from app.services.audit_service import log_action logger = logging.getLogger(__name__) @@ -52,6 +52,10 @@ SIGMA_ZIP_URL = ( _DOWNLOAD_TIMEOUT = 300 _ZIP_ROOT_PREFIX = "sigma-master" +# Safety limits for ZIP extraction — prevent zip-bomb DoS +_MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB +_MAX_ENTRIES = 50_000 + # Regex to extract MITRE ATT&CK technique IDs from Sigma tags # e.g. "attack.t1059.001" → "T1059.001" _ATTACK_TAG_RE = re.compile(r"attack\.(t\d{4}(?:\.\d{3})?)", re.IGNORECASE) @@ -88,11 +92,6 @@ def _safe_extract_zip(zip_bytes: bytes, dest: str) -> None: directory (path traversal / Zip Slip) or if the archive exceeds the safety limits. """ - # Maximum uncompressed size: 500 MB — prevents zip-bomb DoS - _MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 - # Maximum number of entries - _MAX_ENTRIES = 50_000 - dest_path = Path(dest).resolve() with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: @@ -290,11 +289,8 @@ def sync(db: Session) -> dict: skipped = 0 for item in parsed_rules: - # Dedup key: source_id (relative path). A rule file may produce - # multiple entries (one per technique), but we deduplicate by - # source_id so re-runs are safe. For multi-technique rules we - # only skip if the exact same source_id is already present. - dedup_key = f"{item['source_id']}::{item['mitre_technique_id']}" + # Deduplicate by source_id: one rule file may map to multiple techniques, + # but we skip insertion if this source_id was already imported. if item["source_id"] in existing_ids: skipped += 1 continue diff --git a/backend/app/services/snapshot_service.py b/backend/app/services/snapshot_service.py index 9a2e305..82a5cae 100644 --- a/backend/app/services/snapshot_service.py +++ b/backend/app/services/snapshot_service.py @@ -16,9 +16,9 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError -from app.models.technique import Technique from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState from app.models.enums import TechniqueStatus +from app.models.technique import Technique from app.services.scoring_service import ( bulk_technique_scores, calculate_organization_score, @@ -26,6 +26,15 @@ from app.services.scoring_service import ( logger = logging.getLogger(__name__) +# Coverage status ordering for snapshot delta comparisons (higher = better coverage) +_STATUS_ORDER: dict[str, int] = { + "not_evaluated": 0, + "not_covered": 1, + "in_progress": 2, + "partial": 3, + "validated": 4, +} + # --------------------------------------------------------------------------- # Serialization and queries @@ -296,15 +305,6 @@ def compare_snapshots( .all() } - # Status priority for comparison - STATUS_ORDER = { - "not_evaluated": 0, - "not_covered": 1, - "in_progress": 2, - "partial": 3, - "validated": 4, - } - improved = [] worsened = [] unchanged_count = 0 @@ -315,8 +315,8 @@ def compare_snapshots( a = states_a.get(mitre_id, {"status": "not_evaluated", "score": 0}) b = states_b.get(mitre_id, {"status": "not_evaluated", "score": 0}) - a_order = STATUS_ORDER.get(a["status"], 0) - b_order = STATUS_ORDER.get(b["status"], 0) + a_order = _STATUS_ORDER.get(a["status"], 0) + b_order = _STATUS_ORDER.get(b["status"], 0) if b_order > a_order or (b_order == a_order and b["score"] > a["score"]): improved.append({ diff --git a/backend/app/services/test_crud_service.py b/backend/app/services/test_crud_service.py index a5be075..2bf2e13 100644 --- a/backend/app/services/test_crud_service.py +++ b/backend/app/services/test_crud_service.py @@ -14,11 +14,11 @@ from app.domain.errors import ( EntityNotFoundError, PermissionViolation, ) +from app.models.audit import AuditLog from app.models.enums import TestState from app.models.technique import Technique from app.models.test import Test from app.models.test_template import TestTemplate -from app.models.audit import AuditLog from app.utils import escape_like diff --git a/backend/app/services/test_workflow_service.py b/backend/app/services/test_workflow_service.py index d2f0dfd..0c3bbef 100644 --- a/backend/app/services/test_workflow_service.py +++ b/backend/app/services/test_workflow_service.py @@ -18,13 +18,16 @@ from datetime import datetime from sqlalchemy.orm import Session from app.config import settings -from app.domain.exceptions import InvalidOperationError, InvalidTransitionError +from app.domain.exceptions import InvalidOperationError from app.domain.test_entity import TestEntity from app.models.enums import TestState from app.models.test import Test from app.models.user import User from app.services.audit_service import log_action -from app.services.notification_service import notify_test_state_change, create_notification +from app.services.notification_service import ( + create_notification, + notify_test_state_change, +) logger = logging.getLogger(__name__) diff --git a/backend/app/services/threat_actor_import_service.py b/backend/app/services/threat_actor_import_service.py index b086e06..fffcbd4 100644 --- a/backend/app/services/threat_actor_import_service.py +++ b/backend/app/services/threat_actor_import_service.py @@ -38,9 +38,9 @@ from pathlib import Path import requests as _requests from sqlalchemy.orm import Session -from app.models.threat_actor import ThreatActor, ThreatActorTechnique -from app.models.technique import Technique from app.models.data_source import DataSource +from app.models.technique import Technique +from app.models.threat_actor import ThreatActor, ThreatActorTechnique from app.services.audit_service import log_action logger = logging.getLogger(__name__) @@ -241,9 +241,6 @@ def sync(db: Session) -> dict: relationships = _parse_relationships(objects) attack_pattern_map = _build_attack_pattern_map(objects) - # Step 2: Build STIX-ID → actor dict map - stix_to_actor = {a["stix_id"]: a for a in actor_dicts} - # Step 3: Load existing actors and techniques from DB existing_actors = { row.mitre_id: row diff --git a/backend/app/services/threat_actor_service.py b/backend/app/services/threat_actor_service.py index 03db9ab..8636622 100644 --- a/backend/app/services/threat_actor_service.py +++ b/backend/app/services/threat_actor_service.py @@ -15,13 +15,12 @@ from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError from app.models.enums import TechniqueStatus +from app.models.technique import Technique from app.models.test import Test from app.models.test_template import TestTemplate from app.models.threat_actor import ThreatActor, ThreatActorTechnique -from app.models.technique import Technique from app.utils import escape_like - # ── Public service functions ────────────────────────────────────────── diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 004da4b..2c0ae64 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -11,7 +11,11 @@ import uuid from sqlalchemy.orm import Session from app.auth import hash_password -from app.domain.errors import BusinessRuleViolation, DuplicateEntityError, EntityNotFoundError +from app.domain.errors import ( + BusinessRuleViolation, + DuplicateEntityError, + EntityNotFoundError, +) from app.models.user import User VALID_ROLES = {"admin", "red_tech", "blue_tech", "red_lead", "blue_lead", "viewer"} diff --git a/backend/ruff.toml b/backend/ruff.toml index a3bdc3c..c7b8bd6 100644 --- a/backend/ruff.toml +++ b/backend/ruff.toml @@ -1,13 +1,21 @@ +# PEP8 line length: 120 chars — the codebase uses longer identifiers and SQLAlchemy chaining +line-length = 120 + [lint] -# Ignore rules that have widespread pre-existing violations. -# These can be cleaned up incrementally in follow-up PRs. +# PEP8 compliance rules enforced: +# E/W — pycodestyle (core PEP8 style and warnings) +# F — pyflakes (unused imports, undefined names) +# I — isort (import ordering per PEP8 convention) +# N — pep8-naming (class/function/variable naming conventions) +select = ["E", "W", "F", "I", "N"] + ignore = [ - "E402", # module-level import not at top of file (app.main, some services) - "E712", # == True comparisons (required by SQLAlchemy filter syntax) - "F401", # unused imports (widespread; clean up incrementally) - "F841", # unused local variables (a few occurrences) + # SQLAlchemy filter syntax requires `== True` / `== False` comparisons + "E712", ] [lint.per-file-ignores] -# Test files may use broad exception catching and unusual import patterns -"tests/**" = ["E", "F"] +# Tests use broad exception catching and unusual import patterns +"tests/**" = ["E", "F", "N"] +# Data file: D3FEND technique descriptions contain URLs and long strings that cannot be meaningfully wrapped +"app/services/d3fend_import_service.py" = ["E501"]