fix: D3FEND expandable cards, System page cleanup, and multi-source improvements
- Make D3FEND defense cards clickable with expandable details and external link - Fix D3FEND URLs to use PascalCase technique names matching the ontology - Remove duplicate Import Atomic Red Team from System page (use Data Sources) - Add bulk Activate All / Deactivate All buttons with confirmation modal - Fix template admin list to show both active and inactive templates - Add PATCH /test-templates/bulk-activate backend endpoint - Auto-seed data sources on container startup via entrypoint.sh - Fix SigmaHQ, CALDERA, GTFOBins import issues - Register D3FEND sync handler in data sources router - Add CIS Controls v8 compliance framework import - Expand Test Catalog source filters (CALDERA, LOLBAS, GTFOBins) - Campaign Generate from Threat Actor now opens actor selector modal - Add coverage snapshot creation button to Comparison page - Update README with accurate data source and feature documentation
This commit is contained in:
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
D3FEND_ALL_URL = "https://d3fend.mitre.org/api/technique/api-all.json"
|
||||
D3FEND_MAPPING_URL = "https://d3fend.mitre.org/api/offensive-technique/{attack_id}.json"
|
||||
D3FEND_BASE_URL = "https://d3fend.mitre.org/technique/d3f:{d3fend_id}"
|
||||
D3FEND_BASE_URL = "https://d3fend.mitre.org/technique/d3f:{technique_name}"
|
||||
|
||||
|
||||
# ── Tactic extraction helpers ────────────────────────────────────────
|
||||
@@ -139,7 +139,8 @@ def import_d3fend_techniques(db: Session) -> dict[str, int]:
|
||||
.filter(DefensiveTechnique.d3fend_id == tech_data["d3fend_id"])
|
||||
.first()
|
||||
)
|
||||
d3fend_url = D3FEND_BASE_URL.format(d3fend_id=tech_data["d3fend_id"])
|
||||
technique_name = tech_data["name"].replace(" ", "")
|
||||
d3fend_url = D3FEND_BASE_URL.format(technique_name=technique_name)
|
||||
|
||||
if existing:
|
||||
existing.name = tech_data["name"]
|
||||
@@ -416,7 +417,8 @@ def _import_d3fend_fallback(db: Session) -> dict[str, int]:
|
||||
.filter(DefensiveTechnique.d3fend_id == d3fend_id)
|
||||
.first()
|
||||
)
|
||||
d3fend_url = D3FEND_BASE_URL.format(d3fend_id=d3fend_id)
|
||||
technique_name = tech_data["name"].replace(" ", "")
|
||||
d3fend_url = D3FEND_BASE_URL.format(technique_name=technique_name)
|
||||
|
||||
if existing:
|
||||
existing.name = tech_data["name"]
|
||||
@@ -605,6 +607,39 @@ def import_d3fend_mappings(db: Session) -> dict[str, int]:
|
||||
return {"created": created, "skipped": skipped, "total": total}
|
||||
|
||||
|
||||
def sync(db: Session) -> dict:
|
||||
"""Sync D3FEND techniques and ATT&CK mappings.
|
||||
|
||||
Called by the Data Sources router when the user clicks Sync for D3FEND.
|
||||
Returns a flat summary dict suitable for ``last_sync_stats``.
|
||||
"""
|
||||
from app.models.data_source import DataSource
|
||||
from datetime import datetime
|
||||
|
||||
tech_result = import_d3fend_techniques(db)
|
||||
mapping_result = import_d3fend_mappings(db)
|
||||
|
||||
summary = {
|
||||
"techniques_created": tech_result.get("created", 0),
|
||||
"techniques_updated": tech_result.get("updated", 0),
|
||||
"techniques_total": tech_result.get("total", 0),
|
||||
"mappings_created": mapping_result.get("created", 0),
|
||||
"mappings_skipped": mapping_result.get("skipped", 0),
|
||||
"mappings_total": mapping_result.get("total", 0),
|
||||
}
|
||||
|
||||
# Update DataSource record
|
||||
ds = db.query(DataSource).filter(DataSource.name == "d3fend").first()
|
||||
if ds:
|
||||
ds.last_sync_at = datetime.utcnow()
|
||||
ds.last_sync_status = "success"
|
||||
ds.last_sync_stats = summary
|
||||
db.commit()
|
||||
|
||||
logger.info("D3FEND sync complete — %s", summary)
|
||||
return summary
|
||||
|
||||
|
||||
def get_defenses_for_technique(db: Session, technique_id) -> list[dict]:
|
||||
"""Get all D3FEND defensive techniques mapped to a given ATT&CK technique."""
|
||||
mappings = (
|
||||
|
||||
Reference in New Issue
Block a user