Add optional Fingerbank device fingerprinting to deep-check
CI / web (push) Successful in 16s
CI / api (push) Successful in 21s

Folded into the existing on-demand Deep check button: queries
Fingerbank's interrogate API with the device's MAC plus the SSDP
SERVER header when deep-check-device.sh finds one, showing the
confidence band alongside the result. Runs directly from the API
container (no host-level access needed, just an outbound HTTPS call),
unlike the SSDP/mDNS steps.

Confirmed via direct testing: without DHCP fingerprint data (which we
structurally don't have, not being the DHCP server), MAC-only queries
often can't get past manufacturer-level confidence -- same info the
free OUI lookup already provides. Documented honestly in
docs/device-discovery.md rather than overselling it. Still worth
having as opt-in enrichment for devices that do expose richer signals.

Gated behind optional FINGERBANK_API_KEY -- missing key, API errors,
or no match all degrade gracefully without affecting the rest of
deep-check's local findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:35:35 -06:00
parent ee9b6d15cd
commit d0d6ae95f1
11 changed files with 183 additions and 13 deletions
+54
View File
@@ -0,0 +1,54 @@
export interface FingerbankResult {
deviceName: string | null;
score: number | null;
version: string | null;
}
export interface FingerbankSignals {
mac: string;
// "Upnp string" per Fingerbank's docs -- we pass the SSDP SERVER header
// captured by deep-check-device.sh, when present (e.g.
// "Linux/3.10 UPnP/1.0 MiniDLNA/1.1"), as extra fingerprinting signal
// beyond the bare MAC.
upnpServer: string | null;
}
// On-demand only (folded into the existing deep-check button), never
// automatic/bulk -- keeps this well under Fingerbank's free-tier query
// limits and avoids sending every device's MAC to a third party by default.
export async function identifyDevice(
apiKey: string,
signals: FingerbankSignals
): Promise<FingerbankResult | null> {
const body: Record<string, unknown> = { mac: signals.mac };
if (signals.upnpServer) body.upnp_user_agents = [signals.upnpServer];
const res = await fetch("https://api.fingerbank.org/api/v2/combinations/interrogate", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status === 404) {
// No match in Fingerbank's database -- not an error, just nothing found.
return null;
}
if (!res.ok) {
throw new Error(`Fingerbank API returned ${res.status}`);
}
const data = (await res.json()) as {
device_name?: string;
score?: number;
version?: string;
};
return {
deviceName: data.device_name ?? null,
score: data.score ?? null,
version: data.version ?? null,
};
}