Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AI Security Video Analytics

Self hosted video analytics for ONVIF and RTSP IP cameras. People counting, plate recognition, face recognition, rule based alerting, bilingual English and Arabic dashboard in full RTL. Runs on the operator's own hardware or hosted, licensed per deployment.

What's in here

The camera discovery and probe layer, pulled out so it can be read and run: the SSRF guard that decides which targets the server is allowed to connect to, ONVIF probe error classification, brand identification, and the logic that merges two unreliable discovery methods into one candidate list.

The detection models, the licence server and the dashboard aren't published. The reasoning behind the discovery rules is, in docs/SECURITY_NOTES.md.

112 tests, standard library only. Nothing to install except pytest.

pip install -r requirements-dev.txt
python -m pytest discovery/tests -q
python demo.py

demo.py prints the guard's verdict on a list of targets, shows a /8 being refused before it can allocate anything, merges two fixture discovery runs, and classifies some probe failures. No sockets get opened.

Finding a camera

"Add a camera" looks like a form with an IP address in it. In practice the installer doesn't know the IP, the camera is one of a dozen brands that disagree about everything, and the network is a building site.

There are two ways to find them and neither is enough on its own. WS-Discovery over multicast only reaches ONVIF devices that hear the probe, which often fails across VLANs and on wireless, but a hit proves the device speaks ONVIF. A TCP port scan finds anything listening on a camera port regardless of multicast, but proves nothing about ONVIF and also turns up printers and NAS boxes. So both run and the results merge by IP with the union of open ports. The ONVIF flag is sticky: once multicast has proven it, a port scan that can't prove it must not clear it. Vendor is guessed from the MAC prefix via the ARP table, which is best effort and degrades to unknown rather than failing the scan.

The guard

Camera discovery exists to reach private addresses, which inverts the normal SSRF rule. Blocking RFC1918 would block the whole feature. So instead everything that isn't a plausible camera is blocked: loopback, link local, multicast and reserved always, and public unless the deployment opts in. The opt in permits public targets and nothing else, so it doesn't quietly become a bypass for the metadata address.

The case that catches people is ::ffff:169.254.169.254. That's the cloud metadata address written as IPv6, and Python's ipaddress doesn't call it link local, because as an IPv6 address it isn't. Classified naively it reads as an ordinary public address, so it's blocked by default and permitted the moment the public opt in is on, which is exactly when an attacker wants it. The classifier folds IPv4 mapped addresses to their embedded IPv4 before any rule runs.

A hostname can resolve to several addresses and a hostile resolver can answer differently between the check and the connection. So it resolves once, rejects the whole request if any resolved address is blocked, and returns a validated IP literal for the caller to connect to. The name never gets resolved a second time.

The scan takes a subnet and a /8 is about 16.7 million addresses. Materialising the host list and then checking its length puts the process out of memory before the check runs, which turns a validation rule into a DoS. The bound is checked against num_addresses, which is arithmetic on the prefix length.

Notification channels let an operator set an SMTP host, and a real relay is public, so there private is the attack: pointing it at 127.0.0.1 or a metadata IP makes the API connect there. That guard shares the same classifier pointing the other way, which is why classify_ip returns a reason string rather than a boolean.

Probe failures

Wrong credentials, an unreachable host and a device that doesn't speak ONVIF all come through the same exception types, and you can only tell them apart from the message text. They get sorted into auth, unreachable and not_onvif. Auth is checked first, because telling someone to investigate the network when the password is wrong sends them the wrong way. The auth hint names the non obvious cause, which is that Hikvision and Dahua cameras reject the admin web login over ONVIF and want a dedicated ONVIF user made in the camera's own interface.

Vendors also disagree about their own names. Wisenet is Hanwha. TP-Link ships two incompatible lines, Tapo and VIGI, that need different profiles. Ubiquiti reports as UniFi. Brand detection is substring matching against known manufacturer strings then alias resolution, and a test checks every alias resolves to a profile that exists.

The full platform is FastAPI and PostgreSQL with an async worker pool, onvif-zeep, WS-Discovery, go2rtc for RTSP to WebRTC, Caddy in front, Docker Compose, and a Next.js dashboard. The extract here is pure standard library.

MIT.

About

ONVIF and RTSP camera discovery for a self-hosted video analytics platform. SSRF guard extracted, 112 offline tests

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages