test with new analysis layout
This commit is contained in:
@@ -115,6 +115,198 @@ class InterfaceProtocolPathAnalysisResponse(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ConversationEvidence(BaseModel):
|
||||
ingress_interface: Optional[str] = None
|
||||
egress_interface: Optional[str] = None
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
src_port: Optional[int] = None
|
||||
dst_port: Optional[int] = None
|
||||
protocol: str
|
||||
ethernet_protocol: Optional[str] = None
|
||||
ip_protocol: Optional[str] = None
|
||||
hostnames: List[str] = Field(default_factory=list)
|
||||
packet_count: int
|
||||
byte_count: int
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
accept_count: int = 0
|
||||
drop_count: int = 0
|
||||
reject_count: int = 0
|
||||
unknown_count: int = 0
|
||||
|
||||
|
||||
class ConversationAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = None
|
||||
conversations: List[ConversationEvidence] = Field(default_factory=list)
|
||||
notes: List[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"Conversations group directional traffic by source, destination, ports, and detected protocol.",
|
||||
"Byte counts come from packet lengths observed by the MITM and are useful for comparing session size.",
|
||||
"Hostname hints are inferred from app_hostname when present, including DNS, HTTP Host, and TLS SNI.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LabelCountEvidence(BaseModel):
|
||||
label: str
|
||||
packet_count: int
|
||||
|
||||
|
||||
class HostPeerEvidence(BaseModel):
|
||||
ip_address: Optional[str] = None
|
||||
mac_address: Optional[str] = None
|
||||
packet_count: int
|
||||
byte_count: int
|
||||
last_seen: datetime
|
||||
protocols: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HostServiceEvidence(BaseModel):
|
||||
port: Optional[int] = None
|
||||
protocol: str
|
||||
packet_count: int
|
||||
byte_count: int
|
||||
last_seen: datetime
|
||||
hostnames: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HostIntelligenceEvidence(BaseModel):
|
||||
ip_address: Optional[str] = None
|
||||
mac_address: Optional[str] = None
|
||||
packet_count: int
|
||||
byte_count: int
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
interfaces: List[str] = Field(default_factory=list)
|
||||
source_count: int
|
||||
destination_count: int
|
||||
hostnames: List[str] = Field(default_factory=list)
|
||||
top_protocols: List[LabelCountEvidence] = Field(default_factory=list)
|
||||
peers: List[HostPeerEvidence] = Field(default_factory=list)
|
||||
services: List[HostServiceEvidence] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HostIntelligenceAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = None
|
||||
hosts: List[HostIntelligenceEvidence] = Field(default_factory=list)
|
||||
notes: List[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"Host intelligence merges packet direction, protocol usage, peer relationships, and hostname enrichment.",
|
||||
"Services are inferred from traffic where the host appears as the destination on a specific port.",
|
||||
"Hostname hints come from detected app_hostname values and help turn IPs into recognizable assets.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class DiscoveryActivityEvidence(BaseModel):
|
||||
category: str
|
||||
protocol: str
|
||||
ingress_interface: Optional[str] = None
|
||||
egress_interface: Optional[str] = None
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
src_port: Optional[int] = None
|
||||
dst_port: Optional[int] = None
|
||||
hostnames: List[str] = Field(default_factory=list)
|
||||
packet_count: int
|
||||
byte_count: int
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class DiscoveryAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = None
|
||||
activities: List[DiscoveryActivityEvidence] = Field(default_factory=list)
|
||||
notes: List[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"Discovery traffic highlights local network learning and service advertisement protocols.",
|
||||
"This includes ARP, DHCP, mDNS, SSDP, LLMNR, NBNS, and selected ICMPv6 discovery traffic.",
|
||||
"These views are useful for mapping who is present on the segment and which naming systems are active.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ScanCandidateEvidence(BaseModel):
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
packet_count: int
|
||||
target_host_count: int
|
||||
target_port_count: int
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class BeaconCandidateEvidence(BaseModel):
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
dst_port: Optional[int] = None
|
||||
protocol: str
|
||||
packet_count: int
|
||||
avg_interval_seconds: float
|
||||
jitter_ratio: float
|
||||
first_seen: datetime
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class RareServiceEvidence(BaseModel):
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
dst_port: Optional[int] = None
|
||||
protocol: str
|
||||
packet_count: int
|
||||
client_count: int
|
||||
hostnames: List[str] = Field(default_factory=list)
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class ResetHeavyPathEvidence(BaseModel):
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
dst_port: Optional[int] = None
|
||||
total_packets: int
|
||||
reset_count: int
|
||||
reset_ratio: float
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class DropHeavyPathEvidence(BaseModel):
|
||||
src_ip_address: Optional[str] = None
|
||||
src_mac_address: Optional[str] = None
|
||||
dst_ip_address: Optional[str] = None
|
||||
dst_mac_address: Optional[str] = None
|
||||
protocol: str
|
||||
total_packets: int
|
||||
drop_count: int
|
||||
reject_count: int
|
||||
failure_ratio: float
|
||||
last_seen: datetime
|
||||
|
||||
|
||||
class AnomalyAnalysisResponse(BaseModel):
|
||||
since: Optional[datetime] = None
|
||||
scan_candidates: List[ScanCandidateEvidence] = Field(default_factory=list)
|
||||
beacon_candidates: List[BeaconCandidateEvidence] = Field(default_factory=list)
|
||||
rare_services: List[RareServiceEvidence] = Field(default_factory=list)
|
||||
reset_heavy_paths: List[ResetHeavyPathEvidence] = Field(default_factory=list)
|
||||
drop_heavy_paths: List[DropHeavyPathEvidence] = Field(default_factory=list)
|
||||
notes: List[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"Anomaly views are heuristic and intended as leads for investigation, not final verdicts.",
|
||||
"Scan candidates are sources touching many hosts or ports, beacon candidates are conversations with regular intervals.",
|
||||
"Rare services, reset-heavy paths, and drop-heavy paths help surface unusual or unhealthy communication.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/interface-hosts", response_model=InterfaceHostAnalysisResponse)
|
||||
async def analysis_interface_hosts(
|
||||
since_minutes: Optional[int] = Query(
|
||||
@@ -225,3 +417,134 @@ async def analysis_interface_protocol_paths(
|
||||
|
||||
paths = [InterfaceProtocolPathEvidence(**row) for row in rows]
|
||||
return InterfaceProtocolPathAnalysisResponse(since=since, paths=paths)
|
||||
|
||||
|
||||
@router.get("/conversations", response_model=ConversationAnalysisResponse)
|
||||
async def analysis_conversations(
|
||||
since_minutes: Optional[int] = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=60 * 24 * 30,
|
||||
description="Analyze only packets seen within the last N minutes. Omit to cover all captured history.",
|
||||
),
|
||||
limit: int = Query(
|
||||
300,
|
||||
ge=1,
|
||||
le=5000,
|
||||
description="Maximum number of conversations returned.",
|
||||
),
|
||||
) -> ConversationAnalysisResponse:
|
||||
"""Aggregate directional conversations between observed endpoints."""
|
||||
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.analyze_conversations(since=since, limit=limit)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to analyze conversations: {exc}") from exc
|
||||
|
||||
conversations = [ConversationEvidence(**row) for row in rows]
|
||||
return ConversationAnalysisResponse(since=since, conversations=conversations)
|
||||
|
||||
|
||||
@router.get("/host-intelligence", response_model=HostIntelligenceAnalysisResponse)
|
||||
async def analysis_host_intelligence(
|
||||
since_minutes: Optional[int] = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=60 * 24 * 30,
|
||||
description="Analyze only packets seen within the last N minutes. Omit to cover all captured history.",
|
||||
),
|
||||
limit_hosts: int = Query(
|
||||
40,
|
||||
ge=1,
|
||||
le=500,
|
||||
description="Maximum number of hosts returned in the intelligence view.",
|
||||
),
|
||||
) -> HostIntelligenceAnalysisResponse:
|
||||
"""Build host-centric intelligence including peers, services, and hostname hints."""
|
||||
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.analyze_host_intelligence(since=since, limit_hosts=limit_hosts)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to analyze host intelligence: {exc}") from exc
|
||||
|
||||
hosts = [HostIntelligenceEvidence(**row) for row in rows]
|
||||
return HostIntelligenceAnalysisResponse(since=since, hosts=hosts)
|
||||
|
||||
|
||||
@router.get("/discovery", response_model=DiscoveryAnalysisResponse)
|
||||
async def analysis_discovery(
|
||||
since_minutes: Optional[int] = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=60 * 24 * 30,
|
||||
description="Analyze only packets seen within the last N minutes. Omit to cover all captured history.",
|
||||
),
|
||||
limit: int = Query(
|
||||
300,
|
||||
ge=1,
|
||||
le=5000,
|
||||
description="Maximum number of grouped discovery activities returned.",
|
||||
),
|
||||
) -> DiscoveryAnalysisResponse:
|
||||
"""Highlight local discovery, naming, and service advertisement traffic."""
|
||||
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.analyze_discovery_activity(since=since, limit=limit)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to analyze discovery activity: {exc}") from exc
|
||||
|
||||
activities = [DiscoveryActivityEvidence(**row) for row in rows]
|
||||
return DiscoveryAnalysisResponse(since=since, activities=activities)
|
||||
|
||||
|
||||
@router.get("/anomalies", response_model=AnomalyAnalysisResponse)
|
||||
async def analysis_anomalies(
|
||||
since_minutes: Optional[int] = Query(
|
||||
None,
|
||||
ge=1,
|
||||
le=60 * 24 * 30,
|
||||
description="Analyze only packets seen within the last N minutes. Omit to cover all captured history.",
|
||||
),
|
||||
limit: int = Query(
|
||||
50,
|
||||
ge=1,
|
||||
le=500,
|
||||
description="Maximum number of anomaly candidates returned per category.",
|
||||
),
|
||||
) -> AnomalyAnalysisResponse:
|
||||
"""Return heuristic anomaly candidates for scans, beaconing, resets, and failures."""
|
||||
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:
|
||||
result = await db.analyze_anomalies(since=since, limit=limit)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to analyze anomalies: {exc}") from exc
|
||||
|
||||
return AnomalyAnalysisResponse(since=since, **result)
|
||||
|
||||
Reference in New Issue
Block a user