test with new analysis layout
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 12s

This commit is contained in:
2026-03-31 22:15:01 +02:00
parent cedc52eb8d
commit 72bab93aa3
6 changed files with 2632 additions and 145 deletions

View File

@@ -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)