mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-16 01:28:26 +00:00
Compare commits
7 Commits
fix/sdk-aw
...
PROWLER-13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
105bc5fe27 | ||
|
|
9b4667bfac | ||
|
|
a443350d8b | ||
|
|
5c981f5683 | ||
|
|
9922b15391 | ||
|
|
6e77abea01 | ||
|
|
4d57f3bef1 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "openspec"]
|
||||
path = openspec
|
||||
url = https://github.com/prowler-cloud/prowler-openspec-opensource.git
|
||||
1
openspec
Submodule
1
openspec
Submodule
Submodule openspec added at 4e50159a6d
@@ -131,27 +131,16 @@ export function adaptQueryResultToGraphData(
|
||||
// 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;
|
||||
// Add finding to source node (resource -> finding)
|
||||
const sourceNode = normalizedNodes.find((n) => n.id === edge.source);
|
||||
if (sourceNode) {
|
||||
sourceNode.findings.push(edge.target);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// Add resource to target node (finding <- resource)
|
||||
const targetNode = normalizedNodes.find((n) => n.id === edge.target);
|
||||
if (targetNode) {
|
||||
targetNode.resources.push(edge.source);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,20 +302,8 @@ const AttackPathGraphComponent = forwardRef<
|
||||
// Add edges to dagre graph
|
||||
if (data.edges && Array.isArray(data.edges)) {
|
||||
data.edges.forEach((edge) => {
|
||||
const source = edge.source;
|
||||
const target = edge.target;
|
||||
let sourceId =
|
||||
typeof source === "string"
|
||||
? source
|
||||
: typeof source === "object" && source !== null
|
||||
? (source as GraphNode).id
|
||||
: "";
|
||||
let targetId =
|
||||
typeof target === "string"
|
||||
? target
|
||||
: typeof target === "object" && target !== null
|
||||
? (target as GraphNode).id
|
||||
: "";
|
||||
let sourceId = edge.source;
|
||||
let targetId = edge.target;
|
||||
|
||||
// Reverse container relationships for proper hierarchy
|
||||
if (containerRelations.has(edge.type)) {
|
||||
@@ -324,14 +312,8 @@ const AttackPathGraphComponent = forwardRef<
|
||||
|
||||
if (sourceId && targetId) {
|
||||
g.setEdge(sourceId, targetId, {
|
||||
originalSource:
|
||||
typeof edge.source === "string"
|
||||
? edge.source
|
||||
: (edge.source as GraphNode).id,
|
||||
originalTarget:
|
||||
typeof edge.target === "string"
|
||||
? edge.target
|
||||
: (edge.target as GraphNode).id,
|
||||
originalSource: edge.source,
|
||||
originalTarget: edge.target,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -682,17 +664,9 @@ const AttackPathGraphComponent = forwardRef<
|
||||
// Find connected findings for THIS node
|
||||
const connectedFindings = new Set<string>();
|
||||
data.edges?.forEach((edge) => {
|
||||
const sourceId =
|
||||
typeof edge.source === "string"
|
||||
? edge.source
|
||||
: (edge.source as GraphNode).id;
|
||||
const targetId =
|
||||
typeof edge.target === "string"
|
||||
? edge.target
|
||||
: (edge.target as GraphNode).id;
|
||||
|
||||
if (sourceId === node.id || targetId === node.id) {
|
||||
const otherId = sourceId === node.id ? targetId : sourceId;
|
||||
if (edge.source === node.id || edge.target === node.id) {
|
||||
const otherId =
|
||||
edge.source === node.id ? edge.target : edge.source;
|
||||
const otherNode = data.nodes.find((n) => n.id === otherId);
|
||||
if (
|
||||
otherNode?.labels.some((label) =>
|
||||
@@ -847,17 +821,8 @@ const AttackPathGraphComponent = forwardRef<
|
||||
// Build a set of resource nodes that have findings connected to them
|
||||
const resourcesWithFindings = new Set<string>();
|
||||
data.edges?.forEach((edge) => {
|
||||
const sourceId =
|
||||
typeof edge.source === "string"
|
||||
? edge.source
|
||||
: (edge.source as GraphNode).id;
|
||||
const targetId =
|
||||
typeof edge.target === "string"
|
||||
? edge.target
|
||||
: (edge.target as GraphNode).id;
|
||||
|
||||
const sourceNode = nodeDataMap.get(sourceId);
|
||||
const targetNode = nodeDataMap.get(targetId);
|
||||
const sourceNode = nodeDataMap.get(edge.source);
|
||||
const targetNode = nodeDataMap.get(edge.target);
|
||||
|
||||
const sourceIsFinding = sourceNode?.labels.some((l) =>
|
||||
l.toLowerCase().includes("finding"),
|
||||
@@ -868,10 +833,10 @@ const AttackPathGraphComponent = forwardRef<
|
||||
|
||||
// If one end is a finding, the other is a resource with findings
|
||||
if (sourceIsFinding && !targetIsFinding) {
|
||||
resourcesWithFindings.add(targetId);
|
||||
resourcesWithFindings.add(edge.target);
|
||||
}
|
||||
if (targetIsFinding && !sourceIsFinding) {
|
||||
resourcesWithFindings.add(sourceId);
|
||||
resourcesWithFindings.add(edge.source);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { NodeDetailContent, NodeDetailPanel } from "./node-detail-panel";
|
||||
export { NodeOverview } from "./node-overview";
|
||||
export { NodeRelationships } from "./node-relationships";
|
||||
export { NodeRemediation } from "./node-remediation";
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { GraphEdge } from "@/types/attack-paths";
|
||||
|
||||
interface NodeRelationshipsProps {
|
||||
incomingEdges: GraphEdge[];
|
||||
outgoingEdges: GraphEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format edge type to human-readable label
|
||||
* e.g., "HAS_FINDING" -> "Has Finding"
|
||||
*/
|
||||
function formatEdgeType(edgeType: string): string {
|
||||
return edgeType
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
interface EdgeItemProps {
|
||||
edge: GraphEdge;
|
||||
isOutgoing: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable edge item component
|
||||
*/
|
||||
function EdgeItem({ edge, isOutgoing }: EdgeItemProps) {
|
||||
const targetId =
|
||||
typeof edge.target === "string" ? edge.target : String(edge.target);
|
||||
const sourceId =
|
||||
typeof edge.source === "string" ? edge.source : String(edge.source);
|
||||
const displayId = (isOutgoing ? targetId : sourceId).substring(0, 30);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={edge.id}
|
||||
className="border-border-neutral-tertiary dark:border-border-neutral-tertiary flex items-center justify-between rounded border p-2"
|
||||
>
|
||||
<code className="text-text-neutral-secondary dark:text-text-neutral-secondary text-xs">
|
||||
{displayId}
|
||||
</code>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded px-2 py-1 text-xs font-medium",
|
||||
isOutgoing
|
||||
? "bg-bg-data-info text-text-neutral-primary dark:text-text-neutral-primary"
|
||||
: "bg-bg-pass-primary text-text-neutral-primary dark:text-text-neutral-primary",
|
||||
)}
|
||||
>
|
||||
{formatEdgeType(edge.type)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Node relationships section showing incoming and outgoing edges
|
||||
*/
|
||||
export const NodeRelationships = ({
|
||||
incomingEdges,
|
||||
outgoingEdges,
|
||||
}: NodeRelationshipsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Outgoing Relationships */}
|
||||
<div>
|
||||
<h4 className="dark:text-prowler-theme-pale/90 mb-3 text-sm font-semibold">
|
||||
Outgoing Relationships ({outgoingEdges.length})
|
||||
</h4>
|
||||
{outgoingEdges.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{outgoingEdges.map((edge) => (
|
||||
<EdgeItem key={edge.id} edge={edge} isOutgoing />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-text-neutral-tertiary dark:text-text-neutral-tertiary text-xs">
|
||||
No outgoing relationships
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Incoming Relationships */}
|
||||
<div className="border-border-neutral-tertiary dark:border-border-neutral-tertiary border-t pt-6">
|
||||
<h4 className="dark:text-prowler-theme-pale/90 mb-3 text-sm font-semibold">
|
||||
Incoming Relationships ({incomingEdges.length})
|
||||
</h4>
|
||||
{incomingEdges.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{incomingEdges.map((edge) => (
|
||||
<EdgeItem key={edge.id} edge={edge} isOutgoing={false} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-text-neutral-tertiary dark:text-text-neutral-tertiary text-xs">
|
||||
No incoming relationships
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -21,8 +21,6 @@ interface GraphStore extends GraphState, FilteredViewState {
|
||||
setSelectedNodeId: (nodeId: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setZoom: (zoomLevel: number) => void;
|
||||
setPan: (panX: number, panY: number) => void;
|
||||
setFilteredView: (
|
||||
isFiltered: boolean,
|
||||
nodeId: string | null,
|
||||
@@ -37,9 +35,6 @@ const initialState: GraphState & FilteredViewState = {
|
||||
selectedNodeId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
zoomLevel: 1,
|
||||
panX: 0,
|
||||
panY: 0,
|
||||
isFilteredView: false,
|
||||
filteredNodeId: null,
|
||||
fullData: null,
|
||||
@@ -58,8 +53,6 @@ const useGraphStore = create<GraphStore>((set) => ({
|
||||
setSelectedNodeId: (nodeId) => set({ selectedNodeId: nodeId }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setZoom: (zoomLevel) => set({ zoomLevel }),
|
||||
setPan: (panX, panY) => set({ panX, panY }),
|
||||
setFilteredView: (isFiltered, nodeId, filteredData, fullData) =>
|
||||
set({
|
||||
isFilteredView: isFiltered,
|
||||
@@ -106,11 +99,6 @@ export const useGraphState = () => {
|
||||
store.setError(error);
|
||||
};
|
||||
|
||||
const updateZoomAndPan = (zoomLevel: number, panX: number, panY: number) => {
|
||||
store.setZoom(zoomLevel);
|
||||
store.setPan(panX, panY);
|
||||
};
|
||||
|
||||
const resetGraph = () => {
|
||||
store.reset();
|
||||
};
|
||||
@@ -162,9 +150,6 @@ export const useGraphState = () => {
|
||||
selectedNode: getSelectedNode(),
|
||||
loading: store.loading,
|
||||
error: store.error,
|
||||
zoomLevel: store.zoomLevel,
|
||||
panX: store.panX,
|
||||
panY: store.panY,
|
||||
isFilteredView: store.isFilteredView,
|
||||
filteredNodeId: store.filteredNodeId,
|
||||
filteredNode: getFilteredNode(),
|
||||
@@ -173,7 +158,6 @@ export const useGraphState = () => {
|
||||
startLoading,
|
||||
stopLoading,
|
||||
setError,
|
||||
updateZoomAndPan,
|
||||
resetGraph,
|
||||
clearGraph,
|
||||
enterFilteredView,
|
||||
|
||||
@@ -4,23 +4,6 @@
|
||||
|
||||
import type { AttackPathGraphData } from "@/types/attack-paths";
|
||||
|
||||
/**
|
||||
* Type for edge node reference - can be a string ID or an object with id property
|
||||
* Note: We use `object` to match GraphEdge type from attack-paths.ts
|
||||
*/
|
||||
export type EdgeNodeRef = string | object;
|
||||
|
||||
/**
|
||||
* Helper to get edge source/target ID from string or object
|
||||
*/
|
||||
export const getEdgeNodeId = (nodeRef: EdgeNodeRef): string => {
|
||||
if (typeof nodeRef === "string") {
|
||||
return nodeRef;
|
||||
}
|
||||
// Edge node references are objects with an id property
|
||||
return (nodeRef as { id: string }).id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute a filtered subgraph containing only the path through the target node.
|
||||
* This follows the directed graph structure of attack paths:
|
||||
@@ -44,10 +27,8 @@ export const computeFilteredSubgraph = (
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const sourceId = getEdgeNodeId(edge.source);
|
||||
const targetId = getEdgeNodeId(edge.target);
|
||||
forwardEdges.get(sourceId)?.add(targetId);
|
||||
backwardEdges.get(targetId)?.add(sourceId);
|
||||
forwardEdges.get(edge.source)?.add(edge.target);
|
||||
backwardEdges.get(edge.target)?.add(edge.source);
|
||||
});
|
||||
|
||||
const visibleNodeIds = new Set<string>();
|
||||
@@ -85,10 +66,8 @@ export const computeFilteredSubgraph = (
|
||||
|
||||
// Also include findings directly connected to the selected node
|
||||
edges.forEach((edge) => {
|
||||
const sourceId = getEdgeNodeId(edge.source);
|
||||
const targetId = getEdgeNodeId(edge.target);
|
||||
const sourceNode = nodes.find((n) => n.id === sourceId);
|
||||
const targetNode = nodes.find((n) => n.id === targetId);
|
||||
const sourceNode = nodes.find((n) => n.id === edge.source);
|
||||
const targetNode = nodes.find((n) => n.id === edge.target);
|
||||
|
||||
const sourceIsFinding = sourceNode?.labels.some((l) =>
|
||||
l.toLowerCase().includes("finding"),
|
||||
@@ -98,21 +77,20 @@ export const computeFilteredSubgraph = (
|
||||
);
|
||||
|
||||
// Include findings connected to the selected node
|
||||
if (sourceId === targetNodeId && targetIsFinding) {
|
||||
visibleNodeIds.add(targetId);
|
||||
if (edge.source === targetNodeId && targetIsFinding) {
|
||||
visibleNodeIds.add(edge.target);
|
||||
}
|
||||
if (targetId === targetNodeId && sourceIsFinding) {
|
||||
visibleNodeIds.add(sourceId);
|
||||
if (edge.target === targetNodeId && sourceIsFinding) {
|
||||
visibleNodeIds.add(edge.source);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter nodes and edges to only include visible ones
|
||||
const filteredNodes = nodes.filter((node) => visibleNodeIds.has(node.id));
|
||||
const filteredEdges = edges.filter((edge) => {
|
||||
const sourceId = getEdgeNodeId(edge.source);
|
||||
const targetId = getEdgeNodeId(edge.target);
|
||||
return visibleNodeIds.has(sourceId) && visibleNodeIds.has(targetId);
|
||||
});
|
||||
const filteredEdges = edges.filter(
|
||||
(edge) =>
|
||||
visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target),
|
||||
);
|
||||
|
||||
return {
|
||||
nodes: filteredNodes,
|
||||
|
||||
@@ -15,9 +15,4 @@ export {
|
||||
GRAPH_NODE_COLORS,
|
||||
GRAPH_SELECTION_COLOR,
|
||||
} from "./graph-colors";
|
||||
export {
|
||||
computeFilteredSubgraph,
|
||||
type EdgeNodeRef,
|
||||
getEdgeNodeId,
|
||||
getPathEdges,
|
||||
} from "./graph-utils";
|
||||
export { computeFilteredSubgraph, getPathEdges } from "./graph-utils";
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Navbar({ title, icon }: NavbarProps) {
|
||||
title={title}
|
||||
icon={icon}
|
||||
feedsSlot={
|
||||
<Suspense fallback={<FeedsLoadingFallback />}>
|
||||
<Suspense key="feeds" fallback={<FeedsLoadingFallback />}>
|
||||
<FeedsServer limit={15} />
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -180,8 +180,8 @@ export interface GraphNode {
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string;
|
||||
source: string | object;
|
||||
target: string | object;
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
properties?: GraphNodeProperties;
|
||||
}
|
||||
@@ -268,9 +268,6 @@ export interface GraphState {
|
||||
selectedNodeId: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
zoomLevel: number;
|
||||
panX: number;
|
||||
panY: number;
|
||||
}
|
||||
|
||||
// Provider Integration
|
||||
|
||||
Reference in New Issue
Block a user