- Add DeviceDetailModal with 8 tabs (Info, Flows, Apps, Encryption, Servers, Password Leaks, IP Reputation, VPN & Events) - Make device cards in AgentDetailModal clickable (nested modal drill-down) - Expand fetchDeviceDetails backend to query all 10 correlated tables (devices, flows, intel_device_discovery, intel_encryption_audit, intel_server_discovery, intel_unencrypted_passwords, intel_ip_reputation, intel_vpn_detection, events, mac_bandwidth) - Smart combined app/domain display: prioritize actual domain names over port-only labels (Port 443 -> scontent.fcgk42-1.fna.fbcdn.net) - Update Flows tab: domain shown in teal monospace, protocols in purple, port-only entries muted - Add 10 new TypeScript interfaces for device detail data types - Add MAC address fallback queries (by MAC when IP yields no results) - Fix fmtBytes for very large values
838 lines
21 KiB
TypeScript
838 lines
21 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
|
|
// Common Response Wrapper
|
|
export interface ApiResponse<T> {
|
|
ok: boolean;
|
|
data: T;
|
|
message?: string;
|
|
}
|
|
|
|
// Data Types
|
|
export interface FlowStat {
|
|
id: number;
|
|
fetched_at: string;
|
|
flow_id: string;
|
|
src_ip: string;
|
|
src_mac: string | null;
|
|
dst_ip: string;
|
|
dst_port: number;
|
|
protocol: string;
|
|
app_label: string | null;
|
|
domain: string | null;
|
|
bytes_download: number;
|
|
bytes_upload: number;
|
|
first_seen: string;
|
|
last_seen: string;
|
|
}
|
|
|
|
export interface CountryStat {
|
|
id: number;
|
|
country_code: string;
|
|
country_name: string;
|
|
download: number;
|
|
upload: number;
|
|
flow_count: number;
|
|
}
|
|
|
|
export interface DnsStat {
|
|
id: number;
|
|
domain: string;
|
|
query_count: number;
|
|
app_label: string | null;
|
|
category: string | null;
|
|
}
|
|
|
|
export interface EventStat {
|
|
id: number;
|
|
event_type: string;
|
|
severity: string;
|
|
message: string;
|
|
source_ip: string | null;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface TlsVersionStat {
|
|
id: number;
|
|
tls_version: string;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface TlsCipherStat {
|
|
id: number;
|
|
tls_cipher: string;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface TlsSecurityStat {
|
|
id: number;
|
|
tls_security: string;
|
|
color: string;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface IntelligenceStats {
|
|
intel_crypto_mining: number;
|
|
intel_device_discovery: number;
|
|
intel_encryption_audit: number;
|
|
intel_insecure_protocols: number;
|
|
intel_ip_reputation: number;
|
|
intel_server_discovery: number;
|
|
intel_tor_detection: number;
|
|
intel_unencrypted_passwords: number;
|
|
intel_vpn_detection: number;
|
|
}
|
|
|
|
export interface DashboardSummary {
|
|
total_devices: number;
|
|
total_threats: number;
|
|
total_events: number;
|
|
last_fetch: string;
|
|
bandwidth_down: number;
|
|
bandwidth_up: number;
|
|
active_flows: number;
|
|
}
|
|
|
|
export interface TimelinePoint {
|
|
fetched_at: string;
|
|
total_download: number;
|
|
total_upload: number;
|
|
}
|
|
|
|
export interface AppStat {
|
|
app_label: string;
|
|
download: number;
|
|
upload: number;
|
|
favicon?: string;
|
|
category?: string;
|
|
}
|
|
|
|
export interface ProtocolStat {
|
|
protocol_label: string;
|
|
download: number;
|
|
upload: number;
|
|
}
|
|
|
|
export interface DeviceStat {
|
|
id: number;
|
|
ip_address: string;
|
|
mac_address: string | null;
|
|
device_label: string | null;
|
|
device_type: string | null;
|
|
os_label: string | null;
|
|
manufacturer: string | null;
|
|
download: number;
|
|
upload: number;
|
|
last_seen: string;
|
|
}
|
|
|
|
export interface ThreatStat {
|
|
id: number;
|
|
threat_type: string | null;
|
|
severity: string | null;
|
|
ip_address: string | null;
|
|
dst_ip: string | null;
|
|
mac_address: string | null;
|
|
app_label: string | null;
|
|
domain: string | null;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AppCategoryStat {
|
|
id: number;
|
|
category_label: string;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface ContinentStat {
|
|
id: number;
|
|
continent_name: string;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
// Fetcher Function
|
|
export async function fetcher<T>(endpoint: string): Promise<T> {
|
|
const url = `/api/dashboard${endpoint}`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
const json = await res.json();
|
|
if (!json.ok) {
|
|
throw new Error(json.message || 'API responded with ok: false');
|
|
}
|
|
return json.data;
|
|
}
|
|
|
|
// Custom Hooks for React Components
|
|
export function useDashboardSummary() {
|
|
const [data, setData] = useState<DashboardSummary | null>(null);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<DashboardSummary>('/summary')
|
|
.then(setData)
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTimeline(points: number = 60) {
|
|
const [data, setData] = useState<TimelinePoint[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<TimelinePoint[]>(`/timeline?points=${points}`)
|
|
.then((res) => {
|
|
// Reverse array if backend sends newest first, so oldest is first for the chart
|
|
setData(Array.isArray(res) ? res.slice().reverse() : []);
|
|
})
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [points]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTopApps(limit: number = 10) {
|
|
const [data, setData] = useState<AppStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<AppStat[]>(`/apps?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTopProtocols() {
|
|
const [data, setData] = useState<ProtocolStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<ProtocolStat[]>('/protocols')
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useDevices(limit: number = 50) {
|
|
const [data, setData] = useState<DeviceStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<DeviceStat[]>(`/devices?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useThreats(limit: number = 20) {
|
|
const [data, setData] = useState<ThreatStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<ThreatStat[]>(`/threats?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useAppCategories() {
|
|
const [data, setData] = useState<AppCategoryStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<AppCategoryStat[]>('/app-categories')
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useContinents() {
|
|
const [data, setData] = useState<ContinentStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<ContinentStat[]>('/continents')
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useFlows(limit: number = 50) {
|
|
const [data, setData] = useState<FlowStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<FlowStat[]>(`/flows?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useCountries(limit: number = 50) {
|
|
const [data, setData] = useState<CountryStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<CountryStat[]>(`/countries?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useDNS(limit: number = 50) {
|
|
const [data, setData] = useState<DnsStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<DnsStat[]>(`/dns?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useEvents(limit: number = 50) {
|
|
const [data, setData] = useState<EventStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<EventStat[]>(`/events?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTlsVersions(limit: number = 10) {
|
|
const [data, setData] = useState<TlsVersionStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTlsCiphers(limit: number = 15) {
|
|
const [data, setData] = useState<TlsCipherStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useTlsSecurity(limit: number = 10) {
|
|
const [data, setData] = useState<TlsSecurityStat[]>([]);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`)
|
|
.then((res) => setData(res || []))
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [limit]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
export function useIntelligenceStats() {
|
|
const [data, setData] = useState<IntelligenceStats | null>(null);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetcher<IntelligenceStats>('/intelligence/stats')
|
|
.then(setData)
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
// ─── LOOKUP APPLICATIONS ──────────────────────────────────────────────────────
|
|
export interface LookupApp {
|
|
id: number;
|
|
tag: string;
|
|
label: string;
|
|
name: string;
|
|
full_name: string;
|
|
description: string;
|
|
favicon: string;
|
|
icon: string;
|
|
logo: string;
|
|
application_category: { id: number; tag: string; label: string };
|
|
}
|
|
|
|
export interface LookupApplicationsResponse {
|
|
applications: LookupApp[];
|
|
pagination: {
|
|
total_records: number;
|
|
total_pages: number;
|
|
current_page: number;
|
|
start: number;
|
|
length: number;
|
|
limit: number;
|
|
};
|
|
}
|
|
|
|
export function useLookupApplications(page: number = 1, limit: number = 25, search: string = '') {
|
|
const [data, setData] = useState<LookupApplicationsResponse | null>(null);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
setIsLoading(true);
|
|
fetcher<LookupApplicationsResponse>(`/lookup/applications?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`)
|
|
.then(setData)
|
|
.catch(setError)
|
|
.finally(() => setIsLoading(false));
|
|
}, [page, limit, search]);
|
|
|
|
return { data, error, isLoading };
|
|
}
|
|
|
|
// ─── INTERACTIVE DETAIL TYPES ──────────────────────────────────────────────────
|
|
|
|
export interface AgentDeviceItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
device_label: string | null;
|
|
device_type: string | null;
|
|
os_label: string | null;
|
|
manufacturer: string | null;
|
|
last_seen: string | null;
|
|
download: number;
|
|
upload: number;
|
|
encrypted_pct: number | null;
|
|
risk_level: string | null;
|
|
has_insecure: boolean;
|
|
}
|
|
|
|
export interface AgentFlowItem {
|
|
flow_id?: string | null;
|
|
src_ip: string | null;
|
|
dst_ip: string | null;
|
|
dst_port: number | null;
|
|
protocol: string | null;
|
|
app_label?: string | null;
|
|
domain?: string | null;
|
|
download: number;
|
|
upload: number;
|
|
last_seen: string | null;
|
|
}
|
|
|
|
export interface AgentAppItem {
|
|
app_id: number | null;
|
|
app_label: string;
|
|
category: string | null;
|
|
favicon: string | null;
|
|
download: number;
|
|
upload: number;
|
|
}
|
|
|
|
export interface AgentSummary {
|
|
total_devices: number;
|
|
active_flows: number;
|
|
bandwidth_down: number;
|
|
bandwidth_up: number;
|
|
}
|
|
|
|
export interface AgentEncryptionItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
device_label: string | null;
|
|
encrypted_pct: number | null;
|
|
unencrypted: number | null;
|
|
encrypted: number | null;
|
|
total: number | null;
|
|
risk_level: string | null;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentInsecureProtocolItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
protocol: string | null;
|
|
risk: string | null;
|
|
app_label: string | null;
|
|
dst_ip: string | null;
|
|
dst_port: number | null;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentUnencryptedPwdItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
dst_ip: string | null;
|
|
dst_port: number | null;
|
|
protocol: string | null;
|
|
username: string | null;
|
|
severity: string | null;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentIpReputationItem {
|
|
ip_address: string | null;
|
|
local_ip: string | null;
|
|
mac_address: string | null;
|
|
reputation: string | null;
|
|
score: number | null;
|
|
country: string | null;
|
|
app_label: string | null;
|
|
blacklisted: number | boolean;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentTorItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
exit_node: string | null;
|
|
circuit_id: string | null;
|
|
country: string | null;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentVpnItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
vpn_type: string | null;
|
|
remote_ip: string | null;
|
|
protocol: string | null;
|
|
country: string | null;
|
|
confidence: number | null;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentSecurity {
|
|
encryption_audit: AgentEncryptionItem[];
|
|
insecure_protocols: AgentInsecureProtocolItem[];
|
|
unencrypted_passwords: AgentUnencryptedPwdItem[];
|
|
ip_reputation: AgentIpReputationItem[];
|
|
tor_detections: AgentTorItem[];
|
|
vpn_detections: AgentVpnItem[];
|
|
}
|
|
|
|
export interface AgentEventItem {
|
|
event_id: string | null;
|
|
event_type: string | null;
|
|
severity: string | null;
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
description: string | null;
|
|
event_at: string | null;
|
|
}
|
|
|
|
export interface AgentMacBandwidthItem {
|
|
mac_address: string;
|
|
manufacturer: string | null;
|
|
download: number;
|
|
upload: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface AgentServerDiscoveryItem {
|
|
ip_address: string | null;
|
|
mac_address: string | null;
|
|
server_type: string | null;
|
|
hostname: string | null;
|
|
port: number | null;
|
|
protocol: string | null;
|
|
os_label: string | null;
|
|
download: number;
|
|
upload: number;
|
|
detected_at: string | null;
|
|
}
|
|
|
|
export interface AgentDetails {
|
|
agent_uuid: string;
|
|
agent_label: string;
|
|
summary: AgentSummary | null;
|
|
devices: AgentDeviceItem[];
|
|
flows: AgentFlowItem[];
|
|
top_apps: AgentAppItem[];
|
|
security: AgentSecurity | null;
|
|
events: AgentEventItem[];
|
|
mac_bandwidth: AgentMacBandwidthItem[];
|
|
server_discovery: AgentServerDiscoveryItem[];
|
|
}
|
|
|
|
export interface DeviceFlowItem {
|
|
dst_ip: string | null;
|
|
dst_port: number | null;
|
|
protocol: string | null;
|
|
app_label?: string | null;
|
|
domain?: string | null;
|
|
download: number;
|
|
upload: number;
|
|
last_seen: string | null;
|
|
}
|
|
|
|
export interface SecurityInfo {
|
|
encrypted_pct: number | null;
|
|
encrypted_bytes: number | null;
|
|
unencrypted_bytes: number | null;
|
|
risk_level: string | null;
|
|
insecure_protocols: { protocol: string; risk: string }[];
|
|
device_type: string | null;
|
|
os_label: string | null;
|
|
manufacturer: string | null;
|
|
device_label: string | null;
|
|
reputation: string | null;
|
|
rep_score: number | null;
|
|
blacklisted: boolean;
|
|
}
|
|
|
|
export interface DeviceInfo {
|
|
device_label : string | null;
|
|
device_type : string | null;
|
|
os_label : string | null;
|
|
manufacturer : string | null;
|
|
mac_address : string | null;
|
|
is_new : number | null;
|
|
}
|
|
|
|
export interface DeviceAppItem {
|
|
label : string; // domain name OR protocol name
|
|
sub_label : string | null; // protocol when label is domain; null otherwise
|
|
type : 'domain' | 'protocol' | 'port';
|
|
download : number;
|
|
upload : number;
|
|
flow_count : number;
|
|
}
|
|
|
|
export interface DeviceDomainItem {
|
|
domain : string;
|
|
download : number;
|
|
upload : number;
|
|
flow_count : number;
|
|
}
|
|
|
|
export interface DeviceEncryption {
|
|
encrypted_pct : number | null;
|
|
encrypted_bytes : number | null;
|
|
unencrypted_bytes: number | null;
|
|
total_bytes : number | null;
|
|
risk_level : string | null;
|
|
}
|
|
|
|
export interface DeviceServerItem {
|
|
server_type : string | null;
|
|
hostname : string | null;
|
|
port : number | null;
|
|
protocol : string | null;
|
|
os_label : string | null;
|
|
download : number;
|
|
upload : number;
|
|
detected_at : string | null;
|
|
}
|
|
|
|
export interface DevicePwdItem {
|
|
dst_ip : string | null;
|
|
dst_port : number | null;
|
|
protocol : string | null;
|
|
username : string | null;
|
|
severity : string | null;
|
|
download : number;
|
|
upload : number;
|
|
detected_at : string | null;
|
|
}
|
|
|
|
export interface DeviceReputationItem {
|
|
remote_ip : string | null;
|
|
local_ip : string | null;
|
|
reputation : string | null;
|
|
score : number | null;
|
|
country : string | null;
|
|
app_label : string | null;
|
|
blacklisted : boolean;
|
|
download : number;
|
|
upload : number;
|
|
}
|
|
|
|
export interface DeviceVpnItem {
|
|
vpn_type : string | null;
|
|
remote_ip : string | null;
|
|
protocol : string | null;
|
|
country : string | null;
|
|
confidence : number | null;
|
|
download : number;
|
|
upload : number;
|
|
detected_at : string | null;
|
|
}
|
|
|
|
export interface DeviceEventItem {
|
|
event_type : string | null;
|
|
severity : string | null;
|
|
ip_address : string | null;
|
|
mac_address : string | null;
|
|
description : string | null;
|
|
event_at : string | null;
|
|
}
|
|
|
|
export interface DeviceMacBandwidth {
|
|
mac_address : string;
|
|
manufacturer : string | null;
|
|
download : number;
|
|
upload : number;
|
|
total : number;
|
|
}
|
|
|
|
export interface DeviceDetails {
|
|
ip : string;
|
|
mac_address : string | null;
|
|
total_download : number;
|
|
total_upload : number;
|
|
flow_count : number;
|
|
device_info : DeviceInfo;
|
|
top_apps : DeviceAppItem[];
|
|
top_domains : DeviceDomainItem[];
|
|
flows : DeviceFlowItem[];
|
|
encryption : DeviceEncryption | null;
|
|
server_discovery : DeviceServerItem[];
|
|
unencrypted_passwords: DevicePwdItem[];
|
|
ip_reputation : DeviceReputationItem[];
|
|
vpn_detections : DeviceVpnItem[];
|
|
events : DeviceEventItem[];
|
|
mac_bandwidth : DeviceMacBandwidth | null;
|
|
}
|
|
|
|
|
|
export interface AppAgentItem {
|
|
agent_uuid: string | null;
|
|
agent_label: string;
|
|
download: number;
|
|
upload: number;
|
|
flow_count?: number;
|
|
}
|
|
|
|
export interface AppIpItem {
|
|
ip_address: string | null;
|
|
download: number;
|
|
upload: number;
|
|
flow_count?: number;
|
|
reputation?: string | null;
|
|
blacklisted?: boolean;
|
|
}
|
|
|
|
export interface AppDetails {
|
|
app_label: string;
|
|
total_download: number;
|
|
total_upload: number;
|
|
data_source?: string;
|
|
has_domain_breakdown?: boolean;
|
|
agent_scorecard: AppAgentItem[];
|
|
top_ips: AppIpItem[];
|
|
}
|
|
|
|
export interface SecurityDevice {
|
|
ip_address: string;
|
|
mac_address: string | null;
|
|
device_label: string | null;
|
|
encrypted_pct: number;
|
|
unencrypted: number;
|
|
encrypted: number;
|
|
total: number;
|
|
risk_level: 'Rawan' | 'Sedang' | 'Aman';
|
|
has_insecure: boolean;
|
|
device_type: string | null;
|
|
os_label: string | null;
|
|
manufacturer: string | null;
|
|
}
|
|
|
|
// ─── DETAIL FETCH FUNCTIONS (one-shot, no polling) ───────────────────────────
|
|
|
|
export async function fetchAgentDetails(uuid: string): Promise<AgentDetails> {
|
|
return fetcher<AgentDetails>(`/agent-details?uuid=${encodeURIComponent(uuid)}`);
|
|
}
|
|
|
|
export async function fetchDeviceDetails(ip: string): Promise<DeviceDetails> {
|
|
return fetcher<DeviceDetails>(`/device-details?ip=${encodeURIComponent(ip)}`);
|
|
}
|
|
|
|
export async function fetchAppDetails(label: string): Promise<AppDetails> {
|
|
return fetcher<AppDetails>(`/app-details?label=${encodeURIComponent(label)}`);
|
|
}
|
|
|
|
export async function fetchSecurityDevices(): Promise<SecurityDevice[]> {
|
|
return fetcher<SecurityDevice[]>('/security-devices');
|
|
}
|
|
|