Compare commits

..

2 Commits

Author SHA1 Message Date
kitos 21c6febd22 fix(frontend): render markdown descriptions properly, fix tooltip/banner wording
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
- Wrap 8 previously-raw description fields in MarkdownText so imported
  markdown (links, bold, lists, MITRE citations) renders instead of
  showing literal syntax: campaign cards, D3FEND countermeasure
  descriptions, compliance framework/control descriptions, detection
  rule descriptions, threat-actor and technique reference descriptions,
  and the RT-import preview description
- Fix the "Validated" status tooltip claiming a "≥2 tests" threshold
  when the real rule is ≥1 validated test with full detection; reworded
  the adjacent "Partial" tooltip for the same clarity
- Generalize the technique review_required banner text away from
  "MITRE ATT&CK sync" — review can also be triggered by Atomic/Caldera/
  Elastic/Sigma/LOLBAS imports, intel scans, and stale-detection checks
2026-07-07 16:30:27 +02:00
kitos c234fd64c2 feat(d3fend): flag review_required on new ATT&CK-D3FEND mappings
Mirrors the existing Atomic/Caldera/Elastic/Sigma/LOLBAS import pattern —
D3FEND mapping import was the one source silently skipping the
review_required flag, so leads never got prompted to review techniques
that only gained new D3FEND coverage.
2026-07-07 16:20:01 +02:00
11 changed files with 82 additions and 16 deletions
@@ -583,6 +583,8 @@ def import_d3fend_mappings(db: Session) -> dict[str, int]:
created = 0 created = 0
# Assign skipped = 0 # Assign skipped = 0
skipped = 0 skipped = 0
# Assign new_technique_ids = set()
new_technique_ids: set[str] = set()
# Get all ATT&CK techniques from the DB # Get all ATT&CK techniques from the DB
attack_techniques = db.query(Technique).all() attack_techniques = db.query(Technique).all()
@@ -649,6 +651,12 @@ def import_d3fend_mappings(db: Session) -> dict[str, int]:
db.add(mapping) db.add(mapping)
# Assign created = 1 # Assign created = 1
created += 1 created += 1
new_technique_ids.add(mitre_id)
if new_technique_ids:
db.query(Technique).filter(
Technique.mitre_id.in_(new_technique_ids)
).update({"review_required": True}, synchronize_session=False)
# Commit all pending changes to the database # Commit all pending changes to the database
db.commit() db.commit()
@@ -0,0 +1,42 @@
"""Tests for D3FEND mapping import — review_required trigger parity.
Every other import source (Atomic Red Team, Caldera, Elastic, Sigma, LOLBAS)
flags a technique with review_required=True when it gains new content, so a
lead knows to look at it. D3FEND mapping import was missing this — this test
locks in the fix.
"""
from app.models.defensive_technique import DefensiveTechnique
from app.models.technique import Technique
from app.services.d3fend_import_service import import_d3fend_mappings
def test_import_d3fend_mappings_sets_review_required(db):
technique = Technique(mitre_id="T1590", name="Gather Victim Network Information", review_required=False)
db.add(technique)
db.add(DefensiveTechnique(d3fend_id="D3-NTA", name="Network Traffic Analysis"))
db.commit()
result = import_d3fend_mappings(db)
assert result["created"] >= 1
db.refresh(technique)
assert technique.review_required is True
def test_import_d3fend_mappings_skips_existing_without_reflagging(db):
technique = Technique(mitre_id="T1590", name="Gather Victim Network Information", review_required=False)
db.add(technique)
db.add(DefensiveTechnique(d3fend_id="D3-NTA", name="Network Traffic Analysis"))
db.commit()
import_d3fend_mappings(db)
db.refresh(technique)
technique.review_required = False
db.commit()
result = import_d3fend_mappings(db)
assert result["created"] == 0
db.refresh(technique)
assert technique.review_required is False
+2 -2
View File
@@ -29,14 +29,14 @@ const TOOLTIPS: Record<TechniqueStatus, { heading: string; lines: TooltipLine[]
validated: { validated: {
heading: "✅ Validated", heading: "✅ Validated",
lines: [ lines: [
{ label: "Meaning", text: "≥2 tests executed. Blue Team detected the attack in all of them." }, { label: "Meaning", text: "≥1 validated test, with Blue Team detecting the attack in all of them." },
{ label: "Action", text: "Maintain and re-test periodically." }, { label: "Action", text: "Maintain and re-test periodically." },
], ],
}, },
partial: { partial: {
heading: "🟡 Partial", heading: "🟡 Partial",
lines: [ lines: [
{ label: "Meaning", text: "Only 1 validated test, some tests still pending, or detection was not 100%." }, { label: "Meaning", text: "Some tests are validated while others are still pending, or detection was not 100% across all validated tests." },
{ label: "Action", text: "Run more tests and ensure all are validated with 'detected'." }, { label: "Action", text: "Run more tests and ensure all are validated with 'detected'." },
], ],
}, },
@@ -2,6 +2,7 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ChevronDown, ChevronRight, Search, Filter, ExternalLink, Info, ShieldAlert } from "lucide-react"; import { ChevronDown, ChevronRight, Search, Filter, ExternalLink, Info, ShieldAlert } from "lucide-react";
import type { ComplianceControlStatus } from "../../api/compliance"; import type { ComplianceControlStatus } from "../../api/compliance";
import MarkdownText from "../MarkdownText";
interface ControlsTableProps { interface ControlsTableProps {
controls: ComplianceControlStatus[]; controls: ComplianceControlStatus[];
@@ -195,9 +196,10 @@ export default function ControlsTable({ controls }: ControlsTableProps) {
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-blue-400"> <p className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-blue-400">
What this control requires and why it matters What this control requires and why it matters
</p> </p>
<p className="text-xs leading-relaxed text-gray-300"> <MarkdownText
{control.description} content={control.description}
</p> className="text-xs leading-relaxed text-gray-300"
/>
</div> </div>
</div> </div>
)} )}
@@ -16,6 +16,7 @@ import {
type DetectionRuleItem, type DetectionRuleItem,
} from "../../api/detection-rules"; } from "../../api/detection-rules";
import type { User } from "../../types/models"; import type { User } from "../../types/models";
import MarkdownText from "../MarkdownText";
const severityColors: Record<string, string> = { const severityColors: Record<string, string> = {
critical: "bg-red-900/50 text-red-400 border-red-500/30", critical: "bg-red-900/50 text-red-400 border-red-500/30",
@@ -252,7 +253,7 @@ export default function DetectionRuleChecklist({ testId, user, canEdit }: Props)
{isExpanded && ( {isExpanded && (
<div className="border-t border-gray-700 p-3 space-y-3"> <div className="border-t border-gray-700 p-3 space-y-3">
{rule.description && ( {rule.description && (
<p className="text-xs text-gray-400">{rule.description}</p> <MarkdownText content={rule.description} className="text-xs text-gray-400" />
)} )}
{/* Rule content */} {/* Rule content */}
@@ -629,7 +629,10 @@ export default function TeamTabs({
)} )}
</div> </div>
{def.description && ( {def.description && (
<p className="mt-1 text-xs text-gray-400 line-clamp-2">{def.description}</p> <MarkdownText
content={def.description}
className="mt-1 text-xs text-gray-400 line-clamp-2"
/>
)} )}
</div> </div>
{def.d3fend_url && ( {def.d3fend_url && (
+5 -3
View File
@@ -14,6 +14,7 @@ import {
import { listCampaigns, createCampaign, generateCampaignFromThreatActor, type CampaignSummary } from "../api/campaigns"; import { listCampaigns, createCampaign, generateCampaignFromThreatActor, type CampaignSummary } from "../api/campaigns";
import { getThreatActors, type ThreatActorSummary } from "../api/threat-actors"; import { getThreatActors, type ThreatActorSummary } from "../api/threat-actors";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import MarkdownText from "../components/MarkdownText";
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
@@ -378,9 +379,10 @@ export default function CampaignsPage() {
{campaign.name} {campaign.name}
</h3> </h3>
{campaign.description && ( {campaign.description && (
<p className="mt-1 text-xs text-gray-400 line-clamp-2"> <MarkdownText
{campaign.description} content={campaign.description}
</p> className="mt-1 text-xs text-gray-400 line-clamp-2"
/>
)} )}
{/* Threat Actor */} {/* Threat Actor */}
+5 -1
View File
@@ -15,6 +15,7 @@ import {
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import ComplianceGauge from "../components/compliance/ComplianceGauge"; import ComplianceGauge from "../components/compliance/ComplianceGauge";
import ControlsTable from "../components/compliance/ControlsTable"; import ControlsTable from "../components/compliance/ControlsTable";
import MarkdownText from "../components/MarkdownText";
export default function CompliancePage() { export default function CompliancePage() {
const [selectedFrameworkId, setSelectedFrameworkId] = useState<string | null>(null); const [selectedFrameworkId, setSelectedFrameworkId] = useState<string | null>(null);
@@ -238,7 +239,10 @@ export default function CompliancePage() {
</a> </a>
)} )}
</div> </div>
<p className="text-xs leading-relaxed text-gray-400">{activeFramework.description}</p> <MarkdownText
content={activeFramework.description}
className="text-xs leading-relaxed text-gray-400"
/>
</div> </div>
</div> </div>
)} )}
+2 -1
View File
@@ -17,6 +17,7 @@ import {
Braces, Braces,
} from "lucide-react"; } from "lucide-react";
import { importRT, type RTImportPayload, type RTTechniqueEntry, type RTEvidenceEntry } from "../api/tests"; import { importRT, type RTImportPayload, type RTTechniqueEntry, type RTEvidenceEntry } from "../api/tests";
import MarkdownText from "../components/MarkdownText";
/* ── Template JSON ─────────────────────────────────────────────────── */ /* ── Template JSON ─────────────────────────────────────────────────── */
@@ -324,7 +325,7 @@ export default function ImportRTPage() {
<span className="text-amber-400">{preview.techniques.length} techniques</span> <span className="text-amber-400">{preview.techniques.length} techniques</span>
</p> </p>
{preview.description && ( {preview.description && (
<p className="mt-1 text-xs text-gray-400">{preview.description}</p> <MarkdownText content={preview.description} className="mt-1 text-xs text-gray-400" />
)} )}
</div> </div>
<button <button
+6 -3
View File
@@ -289,10 +289,10 @@ export default function TechniqueDetailPage() {
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" /> <AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-amber-300"> <p className="text-sm font-medium text-amber-300">
This technique has been updated in MITRE ATT&CK This technique needs review
</p> </p>
<p className="mt-0.5 text-xs text-amber-400/70"> <p className="mt-0.5 text-xs text-amber-400/70">
The MITRE ATT&CK sync detected changes to this technique. New intel, detection rules, or a MITRE ATT&CK update were found for this technique.
{technique.mitre_last_modified && ( {technique.mitre_last_modified && (
<> Last modified in ATT&CK: <span className="font-mono">{technique.mitre_last_modified.slice(0, 10)}</span>.</> <> Last modified in ATT&CK: <span className="font-mono">{technique.mitre_last_modified.slice(0, 10)}</span>.</>
)} )}
@@ -694,7 +694,10 @@ export default function TechniqueDetailPage() {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-200">{ref.source_name}</p> <p className="text-sm font-medium text-gray-200">{ref.source_name}</p>
{ref.description && ( {ref.description && (
<p className="mt-0.5 line-clamp-2 text-xs text-gray-500">{ref.description}</p> <MarkdownText
content={ref.description}
className="mt-0.5 line-clamp-2 text-xs text-gray-500"
/>
)} )}
</div> </div>
<a <a
+1 -1
View File
@@ -617,7 +617,7 @@ export default function ThreatActorDetailPage() {
<span className="text-gray-400">{ref.source}</span> <span className="text-gray-400">{ref.source}</span>
)} )}
{ref.description && ( {ref.description && (
<span className="ml-2 text-gray-500">{ref.description}</span> <MarkdownText content={ref.description} className="ml-2 inline text-gray-500 [&_p]:inline" />
)} )}
</li> </li>
))} ))}