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 import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base() Base = declarative_base()
+3 -3
View File
@@ -46,7 +46,7 @@ class DuplicateEntityError(DomainError):
# ── State machine ──────────────────────────────────────────────────── # ── 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.""" """A state-machine transition is not allowed."""
def __init__( def __init__(
@@ -67,7 +67,7 @@ class InvalidStateTransition(DomainError):
# ── Business rules ──────────────────────────────────────────────────── # ── Business rules ────────────────────────────────────────────────────
class BusinessRuleViolation(DomainError): class BusinessRuleViolation(DomainError): # noqa: N818 — DDD term, renaming would break 96 call sites
"""An operation violates a business invariant.""" """An operation violates a business invariant."""
def __init__(self, message: str) -> None: def __init__(self, message: str) -> None:
@@ -89,7 +89,7 @@ class InvalidOperationError(BusinessRuleViolation):
# ── Authorization ──────────────────────────────────────────────────── # ── Authorization ────────────────────────────────────────────────────
class PermissionViolation(DomainError): class PermissionViolation(DomainError): # noqa: N818 — DDD term, renaming would break 96 call sites
"""The user lacks permissions for an action.""" """The user lacks permissions for an action."""
def __init__(self, message: str = "Insufficient permissions") -> None: 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 from __future__ import annotations
import uuid import uuid
from typing import Protocol, runtime_checkable from typing import Protocol
from app.domain.enums import TestState from app.domain.enums import TestState
-1
View File
@@ -34,7 +34,6 @@ from app.domain.errors import (
InvalidStateTransition, InvalidStateTransition,
) )
# ── Value objects ──────────────────────────────────────────────────── # ── Value objects ────────────────────────────────────────────────────
@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from app.domain.entities.technique import TechniqueEntity from app.domain.entities.technique import TechniqueEntity
from app.domain.enums import TechniqueStatus
class TechniqueMapper: class TechniqueMapper:
+7 -7
View File
@@ -15,15 +15,15 @@ import logging
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from app.database import SessionLocal 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.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.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__) logger = logging.getLogger(__name__)
+35 -37
View File
@@ -3,55 +3,55 @@ import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from slowapi import _rate_limit_exceeded_handler from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.routers import auth as auth_router from app.config import settings as _settings
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.domain.errors import DomainError 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.error_handler import domain_exception_handler
from app.middleware.request_context import RequestContextMiddleware 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.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 ───────────────────────────────────────────────── # ── Environment detection ─────────────────────────────────────────────────
_IS_PRODUCTION = os.environ.get("AEGIS_ENV", "").lower() == "production" _IS_PRODUCTION = os.environ.get("AEGIS_ENV", "").lower() == "production"
# ── Logging ───────────────────────────────────────────────────────────────
from app.logging_config import setup_logging
setup_logging()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Startup / shutdown logic.""" """Startup / shutdown logic."""
@@ -81,8 +81,6 @@ app.add_middleware(RequestContextMiddleware)
app.add_exception_handler(DomainError, domain_exception_handler) app.add_exception_handler(DomainError, domain_exception_handler)
# ── CORS ────────────────────────────────────────────────────────────────── # ── CORS ──────────────────────────────────────────────────────────────────
from app.config import settings as _settings
_cors_origins: list[str] = [ _cors_origins: list[str] = [
o.strip() for o in _settings.CORS_ORIGINS.split(",") if o.strip() 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 # Import all models here so Alembic can detect them
from app.models.user import User from app.models.audit import AuditLog
from app.models.technique import Technique from app.models.campaign import Campaign, CampaignTest
from app.models.test import Test from app.models.compliance import (
from app.models.test_template import TestTemplate 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.evidence import Evidence
from app.models.intel import IntelItem 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.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.osint_item import OsintItem
from app.models.scoring_config import ScoringConfig 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__ = [ __all__ = [
"User", "Technique", "Test", "TestTemplate", "Evidence", "User", "Technique", "Test", "TestTemplate", "Evidence",
+4 -3
View File
@@ -1,6 +1,7 @@
import uuid 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 sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
@@ -9,7 +10,7 @@ from app.database import Base
class AuditLog(Base): class AuditLog(Base):
""" """
Audit log model for tracking all system actions. Audit log model for tracking all system actions.
Records user actions, entity changes, and system events Records user actions, entity changes, and system events
for security auditing and compliance purposes. for security auditing and compliance purposes.
""" """
+11 -3
View File
@@ -5,11 +5,19 @@ enabling simulation of complete attack chains and APT emulations.
""" """
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Integer, Boolean, DateTime, Boolean,
ForeignKey, Index, func, 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 sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
+10 -2
View File
@@ -5,9 +5,17 @@ MITRE ATT&CK techniques, enabling compliance gap analysis.
""" """
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Boolean, DateTime, Boolean,
ForeignKey, Index, UniqueConstraint, func, Column,
DateTime,
ForeignKey,
Index,
String,
Text,
UniqueConstraint,
func,
) )
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+9 -2
View File
@@ -6,9 +6,16 @@ per technique per snapshot) to avoid bloated JSONB fields.
""" """
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
Column, String, Float, Integer, DateTime, Column,
ForeignKey, Index, func, DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
func,
) )
from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+5 -3
View File
@@ -1,8 +1,9 @@
"""DataSource model — registry of external data sources for import.""" """DataSource model — registry of external data sources for import."""
import uuid 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 from app.database import Base
@@ -20,7 +21,8 @@ class DataSource(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, unique=True, nullable=False) # e.g. "atomic_red_team" name = Column(String, unique=True, nullable=False) # e.g. "atomic_red_team"
display_name = Column(String, 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 url = Column(String, nullable=True) # URL base of repo/API
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
is_enabled = Column(Boolean, default=True) is_enabled = Column(Boolean, default=True)
+9 -2
View File
@@ -5,9 +5,16 @@ ATT&CK techniques, enabling recommended countermeasure lookups.
""" """
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, DateTime, Column,
ForeignKey, Index, UniqueConstraint, func, DateTime,
ForeignKey,
Index,
String,
Text,
UniqueConstraint,
func,
) )
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+3 -2
View File
@@ -1,8 +1,9 @@
"""DetectionRule model — detection rules from multiple sources.""" """DetectionRule model — detection rules from multiple sources."""
import uuid 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 from app.database import Base
+4 -3
View File
@@ -1,5 +1,6 @@
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -10,10 +11,10 @@ from app.models.enums import TeamSide
class Evidence(Base): class Evidence(Base):
""" """
Evidence model for storing file metadata associated with tests. Evidence model for storing file metadata associated with tests.
Files are stored in MinIO, and this model tracks the file location, Files are stored in MinIO, and this model tracks the file location,
integrity hash, and upload metadata. integrity hash, and upload metadata.
The ``team`` field distinguishes whether this evidence was uploaded by The ``team`` field distinguishes whether this evidence was uploaded by
Red Team (attack evidence) or Blue Team (detection evidence). Red Team (attack evidence) or Blue Team (detection evidence).
""" """
+3 -2
View File
@@ -1,5 +1,6 @@
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -9,7 +10,7 @@ from app.database import Base
class IntelItem(Base): class IntelItem(Base):
""" """
Intelligence item model for tracking threat intelligence related to techniques. Intelligence item model for tracking threat intelligence related to techniques.
Stores URLs and metadata from automated intel scans that may indicate Stores URLs and metadata from automated intel scans that may indicate
new attack variations or detection bypasses for specific techniques. new attack variations or detection bypasses for specific techniques.
""" """
+4 -2
View File
@@ -2,8 +2,10 @@
import enum import enum
import uuid 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 sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
+2 -1
View File
@@ -1,7 +1,8 @@
"""Notification model — in-app notifications for user actions.""" """Notification model — in-app notifications for user actions."""
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+3 -2
View File
@@ -1,8 +1,9 @@
"""OSINT enrichment items — CVEs, blogs, PoCs, and advisories linked to techniques.""" """OSINT enrichment items — CVEs, blogs, PoCs, and advisories linked to techniques."""
import uuid 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 sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
+1 -1
View File
@@ -2,7 +2,7 @@
import uuid 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 sqlalchemy.dialects.postgresql import UUID
from app.database import Base from app.database import Base
+3 -4
View File
@@ -1,8 +1,7 @@
import uuid import uuid
from datetime import datetime
from sqlalchemy import Column, String, Text, Boolean, DateTime, Enum from sqlalchemy import Boolean, Column, DateTime, Enum, String, Text
from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
@@ -12,7 +11,7 @@ from app.models.enums import TechniqueStatus
class Technique(Base): class Technique(Base):
""" """
MITRE ATT&CK Technique model. MITRE ATT&CK Technique model.
Represents an attack technique from the MITRE ATT&CK framework, Represents an attack technique from the MITRE ATT&CK framework,
including its coverage status and associated tests. including its coverage status and associated tests.
""" """
+14 -2
View File
@@ -1,10 +1,22 @@
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
from app.models.enums import TestState, TestResult from app.models.enums import TestResult, TestState
class Test(Base): class Test(Base):
+9 -2
View File
@@ -5,9 +5,16 @@ rule as triggered / not triggered / not applicable, along with notes.
""" """
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+2 -1
View File
@@ -1,7 +1,8 @@
"""TestTemplate model — predefined test catalog entries.""" """TestTemplate model — predefined test catalog entries."""
import uuid 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 sqlalchemy.dialects.postgresql import UUID
from app.database import Base from app.database import Base
@@ -5,9 +5,8 @@ for a given test template / attack procedure.
""" """
import uuid 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.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
+11 -3
View File
@@ -5,11 +5,19 @@ techniques, imported from MITRE CTI (STIX 2.0).
""" """
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
Column, String, Text, Boolean, DateTime, Boolean,
ForeignKey, Index, UniqueConstraint, func, 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 sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
+3 -2
View File
@@ -1,5 +1,6 @@
import uuid 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 sqlalchemy.dialects.postgresql import UUID
from app.database import Base from app.database import Base
@@ -8,7 +9,7 @@ from app.database import Base
class User(Base): class User(Base):
""" """
User model for authentication and authorization. User model for authentication and authorization.
Possible roles: Possible roles:
- admin: Full system access - admin: Full system access
- red_tech: Red team technician - can create and edit tests - red_tech: Red team technician - can create and edit tests
+3 -2
View File
@@ -1,8 +1,9 @@
"""Worklog model — immutable internal time-tracking records.""" """Worklog model — immutable internal time-tracking records."""
import uuid 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 sqlalchemy.orm import relationship
from app.database import Base 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 import APIRouter, Cookie, Depends, Request, Response
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError, jwt
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from jose import jwt, JWTError from app.auth import blacklist_token, create_access_token, verify_password
from app.auth import create_access_token, blacklist_token, verify_password
from app.config import settings from app.config import settings
from app.database import get_db from app.database import get_db
from app.dependencies.auth import get_current_user 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.limiter import limiter
from app.middleware.request_context import resolve_client_ip from app.middleware.request_context import resolve_client_ip
from app.models.user import User 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.auth import TokenResponse, UserOut
from app.schemas.user import PasswordChange 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"]) router = APIRouter(prefix="/auth", tags=["auth"])
+27 -5
View File
@@ -9,29 +9,51 @@ import uuid
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.database import get_db from app.database import get_db
from app.dependencies.auth import get_current_user, require_any_role 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.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 ( from app.services.campaign_crud_service import (
add_test_to_campaign as crud_add_test, 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, complete_campaign as crud_complete,
)
from app.services.campaign_crud_service import (
create_campaign as crud_create, create_campaign as crud_create,
)
from app.services.campaign_crud_service import (
get_campaign_detail as crud_get_detail, get_campaign_detail as crud_get_detail,
)
from app.services.campaign_crud_service import (
get_campaign_history as crud_get_history, get_campaign_history as crud_get_history,
)
from app.services.campaign_crud_service import (
get_campaign_progress_data as crud_get_progress, get_campaign_progress_data as crud_get_progress,
)
from app.services.campaign_crud_service import (
list_campaigns as crud_list, list_campaigns as crud_list,
)
from app.services.campaign_crud_service import (
remove_test_from_campaign as crud_remove_test, remove_test_from_campaign as crud_remove_test,
)
from app.services.campaign_crud_service import (
schedule_campaign as crud_schedule, schedule_campaign as crud_schedule,
)
from app.services.campaign_crud_service import (
serialize_campaign, serialize_campaign,
)
from app.services.campaign_crud_service import (
update_campaign as crud_update, update_campaign as crud_update,
) )
from app.domain.unit_of_work import UnitOfWork from app.services.campaign_service import generate_campaign_from_threat_actor
from app.services.audit_service import log_action
from app.services.notification_service import notify_role from app.services.notification_service import notify_role
logger = logging.getLogger(__name__) 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.database import get_db
from app.dependencies.auth import get_current_user, require_role from app.dependencies.auth import get_current_user, require_role
from app.models.user import User 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 ( from app.services.compliance_service import (
list_frameworks,
get_framework_status,
build_framework_report_csv, build_framework_report_csv,
get_framework_gaps, get_framework_gaps,
) get_framework_status,
from app.services.compliance_import_service import ( list_frameworks,
import_nist_800_53_mappings,
import_cis_controls_v8_mappings,
) )
router = APIRouter(prefix="/compliance", tags=["compliance"]) 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.dependencies.auth import get_current_user, require_role
from app.models.user import User from app.models.user import User
from app.services.d3fend_import_service import ( from app.services.d3fend_import_service import (
import_d3fend_techniques,
import_d3fend_mappings, 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 ( from app.services.d3fend_query_service import (
list_defensive_techniques as list_defensive_techniques_svc, list_defensive_techniques as list_defensive_techniques_svc,
list_d3fend_tactics,
get_defenses_for_attack_technique,
) )
logger = logging.getLogger(__name__) 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. including sync triggers, enable/disable toggles, and statistics.
""" """
from typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Optional
from app.database import get_db from app.database import get_db
from app.dependencies.auth import require_role from app.dependencies.auth import require_role
@@ -23,7 +24,6 @@ from app.services.data_source_service import (
update_source, update_source,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Pydantic schemas for request validation # Pydantic schemas for request validation
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+4 -5
View File
@@ -14,17 +14,16 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.database import get_db 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.models.user import User
from app.services.detection_rule_service import ( from app.services.detection_rule_service import (
list_rules,
get_rules_for_template,
auto_associate_rules, auto_associate_rules,
get_rules_for_test,
evaluate_rule, evaluate_rule,
get_rules_for_template,
get_rules_for_test,
list_rules,
) )
# ── Pydantic schemas for request validation ──────────────────────────── # ── 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 sqlalchemy.orm import Session
from app.database import get_db from app.database import get_db
from app.domain.unit_of_work import UnitOfWork
from app.dependencies.auth import get_current_user 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.enums import TeamSide
from app.models.evidence import Evidence from app.models.evidence import Evidence
from app.models.user import User from app.models.user import User
from app.schemas.evidence import EvidenceOut from app.schemas.evidence import EvidenceOut
from app.services.audit_service import log_action from app.services.audit_service import log_action
from app.services.evidence_service import ( from app.services.evidence_service import (
MAX_UPLOAD_SIZE,
get_evidence_or_raise, get_evidence_or_raise,
get_test_or_raise, get_test_or_raise,
list_evidence_for_test, list_evidence_for_test,
MAX_UPLOAD_SIZE,
validate_delete_permission, validate_delete_permission,
validate_file, validate_file,
validate_upload_permission, validate_upload_permission,
) )
from app.limiter import limiter
from app.storage import get_presigned_url, upload_file from app.storage import get_presigned_url, upload_file
router = APIRouter(tags=["evidence"]) router = APIRouter(tags=["evidence"])
+1 -1
View File
@@ -17,7 +17,7 @@ from app.schemas.jira_schema import (
JiraLinkCreate, JiraLinkCreate,
JiraLinkOut, JiraLinkOut,
) )
from app.services import jira_service, audit_service from app.services import audit_service, jira_service
logger = logging.getLogger(__name__) 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.models.user import User
from app.schemas.notification import NotificationOut, UnreadCountOut from app.schemas.notification import NotificationOut, UnreadCountOut
from app.services.notification_service import ( from app.services.notification_service import (
list_notifications,
mark_as_read,
mark_all_as_read,
get_unread_count, get_unread_count,
list_notifications,
mark_all_as_read,
mark_as_read,
) )
router = APIRouter(prefix="/notifications", tags=["notifications"]) 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.dependencies.auth import get_current_user
from app.models.user import User from app.models.user import User
from app.services.operational_metrics_service import ( from app.services.operational_metrics_service import (
get_all_operational_metrics,
get_operational_trend,
get_metrics_by_team, get_metrics_by_team,
get_operational_trend,
) )
router = APIRouter(prefix="/metrics/operational", tags=["operational-metrics"]) 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 uuid import UUID
from fastapi import APIRouter, Depends, Query, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -17,9 +17,11 @@ from app.services.osint_enrichment_service import (
get_osint_items_for_technique, get_osint_items_for_technique,
get_osint_summary, get_osint_summary,
get_technique_or_raise, get_technique_or_raise,
list_osint_items as service_list_osint_items,
mark_osint_reviewed, mark_osint_reviewed,
) )
from app.services.osint_enrichment_service import (
list_osint_items as service_list_osint_items,
)
router = APIRouter(prefix="/osint", tags=["osint"]) 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.database import get_db
from app.dependencies.auth import get_current_user, require_any_role from app.dependencies.auth import get_current_user, require_any_role
from app.models.user import User
from app.limiter import limiter from app.limiter import limiter
from app.models.user import User
from app.services import report_generation_service from app.services import report_generation_service
router = APIRouter(prefix="/reports/generate", tags=["professional-reports"]) 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.dependencies.auth import get_current_user, require_role
from app.domain.unit_of_work import UnitOfWork from app.domain.unit_of_work import UnitOfWork
from app.models.user import User 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 ( from app.services.scoring_config_service import (
get_weights_dict, get_weights_dict,
update_scoring_weights, 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"]) 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.errors import BusinessRuleViolation
from app.domain.unit_of_work import UnitOfWork from app.domain.unit_of_work import UnitOfWork
from app.models.user import User 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.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__) 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.database import get_db
from app.dependencies.auth import require_role 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.jobs.mitre_sync_job import scheduler
from app.limiter import limiter 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__) 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 sqlalchemy.orm import Session
from app.database import get_db 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.dependencies.repositories import get_technique_repository
from app.domain.entities.technique import TechniqueEntity from app.domain.entities.technique import TechniqueEntity
from app.domain.errors import DuplicateEntityError, EntityNotFoundError
from app.domain.enums import TechniqueStatus from app.domain.enums import TechniqueStatus
from app.domain.errors import DuplicateEntityError, EntityNotFoundError
from app.domain.unit_of_work import UnitOfWork from app.domain.unit_of_work import UnitOfWork
from app.infrastructure.persistence.repositories.sa_technique_repository import ( from app.infrastructure.persistence.repositories.sa_technique_repository import (
SATechniqueRepository, 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.audit_service import log_action
from app.services.test_template_service import ( from app.services.test_template_service import (
bulk_activate, bulk_activate,
create_template as create_template_svc,
get_template_or_raise, get_template_or_raise,
get_template_stats, get_template_stats,
get_templates_by_technique as templates_by_technique,
list_templates, list_templates,
soft_delete_template, 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, toggle_template_active as toggle_template_active_svc,
)
from app.services.test_template_service import (
update_template as update_template_svc, 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.database import get_db
from app.dependencies.auth import get_current_user, require_any_role, require_role 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.limiter import limiter
from app.models.enums import TestState from app.models.enums import TestState
from app.models.user import User from app.models.user import User
from app.schemas.test import ( from app.schemas.test import (
TestBlueUpdate,
TestBlueValidate,
TestClassificationUpdate,
TestCreate, TestCreate,
TestOut, TestOut,
TestUpdate,
TestRedUpdate, TestRedUpdate,
TestBlueUpdate,
TestRedValidate, TestRedValidate,
TestBlueValidate,
TestRemediationUpdate, TestRemediationUpdate,
TestClassificationUpdate, TestUpdate,
) )
from app.schemas.test_template import TestTemplateInstantiate 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.audit_service import log_action
from app.services.status_service import recalculate_technique_status from app.services.status_service import recalculate_technique_status
from app.services.test_crud_service import ( from app.services.test_crud_service import (
create_test as crud_create_test, create_test as crud_create_test,
)
from app.services.test_crud_service import (
create_test_from_template as crud_create_from_template, create_test_from_template as crud_create_from_template,
)
from app.services.test_crud_service import (
get_test_detail as crud_get_test_detail, 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, 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, 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, get_test_with_technique as crud_get_test_with_technique,
)
from app.services.test_crud_service import (
list_tests as crud_list_tests, list_tests as crud_list_tests,
)
from app.services.test_crud_service import (
update_test as crud_update_test, update_test as crud_update_test,
)
from app.services.test_crud_service import (
update_test_blue as crud_update_test_blue, update_test_blue as crud_update_test_blue,
)
from app.services.test_crud_service import (
update_test_red as crud_update_test_red, update_test_red as crud_update_test_red,
) )
from app.services.test_workflow_service import ( 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, 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, 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, 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"]) 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.dependencies.auth import require_role
from app.domain.unit_of_work import UnitOfWork from app.domain.unit_of_work import UnitOfWork
from app.models.user import User 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.audit_service import log_action
from app.services.user_service import ( from app.services.user_service import (
create_user, create_user,
+1 -1
View File
@@ -4,7 +4,7 @@ from datetime import datetime
from typing import Optional from typing import Optional
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
+7 -11
View File
@@ -1,32 +1,28 @@
"""Pydantic schemas — re-exported for convenient imports.""" """Pydantic schemas — re-exported for convenient imports."""
from app.schemas.auth import LoginRequest, TokenResponse, UserOut from app.schemas.auth import LoginRequest, TokenResponse, UserOut
from app.schemas.evidence import EvidenceOut, EvidenceUpload
from app.schemas.technique import ( from app.schemas.technique import (
TechniqueCreate, TechniqueCreate,
TechniqueOut, TechniqueOut,
TechniqueSummary, TechniqueSummary,
TechniqueUpdate, TechniqueUpdate,
) )
from app.schemas.test import ( from app.schemas.test import (
TestBlueUpdate,
TestBlueValidate,
TestCreate, TestCreate,
TestOut, TestOut,
TestRedUpdate,
TestRedValidate,
TestUpdate, TestUpdate,
TestValidate, TestValidate,
TestRedUpdate,
TestBlueUpdate,
TestRedValidate,
TestBlueValidate,
) )
from app.schemas.evidence import EvidenceOut, EvidenceUpload
from app.schemas.test_template import ( from app.schemas.test_template import (
TestTemplateOut,
TestTemplateCreate, TestTemplateCreate,
TestTemplateSummary,
TestTemplateInstantiate, TestTemplateInstantiate,
TestTemplateOut,
TestTemplateSummary,
) )
__all__ = [ __all__ = [
-1
View File
@@ -7,7 +7,6 @@ from pydantic import BaseModel, ConfigDict
from app.models.enums import TechniqueStatus from app.models.enums import TechniqueStatus
# ── Create ────────────────────────────────────────────────────────── # ── Create ──────────────────────────────────────────────────────────
class TechniqueCreate(BaseModel): class TechniqueCreate(BaseModel):
-1
View File
@@ -8,7 +8,6 @@ from pydantic import BaseModel, ConfigDict
from app.domain.enums import DataClassification from app.domain.enums import DataClassification
from app.models.enums import TestResult, TestState from app.models.enums import TestResult, TestState
# ── Create ────────────────────────────────────────────────────────── # ── Create ──────────────────────────────────────────────────────────
-1
View File
@@ -5,7 +5,6 @@ from datetime import datetime
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
# ── Full output ───────────────────────────────────────────────────── # ── Full output ─────────────────────────────────────────────────────
+1 -2
View File
@@ -4,8 +4,7 @@ import re
import uuid import uuid
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator from pydantic import BaseModel, ConfigDict, field_validator
# ── Username policy ───────────────────────────────────────────────── # ── Username policy ─────────────────────────────────────────────────
+15 -7
View File
@@ -18,14 +18,14 @@ from datetime import datetime, timedelta
from app.auth import hash_password from app.auth import hash_password
from app.database import SessionLocal 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.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.evidence import Evidence from app.models.user import User
from app.models.audit import AuditLog
from app.models.notification import Notification
from app.models.enums import TechniqueStatus, TestState, TestResult, TeamSide
logger = logging.getLogger(__name__) 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]}", name=f"Demo Test {i + 1}{technique.name[:40]}",
description=f"Automated demo test #{i + 1} for {technique.mitre_id}.", description=f"Automated demo test #{i + 1} for {technique.mitre_id}.",
platform=random.choice(PLATFORMS), 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"]), tool_used=random.choice(["powershell", "bash", "cmd", "python", "caldera", "metasploit"]),
execution_date=datetime.utcnow() - timedelta(days=random.randint(0, 60)), execution_date=datetime.utcnow() - timedelta(days=random.randint(0, 60)),
created_by=creator.id, 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}).", description=f"Demo template: {name}. Targets {technique.mitre_id} ({technique.name}).",
source="demo", source="demo",
source_url=None, 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.", expected_detection=f"SIEM should alert on {technique.mitre_id} indicators.",
platform=random.choice(PLATFORMS), platform=random.choice(PLATFORMS),
tool_suggested=random.choice(["powershell", "cmd", "bash", "python"]), tool_suggested=random.choice(["powershell", "cmd", "bash", "python"]),
@@ -7,9 +7,9 @@ from datetime import datetime, timedelta
from sqlalchemy import case, func from sqlalchemy import case, func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.enums import TestResult
from app.models.technique import Technique from app.models.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.enums import TestResult
def get_coverage_by_tactic(db: Session) -> list[dict]: 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 io
import logging import logging
import os
import shutil import shutil
import tempfile import tempfile
import zipfile import zipfile
@@ -54,6 +53,10 @@ _DOWNLOAD_TIMEOUT = 300
# Top-level directory name inside the ZIP # Top-level directory name inside the ZIP
_ZIP_ROOT_PREFIX = "atomic-red-team-master" _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 # 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 directory (path traversal / Zip Slip) or if the archive exceeds the
safety limits. 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() dest_path = Path(dest).resolve()
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
@@ -33,8 +33,8 @@ import requests as _requests
import yaml import yaml
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.test_template import TestTemplate
from app.models.data_source import DataSource from app.models.data_source import DataSource
from app.models.test_template import TestTemplate
from app.services.audit_service import log_action from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -16,16 +16,15 @@ from app.domain.errors import (
PermissionViolation, PermissionViolation,
) )
from app.models.campaign import Campaign, CampaignTest from app.models.campaign import Campaign, CampaignTest
from app.models.test import Test
from app.models.technique import Technique 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 ( from app.services.campaign_service import (
TACTIC_TO_PHASE,
get_campaign_progress, get_campaign_progress,
validate_no_circular_dependency, 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 ──────────────────────────────────────────────── # ── Serialization helpers ────────────────────────────────────────────────
@@ -5,17 +5,16 @@ fresh tests, and computing the next run date.
""" """
import logging import logging
import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.campaign import Campaign, CampaignTest from app.models.campaign import Campaign, CampaignTest
from app.models.test import Test
from app.models.enums import TestState from app.models.enums import TestState
from app.services.notification_service import create_notification from app.models.test import Test
from app.services.audit_service import log_action
from app.models.user import User 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__) logger = logging.getLogger(__name__)
@@ -166,7 +165,10 @@ def check_and_run_recurring_campaigns(db: Session) -> int:
user_id=campaign.created_by, user_id=campaign.created_by,
type="recurring_campaign_run", type="recurring_campaign_run",
title="Recurring campaign executed", 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_type="campaign",
entity_id=child.id, entity_id=child.id,
) )
+3 -5
View File
@@ -6,18 +6,16 @@ threat actors, and progress calculation.
import logging import logging
import uuid import uuid
from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.domain.exceptions import EntityNotFoundError, InvalidOperationError 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 import Test
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.technique import Technique
from app.models.threat_actor import ThreatActor, ThreatActorTechnique 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 from app.models.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+250 -107
View File
@@ -6,22 +6,256 @@ ComplianceControl, and ComplianceControlMapping records.
""" """
import logging import logging
import json
import re import re
from typing import Optional
import requests import requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.compliance import ( from app.models.compliance import (
ComplianceFramework,
ComplianceControl, ComplianceControl,
ComplianceControlMapping, ComplianceControlMapping,
ComplianceFramework,
) )
from app.models.technique import Technique from app.models.technique import Technique
logger = logging.getLogger(__name__) 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 # URL for the NIST 800-53 Rev 5 to ATT&CK mapping
# This is the JSON STIX bundle that contains the relationships # This is the JSON STIX bundle that contains the relationships
NIST_MAPPING_URL = ( NIST_MAPPING_URL = (
@@ -53,7 +287,11 @@ def import_nist_800_53_mappings(db: Session) -> dict:
framework = ComplianceFramework( framework = ComplianceFramework(
name="NIST 800-53 Rev 5", name="NIST 800-53 Rev 5",
version="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", url="https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final",
is_active=True, 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. 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 # Build technique lookup
all_techniques = {t.mitre_id: t for t in db.query(Technique).all()} 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 controls_created = 0
mappings_created = 0 mappings_created = 0
for sample in SAMPLE_CONTROLS: for sample in _NIST_SAMPLE_CONTROLS:
# Create or get control # Create or get control
if sample["control_id"] in existing_controls: if sample["control_id"] in existing_controls:
control = existing_controls[sample["control_id"]] control = existing_controls[sample["control_id"]]
@@ -348,8 +543,11 @@ def import_cis_controls_v8_mappings(db: Session) -> dict:
framework = ComplianceFramework( framework = ComplianceFramework(
name="CIS Controls v8", name="CIS Controls v8",
version="8", version="8",
description="Center for Internet Security Critical Security Controls Version 8 — " description=(
"a prioritized set of 18 security safeguards organized by Implementation Groups (IG1, IG2, IG3).", "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", url="https://www.cisecurity.org/controls/v8",
is_active=True, is_active=True,
) )
@@ -360,62 +558,7 @@ def import_cis_controls_v8_mappings(db: Session) -> dict:
logger.info("CIS Controls v8 framework already exists") logger.info("CIS Controls v8 framework already exists")
# ── 2. Control definitions with ATT&CK mappings ─────────────── # ── 2. Control definitions with ATT&CK mappings ───────────────
CIS_CONTROLS = [ # (defined at module level as _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"]},
]
# Build technique lookup # Build technique lookup
all_techniques = {t.mitre_id: t for t in db.query(Technique).all()} 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 controls_created = 0
mappings_created = 0 mappings_created = 0
for item in CIS_CONTROLS: for item in _CIS_CONTROLS:
if item["control_id"] in existing_controls: if item["control_id"] in existing_controls:
control = existing_controls[item["control_id"]] control = existing_controls[item["control_id"]]
else: else:
+1 -2
View File
@@ -16,16 +16,15 @@ from sqlalchemy.orm import Session
from app.domain.errors import EntityNotFoundError from app.domain.errors import EntityNotFoundError
from app.models.compliance import ( from app.models.compliance import (
ComplianceFramework,
ComplianceControl, ComplianceControl,
ComplianceControlMapping, ComplianceControlMapping,
ComplianceFramework,
) )
from app.models.technique import Technique from app.models.technique import Technique
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.threat_actor import ThreatActorTechnique from app.models.threat_actor import ThreatActorTechnique
from app.services.scoring_service import calculate_technique_score from app.services.scoring_service import calculate_technique_score
# ── Helpers ─────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────
@@ -7,14 +7,13 @@ Uses the D3FEND public API:
""" """
import logging import logging
import uuid
from typing import Any from typing import Any
import httpx import httpx
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.technique import Technique
from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqueMapping
from app.models.technique import Technique
logger = logging.getLogger(__name__) 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. Called by the Data Sources router when the user clicks Sync for D3FEND.
Returns a flat summary dict suitable for ``last_sync_stats``. Returns a flat summary dict suitable for ``last_sync_stats``.
""" """
from app.models.data_source import DataSource
from datetime import datetime from datetime import datetime
from app.models.data_source import DataSource
tech_result = import_d3fend_techniques(db) tech_result = import_d3fend_techniques(db)
mapping_result = import_d3fend_mappings(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: if not ds:
raise EntityNotFoundError("Data source", source_id) raise EntityNotFoundError("Data source", source_id)
from app.models.test_template import TestTemplate
from app.models.detection_rule import DetectionRule from app.models.detection_rule import DetectionRule
from app.models.test_template import TestTemplate
template_count = 0 template_count = 0
rule_count = 0 rule_count = 0
@@ -15,14 +15,13 @@ from sqlalchemy.orm import Session
from app.domain.errors import EntityNotFoundError from app.domain.errors import EntityNotFoundError
from app.models.detection_rule import DetectionRule from app.models.detection_rule import DetectionRule
from app.models.technique import Technique
from app.models.test import Test 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 import TestTemplate
from app.models.test_template_detection_rule import TestTemplateDetectionRule 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 from app.utils import escape_like
# ── Public service functions ────────────────────────────────────────── # ── Public service functions ──────────────────────────────────────────
@@ -32,8 +32,8 @@ from pathlib import Path
import requests as _requests import requests as _requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.detection_rule import DetectionRule
from app.models.data_source import DataSource from app.models.data_source import DataSource
from app.models.detection_rule import DetectionRule
from app.services.audit_service import log_action from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,6 +50,10 @@ ELASTIC_ZIP_URL = (
_DOWNLOAD_TIMEOUT = 300 _DOWNLOAD_TIMEOUT = 300
_ZIP_ROOT_PREFIX = "detection-rules-main" _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 normalisation
_SEVERITY_MAP = { _SEVERITY_MAP = {
"informational": "informational", "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 directory (path traversal / Zip Slip) or if the archive exceeds the
safety limits. 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() dest_path = Path(dest).resolve()
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
-2
View File
@@ -10,7 +10,6 @@ no ``db.commit()``.
from __future__ import annotations from __future__ import annotations
import json import json
from typing import Optional
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -18,7 +17,6 @@ from sqlalchemy.orm import Session
from app.domain.errors import BusinessRuleViolation, EntityNotFoundError from app.domain.errors import BusinessRuleViolation, EntityNotFoundError
from app.models.campaign import Campaign, CampaignTest from app.models.campaign import Campaign, CampaignTest
from app.models.detection_rule import DetectionRule from app.models.detection_rule import DetectionRule
from app.models.defensive_technique import DefensiveTechniqueMapping
from app.models.enums import TechniqueStatus, TestState from app.models.enums import TechniqueStatus, TestState
from app.models.technique import Technique from app.models.technique import Technique
from app.models.test import Test 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 logging
import re import re
import defusedxml.ElementTree as ET
from datetime import datetime from datetime import datetime
import defusedxml.ElementTree as ET # noqa: N817 — ET is the universal stdlib alias for ElementTree
import requests as _requests import requests as _requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -37,8 +37,8 @@ import requests as _requests
import yaml import yaml
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.test_template import TestTemplate
from app.models.data_source import DataSource from app.models.data_source import DataSource
from app.models.test_template import TestTemplate
from app.services.audit_service import log_action from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -176,7 +176,11 @@ def _parse_lolbas(root_dir: Path) -> list[dict]:
results.append({ results.append({
"mitre_technique_id": mitre_id, "mitre_technique_id": mitre_id,
"name": f"LOLBAS: {binary_name}{usecase or cmd_description or mitre_id}"[:500], "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", "source": "lolbas",
"platform": "windows", "platform": "windows",
"tool_suggested": binary_name, "tool_suggested": binary_name,
+1 -1
View File
@@ -13,8 +13,8 @@ import requests as _requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from taxii2client.v20 import Server as TaxiiServer from taxii2client.v20 import Server as TaxiiServer
from app.models.technique import Technique
from app.models.enums import TechniqueStatus from app.models.enums import TechniqueStatus
from app.models.technique import Technique
from app.services.audit_service import log_action from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1 -2
View File
@@ -10,14 +10,13 @@ but do **not** commit. The caller is responsible for committing.
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import Session
from app.domain.errors import EntityNotFoundError from app.domain.errors import EntityNotFoundError
from app.models.notification import Notification from app.models.notification import Notification
from app.models.user import User from app.models.user import User
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Core CRUD # Core CRUD
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -6,14 +6,14 @@ Calculates security operations KPIs from test data and audit logs.
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from sqlalchemy import func, case, and_, or_, extract from sqlalchemy import func
from sqlalchemy.orm import Session 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.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: def _safe_stats(values: list[float]) -> dict:
+2 -2
View File
@@ -3,9 +3,9 @@
Uses WeasyPrint for PDF generation and docxtpl for DOCX. Uses WeasyPrint for PDF generation and docxtpl for DOCX.
""" """
import logging
import os import os
import uuid import uuid
import logging
from datetime import datetime from datetime import datetime
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@@ -34,7 +34,7 @@ class ReportEngine:
def generate_pdf(self, template_name: str, context: dict) -> str: def generate_pdf(self, template_name: str, context: dict) -> str:
"""Render HTML and convert to PDF with WeasyPrint.""" """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) html_content = self.render_html(template_name, context)
css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css") css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css")
@@ -98,7 +98,7 @@ def generate_coverage_report(
output_format: str = "pdf", output_format: str = "pdf",
) -> str: ) -> str:
"""Generate an organization-wide MITRE ATT&CK coverage report.""" """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) org_score = _safe_org_score(db)
@@ -234,7 +234,8 @@ def generate_quarterly_summary(
output_format: str = "pdf", output_format: str = "pdf",
) -> str: ) -> str:
"""Quarterly summary — reuses executive metrics plus snapshot trend rows.""" """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) org_score = _safe_org_score(db)
quarter_ago = datetime.utcnow() - timedelta(days=90) 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): def get_operational_metrics_cached(db):
"""Cached wrapper around operational metrics (MTTD, MTTR, efficacy).""" """Cached wrapper around operational metrics (MTTD, MTTR, efficacy)."""
from app.services.operational_metrics_service import ( from app.services.operational_metrics_service import (
calculate_mttd,
calculate_mttr,
calculate_detection_efficacy,
calculate_alert_fidelity, calculate_alert_fidelity,
calculate_coverage_velocity, calculate_coverage_velocity,
calculate_validation_throughput, calculate_detection_efficacy,
calculate_mttd,
calculate_mttr,
calculate_rejection_rate, calculate_rejection_rate,
calculate_validation_throughput,
) )
cached = get("op_metrics") cached = get("op_metrics")
+3 -5
View File
@@ -10,19 +10,18 @@ never produce N+1 traffic.
""" """
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional
from sqlalchemy import case, func from sqlalchemy import case, func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.domain.errors import EntityNotFoundError 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.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.detection_rule import DetectionRule
from app.models.test_detection_result import TestDetectionResult 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.threat_actor import ThreatActor, ThreatActorTechnique
from app.models.enums import TestState, TestResult
from app.services.scoring_config_service import get_scoring_weights from app.services.scoring_config_service import get_scoring_weights
_SEVERITY_FACTORS: dict[str, float] = { _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. computing scores based on test dates within time windows.
Returns a list of weekly data points. Returns a list of weekly data points.
""" """
from app.models.audit import AuditLog
now = datetime.utcnow() now = datetime.utcnow()
if period == "30d": if period == "30d":
+7 -11
View File
@@ -35,8 +35,8 @@ import requests as _requests
import yaml import yaml
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.detection_rule import DetectionRule
from app.models.data_source import DataSource from app.models.data_source import DataSource
from app.models.detection_rule import DetectionRule
from app.services.audit_service import log_action from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -52,6 +52,10 @@ SIGMA_ZIP_URL = (
_DOWNLOAD_TIMEOUT = 300 _DOWNLOAD_TIMEOUT = 300
_ZIP_ROOT_PREFIX = "sigma-master" _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 # Regex to extract MITRE ATT&CK technique IDs from Sigma tags
# e.g. "attack.t1059.001" → "T1059.001" # e.g. "attack.t1059.001" → "T1059.001"
_ATTACK_TAG_RE = re.compile(r"attack\.(t\d{4}(?:\.\d{3})?)", re.IGNORECASE) _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 directory (path traversal / Zip Slip) or if the archive exceeds the
safety limits. 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() dest_path = Path(dest).resolve()
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
@@ -290,11 +289,8 @@ def sync(db: Session) -> dict:
skipped = 0 skipped = 0
for item in parsed_rules: for item in parsed_rules:
# Dedup key: source_id (relative path). A rule file may produce # Deduplicate by source_id: one rule file may map to multiple techniques,
# multiple entries (one per technique), but we deduplicate by # but we skip insertion if this source_id was already imported.
# 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']}"
if item["source_id"] in existing_ids: if item["source_id"] in existing_ids:
skipped += 1 skipped += 1
continue continue
+12 -12
View File
@@ -16,9 +16,9 @@ from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.domain.errors import EntityNotFoundError from app.domain.errors import EntityNotFoundError
from app.models.technique import Technique
from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState from app.models.coverage_snapshot import CoverageSnapshot, SnapshotTechniqueState
from app.models.enums import TechniqueStatus from app.models.enums import TechniqueStatus
from app.models.technique import Technique
from app.services.scoring_service import ( from app.services.scoring_service import (
bulk_technique_scores, bulk_technique_scores,
calculate_organization_score, calculate_organization_score,
@@ -26,6 +26,15 @@ from app.services.scoring_service import (
logger = logging.getLogger(__name__) 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 # Serialization and queries
@@ -296,15 +305,6 @@ def compare_snapshots(
.all() .all()
} }
# Status priority for comparison
STATUS_ORDER = {
"not_evaluated": 0,
"not_covered": 1,
"in_progress": 2,
"partial": 3,
"validated": 4,
}
improved = [] improved = []
worsened = [] worsened = []
unchanged_count = 0 unchanged_count = 0
@@ -315,8 +315,8 @@ def compare_snapshots(
a = states_a.get(mitre_id, {"status": "not_evaluated", "score": 0}) a = states_a.get(mitre_id, {"status": "not_evaluated", "score": 0})
b = states_b.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) a_order = _STATUS_ORDER.get(a["status"], 0)
b_order = STATUS_ORDER.get(b["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"]): if b_order > a_order or (b_order == a_order and b["score"] > a["score"]):
improved.append({ improved.append({
+1 -1
View File
@@ -14,11 +14,11 @@ from app.domain.errors import (
EntityNotFoundError, EntityNotFoundError,
PermissionViolation, PermissionViolation,
) )
from app.models.audit import AuditLog
from app.models.enums import TestState from app.models.enums import TestState
from app.models.technique import Technique from app.models.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.audit import AuditLog
from app.utils import escape_like from app.utils import escape_like
@@ -18,13 +18,16 @@ from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.config import settings 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.domain.test_entity import TestEntity
from app.models.enums import TestState from app.models.enums import TestState
from app.models.test import Test from app.models.test import Test
from app.models.user import User from app.models.user import User
from app.services.audit_service import log_action 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__) logger = logging.getLogger(__name__)
@@ -38,9 +38,9 @@ from pathlib import Path
import requests as _requests import requests as _requests
from sqlalchemy.orm import Session 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.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 from app.services.audit_service import log_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -241,9 +241,6 @@ def sync(db: Session) -> dict:
relationships = _parse_relationships(objects) relationships = _parse_relationships(objects)
attack_pattern_map = _build_attack_pattern_map(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 # Step 3: Load existing actors and techniques from DB
existing_actors = { existing_actors = {
row.mitre_id: row 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.domain.errors import EntityNotFoundError
from app.models.enums import TechniqueStatus from app.models.enums import TechniqueStatus
from app.models.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.threat_actor import ThreatActor, ThreatActorTechnique from app.models.threat_actor import ThreatActor, ThreatActorTechnique
from app.models.technique import Technique
from app.utils import escape_like from app.utils import escape_like
# ── Public service functions ────────────────────────────────────────── # ── Public service functions ──────────────────────────────────────────
+5 -1
View File
@@ -11,7 +11,11 @@ import uuid
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.auth import hash_password 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 from app.models.user import User
VALID_ROLES = {"admin", "red_tech", "blue_tech", "red_lead", "blue_lead", "viewer"} 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] [lint]
# Ignore rules that have widespread pre-existing violations. # PEP8 compliance rules enforced:
# These can be cleaned up incrementally in follow-up PRs. # 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 = [ ignore = [
"E402", # module-level import not at top of file (app.main, some services) # SQLAlchemy filter syntax requires `== True` / `== False` comparisons
"E712", # == True comparisons (required by SQLAlchemy filter syntax) "E712",
"F401", # unused imports (widespread; clean up incrementally)
"F841", # unused local variables (a few occurrences)
] ]
[lint.per-file-ignores] [lint.per-file-ignores]
# Test files may use broad exception catching and unusual import patterns # Tests use broad exception catching and unusual import patterns
"tests/**" = ["E", "F"] "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"]