mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-23 20:04:27 +00:00
528 lines
13 KiB
TypeScript
528 lines
13 KiB
TypeScript
import {
|
|
AlertTriangle,
|
|
Bot,
|
|
Box,
|
|
Braces,
|
|
CircleAlert,
|
|
FileKey2,
|
|
Globe2,
|
|
Info,
|
|
KeyRound,
|
|
Route,
|
|
Server,
|
|
Shield,
|
|
ShieldCheck,
|
|
Siren,
|
|
Tags,
|
|
UserCog,
|
|
Users,
|
|
Waypoints,
|
|
} from "lucide-react";
|
|
import type { ElementType } from "react";
|
|
|
|
import {
|
|
AlibabaCloudProviderBadge,
|
|
AWSProviderBadge,
|
|
AzureProviderBadge,
|
|
CloudflareProviderBadge,
|
|
GCPProviderBadge,
|
|
GitHubProviderBadge,
|
|
GoogleWorkspaceProviderBadge,
|
|
IacProviderBadge,
|
|
ImageProviderBadge,
|
|
KS8ProviderBadge,
|
|
M365ProviderBadge,
|
|
MongoDBAtlasProviderBadge,
|
|
OktaProviderBadge,
|
|
OpenStackProviderBadge,
|
|
OracleCloudProviderBadge,
|
|
VercelProviderBadge,
|
|
} from "@/components/icons/providers-badge";
|
|
import {
|
|
AmazonEC2Icon,
|
|
AmazonRDSIcon,
|
|
AmazonS3Icon,
|
|
AmazonVPCIcon,
|
|
AWSIAMIcon,
|
|
AWSLambdaIcon,
|
|
} from "@/components/icons/services/IconServices";
|
|
import { formatNodeLabel } from "@/lib/attack-paths/format";
|
|
import { isProwlerFindingLabel } from "@/lib/attack-paths/node-types";
|
|
import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths";
|
|
|
|
export const NODE_CATEGORY = {
|
|
FINDING: "finding",
|
|
INTERNET: "internet",
|
|
ACCOUNT: "account",
|
|
STORAGE: "storage",
|
|
NETWORK: "network",
|
|
COMPUTE: "compute",
|
|
IDENTITY: "identity",
|
|
SECRET: "secret",
|
|
MISC: "misc",
|
|
} as const;
|
|
|
|
export type NodeCategory = (typeof NODE_CATEGORY)[keyof typeof NODE_CATEGORY];
|
|
|
|
interface KnownNodeVisualMapping {
|
|
category: NodeCategory;
|
|
description: string;
|
|
Icon: ElementType;
|
|
}
|
|
|
|
export interface NodeVisual extends KnownNodeVisualMapping {
|
|
displayName: string;
|
|
fallbackUsed: boolean;
|
|
}
|
|
|
|
const KNOWN_NODE_VISUALS = {
|
|
awsaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "AWS Account",
|
|
Icon: AWSProviderBadge,
|
|
},
|
|
azureaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Azure Account",
|
|
Icon: AzureProviderBadge,
|
|
},
|
|
azuretenant: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Azure Tenant",
|
|
Icon: AzureProviderBadge,
|
|
},
|
|
gcpaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Google Cloud Account",
|
|
Icon: GCPProviderBadge,
|
|
},
|
|
gcpproject: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Google Cloud Project",
|
|
Icon: GCPProviderBadge,
|
|
},
|
|
googlecloudaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Google Cloud Account",
|
|
Icon: GCPProviderBadge,
|
|
},
|
|
kubernetescluster: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Kubernetes Cluster",
|
|
Icon: KS8ProviderBadge,
|
|
},
|
|
k8scluster: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Kubernetes Cluster",
|
|
Icon: KS8ProviderBadge,
|
|
},
|
|
githubaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "GitHub Account",
|
|
Icon: GitHubProviderBadge,
|
|
},
|
|
githuborganization: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "GitHub Organization",
|
|
Icon: GitHubProviderBadge,
|
|
},
|
|
m365tenant: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Microsoft 365 Tenant",
|
|
Icon: M365ProviderBadge,
|
|
},
|
|
googleworkspace: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Google Workspace",
|
|
Icon: GoogleWorkspaceProviderBadge,
|
|
},
|
|
iac: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Infrastructure as Code",
|
|
Icon: IacProviderBadge,
|
|
},
|
|
containerregistry: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Container Registry",
|
|
Icon: ImageProviderBadge,
|
|
},
|
|
oraclecloudaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Oracle Cloud Account",
|
|
Icon: OracleCloudProviderBadge,
|
|
},
|
|
mongodbatlas: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "MongoDB Atlas",
|
|
Icon: MongoDBAtlasProviderBadge,
|
|
},
|
|
alibabacloudaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Alibaba Cloud Account",
|
|
Icon: AlibabaCloudProviderBadge,
|
|
},
|
|
cloudflareaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Cloudflare Account",
|
|
Icon: CloudflareProviderBadge,
|
|
},
|
|
openstackaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "OpenStack Account",
|
|
Icon: OpenStackProviderBadge,
|
|
},
|
|
vercelaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Vercel Account",
|
|
Icon: VercelProviderBadge,
|
|
},
|
|
oktaaccount: {
|
|
category: NODE_CATEGORY.ACCOUNT,
|
|
description: "Okta Account",
|
|
Icon: OktaProviderBadge,
|
|
},
|
|
s3bucket: {
|
|
category: NODE_CATEGORY.STORAGE,
|
|
description: "S3 Bucket",
|
|
Icon: AmazonS3Icon,
|
|
},
|
|
s3: {
|
|
category: NODE_CATEGORY.STORAGE,
|
|
description: "S3",
|
|
Icon: AmazonS3Icon,
|
|
},
|
|
vpc: {
|
|
category: NODE_CATEGORY.NETWORK,
|
|
description: "VPC",
|
|
Icon: AmazonVPCIcon,
|
|
},
|
|
subnet: {
|
|
category: NODE_CATEGORY.NETWORK,
|
|
description: "Subnet",
|
|
Icon: Waypoints,
|
|
},
|
|
securitygroup: {
|
|
category: NODE_CATEGORY.NETWORK,
|
|
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",
|
|
Icon: Route,
|
|
},
|
|
defaultgateway: {
|
|
category: NODE_CATEGORY.NETWORK,
|
|
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",
|
|
Icon: Server,
|
|
},
|
|
nic: {
|
|
category: NODE_CATEGORY.COMPUTE,
|
|
description: "NIC",
|
|
Icon: Server,
|
|
},
|
|
iamuser: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "IAM User",
|
|
Icon: AWSIAMIcon,
|
|
},
|
|
awsuser: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
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",
|
|
Icon: AWSIAMIcon,
|
|
},
|
|
awsrole: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "AWS Role",
|
|
Icon: ShieldCheck,
|
|
},
|
|
permissionrole: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "Permission Role",
|
|
Icon: ShieldCheck,
|
|
},
|
|
awsmanagedpolicy: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "AWS Managed Policy",
|
|
Icon: FileKey2,
|
|
},
|
|
awspolicy: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "AWS Policy",
|
|
Icon: FileKey2,
|
|
},
|
|
policy: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "Policy",
|
|
Icon: FileKey2,
|
|
},
|
|
awspolicystatement: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "AWS Policy Statement",
|
|
Icon: Braces,
|
|
},
|
|
policystatement: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
description: "Policy Statement",
|
|
Icon: Braces,
|
|
},
|
|
accesskey: {
|
|
category: NODE_CATEGORY.SECRET,
|
|
description: "Access Key",
|
|
Icon: KeyRound,
|
|
},
|
|
secret: {
|
|
category: NODE_CATEGORY.SECRET,
|
|
description: "Secret",
|
|
Icon: KeyRound,
|
|
},
|
|
serviceaccount: {
|
|
category: NODE_CATEGORY.IDENTITY,
|
|
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;
|
|
|
|
const normalizeLabel = (label: string): string =>
|
|
label.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
|
|
const isKnownNodeLabel = (label: string): label is KnownNodeLabel =>
|
|
label in KNOWN_NODE_VISUALS;
|
|
|
|
const isFindingLabel = isProwlerFindingLabel;
|
|
|
|
const isInternetLabel = (label: string): boolean =>
|
|
normalizeLabel(label) === "internet";
|
|
|
|
const FINDING_SEVERITY = {
|
|
CRITICAL: "critical",
|
|
HIGH: "high",
|
|
MEDIUM: "medium",
|
|
LOW: "low",
|
|
INFO: "info",
|
|
INFORMATIONAL: "informational",
|
|
} as const;
|
|
|
|
type FindingSeverity = (typeof FINDING_SEVERITY)[keyof typeof FINDING_SEVERITY];
|
|
|
|
const FINDING_SEVERITY_ICONS = {
|
|
[FINDING_SEVERITY.CRITICAL]: Siren,
|
|
[FINDING_SEVERITY.HIGH]: AlertTriangle,
|
|
[FINDING_SEVERITY.MEDIUM]: CircleAlert,
|
|
[FINDING_SEVERITY.LOW]: Info,
|
|
[FINDING_SEVERITY.INFO]: Info,
|
|
[FINDING_SEVERITY.INFORMATIONAL]: Info,
|
|
} as const satisfies Record<FindingSeverity, ElementType>;
|
|
|
|
const stringifyProperty = (
|
|
value: GraphNodePropertyValue,
|
|
): string | undefined => {
|
|
if (value === null || value === undefined) return undefined;
|
|
if (Array.isArray(value)) return value.join(", ");
|
|
return String(value);
|
|
};
|
|
|
|
const firstDefinedProperty = (
|
|
node: GraphNode,
|
|
keys: string[],
|
|
): string | undefined => {
|
|
for (const key of keys) {
|
|
const value = stringifyProperty(node.properties[key]);
|
|
if (value) return value;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
const getPrimaryFormattedLabel = (node: GraphNode): string => {
|
|
const primaryLabel = node.labels[0];
|
|
if (!primaryLabel) return "Unknown";
|
|
return formatNodeLabel(primaryLabel.replace(/[_-]/g, " "));
|
|
};
|
|
|
|
const resolveDisplayName = (node: GraphNode): string =>
|
|
firstDefinedProperty(node, ["name", "display_name", "title", "id"]) ??
|
|
getPrimaryFormattedLabel(node);
|
|
|
|
const resolveFindingDisplayName = (node: GraphNode): string =>
|
|
firstDefinedProperty(node, ["check_title", "title", "name", "id"]) ??
|
|
getPrimaryFormattedLabel(node);
|
|
|
|
const resolveFindingSeverity = (
|
|
node: GraphNode,
|
|
): FindingSeverity | undefined => {
|
|
const severity = firstDefinedProperty(node, ["severity"]);
|
|
if (!severity) return undefined;
|
|
|
|
const normalizedSeverity = severity.toLowerCase();
|
|
return normalizedSeverity in FINDING_SEVERITY_ICONS
|
|
? (normalizedSeverity as FindingSeverity)
|
|
: undefined;
|
|
};
|
|
|
|
const resolveFindingIcon = (node: GraphNode): ElementType => {
|
|
const severity = resolveFindingSeverity(node);
|
|
return severity ? FINDING_SEVERITY_ICONS[severity] : AlertTriangle;
|
|
};
|
|
|
|
const resolveKnownMapping = (
|
|
labels: string[],
|
|
): KnownNodeVisualMapping | undefined => {
|
|
for (const label of labels) {
|
|
const normalizedLabel = normalizeLabel(label);
|
|
if (isKnownNodeLabel(normalizedLabel)) {
|
|
return KNOWN_NODE_VISUALS[normalizedLabel];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
export const resolveNodeVisual = (node: GraphNode): NodeVisual => {
|
|
if (node.labels.some(isFindingLabel)) {
|
|
return {
|
|
category: NODE_CATEGORY.FINDING,
|
|
displayName: resolveFindingDisplayName(node),
|
|
description: "Prowler Finding",
|
|
Icon: resolveFindingIcon(node),
|
|
fallbackUsed: false,
|
|
};
|
|
}
|
|
|
|
if (node.labels.some(isInternetLabel)) {
|
|
return {
|
|
category: NODE_CATEGORY.INTERNET,
|
|
displayName: "Internet",
|
|
description: "Internet",
|
|
Icon: Globe2,
|
|
fallbackUsed: false,
|
|
};
|
|
}
|
|
|
|
const knownMapping = resolveKnownMapping(node.labels);
|
|
if (knownMapping) {
|
|
return {
|
|
...knownMapping,
|
|
displayName: resolveDisplayName(node),
|
|
fallbackUsed: false,
|
|
};
|
|
}
|
|
|
|
const fallbackLabel = getPrimaryFormattedLabel(node);
|
|
|
|
return {
|
|
category: NODE_CATEGORY.MISC,
|
|
displayName: resolveDisplayName(node),
|
|
description: fallbackLabel,
|
|
Icon: Box,
|
|
fallbackUsed: true,
|
|
};
|
|
};
|