diff --git a/src/ui/src/containers/App/scripts-context.tsx b/src/ui/src/containers/App/scripts-context.tsx index d9e1e6e82e1..81eeaad162c 100644 --- a/src/ui/src/containers/App/scripts-context.tsx +++ b/src/ui/src/containers/App/scripts-context.tsx @@ -43,13 +43,53 @@ export const SCRATCH_SCRIPT: Script = { description: 'A clean slate for one-off scripts.\n' + 'This is ephemeral; it disappears upon changing scripts.', vis: { - variables: [], - widgets: [], - globalFuncs: [], - }, - code: 'import px\n\n' - + '# Use this scratch pad to write and run one-off scripts.\n' - + '# If you switch to another script, refresh, or close this browser tab, this script will disappear.\n\n', + "variables": [], + "widgets": [ + { + "name": "Stix Bundle Graph", + "position": { + "x": 0, + "y": 0, + "w": 12, + "h": 5 + }, + "func": { + "name": "fetch_stix", + "args": [] + }, + "displaySpec": { + "@type": "types.px.dev/px.vispb.RequestGraph" + } + }, + { + "name": "Stix Bundles", + "position": { + "x": 0, + "y": 3, + "w": 12, + "h": 3 + }, + "func": { + "name": "fetch_all_stix", + "args": [] + }, + "displaySpec": { + "@type": "types.px.dev/px.vispb.Table" + } + } + ], + "globalFuncs": [] +}, + code: `import px + +def fetch_stix(): + df = px.DataFrame(table="stix.json") + return df[["stix_bundle"]] + +def fetch_all_stix(): + df = px.DataFrame(table="stix.json") + return df +`, hidden: false, }; diff --git a/src/ui/src/containers/live-widgets/graph/graph-utils.ts b/src/ui/src/containers/live-widgets/graph/graph-utils.ts index d7a219a5e6d..2a2d24e9689 100644 --- a/src/ui/src/containers/live-widgets/graph/graph-utils.ts +++ b/src/ui/src/containers/live-widgets/graph/graph-utils.ts @@ -49,10 +49,10 @@ export function getGraphOptions(theme: Theme, edgeLength: number): Options { solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -50, - springLength: edgeLength > 0 ? edgeLength : 100, + springLength: edgeLength > 0 ? edgeLength : 150, }, hierarchicalRepulsion: { - nodeDistance: 100, + nodeDistance: 150, }, stabilization: { iterations: 250, diff --git a/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts b/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts index 344df8fd076..c671e223db3 100644 --- a/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts +++ b/src/ui/src/containers/live-widgets/graph/request-graph-manager.ts @@ -317,6 +317,12 @@ export class RequestGraphManager { const nodeMap = new Map(); // Edges grouped by pod/IP are automatically unique, // so we don't need an equivalent to `clusteredEdgesMap` here. + + const stixBundleData = data.find((d) => d["stix_bundle"]); + if (stixBundleData !== undefined) { + this.renderStix(JSON.parse(stixBundleData["stix_bundle"])); + return + } // Capture the semantic types for the columns. const semTypes: { [key: string]: SemanticType } = {}; @@ -381,4 +387,82 @@ export class RequestGraphManager { }); }); } + + private getNodeImgByType(type: string): string { + const snakeType = type.replace(/-/g, '_'); // e.g., "attack-pattern" -> "attack_pattern" + return `https://raw.githubusercontent.com/oasis-open/cti-stix-visualization/refs/heads/master/stix2viz/stix2viz/icons/stix2_${snakeType}_icon_tiny_round_v1.png`; + } + + private renderStix(stixBundle: { objects: Array }): void { + this.nodes.clear(); + this.edges.clear(); + this.clusteredNodes.clear(); + this.clusteredEdges.clear(); + + const nodeSize = 30; // same size for all + const nodeColor = '#003366'; // dark blue + const fontColor = '#ffffff'; // white + + const levelMap = new Map(); + let currentLevel = 0; + + for (const obj of stixBundle.objects) { + if (!obj.id) continue; + + if (!levelMap.has(obj.type)) { + levelMap.set(obj.type, currentLevel++); + } + + const level = levelMap.get(obj.type); + + // 🎯 Node setup + if (obj.type !== 'relationship') { + this.nodes.add({ + id: obj.id, + label: obj.name || obj.type, + title: JSON.stringify(obj, null, 2).replace(/\n/g, "
").replace(/ /g, " "), + shape: 'image', + size: nodeSize, + image: this.getNodeImgByType(obj.type), + color: { + background: nodeColor, + border: '#002244', + }, + font: { + color: fontColor, + size: 14, + face: 'monospace', + }, + level, + }); + } + + if (obj.type === 'relationship' && obj.source_ref && obj.target_ref) { + this.edges.add({ + id: obj.id, + from: obj.source_ref, + to: obj.target_ref, + arrows: 'to', + label: obj.relationship_type, + font: { face: 'monospace', size: 12 }, + color: { color: '#cccccc' }, + title: JSON.stringify(obj, null, 2).replace(/\n/g, "
").replace(/ /g, " "), + }); + } + + if (obj.type === 'observed-data' && Array.isArray(obj.object_refs)) { + obj.object_refs.forEach((refId) => { + this.edges.add({ + from: obj.id, + to: refId, + arrows: 'to', + label: 'refers-to', + font: { face: 'monospace', size: 12 }, + color: { color: '#aaaaaa' }, + title: `Observed-data refers-to ${refId}`, + }); + }); + } + } + } } diff --git a/src/ui/src/containers/live-widgets/graph/request-graph.tsx b/src/ui/src/containers/live-widgets/graph/request-graph.tsx index 655eede1f2f..d620f5fe542 100644 --- a/src/ui/src/containers/live-widgets/graph/request-graph.tsx +++ b/src/ui/src/containers/live-widgets/graph/request-graph.tsx @@ -71,8 +71,8 @@ export const RequestGraphWidget = React.memo(({ const [network, setNetwork] = React.useState(null); const [graphMgr, setGraphMgr] = React.useState(null); - const [clusteredMode, setClusteredMode] = React.useState(true); - const [hierarchyEnabled, setHierarchyEnabled] = React.useState(false); + const [clusteredMode, setClusteredMode] = React.useState(false); + const [hierarchyEnabled, setHierarchyEnabled] = React.useState(true); const [colorByLatency, setColorByLatency] = React.useState(false); const theme = useTheme();