feat(phase-33): final polish V3 - navigation, performance, and documentation (T-238 to T-240)
This commit is contained in:
78
backend/alembic/versions/b018_add_performance_indexes.py
Normal file
78
backend/alembic/versions/b018_add_performance_indexes.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""add_performance_indexes
|
||||
|
||||
Revision ID: b018perfidx
|
||||
Revises: b017scheduling
|
||||
Create Date: 2026-02-10 06:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b018perfidx"
|
||||
down_revision: Union[str, None] = "b017scheduling"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Composite index for detection rules filtered by technique + source
|
||||
op.create_index(
|
||||
"ix_detection_rules_technique_source",
|
||||
"detection_rules",
|
||||
["mitre_technique_id", "source"],
|
||||
)
|
||||
|
||||
# Composite index for snapshot technique states
|
||||
op.create_index(
|
||||
"ix_snapshot_technique_states_snap_tech",
|
||||
"snapshot_technique_states",
|
||||
["snapshot_id", "technique_id"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# Covering index for tests frequently filtered by technique + state
|
||||
op.create_index(
|
||||
"ix_tests_technique_state",
|
||||
"tests",
|
||||
["technique_id", "state"],
|
||||
)
|
||||
|
||||
# Audit logs — timestamp-based lookups
|
||||
op.create_index(
|
||||
"ix_audit_logs_timestamp",
|
||||
"audit_logs",
|
||||
["timestamp"],
|
||||
)
|
||||
|
||||
# Audit logs — entity lookups
|
||||
op.create_index(
|
||||
"ix_audit_logs_entity",
|
||||
"audit_logs",
|
||||
["entity_type", "entity_id"],
|
||||
)
|
||||
|
||||
# Test detection results — triggered flag for maturity queries
|
||||
op.create_index(
|
||||
"ix_test_detection_results_triggered",
|
||||
"test_detection_results",
|
||||
["triggered"],
|
||||
)
|
||||
|
||||
# Compliance control mappings — composite for joins
|
||||
op.create_index(
|
||||
"ix_compliance_mappings_control_technique",
|
||||
"compliance_control_mappings",
|
||||
["compliance_control_id", "technique_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_compliance_mappings_control_technique", table_name="compliance_control_mappings")
|
||||
op.drop_index("ix_test_detection_results_triggered", table_name="test_detection_results")
|
||||
op.drop_index("ix_audit_logs_entity", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_timestamp", table_name="audit_logs")
|
||||
op.drop_index("ix_tests_technique_state", table_name="tests")
|
||||
op.drop_index("ix_snapshot_technique_states_snap_tech", table_name="snapshot_technique_states")
|
||||
op.drop_index("ix_detection_rules_technique_source", table_name="detection_rules")
|
||||
@@ -27,8 +27,10 @@ def operational_metrics(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all operational metrics (MTTD, MTTR, Detection Efficacy, etc.)."""
|
||||
return get_all_operational_metrics(db)
|
||||
"""Get all operational metrics (MTTD, MTTR, etc.) — cached for 5 min."""
|
||||
from app.services.score_cache import get_operational_metrics_cached
|
||||
|
||||
return get_operational_metrics_cached(db)
|
||||
|
||||
|
||||
# ── GET /metrics/operational/trend ────────────────────────────────────
|
||||
|
||||
@@ -93,8 +93,10 @@ def score_organization(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get the overall organization security score."""
|
||||
return calculate_organization_score(db)
|
||||
"""Get the overall organization security score (cached for 5 min)."""
|
||||
from app.services.score_cache import get_organization_score_cached
|
||||
|
||||
return get_organization_score_cached(db)
|
||||
|
||||
|
||||
# ── GET /scores/history ──────────────────────────────────────────────
|
||||
@@ -170,6 +172,10 @@ def update_scoring_config(
|
||||
if payload.platform_diversity is not None:
|
||||
settings.SCORING_WEIGHT_PLATFORM_DIVERSITY = payload.platform_diversity
|
||||
|
||||
# Weights changed — bust the score cache
|
||||
from app.services.score_cache import invalidate
|
||||
invalidate()
|
||||
|
||||
return {
|
||||
"message": "Scoring config updated",
|
||||
"weights": {
|
||||
|
||||
84
backend/app/services/score_cache.py
Normal file
84
backend/app/services/score_cache.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""In-memory TTL cache for expensive scoring and metrics calculations.
|
||||
|
||||
The cache is a simple dict with timestamps. It is invalidated when tests
|
||||
are validated, scores change, or an explicit ``invalidate`` call is made.
|
||||
|
||||
Thread-safe: each worker process has its own dict, and the TTL ensures
|
||||
stale data does not persist longer than ``CACHE_TTL`` seconds.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
CACHE_TTL = 300 # 5 minutes
|
||||
|
||||
_cache: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def get(key: str) -> Optional[Any]:
|
||||
"""Return cached value if present and not expired, else None."""
|
||||
entry = _cache.get(key)
|
||||
if entry is None:
|
||||
return None
|
||||
if time.time() - entry["ts"] > CACHE_TTL:
|
||||
_cache.pop(key, None)
|
||||
return None
|
||||
return entry["data"]
|
||||
|
||||
|
||||
def put(key: str, data: Any) -> None:
|
||||
"""Store *data* under *key* with the current timestamp."""
|
||||
_cache[key] = {"data": data, "ts": time.time()}
|
||||
|
||||
|
||||
def invalidate(key: Optional[str] = None) -> None:
|
||||
"""Remove one key or clear the whole cache."""
|
||||
if key is None:
|
||||
_cache.clear()
|
||||
else:
|
||||
_cache.pop(key, None)
|
||||
|
||||
|
||||
# ── High-level helpers ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_organization_score_cached(db):
|
||||
"""Cached wrapper around ``calculate_organization_score``."""
|
||||
from app.services.scoring_service import calculate_organization_score
|
||||
|
||||
cached = get("org_score")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = calculate_organization_score(db)
|
||||
put("org_score", result)
|
||||
return result
|
||||
|
||||
|
||||
def get_operational_metrics_cached(db):
|
||||
"""Cached wrapper around operational metrics (MTTD, MTTR, efficacy)."""
|
||||
from app.services.operational_metrics_service import (
|
||||
calculate_mttd,
|
||||
calculate_mttr,
|
||||
calculate_detection_efficacy,
|
||||
calculate_alert_fidelity,
|
||||
calculate_coverage_velocity,
|
||||
calculate_validation_throughput,
|
||||
calculate_rejection_rate,
|
||||
)
|
||||
|
||||
cached = get("op_metrics")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = {
|
||||
"mttd": calculate_mttd(db),
|
||||
"mttr": calculate_mttr(db),
|
||||
"detection_efficacy": calculate_detection_efficacy(db),
|
||||
"alert_fidelity": calculate_alert_fidelity(db),
|
||||
"coverage_velocity": calculate_coverage_velocity(db),
|
||||
"validation_throughput": calculate_validation_throughput(db),
|
||||
"rejection_rate": calculate_rejection_rate(db),
|
||||
}
|
||||
put("op_metrics", result)
|
||||
return result
|
||||
@@ -288,6 +288,12 @@ def check_dual_validation(db: Session, test: Test) -> Test:
|
||||
elif red_status == "approved" and blue_status == "approved":
|
||||
test.state = TestState.validated
|
||||
db.commit()
|
||||
# Invalidate cached scores — a validation changes org-level numbers
|
||||
try:
|
||||
from app.services.score_cache import invalidate
|
||||
invalidate()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
notify_test_state_change(db, test, "validated")
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user