Collectors and parsers
A collector performs network or system I/O and returns evidence
A parser accepts data it was given and does not perform I/O
Keep that boundary visible when adding protocol support
Collector contract
The shared protocol is intentionally small
class Collector(Protocol):
name: str
async def collect(
self,
target: Target,
timeout: float,
) -> list[Evidence]: ...
The name must match a collector name listed for at least one scan mode before the built-in client will select it
Built-in collectors
| Name | Module | Work performed |
|---|---|---|
hostname |
collectors/basic.py |
Reverse DNS lookup |
oui |
collectors/basic.py |
Local MAC manufacturer lookup |
mdns |
collectors/mdns.py |
Enumerate DNS-SD types once, resolve services, then try reverse mDNS |
llmnr |
collectors/local_names.py |
Ask for an IPv4 reverse name over LLMNR |
upnp |
collectors/upnp.py |
Run one multicast SSDP search and fetch target-owned UPnP descriptions |
netbios |
collectors/netbios.py |
Query the NetBIOS node-status name |
services |
collectors/services.py |
Connect to a short list of TCP ports in deep mode |
Adding a collector
- Put protocol I/O in a focused module under
collectors/ - Return
Evidencefor facts the target actually reported - Put byte, header, XML, or packet parsing in
parsers/when it can be tested separately - Export the collector from
collectors/__init__.py - Add it to
_built_in_collectors()inclient.py - Add its name to the intended modes in
config.py - Add unit tests for parsing, timeout behavior, malformed input, and useful output
- Update the scanning and API documentation
Do not make a collector choose the final manufacturer, type, model, or operating system when it only has a hint
Return the hint as evidence and let fusion decide
Timeout behavior
The shared runner wraps every collector in asyncio.timeout
A collector should still pass the timeout to blocking calls, socket timeouts, and HTTP clients so its own resources stop promptly
Multicast collectors share one short scan across target calls and cache that scan briefly
Do not create a multicast browser or send a subnet-wide search once per target
Use asyncio.to_thread for a blocking library call that has no asynchronous API
Do not catch cancellation
Catch narrow protocol errors inside the collector only when a partial result can still be returned
Evidence fields
Final profile selection currently recognizes these string fields
Other fields remain useful as raw evidence and can drive local rules
Current examples include server, upnp_location, device_type_raw, model_number, serial_number, dhcp_fingerprint, and dhcp_vendor
Use stable snake_case names
Confidence
A collector confidence describes the strength of one observation
It must be between 0 and 1
Use higher confidence for explicit metadata returned by the device and lower confidence for indirect inference
Source reliability weights used during conflicts live in identity/fusion.py
Do not add another hidden weighting table to a collector
Descriptions
Add a short evidence description when the raw field and value do not explain themselves well
Evidence(
source="upnp",
field="model",
value="QN90C",
confidence=0.95,
description="UPnP modelName reports QN90C",
)
DeviceProfile.explain() uses this text directly
Never put credentials, authorization headers, or private request data in a description or metadata mapping
Parser rules
Parsers should accept deterministic input and return an empty result for malformed data when malformed packets are expected on the network
They should not open sockets, read environment variables, configure logging, or depend on a HostLens instance
The existing parsers cover DHCP option tuples, SSDP headers, and UPnP description XML
Custom collectors in application code
Applications can replace the built-in list
from hostlens import HostLens
from hostlens.models import Evidence, Target
class InventoryCollector:
name = "inventory"
async def collect(self, target: Target, timeout: float) -> list[Evidence]:
return []
intel = HostLens(collectors=[InventoryCollector()])
This constructor argument replaces the built-ins
If an application wants both, it must pass both its collector and the built-in collector instances it needs