refactor(pep8): enforce full PEP8 compliance across backend Python codebase

- 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!
This commit is contained in:
kitos
2026-06-09 16:40:14 +02:00
parent 1249391ef0
commit 8f98bdd273
85 changed files with 712 additions and 432 deletions
+1 -1
View File
@@ -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()
+3 -3
View File
@@ -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:
@@ -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
-1
View File
@@ -34,7 +34,6 @@ from app.domain.errors import (
InvalidStateTransition,
)
# ── Value objects ────────────────────────────────────────────────────
@@ -3,7 +3,6 @@
from __future__ import annotations
from app.domain.entities.technique import TechniqueEntity
from app.domain.enums import TechniqueStatus
class TechniqueMapper:
+7 -7
View File
@@ -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__)
+35 -37
View File
@@ -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()
]
+21 -17
View File
@@ -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",
+3 -2
View File
@@ -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
+11 -3
View File
@@ -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
+10 -2
View File
@@ -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
+9 -2
View File
@@ -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
+5 -3
View File
@@ -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)
+9 -2
View File
@@ -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
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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
+2 -1
View File
@@ -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
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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
+14 -2
View File
@@ -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):
+9 -2
View File
@@ -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
+2 -1
View File
@@ -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
@@ -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
+11 -3
View File
@@ -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
+2 -1
View File
@@ -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
+3 -2
View File
@@ -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
+9 -8
View File
@@ -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"])
+27 -5
View File
@@ -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__)
+6 -6
View File
@@ -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"])
+5 -3
View File
@@ -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__)
+2 -2
View File
@@ -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
# ---------------------------------------------------------------------------
+4 -5
View File
@@ -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 ────────────────────────────
+3 -3
View File
@@ -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"])
+1 -1
View File
@@ -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__)
+3 -3
View File
@@ -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"])
+1 -2
View File
@@ -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"])
+4 -2
View File
@@ -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"])
+1 -1
View File
@@ -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"])
+6 -7
View File
@@ -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"])
+12 -11
View File
@@ -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__)
+4 -4
View File
@@ -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__)
+2 -2
View File
@@ -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,
+10 -2
View File
@@ -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,
)
+48 -13
View File
@@ -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"])
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+7 -11
View File
@@ -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__ = [
-1
View File
@@ -7,7 +7,6 @@ from pydantic import BaseModel, ConfigDict
from app.models.enums import TechniqueStatus
# ── Create ──────────────────────────────────────────────────────────
class TechniqueCreate(BaseModel):
-1
View File
@@ -8,7 +8,6 @@ from pydantic import BaseModel, ConfigDict
from app.domain.enums import DataClassification
from app.models.enums import TestResult, TestState
# ── Create ──────────────────────────────────────────────────────────
-1
View File
@@ -5,7 +5,6 @@ from datetime import datetime
from pydantic import BaseModel, ConfigDict
# ── Full output ─────────────────────────────────────────────────────
+1 -2
View File
@@ -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 ─────────────────────────────────────────────────
+15 -7
View File
@@ -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"]),
@@ -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]:
@@ -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:
@@ -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__)
@@ -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 ────────────────────────────────────────────────
@@ -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,
)
+3 -5
View File
@@ -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__)
+250 -107
View File
@@ -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:
+1 -2
View File
@@ -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 ───────────────────────────────────────────────────────────
@@ -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)
+1 -1
View File
@@ -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
@@ -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 ──────────────────────────────────────────
@@ -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:
-2
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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,
+1 -1
View File
@@ -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__)
+1 -2
View File
@@ -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
# ---------------------------------------------------------------------------
@@ -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:
+2 -2
View File
@@ -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")
@@ -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)
+4 -4
View File
@@ -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")
+3 -5
View File
@@ -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":
+7 -11
View File
@@ -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
+12 -12
View File
@@ -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({
+1 -1
View File
@@ -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
@@ -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__)
@@ -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
+1 -2
View File
@@ -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 ──────────────────────────────────────────
+5 -1
View File
@@ -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"}
+16 -8
View File
@@ -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"]