test visualization with d3js
This commit is contained in:
@@ -20,11 +20,30 @@ class InterfaceHostEvidence(BaseModel):
|
||||
destination_on_egress_count: int = Field(..., description="Packets where this endpoint appeared as the destination on egress.")
|
||||
|
||||
|
||||
class ProtocolEvidence(BaseModel):
|
||||
protocol: str = Field(..., description="Detected application or fallback transport/network protocol.")
|
||||
packet_count: int = Field(..., description="Packet observations supporting this interface-host-protocol mapping.")
|
||||
last_seen: datetime = Field(..., description="Most recent packet timestamp supporting this protocol mapping.")
|
||||
accept_count: int = Field(0, description="Packets with verdict=accept for this protocol.")
|
||||
drop_count: int = Field(0, description="Packets with verdict=drop for this protocol.")
|
||||
reject_count: int = Field(0, description="Packets with verdict=reject for this protocol.")
|
||||
unknown_count: int = Field(0, description="Packets with verdict pending/unknown or without a verdict.")
|
||||
|
||||
|
||||
class InterfaceHostProtocolEvidence(InterfaceHostEvidence):
|
||||
protocols: List[ProtocolEvidence] = Field(default_factory=list, description="Protocols observed for this host on the interface.")
|
||||
|
||||
|
||||
class InterfaceAttachment(BaseModel):
|
||||
interface: str = Field(..., description="MITM machine interface name.")
|
||||
hosts: List[InterfaceHostEvidence] = Field(default_factory=list, description="Endpoints inferred to be attached to this interface.")
|
||||
|
||||
|
||||
class InterfaceProtocolAttachment(BaseModel):
|
||||
interface: str = Field(..., description="MITM machine interface name.")
|
||||
hosts: List[InterfaceHostProtocolEvidence] = Field(default_factory=list, description="Endpoints inferred to be attached to this interface, with protocol breakdown.")
|
||||
|
||||
|
||||
class InterfaceHostAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = Field(None, description="Only packets at or after this timestamp were analyzed.")
|
||||
interfaces: List[InterfaceAttachment] = Field(default_factory=list)
|
||||
@@ -37,6 +56,18 @@ class InterfaceHostAnalysisResponse(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceHostProtocolAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = Field(None, description="Only packets at or after this timestamp were analyzed.")
|
||||
interfaces: List[InterfaceProtocolAttachment] = Field(default_factory=list)
|
||||
notes: List[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"This is an inference from observed packet direction, not a kernel neighbor-table lookup.",
|
||||
"Each host can carry multiple protocols; protocols prefer app_protocol and fall back to lower-layer protocol names.",
|
||||
"Verdict counts are packet counts grouped per interface, host, and protocol.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/interface-hosts", response_model=InterfaceHostAnalysisResponse)
|
||||
async def analysis_interface_hosts(
|
||||
since_minutes: Optional[int] = Query(
|
||||
@@ -68,3 +99,46 @@ async def analysis_interface_hosts(
|
||||
|
||||
interfaces = [InterfaceAttachment(**row) for row in rows]
|
||||
return InterfaceHostAnalysisResponse(since=since, interfaces=interfaces)
|
||||
|
||||
|
||||
@router.get("/interface-host-protocols", response_model=InterfaceHostProtocolAnalysisResponse)
|
||||
async def analysis_interface_host_protocols(
|
||||
since_minutes: Optional[int] = Query(
|
||||
60,
|
||||
ge=1,
|
||||
le=60 * 24 * 30,
|
||||
description="Analyze only packets seen within the last N minutes. Set to a large value to cover more history.",
|
||||
),
|
||||
limit_per_interface: int = Query(
|
||||
50,
|
||||
ge=1,
|
||||
le=1000,
|
||||
description="Maximum number of inferred hosts returned per interface.",
|
||||
),
|
||||
limit_protocols_per_host: int = Query(
|
||||
12,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of top protocols returned per inferred host.",
|
||||
),
|
||||
) -> InterfaceHostProtocolAnalysisResponse:
|
||||
"""Infer interface-host attachment and break observed traffic down by protocol."""
|
||||
db = shared.db
|
||||
if db is None:
|
||||
raise HTTPException(status_code=503, detail="Database not available")
|
||||
|
||||
since: Optional[datetime] = None
|
||||
if since_minutes is not None:
|
||||
since = datetime.now(timezone.utc) - timedelta(minutes=since_minutes)
|
||||
|
||||
try:
|
||||
rows = await db.infer_interface_host_protocols(
|
||||
since=since,
|
||||
limit_per_interface=limit_per_interface,
|
||||
limit_protocols_per_host=limit_protocols_per_host,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail="Failed to infer interface host protocol mapping") from exc
|
||||
|
||||
interfaces = [InterfaceProtocolAttachment(**row) for row in rows]
|
||||
return InterfaceHostProtocolAnalysisResponse(since=since, interfaces=interfaces)
|
||||
|
||||
@@ -733,6 +733,197 @@ class DatabasePool:
|
||||
for iface, hosts in sorted(grouped.items())
|
||||
]
|
||||
|
||||
async def infer_interface_host_protocols(
|
||||
self,
|
||||
*,
|
||||
since: Optional[datetime] = None,
|
||||
limit_per_interface: int = 50,
|
||||
limit_protocols_per_host: int = 12,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Infer interface-host attachment and aggregate observed protocols and verdicts."""
|
||||
if self._pool is None:
|
||||
await self.init_pool()
|
||||
|
||||
async with self._pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
WITH observations AS (
|
||||
SELECT
|
||||
ingress_if AS iface,
|
||||
src_ip::text AS ip_address,
|
||||
src_mac::text AS mac_address,
|
||||
COALESCE(NULLIF(app_protocol, ''), NULLIF(app_master_protocol, ''), NULLIF(ip_proto, ''), NULLIF(eth_type, ''), 'UNKNOWN') AS protocol_name,
|
||||
COALESCE(NULLIF(verdict, ''), 'unknown') AS verdict_name,
|
||||
timestamp,
|
||||
'source_on_ingress' AS evidence
|
||||
FROM packets
|
||||
WHERE ingress_if IS NOT NULL
|
||||
AND (src_ip IS NOT NULL OR src_mac IS NOT NULL)
|
||||
AND ($1::timestamptz IS NULL OR timestamp >= $1)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
egress_if AS iface,
|
||||
dst_ip::text AS ip_address,
|
||||
dst_mac::text AS mac_address,
|
||||
COALESCE(NULLIF(app_protocol, ''), NULLIF(app_master_protocol, ''), NULLIF(ip_proto, ''), NULLIF(eth_type, ''), 'UNKNOWN') AS protocol_name,
|
||||
COALESCE(NULLIF(verdict, ''), 'unknown') AS verdict_name,
|
||||
timestamp,
|
||||
'destination_on_egress' AS evidence
|
||||
FROM packets
|
||||
WHERE egress_if IS NOT NULL
|
||||
AND (dst_ip IS NOT NULL OR dst_mac IS NOT NULL)
|
||||
AND ($1::timestamptz IS NULL OR timestamp >= $1)
|
||||
),
|
||||
filtered AS (
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE iface IS NOT NULL
|
||||
AND COALESCE(mac_address, '') <> 'ff:ff:ff:ff:ff:ff'
|
||||
AND (
|
||||
COALESCE(ip_address, '') <> ''
|
||||
OR COALESCE(mac_address, '') <> ''
|
||||
)
|
||||
),
|
||||
host_aggregated AS (
|
||||
SELECT
|
||||
iface,
|
||||
ip_address,
|
||||
mac_address,
|
||||
COUNT(*) AS packet_count,
|
||||
MAX(timestamp) AS last_seen,
|
||||
SUM(CASE WHEN evidence = 'source_on_ingress' THEN 1 ELSE 0 END) AS source_on_ingress_count,
|
||||
SUM(CASE WHEN evidence = 'destination_on_egress' THEN 1 ELSE 0 END) AS destination_on_egress_count
|
||||
FROM filtered
|
||||
GROUP BY iface, ip_address, mac_address
|
||||
),
|
||||
selected_hosts AS (
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY iface
|
||||
ORDER BY packet_count DESC, last_seen DESC, ip_address, mac_address
|
||||
) AS row_num
|
||||
FROM host_aggregated
|
||||
) ranked_hosts
|
||||
WHERE row_num <= $2
|
||||
),
|
||||
protocol_aggregated AS (
|
||||
SELECT
|
||||
filtered.iface,
|
||||
filtered.ip_address,
|
||||
filtered.mac_address,
|
||||
filtered.protocol_name,
|
||||
COUNT(*) AS packet_count,
|
||||
MAX(filtered.timestamp) AS last_seen,
|
||||
SUM(CASE WHEN filtered.verdict_name = 'accept' THEN 1 ELSE 0 END) AS accept_count,
|
||||
SUM(CASE WHEN filtered.verdict_name = 'drop' THEN 1 ELSE 0 END) AS drop_count,
|
||||
SUM(CASE WHEN filtered.verdict_name = 'reject' THEN 1 ELSE 0 END) AS reject_count,
|
||||
SUM(CASE WHEN filtered.verdict_name NOT IN ('accept', 'drop', 'reject') THEN 1 ELSE 0 END) AS unknown_count
|
||||
FROM filtered
|
||||
INNER JOIN selected_hosts
|
||||
ON selected_hosts.iface = filtered.iface
|
||||
AND selected_hosts.ip_address IS NOT DISTINCT FROM filtered.ip_address
|
||||
AND selected_hosts.mac_address IS NOT DISTINCT FROM filtered.mac_address
|
||||
GROUP BY filtered.iface, filtered.ip_address, filtered.mac_address, filtered.protocol_name
|
||||
),
|
||||
ranked_protocols AS (
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY iface, ip_address, mac_address
|
||||
ORDER BY packet_count DESC, last_seen DESC, protocol_name
|
||||
) AS row_num
|
||||
FROM protocol_aggregated
|
||||
) ranked
|
||||
WHERE row_num <= $3
|
||||
)
|
||||
SELECT
|
||||
selected_hosts.iface,
|
||||
selected_hosts.ip_address,
|
||||
selected_hosts.mac_address,
|
||||
selected_hosts.packet_count AS host_packet_count,
|
||||
selected_hosts.last_seen AS host_last_seen,
|
||||
selected_hosts.source_on_ingress_count,
|
||||
selected_hosts.destination_on_egress_count,
|
||||
ranked_protocols.protocol_name,
|
||||
ranked_protocols.packet_count AS protocol_packet_count,
|
||||
ranked_protocols.last_seen AS protocol_last_seen,
|
||||
ranked_protocols.accept_count,
|
||||
ranked_protocols.drop_count,
|
||||
ranked_protocols.reject_count,
|
||||
ranked_protocols.unknown_count
|
||||
FROM selected_hosts
|
||||
LEFT JOIN ranked_protocols
|
||||
ON ranked_protocols.iface = selected_hosts.iface
|
||||
AND ranked_protocols.ip_address IS NOT DISTINCT FROM selected_hosts.ip_address
|
||||
AND ranked_protocols.mac_address IS NOT DISTINCT FROM selected_hosts.mac_address
|
||||
ORDER BY
|
||||
selected_hosts.iface,
|
||||
selected_hosts.packet_count DESC,
|
||||
selected_hosts.last_seen DESC,
|
||||
selected_hosts.ip_address,
|
||||
selected_hosts.mac_address,
|
||||
ranked_protocols.packet_count DESC NULLS LAST,
|
||||
ranked_protocols.last_seen DESC NULLS LAST,
|
||||
ranked_protocols.protocol_name
|
||||
""",
|
||||
since,
|
||||
limit_per_interface,
|
||||
limit_protocols_per_host,
|
||||
)
|
||||
|
||||
grouped: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
||||
for row in rows:
|
||||
record = dict(row)
|
||||
iface = str(record["iface"])
|
||||
host_key = f"{record.get('ip_address') or 'no-ip'}|{record.get('mac_address') or 'no-mac'}"
|
||||
|
||||
iface_hosts = grouped.setdefault(iface, {})
|
||||
host_record = iface_hosts.get(host_key)
|
||||
if host_record is None:
|
||||
host_record = {
|
||||
"ip_address": record.get("ip_address"),
|
||||
"mac_address": record.get("mac_address"),
|
||||
"packet_count": int(record.get("host_packet_count") or 0),
|
||||
"last_seen": record["host_last_seen"].isoformat() if hasattr(record.get("host_last_seen"), "isoformat") else record.get("host_last_seen"),
|
||||
"source_on_ingress_count": int(record.get("source_on_ingress_count") or 0),
|
||||
"destination_on_egress_count": int(record.get("destination_on_egress_count") or 0),
|
||||
"protocols": [],
|
||||
}
|
||||
iface_hosts[host_key] = host_record
|
||||
|
||||
protocol_name = record.get("protocol_name")
|
||||
if protocol_name not in (None, ""):
|
||||
host_record["protocols"].append(
|
||||
{
|
||||
"protocol": str(protocol_name),
|
||||
"packet_count": int(record.get("protocol_packet_count") or 0),
|
||||
"last_seen": record["protocol_last_seen"].isoformat()
|
||||
if hasattr(record.get("protocol_last_seen"), "isoformat")
|
||||
else record.get("protocol_last_seen"),
|
||||
"accept_count": int(record.get("accept_count") or 0),
|
||||
"drop_count": int(record.get("drop_count") or 0),
|
||||
"reject_count": int(record.get("reject_count") or 0),
|
||||
"unknown_count": int(record.get("unknown_count") or 0),
|
||||
}
|
||||
)
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
for iface, hosts in sorted(grouped.items()):
|
||||
sorted_hosts = sorted(
|
||||
hosts.values(),
|
||||
key=lambda item: (-int(item.get("packet_count") or 0), str(item.get("last_seen") or ""), str(item.get("ip_address") or ""), str(item.get("mac_address") or "")),
|
||||
)
|
||||
result.append({"interface": iface, "hosts": sorted_hosts})
|
||||
|
||||
return result
|
||||
|
||||
async def clear_all_packets(self, reset_identity: bool = True) -> bool:
|
||||
"""Truncate the packet table and optionally reset identity counters."""
|
||||
if self._pool is None:
|
||||
|
||||
774
frontend/package-lock.json
generated
774
frontend/package-lock.json
generated
@@ -11,8 +11,12 @@
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/d3-sankey": "^0.12.5",
|
||||
"antd": "^6.0.0",
|
||||
"axios": "^1.13.2",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
@@ -2280,6 +2284,283 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/d3-axis": "*",
|
||||
"@types/d3-brush": "*",
|
||||
"@types/d3-chord": "*",
|
||||
"@types/d3-color": "*",
|
||||
"@types/d3-contour": "*",
|
||||
"@types/d3-delaunay": "*",
|
||||
"@types/d3-dispatch": "*",
|
||||
"@types/d3-drag": "*",
|
||||
"@types/d3-dsv": "*",
|
||||
"@types/d3-ease": "*",
|
||||
"@types/d3-fetch": "*",
|
||||
"@types/d3-force": "*",
|
||||
"@types/d3-format": "*",
|
||||
"@types/d3-geo": "*",
|
||||
"@types/d3-hierarchy": "*",
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-path": "*",
|
||||
"@types/d3-polygon": "*",
|
||||
"@types/d3-quadtree": "*",
|
||||
"@types/d3-random": "*",
|
||||
"@types/d3-scale": "*",
|
||||
"@types/d3-scale-chromatic": "*",
|
||||
"@types/d3-selection": "*",
|
||||
"@types/d3-shape": "*",
|
||||
"@types/d3-time": "*",
|
||||
"@types/d3-time-format": "*",
|
||||
"@types/d3-timer": "*",
|
||||
"@types/d3-transition": "*",
|
||||
"@types/d3-zoom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-brush": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-chord": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-contour": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-polygon": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-sankey": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.12.5.tgz",
|
||||
"integrity": "sha512-/3RZSew0cLAtzGQ+C89hq/Rp3H20QJuVRSqFy6RKLe7E0B8kd2iOS1oBsodrgds4PcNVpqWhdUEng/SHvBcJ6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-shape": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-sankey/node_modules/@types/d3-path": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
|
||||
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-sankey/node_modules/@types/d3-shape": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
|
||||
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -2287,6 +2568,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/history": {
|
||||
"version": "4.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
|
||||
@@ -2885,6 +3172,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
@@ -2970,6 +3266,448 @@
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "3",
|
||||
"d3-axis": "3",
|
||||
"d3-brush": "3",
|
||||
"d3-chord": "3",
|
||||
"d3-color": "3",
|
||||
"d3-contour": "4",
|
||||
"d3-delaunay": "6",
|
||||
"d3-dispatch": "3",
|
||||
"d3-drag": "3",
|
||||
"d3-dsv": "3",
|
||||
"d3-ease": "3",
|
||||
"d3-fetch": "3",
|
||||
"d3-force": "3",
|
||||
"d3-format": "3",
|
||||
"d3-geo": "3",
|
||||
"d3-hierarchy": "3",
|
||||
"d3-interpolate": "3",
|
||||
"d3-path": "3",
|
||||
"d3-polygon": "3",
|
||||
"d3-quadtree": "3",
|
||||
"d3-random": "3",
|
||||
"d3-scale": "4",
|
||||
"d3-scale-chromatic": "3",
|
||||
"d3-selection": "3",
|
||||
"d3-shape": "3",
|
||||
"d3-time": "3",
|
||||
"d3-time-format": "4",
|
||||
"d3-timer": "3",
|
||||
"d3-transition": "3",
|
||||
"d3-zoom": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-axis": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
||||
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-brush": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
||||
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "3",
|
||||
"d3-transition": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-chord": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
||||
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-contour": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
||||
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delaunator": "5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dsv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"iconv-lite": "0.6",
|
||||
"rw": "1"
|
||||
},
|
||||
"bin": {
|
||||
"csv2json": "bin/dsv2json.js",
|
||||
"csv2tsv": "bin/dsv2dsv.js",
|
||||
"dsv2dsv": "bin/dsv2dsv.js",
|
||||
"dsv2json": "bin/dsv2json.js",
|
||||
"json2csv": "bin/json2dsv.js",
|
||||
"json2dsv": "bin/json2dsv.js",
|
||||
"json2tsv": "bin/json2dsv.js",
|
||||
"tsv2csv": "bin/dsv2dsv.js",
|
||||
"tsv2json": "bin/dsv2json.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-fetch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dsv": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-geo": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.5.0 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-hierarchy": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-polygon": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
||||
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-random": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
||||
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey": {
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
|
||||
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-array": "1 - 2",
|
||||
"d3-shape": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-array": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"internmap": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/internmap": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@@ -3092,6 +3830,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delaunator": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
|
||||
"integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -4077,9 +4824,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@@ -4153,6 +4898,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -5409,6 +6163,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
||||
"integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
|
||||
@@ -5451,6 +6211,12 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -5510,9 +6276,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.3",
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/d3-sankey": "^0.12.5",
|
||||
"antd": "^6.0.0",
|
||||
"axios": "^1.13.2",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// src/components/Sidebar.tsx
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
ClusterOutlined,
|
||||
HomeOutlined,
|
||||
InfoCircleOutlined,
|
||||
MonitorOutlined,
|
||||
@@ -19,6 +20,7 @@ const { Sider } = Layout;
|
||||
const menuItems = [
|
||||
{ key: PATHS.HOME, icon: <HomeOutlined style={{ fontSize: '18px' }} />, label: 'Home' },
|
||||
{ key: PATHS.NETWORK, icon: <ApartmentOutlined style={{ fontSize: '18px' }} />, label: 'Network' },
|
||||
{ key: PATHS.ANALYSIS, icon: <ClusterOutlined style={{ fontSize: '18px' }} />, label: 'Analysis' },
|
||||
{ key: PATHS.SNIFFING, icon: <MonitorOutlined style={{ fontSize: '18px' }} />, label: 'Sniffing' },
|
||||
{
|
||||
key: PATHS.SCRIPTING,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { InterfaceHostAnalysisResponse, InterfaceHostProtocolAnalysisResponse } from '../types/analysis';
|
||||
import { CreateRuleRequest, ExecResult, RulesetModel } from '../types/firewall';
|
||||
import {
|
||||
BridgeCreateRequest,
|
||||
@@ -172,6 +173,34 @@ export const clearPackets = async (): Promise<any> => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const fetchInterfaceHostAnalysis = async (
|
||||
sinceMinutes = 60,
|
||||
limitPerInterface = 100,
|
||||
): Promise<InterfaceHostAnalysisResponse> => {
|
||||
const res = await api.get<InterfaceHostAnalysisResponse>('/analysis/interface-hosts', {
|
||||
params: {
|
||||
since_minutes: sinceMinutes,
|
||||
limit_per_interface: limitPerInterface,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const fetchInterfaceHostProtocolAnalysis = async (
|
||||
sinceMinutes = 60,
|
||||
limitPerInterface = 50,
|
||||
limitProtocolsPerHost = 12,
|
||||
): Promise<InterfaceHostProtocolAnalysisResponse> => {
|
||||
const res = await api.get<InterfaceHostProtocolAnalysisResponse>('/analysis/interface-host-protocols', {
|
||||
params: {
|
||||
since_minutes: sinceMinutes,
|
||||
limit_per_interface: limitPerInterface,
|
||||
limit_protocols_per_host: limitProtocolsPerHost,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const fetchRuleset = async (): Promise<{ ruleset: RulesetModel }> => {
|
||||
const res = await api.get<{ ruleset: RulesetModel }>('/firewall/rules');
|
||||
return res.data;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import App from './App';
|
||||
import Analysis from './pages/Analysis';
|
||||
import { Firewall } from './pages/Firewall';
|
||||
import Home from './pages/Home';
|
||||
import Network from './pages/Network';
|
||||
@@ -24,6 +25,7 @@ export default function AppRouter() {
|
||||
<Route index element={<Navigate to={PATHS.HOME} replace />} />
|
||||
<Route path={PATHS.HOME.slice(1)} element={<Home />} />
|
||||
<Route path={PATHS.NETWORK.slice(1)} element={<Network />} />
|
||||
<Route path={PATHS.ANALYSIS.slice(1)} element={<Analysis />} />
|
||||
<Route path={PATHS.SNIFFING.slice(1)} element={<Sniffing />} />
|
||||
<Route path={PATHS.SCRIPTING.slice(1)} element={<Scripting />} />
|
||||
<Route path={PATHS.FIREWALL.slice(1)} element={<Firewall />} />
|
||||
|
||||
725
frontend/src/pages/Analysis.tsx
Normal file
725
frontend/src/pages/Analysis.tsx
Normal file
@@ -0,0 +1,725 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Empty,
|
||||
InputNumber,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import * as d3 from 'd3';
|
||||
import { sankey as d3Sankey, sankeyLinkHorizontal, type SankeyGraph, type SankeyLink, type SankeyNode } from 'd3-sankey';
|
||||
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fetchInterfaceHostProtocolAnalysis } from '../api/apiClient';
|
||||
import type {
|
||||
InterfaceHostProtocolAnalysisResponse,
|
||||
InterfaceHostProtocolEvidence,
|
||||
InterfaceProtocolAttachment,
|
||||
ProtocolEvidence,
|
||||
} from '../types/analysis';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
type GraphNodeKind = 'interface' | 'host' | 'protocol';
|
||||
|
||||
type TopologyNode = {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: GraphNodeKind;
|
||||
packetCount: number;
|
||||
interfaceName?: string;
|
||||
ipAddress?: string | null;
|
||||
macAddress?: string | null;
|
||||
protocol?: string;
|
||||
};
|
||||
|
||||
type TopologyLink = {
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type HeatmapRow = {
|
||||
hostId: string;
|
||||
hostLabel: string;
|
||||
interfaceName: string;
|
||||
values: Record<string, number>;
|
||||
};
|
||||
|
||||
type ProtocolTableRow = {
|
||||
key: string;
|
||||
interface: string;
|
||||
ip_address?: string | null;
|
||||
mac_address?: string | null;
|
||||
host_packet_count: number;
|
||||
protocol: string;
|
||||
protocol_packet_count: number;
|
||||
accept_count: number;
|
||||
drop_count: number;
|
||||
reject_count: number;
|
||||
unknown_count: number;
|
||||
last_seen: string;
|
||||
};
|
||||
|
||||
type TopologyData = {
|
||||
nodes: TopologyNode[];
|
||||
links: TopologyLink[];
|
||||
heatmapRows: HeatmapRow[];
|
||||
protocols: string[];
|
||||
tableRows: ProtocolTableRow[];
|
||||
};
|
||||
|
||||
type ForceNode = d3.SimulationNodeDatum & TopologyNode;
|
||||
type ForceLink = d3.SimulationLinkDatum<ForceNode> & TopologyLink;
|
||||
|
||||
type SankeyNodeDatum = SankeyNode<TopologyNode, TopologyLink> & TopologyNode;
|
||||
type SankeyLinkDatum = SankeyLink<TopologyNode, TopologyLink> & TopologyLink;
|
||||
|
||||
function formatTimestamp(value?: string | null) {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return (
|
||||
date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}) + `.${String(date.getMilliseconds()).padStart(3, '0')}`
|
||||
);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function hostIdentity(host: InterfaceHostProtocolEvidence) {
|
||||
return `${host.ip_address ?? 'no-ip'}|${host.mac_address ?? 'no-mac'}`;
|
||||
}
|
||||
|
||||
function hostLabel(interfaceName: string, host: InterfaceHostProtocolEvidence) {
|
||||
const ip = host.ip_address ?? 'unknown ip';
|
||||
const mac = host.mac_address ?? 'unknown mac';
|
||||
return `${interfaceName} • ${ip}\n${mac}`;
|
||||
}
|
||||
|
||||
function protocolColor(protocol: string) {
|
||||
const palette = d3.schemeTableau10;
|
||||
let hash = 0;
|
||||
for (let index = 0; index < protocol.length; index += 1) {
|
||||
hash = (hash * 31 + protocol.charCodeAt(index)) >>> 0;
|
||||
}
|
||||
return palette[hash % palette.length];
|
||||
}
|
||||
|
||||
function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyData {
|
||||
const nodes = new Map<string, TopologyNode>();
|
||||
const links = new Map<string, TopologyLink>();
|
||||
const heatmapByHost = new Map<string, HeatmapRow>();
|
||||
const protocols = new Set<string>();
|
||||
const tableRows: ProtocolTableRow[] = [];
|
||||
|
||||
for (const entry of interfaces) {
|
||||
const interfaceNodeId = `iface:${entry.interface}`;
|
||||
nodes.set(interfaceNodeId, {
|
||||
id: interfaceNodeId,
|
||||
label: entry.interface,
|
||||
kind: 'interface',
|
||||
packetCount: entry.hosts.reduce((sum, host) => sum + host.packet_count, 0),
|
||||
interfaceName: entry.interface,
|
||||
});
|
||||
|
||||
for (const host of entry.hosts) {
|
||||
const hostId = `host:${entry.interface}:${hostIdentity(host)}`;
|
||||
nodes.set(hostId, {
|
||||
id: hostId,
|
||||
label: hostLabel(entry.interface, host),
|
||||
kind: 'host',
|
||||
packetCount: host.packet_count,
|
||||
interfaceName: entry.interface,
|
||||
ipAddress: host.ip_address,
|
||||
macAddress: host.mac_address,
|
||||
});
|
||||
|
||||
const interfaceHostLinkId = `${interfaceNodeId}->${hostId}`;
|
||||
links.set(interfaceHostLinkId, {
|
||||
source: interfaceNodeId,
|
||||
target: hostId,
|
||||
value: host.packet_count,
|
||||
label: `${entry.interface} -> ${host.ip_address ?? host.mac_address ?? 'host'} (${host.packet_count})`,
|
||||
});
|
||||
|
||||
const heatmapRow: HeatmapRow = {
|
||||
hostId,
|
||||
hostLabel: `${entry.interface} • ${host.ip_address ?? 'unknown ip'}`,
|
||||
interfaceName: entry.interface,
|
||||
values: {},
|
||||
};
|
||||
|
||||
for (const protocol of host.protocols) {
|
||||
const protocolId = `protocol:${protocol.protocol}`;
|
||||
protocols.add(protocol.protocol);
|
||||
if (!nodes.has(protocolId)) {
|
||||
nodes.set(protocolId, {
|
||||
id: protocolId,
|
||||
label: protocol.protocol,
|
||||
kind: 'protocol',
|
||||
packetCount: 0,
|
||||
protocol: protocol.protocol,
|
||||
});
|
||||
}
|
||||
const existingProtocolNode = nodes.get(protocolId)!;
|
||||
existingProtocolNode.packetCount += protocol.packet_count;
|
||||
|
||||
const hostProtocolLinkId = `${hostId}->${protocolId}`;
|
||||
links.set(hostProtocolLinkId, {
|
||||
source: hostId,
|
||||
target: protocolId,
|
||||
value: protocol.packet_count,
|
||||
label: `${host.ip_address ?? host.mac_address ?? 'host'} -> ${protocol.protocol} (${protocol.packet_count})`,
|
||||
});
|
||||
|
||||
heatmapRow.values[protocol.protocol] = protocol.packet_count;
|
||||
|
||||
tableRows.push({
|
||||
key: `${entry.interface}-${hostIdentity(host)}-${protocol.protocol}`,
|
||||
interface: entry.interface,
|
||||
ip_address: host.ip_address,
|
||||
mac_address: host.mac_address,
|
||||
host_packet_count: host.packet_count,
|
||||
protocol: protocol.protocol,
|
||||
protocol_packet_count: protocol.packet_count,
|
||||
accept_count: protocol.accept_count,
|
||||
drop_count: protocol.drop_count,
|
||||
reject_count: protocol.reject_count,
|
||||
unknown_count: protocol.unknown_count,
|
||||
last_seen: protocol.last_seen,
|
||||
});
|
||||
}
|
||||
|
||||
heatmapByHost.set(hostId, heatmapRow);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: Array.from(nodes.values()),
|
||||
links: Array.from(links.values()),
|
||||
heatmapRows: Array.from(heatmapByHost.values()).sort((left, right) => left.hostLabel.localeCompare(right.hostLabel)),
|
||||
protocols: Array.from(protocols).sort(),
|
||||
tableRows: tableRows.sort((left, right) => right.protocol_packet_count - left.protocol_packet_count || left.interface.localeCompare(right.interface)),
|
||||
};
|
||||
}
|
||||
|
||||
function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const width = 1160;
|
||||
const height = Math.max(420, data.nodes.length * 26);
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const graph: SankeyGraph<TopologyNode, TopologyLink> = {
|
||||
nodes: data.nodes.map((node) => ({ ...node })),
|
||||
links: data.links.map((link) => ({ ...link })),
|
||||
};
|
||||
|
||||
const sankeyLayout = d3Sankey<TopologyNode, TopologyLink>()
|
||||
.nodeId((node) => node.id)
|
||||
.nodeWidth(18)
|
||||
.nodePadding(16)
|
||||
.extent([
|
||||
[24, 28],
|
||||
[width - 24, height - 28],
|
||||
]);
|
||||
|
||||
const layout = sankeyLayout(graph);
|
||||
|
||||
const defs = svg.append('defs');
|
||||
defs
|
||||
.append('marker')
|
||||
.attr('id', 'sankey-arrow')
|
||||
.attr('markerWidth', 10)
|
||||
.attr('markerHeight', 10)
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L10,5 L0,10 z')
|
||||
.attr('fill', '#8394a7');
|
||||
|
||||
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#f8fbff');
|
||||
|
||||
const linkLayer = svg.append('g').attr('fill', 'none').attr('stroke-opacity', 0.35);
|
||||
linkLayer
|
||||
.selectAll('path')
|
||||
.data(layout.links as SankeyLinkDatum[])
|
||||
.join('path')
|
||||
.attr('d', sankeyLinkHorizontal())
|
||||
.attr('stroke', (link) => {
|
||||
const target = link.target as SankeyNodeDatum;
|
||||
if (target.kind === 'protocol' && target.protocol) {
|
||||
return protocolColor(target.protocol);
|
||||
}
|
||||
return '#7f8ea3';
|
||||
})
|
||||
.attr('stroke-width', (link) => Math.max(1, link.width || 1))
|
||||
.attr('marker-end', 'url(#sankey-arrow)')
|
||||
.append('title')
|
||||
.text((link) => `${link.label}\nPackets: ${link.value}`);
|
||||
|
||||
const nodeLayer = svg.append('g');
|
||||
const node = nodeLayer
|
||||
.selectAll('g')
|
||||
.data(layout.nodes as SankeyNodeDatum[])
|
||||
.join('g');
|
||||
|
||||
node
|
||||
.append('rect')
|
||||
.attr('x', (d) => d.x0 ?? 0)
|
||||
.attr('y', (d) => d.y0 ?? 0)
|
||||
.attr('width', (d) => (d.x1 ?? 0) - (d.x0 ?? 0))
|
||||
.attr('height', (d) => Math.max(8, (d.y1 ?? 0) - (d.y0 ?? 0)))
|
||||
.attr('rx', 8)
|
||||
.attr('fill', (d) => {
|
||||
if (d.kind === 'interface') return '#20405d';
|
||||
if (d.kind === 'host') return '#d7e7f5';
|
||||
return d.protocol ? protocolColor(d.protocol) : '#d8d8d8';
|
||||
})
|
||||
.attr('stroke', (d) => (d.kind === 'host' ? '#9bb8d6' : 'none'))
|
||||
.append('title')
|
||||
.text((d) => `${d.label}\nPackets: ${d.value ?? d.packetCount}`);
|
||||
|
||||
node
|
||||
.append('text')
|
||||
.attr('x', (d) => ((d.x0 ?? 0) < 360 ? (d.x1 ?? 0) + 8 : (d.x0 ?? 0) - 8))
|
||||
.attr('y', (d) => ((d.y0 ?? 0) + (d.y1 ?? 0)) / 2)
|
||||
.attr('dy', '0.35em')
|
||||
.attr('text-anchor', (d) => ((d.x0 ?? 0) < 360 ? 'start' : 'end'))
|
||||
.attr('font-size', 12)
|
||||
.attr('font-weight', (d) => (d.kind === 'interface' ? 700 : 500))
|
||||
.attr('fill', '#22374f')
|
||||
.text((d) => {
|
||||
if (d.kind === 'host') {
|
||||
return d.ipAddress ?? d.macAddress ?? d.label;
|
||||
}
|
||||
return d.label;
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
return <Empty description="No interface, host, and protocol relationships found yet" />;
|
||||
}
|
||||
|
||||
return <svg ref={svgRef} style={{ width: '100%', minWidth: 960 }} />;
|
||||
}
|
||||
|
||||
function ForceTopology({ data }: { data: TopologyData }) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
const width = 1160;
|
||||
const height = 720;
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes: ForceNode[] = data.nodes.map((node) => ({ ...node }));
|
||||
const links: ForceLink[] = data.links.map((link) => ({ ...link }));
|
||||
|
||||
const simulation = d3
|
||||
.forceSimulation<ForceNode>(nodes)
|
||||
.force(
|
||||
'link',
|
||||
d3
|
||||
.forceLink<ForceNode, ForceLink>(links)
|
||||
.id((node) => node.id)
|
||||
.distance((link) => {
|
||||
const source = link.source as ForceNode;
|
||||
return source.kind === 'interface' ? 140 : 120;
|
||||
}),
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-280))
|
||||
.force('collision', d3.forceCollide<ForceNode>().radius((node) => (node.kind === 'host' ? 38 : 28)))
|
||||
.force(
|
||||
'x',
|
||||
d3.forceX<ForceNode>().x((node) => {
|
||||
if (node.kind === 'interface') return 150;
|
||||
if (node.kind === 'host') return width / 2;
|
||||
return width - 180;
|
||||
}).strength(0.32),
|
||||
)
|
||||
.force('y', d3.forceY<ForceNode>(height / 2).strength(0.08));
|
||||
|
||||
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#fbfcfe');
|
||||
|
||||
const link = svg
|
||||
.append('g')
|
||||
.attr('stroke-opacity', 0.45)
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.join('line')
|
||||
.attr('stroke', (d) => {
|
||||
const target = d.target as ForceNode;
|
||||
return target.kind === 'protocol' && target.protocol ? protocolColor(target.protocol) : '#92a1b2';
|
||||
})
|
||||
.attr('stroke-width', (d) => Math.max(1.5, Math.sqrt(d.value)));
|
||||
|
||||
link.append('title').text((d) => `${d.label}\nPackets: ${d.value}`);
|
||||
|
||||
const node = svg
|
||||
.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.join('g');
|
||||
|
||||
node
|
||||
.append('circle')
|
||||
.attr('r', (d) => {
|
||||
if (d.kind === 'interface') return 26;
|
||||
if (d.kind === 'host') return 22;
|
||||
return 18;
|
||||
})
|
||||
.attr('fill', (d) => {
|
||||
if (d.kind === 'interface') return '#20405d';
|
||||
if (d.kind === 'host') return '#d7e7f5';
|
||||
return d.protocol ? protocolColor(d.protocol) : '#cfd7df';
|
||||
})
|
||||
.attr('stroke', (d) => (d.kind === 'host' ? '#8aa8c6' : '#ffffff'))
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
node
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', 40)
|
||||
.attr('font-size', 11)
|
||||
.attr('font-weight', 600)
|
||||
.attr('fill', '#29445d')
|
||||
.text((d) => {
|
||||
if (d.kind === 'host') return d.ipAddress ?? d.macAddress ?? 'host';
|
||||
return d.label;
|
||||
});
|
||||
|
||||
node.append('title').text((d) => `${d.label}\nPackets: ${d.packetCount}`);
|
||||
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', (d) => (d.source as ForceNode).x ?? 0)
|
||||
.attr('y1', (d) => (d.source as ForceNode).y ?? 0)
|
||||
.attr('x2', (d) => (d.target as ForceNode).x ?? 0)
|
||||
.attr('y2', (d) => (d.target as ForceNode).y ?? 0);
|
||||
|
||||
node.attr('transform', (d) => `translate(${d.x ?? 0},${d.y ?? 0})`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
simulation.stop();
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
return <Empty description="No graph data available yet" />;
|
||||
}
|
||||
|
||||
return <svg ref={svgRef} style={{ width: '100%', minWidth: 960 }} />;
|
||||
}
|
||||
|
||||
function ProtocolHeatmap({ data }: { data: TopologyData }) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const margin = { top: 140, right: 30, bottom: 30, left: 240 };
|
||||
const cellWidth = 92;
|
||||
const cellHeight = 34;
|
||||
const width = Math.max(920, margin.left + margin.right + data.protocols.length * cellWidth);
|
||||
const height = Math.max(320, margin.top + margin.bottom + data.heatmapRows.length * cellHeight);
|
||||
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
if (data.protocols.length === 0 || data.heatmapRows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = d3.scaleBand<string>().domain(data.protocols).range([margin.left, width - margin.right]).paddingInner(0.08);
|
||||
const y = d3.scaleBand<string>().domain(data.heatmapRows.map((row) => row.hostId)).range([margin.top, height - margin.bottom]).paddingInner(0.08);
|
||||
const maxValue = d3.max(data.heatmapRows.flatMap((row) => data.protocols.map((protocol) => row.values[protocol] || 0))) ?? 1;
|
||||
const color = d3.scaleSequential(d3.interpolateYlGnBu).domain([0, maxValue]);
|
||||
|
||||
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#fbfcfe');
|
||||
|
||||
const cells = svg.append('g');
|
||||
for (const row of data.heatmapRows) {
|
||||
for (const protocol of data.protocols) {
|
||||
const value = row.values[protocol] || 0;
|
||||
const cell = cells
|
||||
.append('g')
|
||||
.attr('transform', `translate(${x(protocol) ?? 0},${y(row.hostId) ?? 0})`);
|
||||
|
||||
cell
|
||||
.append('rect')
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('height', y.bandwidth())
|
||||
.attr('rx', 8)
|
||||
.attr('fill', value > 0 ? color(value) : '#eef3f8')
|
||||
.attr('stroke', '#dce5ef');
|
||||
|
||||
if (value > 0) {
|
||||
cell
|
||||
.append('text')
|
||||
.attr('x', x.bandwidth() / 2)
|
||||
.attr('y', y.bandwidth() / 2 + 4)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', 11)
|
||||
.attr('font-weight', 700)
|
||||
.attr('fill', value > maxValue * 0.45 ? '#ffffff' : '#23415c')
|
||||
.text(value);
|
||||
}
|
||||
|
||||
cell.append('title').text(`${row.hostLabel}\n${protocol}: ${value} packets`);
|
||||
}
|
||||
}
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.selectAll('text.protocol-label')
|
||||
.data(data.protocols)
|
||||
.join('text')
|
||||
.attr('class', 'protocol-label')
|
||||
.attr('x', (protocol) => (x(protocol) ?? 0) + x.bandwidth() / 2)
|
||||
.attr('y', margin.top - 12)
|
||||
.attr('transform', (protocol) => `rotate(-35, ${(x(protocol) ?? 0) + x.bandwidth() / 2}, ${margin.top - 12})`)
|
||||
.attr('text-anchor', 'start')
|
||||
.attr('font-size', 12)
|
||||
.attr('font-weight', 600)
|
||||
.attr('fill', '#29445d')
|
||||
.text((protocol) => protocol);
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.selectAll('text.host-label')
|
||||
.data(data.heatmapRows)
|
||||
.join('text')
|
||||
.attr('class', 'host-label')
|
||||
.attr('x', margin.left - 12)
|
||||
.attr('y', (row) => (y(row.hostId) ?? 0) + y.bandwidth() / 2 + 4)
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('font-size', 12)
|
||||
.attr('fill', '#29445d')
|
||||
.text((row) => row.hostLabel);
|
||||
}, [data]);
|
||||
|
||||
if (data.protocols.length === 0 || data.heatmapRows.length === 0) {
|
||||
return <Empty description="No protocol heatmap data available yet" />;
|
||||
}
|
||||
|
||||
return <svg ref={svgRef} style={{ width: '100%', minWidth: 960 }} />;
|
||||
}
|
||||
|
||||
export default function Analysis(): ReactElement {
|
||||
const [sinceMinutes, setSinceMinutes] = useState(60);
|
||||
const [limitPerInterface, setLimitPerInterface] = useState(50);
|
||||
const [limitProtocolsPerHost, setLimitProtocolsPerHost] = useState(12);
|
||||
const [data, setData] = useState<InterfaceHostProtocolAnalysisResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchInterfaceHostProtocolAnalysis(sinceMinutes, limitPerInterface, limitProtocolsPerHost);
|
||||
setData(response);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message ?? 'Failed to load analysis data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sinceMinutes, limitPerInterface, limitProtocolsPerHost]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData().catch(() => undefined);
|
||||
}, [loadData]);
|
||||
|
||||
const topologyData = useMemo(() => buildTopologyData(data?.interfaces ?? []), [data]);
|
||||
|
||||
const columns = useMemo<ColumnsType<ProtocolTableRow>>(
|
||||
() => [
|
||||
{
|
||||
title: 'Interface',
|
||||
dataIndex: 'interface',
|
||||
key: 'interface',
|
||||
width: 140,
|
||||
render: (value: string) => <Tag color="blue">{value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'ip_address',
|
||||
key: 'ip_address',
|
||||
render: (value?: string | null) => value ?? '—',
|
||||
},
|
||||
{
|
||||
title: 'MAC',
|
||||
dataIndex: 'mac_address',
|
||||
key: 'mac_address',
|
||||
render: (value?: string | null) => value ?? '—',
|
||||
},
|
||||
{
|
||||
title: 'Protocol',
|
||||
dataIndex: 'protocol',
|
||||
key: 'protocol',
|
||||
width: 140,
|
||||
render: (value: string) => <Tag color={protocolColor(value)}>{value}</Tag>,
|
||||
},
|
||||
{ title: 'Host Packets', dataIndex: 'host_packet_count', key: 'host_packet_count', width: 110 },
|
||||
{ title: 'Protocol Packets', dataIndex: 'protocol_packet_count', key: 'protocol_packet_count', width: 130 },
|
||||
{ title: 'Accept', dataIndex: 'accept_count', key: 'accept_count', width: 90 },
|
||||
{ title: 'Drop', dataIndex: 'drop_count', key: 'drop_count', width: 90 },
|
||||
{ title: 'Reject', dataIndex: 'reject_count', key: 'reject_count', width: 90 },
|
||||
{ title: 'Unknown', dataIndex: 'unknown_count', key: 'unknown_count', width: 90 },
|
||||
{
|
||||
title: 'Last Seen',
|
||||
dataIndex: 'last_seen',
|
||||
key: 'last_seen',
|
||||
width: 220,
|
||||
render: (value: string) => formatTimestamp(value),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||
<Col>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
Analysis
|
||||
</Title>
|
||||
<Text type="secondary">Explore inferred interface, host, and protocol relationships from captured traffic.</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space wrap size={[12, 12]}>
|
||||
<Space>
|
||||
<Text>Look back</Text>
|
||||
<InputNumber min={1} max={60 * 24 * 30} value={sinceMinutes} onChange={(value) => setSinceMinutes(value ?? 60)} />
|
||||
<Text type="secondary">minutes</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
<Text>Max hosts per interface</Text>
|
||||
<InputNumber min={1} max={1000} value={limitPerInterface} onChange={(value) => setLimitPerInterface(value ?? 50)} />
|
||||
</Space>
|
||||
<Space>
|
||||
<Text>Max protocols per host</Text>
|
||||
<InputNumber min={1} max={100} value={limitProtocolsPerHost} onChange={(value) => setLimitProtocolsPerHost(value ?? 12)} />
|
||||
</Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => loadData()} loading={loading} type="primary">
|
||||
Refresh
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{data?.notes?.length ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="How to read this"
|
||||
description={
|
||||
<ul style={{ margin: '8px 0 0 18px', padding: 0 }}>
|
||||
{data.notes.map((note) => (
|
||||
<li key={note}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Card
|
||||
title="Topology Views"
|
||||
extra={data?.since ? <Text type="secondary">Since {formatTimestamp(data.since)}</Text> : null}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'sankey',
|
||||
label: 'Sankey',
|
||||
children: (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
Best for understanding how traffic flows from MITM interfaces to inferred hosts and then into protocols.
|
||||
</Paragraph>
|
||||
<SankeyTopology data={topologyData} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'force',
|
||||
label: 'Force Graph',
|
||||
children: (
|
||||
<div>
|
||||
<Paragraph type="secondary">Useful for exploring clusters and protocol neighborhoods across interfaces and hosts.</Paragraph>
|
||||
<ForceTopology data={topologyData} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'heatmap',
|
||||
label: 'Heatmap',
|
||||
children: (
|
||||
<div>
|
||||
<Paragraph type="secondary">Useful for comparing which hosts are most active in which protocols.</Paragraph>
|
||||
<ProtocolHeatmap data={topologyData} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Card title="Protocol Evidence Table">
|
||||
<Paragraph type="secondary" style={{ marginTop: -4 }}>
|
||||
This is the underlying aggregated evidence used by the visualizations, including verdict counts per interface, host, and protocol.
|
||||
</Paragraph>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
dataSource={topologyData.tableRows}
|
||||
size="small"
|
||||
bordered
|
||||
pagination={{ pageSize: 25 }}
|
||||
locale={{ emptyText: loading ? 'Loading…' : 'No interface-host-protocol evidence available yet.' }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,8 @@ export const PATHS = {
|
||||
ROOT: '/',
|
||||
HOME: '/home',
|
||||
NETWORK: '/network',
|
||||
ANALYSIS: '/analysis',
|
||||
SNIFFING: '/sniffing',
|
||||
SCRIPTING: '/scripting',
|
||||
FIREWALL: '/firewall',
|
||||
};
|
||||
};
|
||||
|
||||
44
frontend/src/types/analysis.ts
Normal file
44
frontend/src/types/analysis.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface InterfaceHostEvidence {
|
||||
ip_address?: string | null;
|
||||
mac_address?: string | null;
|
||||
packet_count: number;
|
||||
last_seen: string;
|
||||
source_on_ingress_count: number;
|
||||
destination_on_egress_count: number;
|
||||
}
|
||||
|
||||
export interface InterfaceAttachment {
|
||||
interface: string;
|
||||
hosts: InterfaceHostEvidence[];
|
||||
}
|
||||
|
||||
export interface InterfaceHostAnalysisResponse {
|
||||
since?: string | null;
|
||||
interfaces: InterfaceAttachment[];
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface ProtocolEvidence {
|
||||
protocol: string;
|
||||
packet_count: number;
|
||||
last_seen: string;
|
||||
accept_count: number;
|
||||
drop_count: number;
|
||||
reject_count: number;
|
||||
unknown_count: number;
|
||||
}
|
||||
|
||||
export interface InterfaceHostProtocolEvidence extends InterfaceHostEvidence {
|
||||
protocols: ProtocolEvidence[];
|
||||
}
|
||||
|
||||
export interface InterfaceProtocolAttachment {
|
||||
interface: string;
|
||||
hosts: InterfaceHostProtocolEvidence[];
|
||||
}
|
||||
|
||||
export interface InterfaceHostProtocolAnalysisResponse {
|
||||
since?: string | null;
|
||||
interfaces: InterfaceProtocolAttachment[];
|
||||
notes: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user