153 lines
5.2 KiB
Python
153 lines
5.2 KiB
Python
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import socket
|
|
import subprocess
|
|
import uuid
|
|
from typing import List, Tuple, Dict, Optional
|
|
|
|
|
|
class HardwareProvider:
|
|
"""Abstracts hardware fingerprint collection."""
|
|
def fingerprint(self) -> Tuple[str, Dict[str, str]]:
|
|
raise NotImplementedError
|
|
|
|
|
|
class DefaultHardwareProvider(HardwareProvider):
|
|
"""
|
|
Collects host-agnostic identifiers.
|
|
Supported field names (case-insensitive):
|
|
- mac: first non-loopback MAC address (default)
|
|
- hostname: OS host name (default)
|
|
- cpu: CPU model / brand string (best-effort)
|
|
- memory: total physical memory capacity (best-effort)
|
|
"""
|
|
def __init__(self, fields: Optional[List[str]] = None):
|
|
if not fields:
|
|
self.fields = ["mac", "hostname"]
|
|
else:
|
|
self.fields = [f.strip().lower() for f in fields if f.strip()]
|
|
if not self.fields:
|
|
self.fields = ["mac", "hostname"]
|
|
|
|
def fingerprint(self) -> Tuple[str, Dict[str, str]]:
|
|
data: Dict[str, str] = {}
|
|
for field in self.fields:
|
|
if field == "hostname":
|
|
data["hostname"] = self._get_hostname()
|
|
elif field == "mac":
|
|
mac = self._get_mac()
|
|
if mac:
|
|
data["mac"] = mac
|
|
elif field == "cpu":
|
|
cpu = self._get_cpu()
|
|
if cpu:
|
|
data["cpu"] = cpu
|
|
elif field == "memory":
|
|
mem = self._get_memory()
|
|
if mem:
|
|
data["memory"] = mem
|
|
|
|
# Deterministic serialization
|
|
keys = sorted(data.keys())
|
|
builder = []
|
|
for k in keys:
|
|
val = str(data[k]).lower()
|
|
builder.append(f"{k}={val};")
|
|
|
|
full_string = "".join(builder)
|
|
hash_sum = hashlib.sha256(full_string.encode('utf-8')).hexdigest()
|
|
return hash_sum, data
|
|
|
|
def _get_hostname(self) -> str:
|
|
try:
|
|
return socket.gethostname()
|
|
except Exception:
|
|
return "unknown"
|
|
|
|
def _get_mac(self) -> str:
|
|
try:
|
|
# uuid.getnode() returns MAC of the first active interface
|
|
# or a random 48-bit number if not found (has multicast bit set)
|
|
node = uuid.getnode()
|
|
if (node >> 40) % 2 == 0: # Not a random MAC
|
|
mac = ':'.join(('%012x' % node)[i:i+2] for i in range(0, 12, 2))
|
|
return mac.lower()
|
|
return ""
|
|
except Exception:
|
|
return ""
|
|
|
|
def _get_cpu(self) -> str:
|
|
system = platform.system().lower()
|
|
try:
|
|
if system == "linux":
|
|
with open("/proc/cpuinfo", "r") as f:
|
|
for line in f:
|
|
if line.lower().startswith("model name"):
|
|
return line.split(":")[1].strip()
|
|
elif system == "darwin":
|
|
return self._cmd_output(["sysctl", "-n", "machdep.cpu.brand_string"])
|
|
elif system == "windows":
|
|
wmic_out = self._cmd_output(["wmic", "cpu", "get", "Name"])
|
|
if wmic_out and "name" not in wmic_out.lower():
|
|
return wmic_out
|
|
# Fallback
|
|
env_proc = os.getenv("PROCESSOR_IDENTIFIER")
|
|
if env_proc:
|
|
return env_proc
|
|
except Exception:
|
|
pass
|
|
|
|
return platform.processor() or platform.machine()
|
|
|
|
def _get_memory(self) -> str:
|
|
system = platform.system().lower()
|
|
try:
|
|
if system == "linux":
|
|
with open("/proc/meminfo", "r") as f:
|
|
for line in f:
|
|
if line.lower().startswith("memtotal"):
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
val_kb = int(parts[1])
|
|
return self._format_bytes(val_kb * 1024)
|
|
elif system == "darwin":
|
|
out = self._cmd_output(["sysctl", "-n", "hw.memsize"])
|
|
if out:
|
|
return self._format_bytes(int(out))
|
|
elif system == "windows":
|
|
out = self._cmd_output(["wmic", "computersystem", "get", "TotalPhysicalMemory"])
|
|
if out:
|
|
return self._format_bytes(int(out))
|
|
except Exception:
|
|
pass
|
|
return ""
|
|
|
|
def _cmd_output(self, cmd: List[str]) -> str:
|
|
try:
|
|
out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, timeout=2).decode('utf-8')
|
|
lines = [line.strip() for line in out.split('\n') if line.strip()]
|
|
for line in lines:
|
|
lower_line = line.lower()
|
|
if "name" in lower_line or "totalphysicalmemory" in lower_line:
|
|
continue
|
|
return line
|
|
return ""
|
|
except Exception:
|
|
return ""
|
|
|
|
def _format_bytes(self, b: int) -> str:
|
|
kb = 1024
|
|
mb = 1024 * kb
|
|
gb = 1024 * mb
|
|
if b >= gb:
|
|
return f"{b // gb}GB"
|
|
elif b >= mb:
|
|
return f"{b // mb}MB"
|
|
elif b >= kb:
|
|
return f"{b // kb}KB"
|
|
else:
|
|
return f"{b}B"
|