Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions src/ui/src/containers/App/scripts-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
4 changes: 2 additions & 2 deletions src/ui/src/containers/live-widgets/graph/graph-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions src/ui/src/containers/live-widgets/graph/request-graph-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ export class RequestGraphManager {
const nodeMap = new Map<string, Entity>();
// 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 } = {};
Expand Down Expand Up @@ -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<any> }): 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<string, number>();
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, "<br>").replace(/ /g, "&nbsp;"),
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, "<br>").replace(/ /g, "&nbsp;"),
});
}

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}`,
});
});
}
}
}
}
4 changes: 2 additions & 2 deletions src/ui/src/containers/live-widgets/graph/request-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export const RequestGraphWidget = React.memo<RequestGraphProps>(({
const [network, setNetwork] = React.useState<Network>(null);
const [graphMgr, setGraphMgr] = React.useState<RequestGraphManager>(null);

const [clusteredMode, setClusteredMode] = React.useState<boolean>(true);
const [hierarchyEnabled, setHierarchyEnabled] = React.useState<boolean>(false);
const [clusteredMode, setClusteredMode] = React.useState<boolean>(false);
const [hierarchyEnabled, setHierarchyEnabled] = React.useState<boolean>(true);
const [colorByLatency, setColorByLatency] = React.useState<boolean>(false);

const theme = useTheme();
Expand Down