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! Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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 ────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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,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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__ = [
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
# ── Full output ─────────────────────────────────────────────────────
|
# ── Full output ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user