mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 08:47:18 +00:00
feat(ui): expand attack paths node legend coverage
This commit is contained in:
+76
-13
@@ -24,21 +24,28 @@ const graphData: AttackPathGraphData = {
|
||||
properties: { check_title: "Public bucket", severity: "critical" },
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{ id: "r1", source: "aws-account", target: "bucket", label: "HAS" },
|
||||
{ id: "r2", source: "bucket", target: "vpc", label: "CAN_ACCESS" },
|
||||
{ id: "r3", source: "bucket", target: "finding", label: "HAS_FINDING" },
|
||||
],
|
||||
};
|
||||
|
||||
describe("GraphLegend", () => {
|
||||
it("should explain graph visuals by semantic groups instead of repeating node labels", () => {
|
||||
it("should explain concrete visible node types without generic categories", () => {
|
||||
// Given - A graph with provider, resource, and finding nodes
|
||||
|
||||
// When
|
||||
render(<GraphLegend data={graphData} />);
|
||||
render(
|
||||
<GraphLegend data={graphData} expandedResources={new Set(["bucket"])} />,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /provider roots/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /resource categories/i }),
|
||||
screen.getByRole("heading", { name: /node types/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /findings by risk/i }),
|
||||
@@ -49,16 +56,18 @@ describe("GraphLegend", () => {
|
||||
expect(screen.getByRole("heading", { name: /edges/i })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Provider / account root")).toBeInTheDocument();
|
||||
expect(screen.getByText("Storage")).toBeInTheDocument();
|
||||
expect(screen.getByText("Network")).toBeInTheDocument();
|
||||
expect(screen.getByText("Compute")).toBeInTheDocument();
|
||||
expect(screen.getByText("Identity")).toBeInTheDocument();
|
||||
expect(screen.getByText("Secret / misc")).toBeInTheDocument();
|
||||
expect(screen.getByText("S3 Bucket")).toBeInTheDocument();
|
||||
expect(screen.getByText("VPC")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Storage")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Network")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Compute")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Identity")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Secret / misc")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Critical")).toBeInTheDocument();
|
||||
expect(screen.getByText("High")).toBeInTheDocument();
|
||||
expect(screen.getByText("Medium")).toBeInTheDocument();
|
||||
expect(screen.getByText("Low / Info")).toBeInTheDocument();
|
||||
expect(screen.queryByText("High")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Medium")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Low / Info")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Selected node")).toBeInTheDocument();
|
||||
expect(screen.getByText("Node with findings")).toBeInTheDocument();
|
||||
@@ -66,12 +75,66 @@ describe("GraphLegend", () => {
|
||||
expect(screen.getByText("Finding edge")).toBeInTheDocument();
|
||||
expect(screen.getByText("Highlighted path")).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText("S3 Bucket")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("VPC")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/ctrl/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/scroll to zoom/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide finding legend items when finding nodes are hidden", () => {
|
||||
// Given - A resource has related findings, but it is not expanded yet
|
||||
|
||||
// When
|
||||
render(<GraphLegend data={graphData} />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: /findings by risk/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Finding edge")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Node with findings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should list policy and role node types separately", () => {
|
||||
// Given - A graph whose visible nodes are all identity-related, but distinct
|
||||
const identityGraphData: AttackPathGraphData = {
|
||||
nodes: [
|
||||
{
|
||||
id: "aws-account",
|
||||
labels: ["AWSAccount"],
|
||||
properties: { name: "Production" },
|
||||
},
|
||||
{
|
||||
id: "role",
|
||||
labels: ["PermissionRole"],
|
||||
properties: { name: "prowler-pro-dev-gha-role" },
|
||||
},
|
||||
{
|
||||
id: "policy",
|
||||
labels: ["AWSPolicy"],
|
||||
properties: { name: "IAMPermissions" },
|
||||
},
|
||||
{
|
||||
id: "statement",
|
||||
labels: ["AWSPolicyStatement"],
|
||||
properties: { name: "policy statement" },
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{ id: "r1", source: "aws-account", target: "role", label: "HAS" },
|
||||
{ id: "r2", source: "role", target: "policy", label: "HAS" },
|
||||
{ id: "r3", source: "policy", target: "statement", label: "HAS" },
|
||||
],
|
||||
};
|
||||
|
||||
// When
|
||||
render(<GraphLegend data={identityGraphData} />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByText("Permission Role")).toBeInTheDocument();
|
||||
expect(screen.getByText("AWS Policy")).toBeInTheDocument();
|
||||
expect(screen.getByText("AWS Policy Statement")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Identity")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should stay hidden until graph nodes are available", () => {
|
||||
// Given - No graph nodes have been loaded yet
|
||||
const emptyGraphData: AttackPathGraphData = { nodes: [] };
|
||||
|
||||
+226
-75
@@ -13,6 +13,8 @@ import {
|
||||
import type { AttackPathGraphData, GraphNode } from "@/types/attack-paths";
|
||||
|
||||
import {
|
||||
getNodeBorderColor,
|
||||
getNodeColor,
|
||||
GRAPH_ALERT_BORDER_COLOR,
|
||||
GRAPH_EDGE_COLOR_DARK,
|
||||
GRAPH_EDGE_COLOR_LIGHT,
|
||||
@@ -75,6 +77,16 @@ interface LegendItemProps {
|
||||
|
||||
interface GraphLegendProps {
|
||||
data?: AttackPathGraphData;
|
||||
expandedResources?: ReadonlySet<string>;
|
||||
isFilteredView?: boolean;
|
||||
}
|
||||
|
||||
interface GraphLegendState {
|
||||
visibleNodes: GraphNode[];
|
||||
visibleNodeIds: Set<string>;
|
||||
visibleFindingIds: Set<string>;
|
||||
visibleEdges: Array<{ source: string; target: string }>;
|
||||
resourcesWithFindings: Set<string>;
|
||||
}
|
||||
|
||||
const buildNode = (
|
||||
@@ -110,44 +122,6 @@ const providerRootItem = buildVisualItem(
|
||||
GRAPH_NODE_BORDER_COLORS.awsAccount,
|
||||
);
|
||||
|
||||
const resourceCategoryItems: LegendVisualItem[] = [
|
||||
buildVisualItem(
|
||||
"Storage",
|
||||
"Data stores such as buckets and object storage.",
|
||||
buildNode(["S3Bucket"], { name: NODE_CATEGORY.STORAGE }),
|
||||
GRAPH_NODE_COLORS.s3Bucket,
|
||||
GRAPH_NODE_BORDER_COLORS.s3Bucket,
|
||||
),
|
||||
buildVisualItem(
|
||||
"Network",
|
||||
"VPCs, security boundaries, gateways, and reachable network resources.",
|
||||
buildNode(["VPC"], { name: NODE_CATEGORY.NETWORK }),
|
||||
GRAPH_NODE_COLORS.securityGroup,
|
||||
GRAPH_NODE_BORDER_COLORS.securityGroup,
|
||||
),
|
||||
buildVisualItem(
|
||||
"Compute",
|
||||
"Instances, virtual machines, functions, and execution surfaces.",
|
||||
buildNode(["EC2Instance"], { name: NODE_CATEGORY.COMPUTE }),
|
||||
GRAPH_NODE_COLORS.ec2Instance,
|
||||
GRAPH_NODE_BORDER_COLORS.ec2Instance,
|
||||
),
|
||||
buildVisualItem(
|
||||
"Identity",
|
||||
"Users, roles, policies, and permission-bearing principals.",
|
||||
buildNode(["IAMRole"], { name: NODE_CATEGORY.IDENTITY }),
|
||||
GRAPH_NODE_COLORS.iamRole,
|
||||
GRAPH_NODE_BORDER_COLORS.iamRole,
|
||||
),
|
||||
buildVisualItem(
|
||||
"Secret / misc",
|
||||
"Secrets, access keys, or fallback resources that do not map to a known category.",
|
||||
buildNode(["AccessKey"], { name: NODE_CATEGORY.SECRET }),
|
||||
GRAPH_NODE_COLORS.default,
|
||||
GRAPH_NODE_BORDER_COLORS.default,
|
||||
),
|
||||
];
|
||||
|
||||
const findingRiskItems: LegendVisualItem[] = [
|
||||
buildVisualItem(
|
||||
"Critical",
|
||||
@@ -220,8 +194,128 @@ const edgeItems: LegendEdgeItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const hasLegendData = (data?: AttackPathGraphData): boolean =>
|
||||
(data?.nodes.length ?? 0) > 0;
|
||||
const isFindingNode = (node: GraphNode): boolean =>
|
||||
node.labels.some((label) => label.toLowerCase().includes("finding"));
|
||||
|
||||
const getGraphEdges = (
|
||||
data: AttackPathGraphData,
|
||||
): Array<{ source: string; target: string }> =>
|
||||
data.relationships ?? data.edges ?? [];
|
||||
|
||||
const resolveLegendState = (
|
||||
data: AttackPathGraphData,
|
||||
expandedResources: ReadonlySet<string>,
|
||||
isFilteredView: boolean,
|
||||
): GraphLegendState => {
|
||||
const findingNodeIds = new Set(
|
||||
data.nodes.filter(isFindingNode).map((node) => node.id),
|
||||
);
|
||||
const findingToResources = new Map<string, Set<string>>();
|
||||
const resourcesWithFindings = new Set<string>();
|
||||
const graphEdges = getGraphEdges(data);
|
||||
|
||||
for (const edge of graphEdges) {
|
||||
const sourceIsFinding = findingNodeIds.has(edge.source);
|
||||
const targetIsFinding = findingNodeIds.has(edge.target);
|
||||
|
||||
if (sourceIsFinding) {
|
||||
resourcesWithFindings.add(edge.target);
|
||||
const resources = findingToResources.get(edge.source) ?? new Set();
|
||||
resources.add(edge.target);
|
||||
findingToResources.set(edge.source, resources);
|
||||
}
|
||||
|
||||
if (targetIsFinding) {
|
||||
resourcesWithFindings.add(edge.source);
|
||||
const resources = findingToResources.get(edge.target) ?? new Set();
|
||||
resources.add(edge.source);
|
||||
findingToResources.set(edge.target, resources);
|
||||
}
|
||||
}
|
||||
|
||||
const hiddenFindingIds = new Set<string>();
|
||||
if (!isFilteredView) {
|
||||
for (const findingId of Array.from(findingNodeIds)) {
|
||||
const connectedResources = findingToResources.get(findingId);
|
||||
const isVisible = connectedResources
|
||||
? Array.from(connectedResources).some((resourceId) =>
|
||||
expandedResources.has(resourceId),
|
||||
)
|
||||
: false;
|
||||
|
||||
if (!isVisible) hiddenFindingIds.add(findingId);
|
||||
}
|
||||
}
|
||||
|
||||
const visibleNodes = data.nodes.filter(
|
||||
(node) => !hiddenFindingIds.has(node.id),
|
||||
);
|
||||
const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const visibleFindingIds = new Set(
|
||||
visibleNodes.filter(isFindingNode).map((node) => node.id),
|
||||
);
|
||||
const visibleEdges = graphEdges.filter(
|
||||
(edge) =>
|
||||
visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target),
|
||||
);
|
||||
|
||||
return {
|
||||
visibleNodes,
|
||||
visibleNodeIds,
|
||||
visibleFindingIds,
|
||||
visibleEdges,
|
||||
resourcesWithFindings,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveNodeTypeItems = (
|
||||
visibleNodes: GraphNode[],
|
||||
): LegendVisualItem[] => {
|
||||
const itemsByType = new Map<string, LegendVisualItem>();
|
||||
|
||||
for (const node of visibleNodes) {
|
||||
if (isFindingNode(node)) continue;
|
||||
|
||||
const visual = resolveNodeVisual(node);
|
||||
if (visual.category === NODE_CATEGORY.ACCOUNT) continue;
|
||||
|
||||
const key = `${visual.category}:${visual.description}`;
|
||||
|
||||
if (!itemsByType.has(key)) {
|
||||
itemsByType.set(key, {
|
||||
label: visual.description,
|
||||
description: `${visual.description} node`,
|
||||
Icon: visual.Icon,
|
||||
fillColor: getNodeColor(node.labels),
|
||||
borderColor: getNodeBorderColor(node.labels),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(itemsByType.values());
|
||||
};
|
||||
|
||||
const resolveFindingRiskItems = (
|
||||
visibleNodes: GraphNode[],
|
||||
): LegendVisualItem[] => {
|
||||
const visibleSeverities = new Set(
|
||||
visibleNodes
|
||||
.filter(isFindingNode)
|
||||
.map((node) => String(node.properties.severity ?? "").toLowerCase()),
|
||||
);
|
||||
|
||||
return findingRiskItems.filter((item) => {
|
||||
if (item.label === "Low / Info") {
|
||||
return (
|
||||
visibleSeverities.has("low") ||
|
||||
visibleSeverities.has("info") ||
|
||||
visibleSeverities.has("informational")
|
||||
);
|
||||
}
|
||||
|
||||
return visibleSeverities.has(item.label.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
const LegendSection = ({ title, children }: LegendSectionProps) => (
|
||||
<section className="bg-bg-neutral-secondary/60 border-border-neutral-primary flex w-full min-w-0 flex-col gap-2 rounded-lg border p-3 sm:w-fit sm:max-w-full">
|
||||
@@ -375,10 +469,57 @@ const EdgePreview = ({
|
||||
/**
|
||||
* Compact semantic legend for the Attack Paths graph visual language.
|
||||
*/
|
||||
export const GraphLegend = ({ data }: GraphLegendProps) => {
|
||||
export const GraphLegend = ({
|
||||
data,
|
||||
expandedResources = new Set(),
|
||||
isFilteredView = false,
|
||||
}: GraphLegendProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
if (!hasLegendData(data)) {
|
||||
if (!data || data.nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const legendState = resolveLegendState(
|
||||
data,
|
||||
expandedResources,
|
||||
isFilteredView,
|
||||
);
|
||||
const providerItem = legendState.visibleNodes.some(
|
||||
(node) => resolveNodeVisual(node).category === NODE_CATEGORY.ACCOUNT,
|
||||
)
|
||||
? providerRootItem
|
||||
: null;
|
||||
const visibleNodeTypeItems = resolveNodeTypeItems(legendState.visibleNodes);
|
||||
const visibleFindingRiskItems = resolveFindingRiskItems(
|
||||
legendState.visibleNodes,
|
||||
);
|
||||
const visibleStateItems = stateItems.filter(
|
||||
(item) =>
|
||||
item.label === "Selected node" ||
|
||||
Array.from(legendState.resourcesWithFindings).some((resourceId) =>
|
||||
legendState.visibleNodeIds.has(resourceId),
|
||||
),
|
||||
);
|
||||
const visibleEdgeItems = edgeItems.filter((item) => {
|
||||
if (item.variant === EDGE_VARIANT.FINDING) {
|
||||
return legendState.visibleEdges.some(
|
||||
(edge) =>
|
||||
legendState.visibleFindingIds.has(edge.source) ||
|
||||
legendState.visibleFindingIds.has(edge.target),
|
||||
);
|
||||
}
|
||||
|
||||
return legendState.visibleEdges.length > 0;
|
||||
});
|
||||
|
||||
if (
|
||||
!providerItem &&
|
||||
visibleNodeTypeItems.length === 0 &&
|
||||
visibleFindingRiskItems.length === 0 &&
|
||||
visibleStateItems.length === 0 &&
|
||||
visibleEdgeItems.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -390,43 +531,53 @@ export const GraphLegend = ({ data }: GraphLegendProps) => {
|
||||
<CardContent className="p-3">
|
||||
<TooltipProvider>
|
||||
<div className="flex w-full flex-wrap items-stretch gap-2">
|
||||
<LegendSection title="Provider roots">
|
||||
<LegendItem {...providerRootItem}>
|
||||
<BadgePreview {...providerRootItem} />
|
||||
</LegendItem>
|
||||
</LegendSection>
|
||||
|
||||
<LegendSection title="Resource categories">
|
||||
{resourceCategoryItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<BadgePreview {...item} />
|
||||
{providerItem && (
|
||||
<LegendSection title="Provider roots">
|
||||
<LegendItem {...providerItem}>
|
||||
<BadgePreview {...providerItem} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
</LegendSection>
|
||||
)}
|
||||
|
||||
<LegendSection title="Findings by risk">
|
||||
{findingRiskItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<BadgePreview {...item} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
{visibleNodeTypeItems.length > 0 && (
|
||||
<LegendSection title="Node types">
|
||||
{visibleNodeTypeItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<BadgePreview {...item} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
)}
|
||||
|
||||
<LegendSection title="States">
|
||||
{stateItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<StatePreview {...item} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
{visibleFindingRiskItems.length > 0 && (
|
||||
<LegendSection title="Findings by risk">
|
||||
{visibleFindingRiskItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<BadgePreview {...item} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
)}
|
||||
|
||||
<LegendSection title="Edges">
|
||||
{edgeItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<EdgePreview variant={item.variant} edgeColor={edgeColor} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
{visibleStateItems.length > 0 && (
|
||||
<LegendSection title="States">
|
||||
{visibleStateItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<StatePreview {...item} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
)}
|
||||
|
||||
{visibleEdgeItems.length > 0 && (
|
||||
<LegendSection title="Edges">
|
||||
{visibleEdgeItems.map((item) => (
|
||||
<LegendItem key={item.label} {...item}>
|
||||
<EdgePreview variant={item.variant} edgeColor={edgeColor} />
|
||||
</LegendItem>
|
||||
))}
|
||||
</LegendSection>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
Bot,
|
||||
Braces,
|
||||
CircleAlert,
|
||||
FileKey2,
|
||||
Globe2,
|
||||
Info,
|
||||
Route,
|
||||
Server,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Siren,
|
||||
Tags,
|
||||
UserCog,
|
||||
Users,
|
||||
Waypoints,
|
||||
} from "lucide-react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -17,9 +25,11 @@ import {
|
||||
KS8ProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import {
|
||||
AmazonRDSIcon,
|
||||
AmazonS3Icon,
|
||||
AmazonVPCIcon,
|
||||
AWSIAMIcon,
|
||||
AWSLambdaIcon,
|
||||
} from "@/components/icons/services/IconServices";
|
||||
import type { GraphNode } from "@/types/attack-paths";
|
||||
|
||||
@@ -274,6 +284,130 @@ describe("resolveNodeVisual", () => {
|
||||
expect(visual.Icon).toBe(identityNode.Icon);
|
||||
}
|
||||
});
|
||||
|
||||
it("should resolve all AWS labels used by predefined Attack Paths queries", () => {
|
||||
// Given
|
||||
const awsQueryNodes = [
|
||||
{
|
||||
label: "AWSTag",
|
||||
category: NODE_CATEGORY.MISC,
|
||||
description: "AWS Tag",
|
||||
Icon: Tags,
|
||||
},
|
||||
{
|
||||
label: "EC2SecurityGroup",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "EC2 Security Group",
|
||||
Icon: Shield,
|
||||
},
|
||||
{
|
||||
label: "IpPermissionInbound",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Inbound IP Permission",
|
||||
Icon: Shield,
|
||||
},
|
||||
{
|
||||
label: "IpRange",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "IP Range",
|
||||
Icon: Globe2,
|
||||
},
|
||||
{
|
||||
label: "AWSPrincipal",
|
||||
category: NODE_CATEGORY.IDENTITY,
|
||||
description: "AWS Principal",
|
||||
Icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
label: "AWSGroup",
|
||||
category: NODE_CATEGORY.IDENTITY,
|
||||
description: "AWS Group",
|
||||
Icon: Users,
|
||||
},
|
||||
{
|
||||
label: "RDSInstance",
|
||||
category: NODE_CATEGORY.STORAGE,
|
||||
description: "RDS Instance",
|
||||
Icon: AmazonRDSIcon,
|
||||
},
|
||||
{
|
||||
label: "LoadBalancer",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Load Balancer",
|
||||
Icon: Route,
|
||||
},
|
||||
{
|
||||
label: "ELBListener",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "ELB Listener",
|
||||
Icon: Route,
|
||||
},
|
||||
{
|
||||
label: "LoadBalancerV2",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Load Balancer V2",
|
||||
Icon: Route,
|
||||
},
|
||||
{
|
||||
label: "ELBV2Listener",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "ELB V2 Listener",
|
||||
Icon: Route,
|
||||
},
|
||||
{
|
||||
label: "ElasticIPAddress",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Elastic IP Address",
|
||||
Icon: Globe2,
|
||||
},
|
||||
{
|
||||
label: "EC2PrivateIp",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "EC2 Private IP",
|
||||
Icon: Waypoints,
|
||||
},
|
||||
{
|
||||
label: "NetworkInterface",
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Network Interface",
|
||||
Icon: Waypoints,
|
||||
},
|
||||
{
|
||||
label: "LaunchTemplate",
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "Launch Template",
|
||||
Icon: Server,
|
||||
},
|
||||
{
|
||||
label: "AWSLambda",
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "AWS Lambda",
|
||||
Icon: AWSLambdaIcon,
|
||||
},
|
||||
{
|
||||
label: "AWSSageMakerNotebookInstance",
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "SageMaker Notebook Instance",
|
||||
Icon: Bot,
|
||||
},
|
||||
];
|
||||
|
||||
for (const awsQueryNode of awsQueryNodes) {
|
||||
// When
|
||||
const visual = resolveNodeVisual(
|
||||
buildNode([awsQueryNode.label], { name: awsQueryNode.description }),
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(visual).toMatchObject({
|
||||
category: awsQueryNode.category,
|
||||
displayName: awsQueryNode.description,
|
||||
description: awsQueryNode.description,
|
||||
fallbackUsed: false,
|
||||
});
|
||||
expect(visual.Icon).toBe(awsQueryNode.Icon);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("fallback behavior", () => {
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Siren,
|
||||
Tags,
|
||||
UserCog,
|
||||
Users,
|
||||
Waypoints,
|
||||
} from "lucide-react";
|
||||
import type { ElementType } from "react";
|
||||
@@ -37,9 +39,11 @@ import {
|
||||
} from "@/components/icons/providers-badge";
|
||||
import {
|
||||
AmazonEC2Icon,
|
||||
AmazonRDSIcon,
|
||||
AmazonS3Icon,
|
||||
AmazonVPCIcon,
|
||||
AWSIAMIcon,
|
||||
AWSLambdaIcon,
|
||||
} from "@/components/icons/services/IconServices";
|
||||
import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths";
|
||||
|
||||
@@ -196,6 +200,36 @@ const KNOWN_NODE_VISUALS = {
|
||||
description: "Security Group",
|
||||
Icon: Shield,
|
||||
},
|
||||
ec2securitygroup: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "EC2 Security Group",
|
||||
Icon: Shield,
|
||||
},
|
||||
ippermissioninbound: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Inbound IP Permission",
|
||||
Icon: Shield,
|
||||
},
|
||||
iprange: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "IP Range",
|
||||
Icon: Globe2,
|
||||
},
|
||||
elasticipaddress: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Elastic IP Address",
|
||||
Icon: Globe2,
|
||||
},
|
||||
ec2privateip: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "EC2 Private IP",
|
||||
Icon: Waypoints,
|
||||
},
|
||||
networkinterface: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Network Interface",
|
||||
Icon: Waypoints,
|
||||
},
|
||||
internetgateway: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Internet Gateway",
|
||||
@@ -206,16 +240,56 @@ const KNOWN_NODE_VISUALS = {
|
||||
description: "Default Gateway",
|
||||
Icon: Route,
|
||||
},
|
||||
loadbalancer: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Load Balancer",
|
||||
Icon: Route,
|
||||
},
|
||||
loadbalancerv2: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "Load Balancer V2",
|
||||
Icon: Route,
|
||||
},
|
||||
elblistener: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "ELB Listener",
|
||||
Icon: Route,
|
||||
},
|
||||
elbv2listener: {
|
||||
category: NODE_CATEGORY.NETWORK,
|
||||
description: "ELB V2 Listener",
|
||||
Icon: Route,
|
||||
},
|
||||
ec2instance: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "EC2 Instance",
|
||||
Icon: AmazonEC2Icon,
|
||||
},
|
||||
launchtemplate: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "Launch Template",
|
||||
Icon: Server,
|
||||
},
|
||||
awslambda: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "AWS Lambda",
|
||||
Icon: AWSLambdaIcon,
|
||||
},
|
||||
awssagemakernotebookinstance: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "SageMaker Notebook Instance",
|
||||
Icon: Bot,
|
||||
},
|
||||
virtualmachine: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "Virtual Machine",
|
||||
Icon: AmazonEC2Icon,
|
||||
},
|
||||
rdsinstance: {
|
||||
category: NODE_CATEGORY.STORAGE,
|
||||
description: "RDS Instance",
|
||||
Icon: AmazonRDSIcon,
|
||||
},
|
||||
compute: {
|
||||
category: NODE_CATEGORY.COMPUTE,
|
||||
description: "Compute",
|
||||
@@ -236,6 +310,16 @@ const KNOWN_NODE_VISUALS = {
|
||||
description: "AWS User",
|
||||
Icon: UserCog,
|
||||
},
|
||||
awsgroup: {
|
||||
category: NODE_CATEGORY.IDENTITY,
|
||||
description: "AWS Group",
|
||||
Icon: Users,
|
||||
},
|
||||
awsprincipal: {
|
||||
category: NODE_CATEGORY.IDENTITY,
|
||||
description: "AWS Principal",
|
||||
Icon: ShieldCheck,
|
||||
},
|
||||
iamrole: {
|
||||
category: NODE_CATEGORY.IDENTITY,
|
||||
description: "IAM Role",
|
||||
@@ -291,6 +375,11 @@ const KNOWN_NODE_VISUALS = {
|
||||
description: "Service Account",
|
||||
Icon: Bot,
|
||||
},
|
||||
awstag: {
|
||||
category: NODE_CATEGORY.MISC,
|
||||
description: "AWS Tag",
|
||||
Icon: Tags,
|
||||
},
|
||||
} as const satisfies Record<string, KnownNodeVisualMapping>;
|
||||
|
||||
type KnownNodeLabel = keyof typeof KNOWN_NODE_VISUALS;
|
||||
|
||||
@@ -667,7 +667,11 @@ export default function AttackPathsPage() {
|
||||
|
||||
{/* Legend below */}
|
||||
<div className="flex justify-center overflow-x-auto">
|
||||
<GraphLegend data={graphState.data} />
|
||||
<GraphLegend
|
||||
data={graphState.data}
|
||||
expandedResources={graphState.expandedResources}
|
||||
isFilteredView={graphState.isFilteredView}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user