Skip to content

feat(scanner): device-type classifier (P2-03 / 26.11)#14

Merged
CryptoJones merged 1 commit into
mainfrom
sprint/26.11-device-classifier
May 27, 2026
Merged

feat(scanner): device-type classifier (P2-03 / 26.11)#14
CryptoJones merged 1 commit into
mainfrom
sprint/26.11-device-classifier

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Summary

Populates `Host.DeviceType` automatically. Until now the field existed in the schema and was rendered by all three admin templates, but nothing ever wrote it. After this PR the admin console shows tags like `printer`, `router`, `windows-server`, `database (postgres)`, `linux-host`, …

Pure heuristic — no new daemons, no new deps, no new network calls beyond the ones already happening. Just rules over the (vendor, os fingerprint, open-port set) tuple the scanner already collects.

Classifier rules (in priority order)

Match Label
Port 9100, 631, or 515 `printer`
MikroTik vendor OR port 8728/8729 `router`
Cisco vendor + management port `router`
VMware vendor + 902/5988/5989 `hypervisor`
Port 3306 / 5432 / 1433 / 27017 / 6379 / 11211 `database (mysql
Port 88+389 `windows-dc`
Port 445+web `windows-server`
Port 445 or 3389 `windows-host`
SMTP + IMAP combo `mail-server`
Port 53 (tcp or udp) without 88+389 `dns-server`
Port 1883/8883 `iot-broker`
Raspberry Pi vendor `embedded`
OpenSSH banner `linux-host`
Web port only, no shell `appliance`
SSH alone `linux-host`
(no match) `""`

Conservative: "" beats a wrong tag.

Test plan

  • `go test -race ./internal/scanner/...` — 27 classify table cases + integration test confirms the scanner round-trips DeviceType for a host listening on memcached/11211
  • `golangci-lint run ./...` — 0 issues
  • Mock host store parity bug caught + fixed: `mockHostStore.Upsert` was no-op on conflict, diverging from the real sqlite UPSERT; classifier path exposed it

Notes

  • Per-host cost: one extra small SQL write per live host per scan cycle, only when classify() returns non-empty.
  • Admin templates already had `{{.DeviceType}}` blocks since 26.06.

🤖 Generated with Claude Code

Pure heuristic that maps (vendor, os fingerprint, open TCP + UDP
ports) to a coarse DeviceType label: printer / router / hypervisor /
windows-host / windows-server / windows-dc / mail-server / dns-server
/ database (mysql|postgres|mssql|mongodb|redis|memcached) / iot-broker
/ embedded / linux-host / appliance. Conservative — returns "" rather
than guess when no rule fires confidently.

The admin console templates already had {{.DeviceType}} blocks
plumbed since 26.06; they just had no data to show. Now they do.

Mechanism:
- internal/scanner/classify.go: 27 rules in priority order, with a
  table-driven test for every one
- deepScan / udpScan now return their open-port lists so the per-
  host goroutine has the full picture for classify()
- Re-upsert the host with DeviceType after probing completes if the
  classifier returned non-empty; one extra small SQL write per live
  host per cycle

Bug caught in passing:
- mockHostStore.Upsert returned the existing row unchanged on
  conflict instead of mirroring the real INSERT … ON CONFLICT
  UPDATE. The classifier path would have silently lost the
  device_type write against any real store. Fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CryptoJones CryptoJones merged commit 36b4a05 into main May 27, 2026
@CryptoJones CryptoJones deleted the sprint/26.11-device-classifier branch May 27, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant