Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c6febd22 | |||
| c234fd64c2 |
@@ -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
|
||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user