I want to share our device fingerprinting system that can identify hundreds of different devices based on their network signatures.This system is designed for security monitoring, asset discovery, and vulnerability assessment, and is also intended as a small addendum to the previous thread to encourage the HF JULY CONTEST.
Core Architecture
The system uses a comprehensive signature-based approach to identify devices by analyzing:
Banner Grabbing - Service banners from various ports
HTTP Headers - Web server responses
HTML Content - Web interface fingerprints
Endpoints - Specific paths that indicate device type
Port Combinations - Open ports patterns
DeviceSignatureMatcher Implementation
Code
public class DeviceSignatureMatcher : IDeviceSignatureMatcher, IDisposable
{
private readonly ILogger<DeviceSignatureMatcher> _logger;
private readonly IDeviceAnalysisConfigProvider _configProvider;
private readonly CancellationTokenSource _internalCts = new CancellationTokenSource();
private bool _disposed;
public DeviceSignatureMatcher(
ILogger<DeviceSignatureMatcher> logger,
IDeviceAnalysisConfigProvider configProvider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configProvider = configProvider ?? throw new ArgumentNullException(nameof(configProvider));
}
public async Task<DeviceSignatureMatchResult> MatchSignaturesAsync(
AnalysisResult analysisResult,
CancellationToken ct)
{
var result = new DeviceSignatureMatchResult();
if (string.IsNullOrEmpty(analysisResult.Banner) &&
(analysisResult.HttpHeaders == null || !analysisResult.HttpHeaders.Any()))
{
_logger.LogInformation("No banner or headers available for signature matching");
return result;
}
var combinedText = $"{analysisResult.Banner}\n{analysisResult.HtmlContent}\n{headerText}".ToLowerInvariant();
_logger.LogInformation("Matching against combined text");
// Create a linked token source with a timeout
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, _internalCts.Token);
linkedCts.CancelAfter(TimeSpan.FromSeconds(30));
var matches = await Task.Run(() =>
{
var foundMatches = new List<(string Type, DeviceSignatureConfig Signature)>();
foreach (var (deviceType, signature) in _configProvider.DeviceSignatures)
{
// Check if port matches
if (!signature.Ports.Contains(analysisResult.Port))
{
continue;
}
// Check each pattern
foreach (var pattern in signature.Patterns)
{
try
{
if (string.IsNullOrWhiteSpace(pattern))
{
_logger.LogWarning("Empty pattern found for device {DeviceType}", deviceType);
continue;
}
// Calculate score based on pattern matches and port
var patternScore = matchingPatterns.Sum(p => p.Length) / (float)matchingPatterns.Count;
var portScore = signature.Ports.Any(p => p < 1024) ? 0.7f : 1.0f;
// Weighted average (60% pattern score, 40% port score)
var confidence = patternScore * 0.6f + portScore * 0.4f;
return Math.Min(confidence, 1.0f);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Pattern score based on length of matched patterns
var patternScore = matchingPatterns.Sum(p => p.Length) / (float)matchingPatterns.Count;
// Port score (privileged ports indicate more reliable detection)
var portScore = signature.Ports.Any(p => p < 1024) ? 0.7f : 1.0f;
// Weighted average (60% pattern score, 40% port score)
var confidence = patternScore * 0.6f + portScore * 0.4f;
return Math.Min(confidence, 1.0f);
}
Usage Example
Code
var analysisResult = new AnalysisResult
{
IpAddress = "192.168.1.100",
Port = 80,
Banner = "HTTP/1.1 200 OK\r\nServer: Cisco-IOS\r\n",
HttpHeaders = new List<HttpHeader>
{
new() { Key = "Server", Value = "Cisco-IOS" }
},
HtmlContent = "<html><title>Cisco Device Manager</title></html>"
};
var matcher = new DeviceSignatureMatcher(logger, configProvider);
var result = await matcher.MatchSignaturesAsync(analysisResult, CancellationToken.None);
void ReloadConfiguration();
bool TryGetDeviceSignature(string deviceType, out DeviceSignatureConfig signature);
bool TryGetVendorProfile(string vendor, out VendorProfileConfig profile);
}
}
Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using SOC.Agent.Interfaces;
using SOC.Agent.Models;
namespace SOC.Agent.Providers
{
public class JsonDeviceAnalysisConfigProvider : IDeviceAnalysisConfigProvider
{
private readonly IConfigurationRoot _configuration;
private readonly string _configPath;
public DeviceAnalysisConfig AnalysisConfig { get; private set; }
public ProtocolAnalyzerConfig ProtocolAnalyzerConfig { get; private set; }
public IReadOnlyDictionary<string, DeviceSignatureConfig> DeviceSignatures { get; private set; }
public IReadOnlyDictionary<string, VendorProfileConfig> VendorProfiles { get; private set; }
public IReadOnlyDictionary<string, Dictionary<string, List<string>>> DeviceModels { get; private set; }
public IReadOnlyDictionary<string, SoftwareSignature> WebSoftwareSignatures { get; private set; }
public IReadOnlyDictionary<string, TechnologySignature> TechnologySignatures { get; private set; }
public IReadOnlyDictionary<string, VulnerabilitySignature> VulnerabilitySignatures { get; private set; }
public DeviceDetectionSettings DeviceDetectionSettings { get; private set; }
public JsonDeviceAnalysisConfigProvider(string configPath = "devicesettings.json")
{
_configPath = configPath;
if (!File.Exists(_configPath))
{
throw new FileNotFoundException($"Configuration file not found: {_configPath}");
}
WebSoftwareSignatures = _configuration.GetSection("ContentAnalysis:WebSoftwareSignatures")
.Get<Dictionary<string, SoftwareSignature>>() ?? new Dictionary<string, SoftwareSignature>();
TechnologySignatures = _configuration.GetSection("ContentAnalysis:TechnologySignatures")
.Get<Dictionary<string, TechnologySignature>>() ?? new Dictionary<string, TechnologySignature>();
VulnerabilitySignatures = _configuration.GetSection("ContentAnalysis:VulnerabilitySignatures")
.Get<Dictionary<string, VulnerabilitySignature>>() ?? new Dictionary<string, VulnerabilitySignature>();
DeviceDetectionSettings = _configuration.GetSection("DeviceDetection")
.Get<DeviceDetectionSettings>() ?? new DeviceDetectionSettings();
}
public void ReloadConfiguration()
{
_configuration.Reload();
LoadConfiguration();
}
public bool TryGetDeviceSignature(string deviceType, out DeviceSignatureConfig signature)
{
return DeviceSignatures.TryGetValue(deviceType, out signature);
}
public bool TryGetVendorProfile(string vendor, out VendorProfileConfig profile)
{
return VendorProfiles.TryGetValue(vendor, out profile);
}
}
}
Code
using System.Threading;
using System.Threading.Tasks;
using SOC.Agent.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SOC.Agent.Interfaces;
using SOC.Agent.Models;
public DeviceSignatureMatcher(
ILogger<DeviceSignatureMatcher> logger,
IDeviceAnalysisConfigProvider configProvider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configProvider = configProvider ?? throw new ArgumentNullException(nameof(configProvider));
}
#region Public Methods
public async Task<DeviceSignatureMatchResult> MatchSignaturesAsync(AnalysisResult analysisResult, CancellationToken ct)
{
if (analysisResult == null)
throw new ArgumentNullException(nameof(analysisResult));
var result = new DeviceSignatureMatchResult
{
Timestamp = DateTime.UtcNow
};
if (string.IsNullOrEmpty(analysisResult.Banner) &&
(analysisResult.HttpHeaders == null || !analysisResult.HttpHeaders.Any()) &&
string.IsNullOrEmpty(analysisResult.HtmlContent))
{
_logger.LogInformation("No data available for signature matching");
return result;
}
try
{
// Build combined text for matching
var combinedText = BuildCombinedText(analysisResult);
_logger.LogDebug("Matching against combined text of length {Length}", combinedText.Length);
// Create linked token source with timeout
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, _internalCts.Token);
linkedCts.CancelAfter(TimeSpan.FromSeconds(_configProvider.AnalysisConfig.ScanTimeoutSeconds));
var matches = await MatchSignaturesParallelAsync(combinedText, analysisResult.Port, linkedCts.Token);
if (matches.Any())
{
ProcessMatches(matches, result, combinedText);
}
else
{
_logger.LogInformation("No device signatures matched for port {Port}", analysisResult.Port);
}
// Try matching by headers if available
if (analysisResult.HttpHeaders != null && analysisResult.HttpHeaders.Any())
{
var headerMatches = await MatchByHeadersAsync(analysisResult.HttpHeaders, analysisResult.Port, ct);
if (headerMatches.Confidence > result.Confidence)
{
result = headerMatches;
}
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("Device signature matching timed out for {IP}:{Port}",
analysisResult.IpAddress, analysisResult.Port);
}
catch (Exception ex)
{
_logger.LogError(ex, "Device signature matching failed for {IP}:{Port}",
analysisResult.IpAddress, analysisResult.Port);
}
return result;
}
public async Task<DeviceSignatureMatchResult> MatchByPatternsAsync(string text, int port, CancellationToken ct)
{
var result = new DeviceSignatureMatchResult();
public async Task<DeviceSignatureMatchResult> MatchByHeadersAsync(List<HttpHeader> headers, int port, CancellationToken ct)
{
var result = new DeviceSignatureMatchResult();
if (headers == null || !headers.Any())
return result;
try
{
var text = string.Join("\n", headers.Select(h => $"{h.Key}: {h.Value}"));
public async Task<List<DeviceSignatureMatchResult>> MatchAllAsync(AnalysisResult analysisResult, CancellationToken ct)
{
var results = new List<DeviceSignatureMatchResult>();
if (analysisResult == null)
return results;
var combinedText = BuildCombinedText(analysisResult);
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, _internalCts.Token);
linkedCts.CancelAfter(TimeSpan.FromSeconds(_configProvider.AnalysisConfig.ScanTimeoutSeconds));
var matches = await Task.Run(() =>
{
var foundMatches = new ConcurrentBag<(string Type, DeviceSignatureConfig Signature, float Confidence)>();
var options = new ParallelOptions
{
CancellationToken = linkedCts.Token,
MaxDegreeOfParallelism = Environment.ProcessorCount * 2
};
Parallel.ForEach(_configProvider.DeviceSignatures, options, item =>
{
try
{
var signature = item.Value;
if (!signature.Ports.Contains(analysisResult.Port))
return;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SOC.Agent.Interfaces;
using SOC.Agent.Models;
public DeviceAnalyzer(
ILogger<DeviceAnalyzer> logger,
IDeviceSignatureMatcher signatureMatcher,
IContentAnalyzer contentAnalyzer,
IDeviceAnalysisConfigProvider configProvider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_signatureMatcher = signatureMatcher ?? throw new ArgumentNullException(nameof(signatureMatcher));
_contentAnalyzer = contentAnalyzer ?? throw new ArgumentNullException(nameof(contentAnalyzer));
_configProvider = configProvider ?? throw new ArgumentNullException(nameof(configProvider));
}
public async Task<DeviceAnalysisResult> AnalyzeDeviceAsync(
string ipAddress,
int port,
CancellationToken ct)
{
var result = new DeviceAnalysisResult
{
IpAddress = ipAddress,
Port = port,
Timestamp = DateTime.UtcNow
};
try
{
// Step 1: Gather initial data
var analysisResult = await GatherDeviceDataAsync(ipAddress, port, ct);
// Step 2: Match signatures
var signatureResult = await _signatureMatcher.MatchSignaturesAsync(analysisResult, ct);
public async Task<DeviceAnalysisResult> AnalyzeDeviceAsync(
string ipAddress,
int port,
AnalysisResult analysisResult,
CancellationToken ct)
{
var result = new DeviceAnalysisResult
{
IpAddress = ipAddress,
Port = port,
Timestamp = DateTime.UtcNow
};
try
{
// Match signatures
var signatureResult = await _signatureMatcher.MatchSignaturesAsync(analysisResult, ct);
public async Task<List<DeviceAnalysisResult>> AnalyzeNetworkRangeAsync(
string network,
int[] ports,
CancellationToken ct)
{
var results = new List<DeviceAnalysisResult>();
var tasks = new List<Task>();
// Parse network range (simplified)
var ips = ParseNetworkRange(network);
var semaphore = new SemaphoreSlim(_configProvider.AnalysisConfig.MaxConcurrentScans);
foreach (var ip in ips)
{
foreach (var port in ports)
{
tasks.Add(Task.Run(async () =>
{
await semaphore.WaitAsync(ct);
try
{
var result = await AnalyzeDeviceAsync(ip, port, ct);
lock (results)
{
results.Add(result);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to analyze {IP}:{Port}", ip, port);
}
finally
{
semaphore.Release();
}
}, ct));
}
}
private async Task<AnalysisResult> GatherDeviceDataAsync(
string ipAddress,
int port,
CancellationToken ct)
{
// This would connect to the device and gather banner, headers, etc.
// Simplified for example
return new AnalysisResult
{
IpAddress = ipAddress,
Port = port,
Timestamp = DateTime.UtcNow
};
}
private List<string> ParseNetworkRange(string network)
{
// Simplified network range parsing
// In production, use proper IP range parsing
var ips = new List<string>();
// Example: "192.168.1.0/24" -> 192.168.1.1 to 192.168.1.254
// This is a placeholder - implement proper parsing
for (int i = 1; i < 255; i++)
{
ips.Add($"192.168.1.{i}");
}
return ips;
}
}
}
Code
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using SOC.Agent.Models;
namespace SOC.Agent.Interfaces
{
public interface IDeviceAnalyzer
{
Task<DeviceAnalysisResult> AnalyzeDeviceAsync(
string ipAddress,
int port,
CancellationToken ct);
Wow, it is insane to me that you just post stuff like this because you want to share something HQ.
I truly appreciate every thread you make, there is so much to learn from reading your code and seeing whats possible!
This is true legends high quality and a perfect start into this July contest!
(Jul 4, 2026 11:31 AM)HerrWinston Wrote: Wow, it is insane to me that you just post stuff like this because you want to share something HQ.
I truly appreciate every thread you make, there is so much to learn from reading your code and seeing whats possible!
This is true legends high quality and a perfect start into this July contest!
Thank you for the kind words, I'm glad the post was informative for you
(Jul 10, 2026 05:11 PM)itcorp Wrote: thank you for the kind words, I'm available for any questions, but I'm also interested in your view of the whole story and idea
Oops I think you posted several things in a single post, a similar thread, yet somehow I commented wrong thread lol. I’ll take a look after to see which of your posts I really meant that for.
But this looks pretty slick too
Edit: maybe I gotta read everything again when I’m not braindead tired. This is the post lol