Files
prowler/ui/actions/attack-paths/query-result.adapter.ts
Josema Camacho 032499c29a feat(attack-paths): The complete Attack Paths feature (#9805)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Chandrapal Badshah <Chan9390@users.noreply.github.com>
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Adrián Peña <adrianjpr@gmail.com>
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: KonstGolfi <73020281+KonstGolfi@users.noreply.github.com>
Co-authored-by: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com>
Co-authored-by: Prowler Bot <bot@prowler.com>
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: StylusFrost <43682773+StylusFrost@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
Co-authored-by: bota4go <108249054+bota4go@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: mchennai <50082780+mchennai@users.noreply.github.com>
Co-authored-by: Ryan Nolette <sonofagl1tch@users.noreply.github.com>
Co-authored-by: Ulissis Correa <123517149+ulissisc@users.noreply.github.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
Co-authored-by: Lee Trout <ltrout@watchpointlabs.com>
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
Co-authored-by: Alan-TheGentleman <alan@thegentleman.dev>
2026-01-16 13:37:09 +01:00

165 lines
5.0 KiB
TypeScript

import {
AttackPathGraphData,
GraphEdge,
GraphNodeProperties,
GraphNodePropertyValue,
GraphRelationship,
} from "@/types/attack-paths";
/**
* Normalizes property values to ensure they are primitives
* Arrays are converted to comma-separated strings
*
* @param value - The property value to normalize
* @returns Normalized primitive value
*/
function normalizePropertyValue(
value:
| GraphNodePropertyValue
| GraphNodePropertyValue[]
| Record<string, unknown>,
): string | number | boolean | null | undefined {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
// Convert arrays to comma-separated strings
return value.join(", ");
}
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
return value;
}
// For any other type, convert to string
return String(value);
}
/**
* Normalizes all properties in an object to ensure they are primitives
*
* @param properties - The properties object to normalize
* @returns Normalized properties object
*/
function normalizeProperties(
properties: Record<
string,
GraphNodePropertyValue | GraphNodePropertyValue[] | Record<string, unknown>
>,
): GraphNodeProperties {
const normalized: GraphNodeProperties = {};
for (const [key, value] of Object.entries(properties)) {
normalized[key] = normalizePropertyValue(value);
}
return normalized;
}
/**
* Adapts graph query result data for D3 visualization
* Transforms relationships array into edges array for D3 force-directed graph
*
* The adapter handles:
* - Converting relationship objects to edge objects compatible with D3
* - Mapping relationship labels to edge types for graph styling
* - Normalizing array properties to strings (e.g., anonymous_actions: ["s3:GetObject"] -> "s3:GetObject")
* - Preserving node and relationship data structure
* - Adding findings array to each node based on HAS_FINDING edges
* - Adding resources array to finding nodes based on HAS_FINDING edges (reverse relationship)
*
* @param graphData - Raw graph data with nodes and relationships from API
* @returns Graph data with edges array formatted for D3 visualization and findings/resources on nodes
*/
export function adaptQueryResultToGraphData(
graphData: AttackPathGraphData,
): AttackPathGraphData {
// Normalize node properties to ensure all values are primitives
const normalizedNodes = graphData.nodes.map((node) => ({
...node,
properties: normalizeProperties(
node.properties as Record<
string,
GraphNodePropertyValue | GraphNodePropertyValue[]
>,
),
findings: [] as string[], // Will be populated below
resources: [] as string[], // Will be populated below for finding nodes
}));
// Transform relationships into D3-compatible edges if relationships exist
// Also handle case where edges are already provided (e.g., from mock data)
let edges: GraphEdge[] = [];
if (graphData.relationships) {
edges = (graphData.relationships as GraphRelationship[]).map(
(relationship) => ({
id: relationship.id,
source: relationship.source,
target: relationship.target,
type: relationship.label, // D3 uses 'type' for styling edge appearance
properties: relationship.properties
? normalizeProperties(
relationship.properties as Record<
string,
GraphNodePropertyValue | GraphNodePropertyValue[]
>,
)
: undefined,
}),
);
} else if (graphData.edges) {
// If edges are already provided, just normalize their properties
edges = (graphData.edges as GraphEdge[]).map((edge) => ({
...edge,
properties: edge.properties
? normalizeProperties(
edge.properties as Record<
string,
GraphNodePropertyValue | GraphNodePropertyValue[]
>,
)
: undefined,
}));
}
// Populate findings and resources based on HAS_FINDING edges
edges.forEach((edge) => {
if (edge.type === "HAS_FINDING") {
const sourceId =
typeof edge.source === "string"
? edge.source
: (edge.source as { id?: string })?.id;
const targetId =
typeof edge.target === "string"
? edge.target
: (edge.target as { id?: string })?.id;
if (sourceId && targetId) {
// Add finding to source node (resource -> finding)
const sourceNode = normalizedNodes.find((n) => n.id === sourceId);
if (sourceNode) {
sourceNode.findings.push(targetId);
}
// Add resource to target node (finding <- resource)
const targetNode = normalizedNodes.find((n) => n.id === targetId);
if (targetNode) {
targetNode.resources.push(sourceId);
}
}
}
});
return {
nodes: normalizedNodes,
edges,
relationships: graphData.relationships, // Preserve original relationships data
};
}