feat(techniques): show detection rules on technique detail page
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Backend:
- technique_query_service.get_technique_detail() now queries DetectionRule
  by mitre_technique_id == mitre_id (same field the heatmap uses)
- Rules sorted: critical → high → medium → low → informational, then alphabetically
- Returns: id, title, description, source, source_url, rule_format,
  severity, platforms, false_positive_rate

Frontend:
- New DetectionRulesSection component with expandable rows per rule
- Color-coded severity dots and badges (red/orange/yellow/blue/gray)
- Source badges (sigma=purple, elastic=blue, splunk=orange, custom=cyan)
- Shows format, false positive rate, platforms, source link on expand
- Empty state when no rules exist

Fixes: T1189 showed green in heatmap but no rules on detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 16:26:46 +02:00
parent 2371318e9e
commit fa8e7f311b
2 changed files with 187 additions and 1 deletions

View File

@@ -18,6 +18,7 @@ import {
FlaskConical,
ChevronDown,
ChevronUp,
Radar,
} from "lucide-react";
import { getTechniqueByMitreId, markTechniqueReviewed } from "../api/techniques";
import { getTemplatesByTechnique } from "../api/test-templates";
@@ -345,6 +346,9 @@ export default function TechniqueDetailPage() {
)}
</div>
{/* Detection Rules Section */}
<DetectionRulesSection rules={(technique as any).detection_rules ?? []} />
{/* Available Test Templates Section */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
@@ -465,6 +469,154 @@ export default function TechniqueDetailPage() {
);
}
// ── Detection Rules Section ───────────────────────────────────────────
interface DetectionRule {
id: string;
title: string;
description: string | null;
source: string;
source_id: string | null;
source_url: string | null;
rule_format: string;
severity: string | null;
platforms: string[];
false_positive_rate: string | null;
}
const severityColors: Record<string, string> = {
critical: "bg-red-900/40 text-red-400 border-red-500/30",
high: "bg-orange-900/40 text-orange-400 border-orange-500/30",
medium: "bg-yellow-900/40 text-yellow-400 border-yellow-500/30",
low: "bg-blue-900/40 text-blue-400 border-blue-500/30",
informational: "bg-gray-800/60 text-gray-400 border-gray-600/30",
};
const sourceColors: Record<string, string> = {
sigma: "bg-purple-900/40 text-purple-400 border-purple-500/30",
elastic: "bg-blue-900/40 text-blue-400 border-blue-500/30",
splunk: "bg-orange-900/40 text-orange-400 border-orange-500/30",
custom: "bg-cyan-900/40 text-cyan-400 border-cyan-500/30",
};
function DetectionRulesSection({ rules }: { rules: DetectionRule[] }) {
const [expandedId, setExpandedId] = useState<string | null>(null);
return (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="flex items-center gap-2 text-lg font-semibold text-white">
<Radar className="h-5 w-5 text-cyan-400" />
Detection Rules
</h2>
<span
className={`rounded-full border px-2.5 py-0.5 text-xs font-medium ${
rules.length > 0
? "border-cyan-500/30 bg-cyan-900/40 text-cyan-400"
: "border-gray-600/30 bg-gray-800/50 text-gray-500"
}`}
>
{rules.length} rule{rules.length !== 1 ? "s" : ""}
</span>
</div>
{rules.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
<Radar className="mb-2 h-8 w-8 text-gray-700" />
<p className="text-sm">No detection rules linked to this technique.</p>
</div>
) : (
<div className="space-y-2">
{rules.map((rule) => {
const isExpanded = expandedId === rule.id;
const sevColor = severityColors[rule.severity ?? ""] ?? severityColors.informational;
const srcColor = sourceColors[rule.source] ?? sourceColors.custom;
return (
<div
key={rule.id}
className="rounded-lg border border-gray-800 bg-gray-800/30 transition-colors hover:border-gray-700 cursor-pointer"
onClick={() => setExpandedId(isExpanded ? null : rule.id)}
>
<div className="flex items-center gap-3 p-3">
{/* Severity dot */}
<div
className={`h-2 w-2 shrink-0 rounded-full ${
rule.severity === "critical" ? "bg-red-400"
: rule.severity === "high" ? "bg-orange-400"
: rule.severity === "medium" ? "bg-yellow-400"
: rule.severity === "low" ? "bg-blue-400"
: "bg-gray-600"
}`}
/>
{/* Title */}
<span className="flex-1 min-w-0 truncate text-sm font-medium text-gray-200">
{rule.title}
</span>
{/* Badges */}
<div className="flex shrink-0 items-center gap-1.5">
{rule.severity && (
<span className={`rounded border px-1.5 py-0.5 text-[10px] font-semibold uppercase ${sevColor}`}>
{rule.severity}
</span>
)}
<span className={`rounded border px-1.5 py-0.5 text-[10px] font-medium uppercase ${srcColor}`}>
{rule.source}
</span>
<span className="rounded border border-gray-700 bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-500">
{rule.rule_format.replace("_yaml", "").toUpperCase()}
</span>
{isExpanded ? (
<ChevronUp className="h-3.5 w-3.5 text-gray-500" />
) : (
<ChevronDown className="h-3.5 w-3.5 text-gray-500" />
)}
</div>
</div>
{isExpanded && (
<div
className="border-t border-gray-700/50 px-4 py-3 space-y-2"
onClick={(e) => e.stopPropagation()}
>
{rule.description && (
<p className="text-xs text-gray-400 leading-relaxed">{rule.description}</p>
)}
<div className="flex flex-wrap gap-3 text-xs text-gray-500">
{rule.rule_format && (
<span>Format: <span className="text-gray-300">{rule.rule_format}</span></span>
)}
{rule.false_positive_rate && (
<span>False positives: <span className="text-gray-300 capitalize">{rule.false_positive_rate}</span></span>
)}
{rule.platforms && rule.platforms.length > 0 && (
<span>Platforms: <span className="text-gray-300">{rule.platforms.join(", ")}</span></span>
)}
{rule.source_id && (
<span>Source ID: <span className="font-mono text-gray-300">{rule.source_id}</span></span>
)}
</div>
{rule.source_url && (
<a
href={rule.source_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-cyan-400 hover:underline"
>
<ExternalLink className="h-3 w-3" />
View source rule
</a>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}
// ── D3FEND Section ────────────────────────────────────────────────────
function D3FENDSection({ defenses }: { defenses: Array<{