Appearance
Cyber security log analysis
tsx
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import { createRoot } from 'react-dom/client';
import App from './App';
import './style.css';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
html
<!doctype html>
<html lang="en">
<head>
<title>Ogma cyber security analysis</title>
<link
href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
rel="stylesheet"
/>
<link type="text/css" rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter2/1.4.6/crossfilter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/vis-timeline@latest/styles/vis-timeline-graph2d.min.css"
rel="stylesheet"
type="text/css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@linkurious/ogma-timeline-plugin@latest/dist/style.css"
/>
<script src="https://cdn.jsdelivr.net/npm/@linkurious/ogma-ui-kit@0.0.9/dist/ogma-ui-kit.min.js"></script>
<!-- this importmap is only needed for Ogma playground -->
<script type="importmap">
{
"imports": {
"@shoelace-style/shoelace": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace.js",
"@shoelace-style/shoelace/dist/react": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/react/index.js",
"@shoelace-style/shoelace/react/checkbox": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/react/checkbox/index.js",
"@shoelace-style/shoelace/react/checkbox": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/react/checkbox/index.js",
"@lit/react": "https://cdn.jsdelivr.net/npm/@lit/react/+esm"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
css
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
font-family: IBM Plex Sans;
font-size: 12px;
}
#root {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.ogma-container {
width: 100%;
height: 100%;
}
json
{
"name": "@linkurious/cyber-security-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@linkurious/ogma-react": "latest",
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.4.1",
"vite": "^6.3.5"
}
}
tsx
import React from 'react';
import { Ogma } from '@linkurious/ogma-react';
import OgmaLib, { RawGraph } from '@linkurious/ogma';
import { AppProvider } from './Context';
import { Panel } from './Panel';
import { recordsToGraph } from './parse';
import { CSVRow, EventTypes } from './types';
import { Styles } from './Styles';
import { Timeline } from './Timeline';
import './style.css';
function App() {
const [graph, setGraph] = React.useState<RawGraph<unknown, unknown>>();
const [events, setEvents] = React.useState<EventTypes>({});
const ogmaRef = React.useRef<OgmaLib<unknown, unknown>>(null);
React.useEffect(() => {
// load data
const loadAndParseCSV = (url: string) =>
new Promise((complete, error) => {
// @ts-ignore
Papa.parse(url, {
download: true,
complete: complete,
error: error
});
}) as Promise<{ data: CSVRow[] }>;
loadAndParseCSV('./logs-merged_sample.csv')
.then(records => recordsToGraph(records.data))
.then(graph => setGraph(graph));
}, []);
const onReady = React.useCallback(ogma => {
ogma.layouts.force({ locate: true });
ogmaRef.current = ogma;
}, []);
if (!graph) return <div>Loading...</div>;
return (
<AppProvider>
<Ogma
ref={ogmaRef}
graph={graph}
onReady={onReady}
onNodesSelected={events.nodesSelected}
onEdgesSelected={events.edgesSelected}
onNodesUnselected={events.nodesUnselected}
onEdgesUnselected={events.edgesUnselected}
>
<Styles />
<Panel />
<Timeline setEvents={setEvents} />
</Ogma>
</AppProvider>
);
}
export default App;
tsx
import React from 'react';
interface AppContextType {
isDarkMode: boolean;
setIsDarkMode: (isDarkMode: boolean) => void;
}
const AppContext = React.createContext<AppContextType>({
isDarkMode: false,
setIsDarkMode: () => {}
});
export const AppProvider: React.FC<React.PropsWithChildren<{}>> = ({
children
}) => {
const [isDarkMode, setIsDarkMode] = React.useState(false);
return (
<AppContext.Provider value={{ isDarkMode, setIsDarkMode }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = (): AppContextType => {
const context = React.useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within an AppProvider');
}
return context;
};
css
ul {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: 16px auto;
align-items: center;
gap: 5px 10px;
}
ul > li {
display: contents;
}
.legend {
align-self: center;
justify-self: center;
}
/* legend item styles */
.legend.ip {
width: 9px;
aspect-ratio: 1;
border-radius: 50%;
background-color: #0094ff;
}
.legend.port {
width: 12px;
aspect-ratio: 1;
background-color: #5bdbe2;
}
.icon-move-right {
justify-self: center;
align-self: center;
}
li > svg {
background: none;
justify-self: center;
align-self: center;
}
tsx
import React from 'react';
import './Legend.css';
export const Legend = () => {
return (
<ul>
<li>
<span className="legend ip"></span>
<span>IP Address</span>
</li>
<li>
<span className="legend port"> </span>
<span>Port</span>
</li>
<li>
<svg
width="12"
height="12"
viewBox="-1 -1 14 14"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6" cy="6" r="6" stroke="#DE425B" fill="none"></circle>
<rect x="3" y="3" width="6" height="6" fill="#5bdbe2"></rect>
</svg>
<span>Compromised Port</span>
</li>
<li>
<i className="icon-move-right" />
<span>Listens to</span>
</li>
</ul>
);
};
css
.hide-closed {
font-size: 13px;
}
.menu-toggle {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 24px;
height: 24px;
padding: 0;
border: none;
background-color: white;
color: black;
box-shadow: 0px 1px 4px 0px #00000028;
border-radius: 50%;
font-size: 18px;
transform: translateX(200px);
transition: transform 0.3s ease;
}
.menu-toggle:hover {
cursor: pointer;
background-color: #f6f6f6;
}
/* dark mode style */
.menu-toggle.dark {
background-color: #3c4453;
box-shadow: 0px 1px 4px 0px #222;
color: white;
}
.menu-toggle.dark:hover {
background-color: #353b46;
}
.title {
font-size: 13px;
margin-bottom: 12px;
}
sl-divider {
margin: 14px 0;
}
.panel .title-container {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
/* when the panel is closed */
.panel {
gap: 0;
bottom: 0;
top: 15px;
padding: 21px 16px;
transition: width 0.3s ease;
}
.panel-left {
top: 15px;
height: 420px;
}
.panel-right {
right: 20px;
left: unset;
height: 145px;
width: 160px;
}
.panel-right .title {
margin-bottom: 8px;
}
.panel.closed {
width: 60px; /* Enough for the icons */
height: 50px;
top: 15px;
padding: 11px 0 11px 11px;
}
.panel.closed .title-container {
margin: 0 8px 0 0;
}
/* Hide everything inside the panel *except* the title */
.panel.closed > *:not(.title-container) {
display: none;
}
.panel.closed .hide-closed {
display: none;
}
.panel.closed .menu-toggle {
position: absolute;
transform: translateX(36px) rotate(180deg);
transition: transform 0.3s ease;
}
/* redefine the style for the checkbox labels */
sl-checkbox::part(label) {
font-size: 12px;
font-family: 'IBM Plex Sans';
}
.toggle-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.title-container > .container {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 2px;
border-radius: 5px;
font-size: 22px;
color: white;
background: black;
}
.ports-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.ports {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
tsx
import React from 'react';
import { useOgma } from '@linkurious/ogma-react';
import { SlSwitch, SlDivider } from '@shoelace-style/shoelace/dist/react';
import { useAppContext } from './Context';
import { PortFilter } from './PortFilter';
import { Legend } from './Legend';
import { Toggle } from './Toggle';
import './Panel.css';
export const Panel = () => {
const { isDarkMode, setIsDarkMode } = useAppContext();
const panelRef = React.useRef<HTMLDivElement>(null);
const ogma = useOgma();
const leftPorts = [0, 3, 53, 67, 80, 123];
const rightPorts = [443, 1900, 8080, 16464, 16471, 65243];
const panelToggler = evt => {
panelRef.current?.classList.toggle('closed');
document.querySelector('.timeline')?.classList.toggle('closed');
evt.currentTarget.title = panelRef.current?.classList.contains('closed')
? 'Open Menu'
: 'Hide Menu';
};
const toggleDarkMode = () => {
document.documentElement.classList.toggle('sl-theme-dark');
document.querySelector('.menu-toggle')?.classList.toggle('dark');
ogma.setOptions({
backgroundColor: document.documentElement.classList.contains(
'sl-theme-dark'
)
? '#333'
: undefined
});
document.querySelector('.timeline')?.classList.toggle('dark');
setIsDarkMode(!isDarkMode);
};
return (
<>
<div className="panel panel-left" ref={panelRef}>
{/* Title and menu toggler button */}
<div className="title-container">
<span className="container">
<i className="icon-shield-user" />
</span>
<span className="hide-closed">Cyber Security Company</span>
<button
title="Hide Menu"
className="menu-toggle"
onClick={panelToggler}
>
<i className="icon-chevron-left" />
</button>
</div>
{/* Ports */}
<div>
<div className="title">PORTS</div>
<div className="ports-container">
<div className="ports">
{leftPorts.map(port => (
<PortFilter key={port} port={port} />
))}
</div>
<div className="ports">
{rightPorts.map(port => (
<PortFilter key={port} port={port} />
))}
</div>
</div>
</div>
<SlDivider role="separator" aria-orientation="horizontal" />
{/* Connections */}
<div>
<div className="title">CONNECTIONS</div>
<div className="toggle-container">
<Toggle title="Grouping" checked />
</div>
</div>
<SlDivider role="separator" aria-orientation="horizontal" />
{/* Theme */}
<div className="title">THEME</div>
<div className="toggle-container">
<span>Dark Mode</span>
<SlSwitch onSlChange={toggleDarkMode} />
</div>
</div>
<div className="panel panel-right">
<div className="title">KEY</div>
<Legend />
</div>
</>
);
};
ts
import { RawEdge, RawNode } from '@linkurious/ogma';
import {
SERVER,
PORT,
IP,
LISTENS_TO,
CONNECTS_TO,
ServerData,
PortData,
ListenData,
IPData,
ConnectData,
CSVRow
} from './types';
export function recordsToGraph(records: CSVRow[]) {
// deduplication lookup hashes
const portsMap: Record<string, RawNode<PortData>> = {};
const ipsMap: Record<string, RawNode<IPData>> = {};
const serversMap: Record<string, RawNode<ServerData>> = {};
const listensMap: Record<string, RawEdge<ListenData>> = {};
const nodes: RawNode[] = [],
edges: RawEdge[] = [];
records.forEach(record => {
const [
timestamp,
src_ip,
dest_ip,
dest_port,
protocol,
service,
out_bytes,
in_bytes
] = record;
// Port node
const portId = dest_ip + ':' + dest_port;
let port = portsMap[portId];
if (!port) {
port = portsMap[portId] = {
id: portId,
data: {
ip: dest_ip,
port: dest_port,
type: PORT
}
};
nodes.push(port);
}
// Server node
const serverId = dest_ip;
let server = serversMap[serverId];
if (!server) {
server = serversMap[serverId] = {
id: serverId,
data: {
ip: dest_ip,
type: SERVER
}
};
nodes.push(server);
}
// "Listens to" edge
let listen = listensMap[portId];
if (!listen) {
listen = listensMap[portId] = {
id: portId,
source: serverId,
target: portId,
data: {
type: LISTENS_TO
}
};
edges.push(listen);
}
// IP node
const ipId = src_ip;
let ip = ipsMap[ipId];
if (!ip) {
ip = ipsMap[ipId] = {
id: ipId,
data: {
ip: src_ip,
type: IP
}
};
nodes.push(ip);
}
// Connection edge
const connectionId = `${src_ip}-${dest_ip}:${dest_port}@${Math.round(
timestamp * 1000
)}`;
const start = Number(new Date('Fri May 16 2025 15:49:22'));
const connection: RawEdge<ConnectData> = {
id: connectionId,
source: ipId,
target: portId,
data: {
type: CONNECTS_TO,
start: Math.round(timestamp) + start,
query_bytes: isNaN(out_bytes as number)
? 0
: parseInt(out_bytes as string),
response_bytes: isNaN(in_bytes as number)
? 0
: parseInt(in_bytes as string),
protocol: protocol,
service: service
}
};
edges.push(connection);
});
return { nodes: nodes, edges: edges };
}
tsx
import React from 'react';
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import { NodeFilter } from '@linkurious/ogma-react';
import { PORT } from './types';
export const PortFilter = (props: { port: number }) => {
const [checked, setChecked] = React.useState<boolean>(true);
return (
<>
<SlCheckbox checked={checked} onSlChange={() => setChecked(!checked)}>
{props.port}
</SlCheckbox>
<NodeFilter
criteria={node =>
node.getData('type') !== PORT ||
node.getData('port') !== props.port.toString() ||
checked
}
/>
</>
);
};
tsx
import React from 'react';
import { EdgeStyle, NodeStyle } from '@linkurious/ogma-react';
import { CONNECTS_TO, IP, LISTENS_TO, PORT, SERVER } from './types';
import { formatPutThrough, getIconCode } from './utils';
import { useAppContext } from './Context';
export const Styles = () => {
const { isDarkMode } = useAppContext();
const edgeColor = isDarkMode ? '#80F0F6' : '#007C83';
return (
<>
{/* Style for server nodes */}
<NodeStyle
selector={node => node.getData('type') === SERVER}
attributes={{
radius: 10,
color: '#0094ff',
icon: {
font: 'Lucide',
content: getIconCode('icon-server'),
color: '#fff',
style: 'bold',
minVisibleSize: 0,
scale: 0.7
}
}}
/>
{/* Style for IP nodes */}
<NodeStyle
selector={node => node.getData('type') === IP}
attributes={{
innerStroke: '#0094ff',
color: '#0094ff',
// number of outgoing “CONNECTS_TO” edges (taking filters into account)
radius: node =>
10 +
Math.min(
node.getAdjacentEdges({ direction: 'out' }).filter(edge => {
return edge.getData('type') === CONNECTS_TO;
}).size,
40
),
icon: {
font: 'Lucide',
content: getIconCode('icon-server'),
style: 'bold',
color: '#fff',
minVisibleSize: 0,
scale: 0.4
},
text: {
content: node => node.getData('ip'),
color: '#fff',
secondary: {
// number of outgoing “CONNECTS_TO” edges (taking filters into account)
content: node =>
node.getAdjacentEdges({ direction: 'out' }).filter(edge => {
return edge.getData('type') === CONNECTS_TO;
}).size +
' connections\n' +
// received bytes (sum of all “response_bytes” from outgoing
// “CONNECTS_TO” edges, taking filters into account)
formatPutThrough(
node
.getAdjacentEdges({ direction: 'out' })
.filter(edge => {
return edge.getData('type') === CONNECTS_TO;
})
.reduce(
(total, edge) => total + edge.getData('response_bytes'),
0
) as number
) +
' received\n' +
// sent bytes (sum of all “query_bytes” from outgoing “CONNECTS_TO” edges,
// taking filters into account)
formatPutThrough(
node
.getAdjacentEdges({ direction: 'out' })
.filter(edge => edge.getData('type') === CONNECTS_TO)
.reduce(
(total, edge) => total + edge.getData('query_bytes'),
0
) as number
) +
' sent\n',
color: '#0094ff',
backgroundColor: 'white',
minVisibleSize: 60,
margin: 1
},
backgroundColor: '#0094ff'
}
}}
/>
{/* Style for Port nodes */}
<NodeStyle
selector={node => node.getData('type') === PORT}
attributes={{
color: '#5BDBE2',
shape: 'square',
text: {
content: node => ':' + node.getData('port'),
position: 'center'
},
radius: 8,
// Highlight Port 1900 as dangerous
pulse: {
enabled: node => node.getData('port') === '1900',
endRatio: 5,
width: 1,
startColor: 'red',
endColor: 'red',
interval: 1000,
startRatio: 1.0
}
}}
/>
{/* Style for hovered nodes */}
<NodeStyle.Hovered
attributes={{
outerStroke: {
width: 2,
color: '#ff6c52',
scalingMethod: 'fixed'
},
text: {
backgroundColor: '0094ff'
}
}}
/>
{/* Style for selected nodes */}
<NodeStyle.Selected
attributes={{
outerStroke: {
width: 2,
color: '#ff6c52',
scalingMethod: 'fixed'
},
text: {
style: 'bold',
backgroundColor: '0094ff'
}
}}
/>
{/* Default style for edges */}
<EdgeStyle
attributes={{
color: edge =>
edge.getData('type') === CONNECTS_TO ? '#0094ff' : edgeColor,
text: {
content: edge => {
if (edge.getData('type') === CONNECTS_TO) {
const response_bytes = edge.getData('response_bytes') || 0;
const query_bytes = edge.getData('query_bytes') || 0;
return (
formatPutThrough(query_bytes) +
' in, ' +
formatPutThrough(response_bytes) +
' out'
);
} else {
return LISTENS_TO;
}
}
}
}}
/>
{/* Style for hovered edges */}
<EdgeStyle.Hovered
attributes={{
color: '#ff6c52',
text: {
backgroundColor: 'white'
}
}}
/>
{/* Style for selected edges */}
<EdgeStyle.Selected
attributes={{
color: '#ff6c52',
text: {
style: 'bold',
backgroundColor: 'white'
}
}}
/>
</>
);
};
css
.timeline {
width: 100%;
height: 150px;
transform: translate(0, -150px);
background-color: white;
transition: all 0.3s ease-in-out;
}
/* dark mode */
.timeline.dark {
background-color: #3c4453;
}
/* when the panel is closed, center the timeline */
.timeline.closed {
width: 100%;
transform: translate(0, -150px);
}
.vis-timeline .vis-bar {
fill-opacity: 1 !important;
}
/* redefine the text color of the labels in the timeline for dark mode */
.sl-theme-dark .vis-data-axis .vis-y-axis.vis-minor,
.sl-theme-dark .vis-data-axis .vis-y-axis.vis-major,
.sl-theme-dark .vis-time-axis .vis-text {
color: #fff;
}
/* redefine the color of the separators in the timeline */
.sl-theme-dark .vis-panel.vis-background.vis-horizontal .vis-grid,
.sl-theme-dark .vis-grid.vis-vertical {
border-color: rgba(179, 181, 219, 0.267);
}
.vis-timeline {
box-shadow: 0 1px 4px #0006;
border: none;
}
.vis-text,
.vis-data-axis {
font-family: 'IBM Plex Sans', sans-serif;
}
tsx
import React from 'react';
import { Controller as TimelinePlugin } from '@linkurious/ogma-timeline-plugin';
import { EdgeFilter as EdgeFilterC, useOgma } from '@linkurious/ogma-react';
import { EdgeFilter, EdgeId, EdgeList } from '@linkurious/ogma';
import { CONNECTS_TO, EventTypes } from './types';
import './Timeline.css';
export const Timeline = (props: {
setEvents: (events: EventTypes) => void;
}) => {
const ogma = useOgma();
const timelineRef = React.useRef<HTMLDivElement>(null);
const pluginRef = React.useRef<TimelinePlugin>(null);
const isSelectingRef = React.useRef(false);
const filterRef = React.useRef<EdgeFilter<unknown, unknown>>(null);
const events: EventTypes = {
nodesSelected: () => {},
edgesSelected: () => {},
nodesUnselected: () => {},
edgesUnselected: () => {}
};
React.useEffect(() => {
const timelinePlugin = new TimelinePlugin(ogma, timelineRef.current!, {
barchart: {
graph2dOptions: {
drawPoints: false,
barChart: { width: 30, align: 'center' }
}
},
timeline: {
timelineOptions: {}
}
});
pluginRef.current = timelinePlugin;
timelinePlugin.on('timechange', () => {
filterRef.current!.refresh();
});
timelinePlugin.once('timechange', () => {
const { start, end } = timelinePlugin.getWindow();
timelinePlugin.addTimeBar(start);
timelinePlugin.addTimeBar(end);
});
timelinePlugin.on('select', evt => {
const edges = evt.edges as EdgeList;
isSelectingRef.current = true;
if (!edges.size) return;
ogma.getNodes().setSelected(false);
ogma.getEdges().setSelected(false);
const edgeIds = new Set(
edges
.map(edge =>
edge.isVisible() ? edge.getId() : edge.getMetaEdge()?.getId()
)
.filter(e => e)
) as unknown as EdgeId[];
const edgeToSelect = ogma.getEdges(Array.from(edgeIds));
const bounds = edgeToSelect.getBoundingBox();
edgeToSelect.setSelected(true);
if (bounds.width < 256) {
bounds.pad(256 - bounds.width);
}
ogma.view.moveToBounds(bounds, {
easing: 'quadraticInOut',
duration: 1000
});
isSelectingRef.current = false;
});
Object.keys(events).forEach(eventName => {
const event = () => {
if (isSelectingRef.current) return;
timelinePlugin.setSelection({
nodes: ogma.getSelectedNodes(),
edges: ogma.getSelectedEdges()
});
const minMax = ogma
.getSelectedNodes()
.getData('start')
.concat(ogma.getSelectedEdges().getData('start'))
.reduce(
(acc, time) => {
if (time < acc.min) acc.min = time;
if (time > acc.max) acc.max = time;
return acc;
},
{ min: Infinity, max: -Infinity }
);
const year = 1000 * 60 * 60 * 24 * 365;
// add one year margin between min and max
minMax.min -= year;
minMax.max += year;
timelinePlugin.setWindow(minMax.min, minMax.max);
};
events[eventName] = event;
});
props.setEvents(events);
}, []);
React.useEffect(() => {
if (filterRef.current) filterRef.current.setIndex(0);
}, [filterRef.current]);
const filter = React.useCallback(
edge => {
return (
edge.getData('type') !== CONNECTS_TO ||
pluginRef.current!.filteredEdges.has(edge.getId())
);
},
[pluginRef.current]
);
return (
<>
<div className="timeline" ref={timelineRef} />
<EdgeFilterC ref={filterRef} criteria={filter} />
</>
);
};
tsx
import React from 'react';
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import { EdgeGrouping } from '@linkurious/ogma-react';
import { CONNECTS_TO } from './types';
// This is a separate component to not refresh the whole panel when the switch is toggled
export const Toggle = (props: { title: string; checked: boolean }) => {
const [checked, setChecked] = React.useState<boolean>(props.checked);
return (
<>
<span>{props.title}</span>
<SlSwitch checked={checked} onSlChange={() => setChecked(!checked)} />
<EdgeGrouping
disabled={!checked}
selector={edge =>
!edge.isExcluded() && edge.getData('type') === CONNECTS_TO
}
groupIdFunction={e =>
e.getData('protocol') + '/' + e.getData('service')
}
generator={edges => {
const e = edges.get(0);
const protocol = e.getData('protocol');
const service = e.getData('service');
let query_bytes = 0,
response_bytes = 0;
edges.getData().forEach((edgeData: any) => {
query_bytes += edgeData.query_bytes || 0;
response_bytes += edgeData.response_bytes || 0;
});
return {
data: {
type: CONNECTS_TO,
query_bytes: query_bytes,
response_bytes: response_bytes,
group_size: edges.size,
protocol: protocol,
service: service
}
};
}}
/>
</>
);
};
ts
// node types
export const PORT = 'port';
export const SERVER = 'server';
export const IP = 'ip';
// edge types
export const LISTENS_TO = 'listens_to';
export const CONNECTS_TO = 'connects_to';
export type CSVRow = [
number, // timestamp
string, // src_ip
string, // dest_ip
string, // dest_port
string, // protocol
string, // service
string, // duration
string | number, // out_bytes
string | number, // in_bytes
string // status
];
export type ServerData = {
ip: string;
type: typeof SERVER;
};
export type PortData = {
ip: string;
port: string;
type: typeof PORT;
};
export type IPData = {
ip: string;
type: typeof IP;
};
export type ListenData = {
type: typeof LISTENS_TO;
};
export type ConnectData = {
type: typeof CONNECTS_TO;
start: number;
query_bytes: number;
response_bytes: number;
protocol: string;
service: string;
};
export type ND = ServerData | PortData | IPData;
export type ED = ListenData | ConnectData;
export type EventTypes = {
nodesSelected?: () => void;
edgesSelected?: () => void;
nodesUnselected?: () => void;
edgesUnselected?: () => void;
}
ts
// dummy icon element to retrieve the HEX code, it should be hidden
const placeholder = document.createElement('span');
document.body.appendChild(placeholder);
placeholder.style.visibility = 'hidden';
// helper routine to get the icon HEX code
export function getIconCode(className: string) {
placeholder.className = className;
const code = getComputedStyle(placeholder, ':before').content;
return code[1];
}
// format traffic values
export const formatPutThrough = (bytes: number) => {
bytes = +bytes;
return bytes > 1024 * 1024
? Math.round(bytes / (1024 * 1024)) + 'M'
: bytes > 1024
? Math.round(bytes / 1024) + 'KB'
: bytes + 'B';
};
ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()]
});