Skip to content

Commit 9e53f3a

Browse files
authored
Network - 27023 - Azure workloads are protected across the inbound, outbound, and availability defense planes (#1442)
2 parents fb58e39 + 98c97a4 commit 9e53f3a

4 files changed

Lines changed: 241 additions & 8 deletions

File tree

src/powershell/assets/ReportTemplate.html

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { useMemo, useState } from "react";
2+
import { Badge } from "@/components/ui/badge";
3+
import {
4+
Accordion,
5+
AccordionContent,
6+
AccordionItem,
7+
AccordionTrigger,
8+
} from "@/components/ui/accordion";
9+
import { reportData, Test } from "@/config/report-data";
10+
import { CheckCircledIcon, CrossCircledIcon, MinusCircledIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
11+
12+
// ─── Plane spec-ID constants ──────────────────────────────────────────────────
13+
14+
const AVAILABILITY_IDS = ["25533"];
15+
const APPGW_WAF_IDS = ["25541", "26879", "26881", "26882", "27015", "27016", "27017"];
16+
const FRONTDOOR_IDS = ["25543", "26880", "26883", "26884", "27018", "27019", "27020", "27024"];
17+
const OUTBOUND_IDS = ["25535", "25537", "25539", "25550"];
18+
const ALL_SPEC_IDS = [...AVAILABILITY_IDS, ...APPGW_WAF_IDS, ...FRONTDOOR_IDS, ...OUTBOUND_IDS];
19+
20+
// ─── Types ────────────────────────────────────────────────────────────────────
21+
22+
type PlaneStatus = "pass" | "partial" | "fail" | "na";
23+
24+
interface GroupResult {
25+
passed: number;
26+
total: number;
27+
status: PlaneStatus;
28+
tests: Test[];
29+
}
30+
31+
// ─── Helper functions ─────────────────────────────────────────────────────────
32+
33+
function calcStatus(passed: number, failed: number, investigate: number, total: number): PlaneStatus {
34+
if (total === 0) return "na";
35+
if (failed > 0) return "fail";
36+
if (investigate > 0) return "partial";
37+
if (passed === total) return "pass";
38+
if (passed === 0) return "na";
39+
return "partial";
40+
}
41+
42+
function computeGroup(specIds: string[]): GroupResult {
43+
const tests = reportData.Tests.filter((test) => specIds.includes(String(test.TestId)));
44+
const passed = tests.filter((test) => test.TestStatus === "Passed").length;
45+
const failed = tests.filter((test) => test.TestStatus === "Failed" || test.TestStatus === "Error").length;
46+
const investigate = tests.filter((test) => test.TestStatus === "Investigate").length;
47+
48+
return { passed, total: tests.length, status: calcStatus(passed, failed, investigate, tests.length), tests };
49+
}
50+
51+
// ─── Guard for conditional rendering in Dashboard.tsx ─────────────────────────
52+
53+
export function hasAzureNetSecData(): boolean {
54+
return reportData.Tests.some((test) => ALL_SPEC_IDS.includes(String(test.TestId)));
55+
}
56+
57+
function getStatusBadgeVariant(status: PlaneStatus): "success" | "destructive" | "warning" | "secondary" {
58+
switch (status) {
59+
case "pass": return "success";
60+
case "partial": return "warning";
61+
case "fail": return "destructive";
62+
default: return "secondary";
63+
}
64+
}
65+
66+
function getStatusLabel(status: PlaneStatus): string {
67+
switch (status) {
68+
case "pass": return "Pass";
69+
case "partial": return "Partial";
70+
case "fail": return "Fail";
71+
default: return "N/A";
72+
}
73+
}
74+
75+
function getStatusFill(status: PlaneStatus): string {
76+
switch (status) {
77+
case "pass": return "#22C55E";
78+
case "partial": return "#EAB308";
79+
case "fail": return "#EF4444";
80+
default: return "#9CA3AF";
81+
}
82+
}
83+
84+
function getTestStatusIcon(testStatus: string) {
85+
switch (testStatus) {
86+
case "Passed":
87+
return <CheckCircledIcon className="size-4 shrink-0 text-teal-600" />;
88+
case "Failed":
89+
case "Error":
90+
return <CrossCircledIcon className="size-4 shrink-0 text-rose-500" />;
91+
case "Investigate":
92+
return <QuestionMarkCircledIcon className="size-4 shrink-0 text-amber-500" />;
93+
case "Skipped":
94+
case "Planned":
95+
case "NotApplicable":
96+
default:
97+
return <MinusCircledIcon className="size-4 shrink-0 text-muted-foreground" />;
98+
}
99+
}
100+
101+
// ─── Main exported component ──────────────────────────────────────────────────
102+
103+
export function AzureNetSecPlanes() {
104+
const [hoveredPlane, setHoveredPlane] = useState<number | null>(null);
105+
const {
106+
av, ag, fd, out,
107+
inbPassed, inbTotal, inbStatus,
108+
} = useMemo(() => {
109+
const av = computeGroup(AVAILABILITY_IDS);
110+
const ag = computeGroup(APPGW_WAF_IDS);
111+
const fd = computeGroup(FRONTDOOR_IDS);
112+
const out = computeGroup(OUTBOUND_IDS);
113+
114+
const inbPassed = ag.passed + fd.passed;
115+
const inbTotal = ag.total + fd.total;
116+
const inbFailed = [...ag.tests, ...fd.tests].filter(
117+
(test) => test.TestStatus === "Failed" || test.TestStatus === "Error"
118+
).length;
119+
const inbInvestigate = [...ag.tests, ...fd.tests].filter(
120+
(test) => test.TestStatus === "Investigate"
121+
).length;
122+
const inbStatus = calcStatus(inbPassed, inbFailed, inbInvestigate, inbTotal);
123+
124+
return {
125+
av, ag, fd, out,
126+
inbPassed, inbTotal, inbStatus,
127+
};
128+
}, []);
129+
130+
const planes = [
131+
{ id: 1, name: "Availability protection", summary: "Azure DDoS Protection", result: av, tests: av.tests, rx: 190, ry: 190 },
132+
{ id: 2, name: "Inbound application protection", summary: "Application Gateway WAF and Front Door WAF", result: { passed: inbPassed, total: inbTotal, status: inbStatus }, tests: [...ag.tests, ...fd.tests], rx: 140, ry: 140 },
133+
{ id: 3, name: "Outbound and east-west protection", summary: "Azure Firewall", result: out, tests: out.tests, rx: 88, ry: 88 },
134+
];
135+
const applicablePlanes = planes.filter((plane) => plane.result.status !== "na");
136+
const failingPlanes = applicablePlanes.filter((plane) => plane.result.status === "fail" || plane.result.status === "partial");
137+
const allPlanesPass = applicablePlanes.length > 0 && applicablePlanes.every((plane) => plane.result.status === "pass");
138+
139+
return (
140+
<div className="flex flex-col gap-4">
141+
<div className="flex flex-col items-center gap-6 lg:flex-row lg:items-center">
142+
<div className="shrink-0">
143+
<svg width="420" height="420" viewBox="0 0 420 420" className="h-auto max-w-full" aria-label="Azure network security defense planes">
144+
{planes.map((plane) => {
145+
const isHovered = hoveredPlane === plane.id;
146+
const percentage = plane.result.total > 0 ? Math.round((plane.result.passed / plane.result.total) * 100) : 0;
147+
const labelY = 210 - plane.ry + (plane.id === 1 ? 30 : 25);
148+
const fill = getStatusFill(plane.result.status);
149+
150+
return (
151+
<g key={plane.id} className="cursor-pointer" opacity={hoveredPlane !== null && !isHovered ? 0.65 : 1} onMouseEnter={() => setHoveredPlane(plane.id)} onMouseLeave={() => setHoveredPlane(null)}>
152+
<ellipse cx="210" cy="210" rx={plane.rx} ry={plane.ry} fill={fill} stroke={isHovered ? "#ffffff" : "rgba(255,255,255,0.35)"} strokeWidth={isHovered ? 3 : 1.5} />
153+
<text x="210" y={labelY} textAnchor="middle" fill="#ffffff" fontSize={plane.id === 1 ? 14 : 12} fontWeight="600">
154+
{plane.name}
155+
</text>
156+
<text x="210" y={labelY + 18} textAnchor="middle" fill="#ffffff" fontSize="12">
157+
{plane.result.total === 0 ? "No applicable results" : `${plane.result.passed}/${plane.result.total} passing (${percentage}%)`}
158+
</text>
159+
</g>
160+
);
161+
})}
162+
</svg>
163+
</div>
164+
165+
<div className="w-full min-w-0 flex-1">
166+
<div className="mb-3 flex items-center gap-2">
167+
<Badge variant={applicablePlanes.length === 0 ? "secondary" : allPlanesPass ? "success" : "destructive"}>
168+
{applicablePlanes.length === 0
169+
? "No applicable plane results"
170+
: allPlanesPass
171+
? "All applicable defense planes pass"
172+
: `${failingPlanes.length} of ${applicablePlanes.length} applicable defense planes have issues`}
173+
</Badge>
174+
</div>
175+
<Accordion type="multiple" className="w-full">
176+
{planes.map((plane) => (
177+
<AccordionItem key={plane.id} value={`plane-${plane.id}`} className="mb-1 rounded-md border px-3">
178+
<AccordionTrigger className="py-3 hover:no-underline">
179+
<div className="grid w-full grid-cols-[1rem_minmax(0,1fr)_5rem_2rem] items-center gap-3 text-left">
180+
<span className="w-4 shrink-0 text-xs font-mono text-muted-foreground">{plane.id}</span>
181+
<span className="min-w-0 text-sm font-medium">{plane.name}</span>
182+
<Badge variant={getStatusBadgeVariant(plane.result.status)} className="w-16 justify-center justify-self-end">{getStatusLabel(plane.result.status)}</Badge>
183+
<span className="text-right text-xs tabular-nums text-muted-foreground">{plane.result.total === 0 ? "N/A" : `${plane.result.passed}/${plane.result.total}`}</span>
184+
</div>
185+
</AccordionTrigger>
186+
<AccordionContent>
187+
<p className="mb-2 text-xs text-muted-foreground">{plane.summary}</p>
188+
{plane.tests.length === 0 ? (
189+
<p className="py-1 text-xs italic text-muted-foreground">No applicable test results were included in the report.</p>
190+
) : (
191+
<div className="space-y-1 py-1">
192+
{plane.tests.map((test) => (
193+
<div key={test.TestId} className="flex items-center gap-2 py-1 text-xs">
194+
{getTestStatusIcon(test.TestStatus)}
195+
<span className="font-mono text-muted-foreground">{test.TestId}</span>
196+
<span className="flex-1">{test.TestTitle}</span>
197+
<span className="shrink-0 text-muted-foreground">{test.TestStatus}</span>
198+
</div>
199+
))}
200+
</div>
201+
)}
202+
</AccordionContent>
203+
</AccordionItem>
204+
))}
205+
</Accordion>
206+
</div>
207+
</div>
208+
</div>
209+
);
210+
}

src/report/src/components/overview/swg-defense-layers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export function SwgDefenseLayers() {
286286
</span>
287287
<Badge
288288
variant={getStatusBadgeVariant(result.status)}
289-
className="mr-2"
289+
className="mr-2 w-16 justify-center"
290290
>
291291
{getStatusLabel(result.status)}
292292
</Badge>

src/report/src/pages/Dashboard.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { AuthMethodSankey } from "@/components/overview/authMethod-sankey";
4040
import { DesktopDevicesSankey } from "@/components/overview/desktop-devices-sankey";
4141
import { MobileSankey } from "@/components/overview/mobile-sankey";
4242
import { SwgDefenseLayers, hasSwgData } from "@/components/overview/swg-defense-layers";
43+
import { AzureNetSecPlanes, hasAzureNetSecData } from "@/components/overview/azure-netsec-planes";
4344
import { Separator } from "@/components/ui/separator";
4445
import { formatNumber } from "@/lib/format-utils";
4546

@@ -1491,6 +1492,28 @@ export default function Dashboard() {
14911492
</Card>
14921493
</div>
14931494
)}
1495+
1496+
{/* Network - Azure Network Security Defense Planes Section */}
1497+
{hasAzureNetSecData() && (
1498+
<div className="flex max-w-7xl flex-col gap-6 mt-6">
1499+
<Card>
1500+
<CardHeader className="space-y-0 pb-2 flex-row">
1501+
<ShieldCheck className="pr-2 size-8" />
1502+
<div>
1503+
<CardTitle className="text-2xl tabular-nums">
1504+
Azure network security
1505+
</CardTitle>
1506+
<CardDescription className="mt-1">
1507+
Defense plane posture — availability, inbound, and outbound protection.
1508+
</CardDescription>
1509+
</div>
1510+
</CardHeader>
1511+
<CardContent>
1512+
<AzureNetSecPlanes />
1513+
</CardContent>
1514+
</Card>
1515+
</div>
1516+
)}
14941517
</TooltipProvider>
14951518
)
14961519
}

0 commit comments

Comments
 (0)