Use HackTricks package criticality only

This commit is contained in:
Carlos Polop
2026-06-30 15:57:25 +02:00
parent 8a9a5e2c94
commit 09f5bf7edc
2 changed files with 16 additions and 421 deletions
@@ -275,7 +275,7 @@ namespace winPEAS.Tests
}
[TestMethod]
public void PackageVulnerabilityFormatter_SortsByCriticalityBeforeCapping()
public void PackageVulnerabilityFormatter_SortsHackTricksCriticalityBeforeCapping()
{
var responseJson =
"{\"package_vulnerabilities\":{\"checked\":4,\"affected\":4,\"vulnerable_packages\":[" +
@@ -295,20 +295,20 @@ namespace winPEAS.Tests
}
[TestMethod]
public void PackageVulnerabilityFormatter_UsesCvssCriticalityWhenSeverityMissing()
public void PackageVulnerabilityFormatter_UsesHackTricksCvssWhenSeverityMissing()
{
var responseJson =
"{\"package_vulnerabilities\":{\"checked\":3,\"affected\":3,\"vulnerable_packages\":[" +
"{\"name\":\"mediumcvss\",\"version\":\"1.0\",\"manager\":\"windows-registry\",\"cvss_score\":5.0,\"vulns\":[\"CVE-2026-10001\"]}," +
"{\"name\":\"criticalcvss\",\"version\":\"2.0\",\"manager\":\"windows-registry\",\"cvss_score\":9.8,\"vulns\":[\"CVE-2026-10002\"]}," +
"{\"name\":\"highobject\",\"version\":\"3.0\",\"manager\":\"windows-registry\",\"vulns\":[{\"id\":\"CVE-2026-10003\",\"severity\":\"high\",\"cvss_score\":8.1}]}" +
"{\"name\":\"highcvss\",\"version\":\"3.0\",\"manager\":\"windows-registry\",\"cvss_score\":8.1,\"vulns\":[\"CVE-2026-10003\"]}" +
"]}}";
var summary = winPEAS.Info.NetworkInfo.HackTricksHostChecker.ParsePackageVulnerabilities(responseJson, 50);
Assert.AreEqual(3, summary.Lines.Count);
Assert.IsTrue(summary.Lines[0].StartsWith("- criticalcvss 2.0 [windows-registry] [Critical]: CVE-2026-10002"));
Assert.IsTrue(summary.Lines[1].StartsWith("- highobject 3.0 [windows-registry] [High]: CVE-2026-10003"));
Assert.IsTrue(summary.Lines[1].StartsWith("- highcvss 3.0 [windows-registry] [High]: CVE-2026-10003"));
Assert.IsTrue(summary.Lines[2].StartsWith("- mediumcvss 1.0 [windows-registry] [Medium]: CVE-2026-10001"));
}
}
@@ -35,25 +35,13 @@ namespace winPEAS.Info.NetworkInfo
public int OriginalIndex { get; set; }
public int SeverityPriority { get; set; }
public double CvssScore { get; set; }
public string Severity { get; set; }
public string Line { get; set; }
public List<string> VulnerabilityIds { get; } = new List<string>();
}
internal sealed class VulnerabilityCriticality
{
public int SeverityPriority { get; set; }
public double CvssScore { get; set; }
public string Severity { get; set; }
}
public static class HackTricksHostChecker
{
private const int TimeoutSeconds = 15;
private const int OsvTimeoutSeconds = 12;
private const int MaxOsvVulnerabilityLookups = 100;
private const string DefaultUrl = "https://tools.hacktricks.wiki/api/host-checker";
private const string OsvVulnerabilityUrl = "https://api.osv.dev/v1/vulns/";
private static readonly object LockObj = new object();
private static Task<HostCheckerResult> lookupTask;
private static bool startedWithPackages;
@@ -107,21 +95,10 @@ namespace winPEAS.Info.NetworkInfo
return new PackageVulnerabilitySummary { Error = result.Error };
}
return ParsePackageVulnerabilities(
result.RawResponse,
maxLines,
ResolveVulnerabilityCriticalitiesFromOsv);
return ParsePackageVulnerabilities(result.RawResponse, maxLines);
}
public static PackageVulnerabilitySummary ParsePackageVulnerabilities(string responseJson, int maxLines)
{
return ParsePackageVulnerabilities(responseJson, maxLines, null);
}
private static PackageVulnerabilitySummary ParsePackageVulnerabilities(
string responseJson,
int maxLines,
Func<IEnumerable<string>, Dictionary<string, VulnerabilityCriticality>> vulnerabilityCriticalityResolver)
{
var summary = new PackageVulnerabilitySummary();
if (string.IsNullOrWhiteSpace(responseJson))
@@ -173,21 +150,17 @@ namespace winPEAS.Info.NetworkInfo
var sourcePart = !string.IsNullOrWhiteSpace(source) ? $" [{source}]" : string.Empty;
var severity = GetPackageSeverity(package);
var severityPart = !string.IsNullOrWhiteSpace(severity) ? $" [{severity}]" : string.Empty;
var entry = new PackageVulnerabilityEntry
var cvssScore = GetPackageCvssScore(package);
entries.Add(new PackageVulnerabilityEntry
{
OriginalIndex = index,
Severity = severity,
SeverityPriority = GetSeverityPriority(severity),
CvssScore = GetPackageCvssScore(package),
CvssScore = cvssScore,
Line = $"- {name} {version}{sourcePart}{severityPart}: {string.Join(", ", vulns)}".Trim()
};
entry.VulnerabilityIds.AddRange(vulns);
entries.Add(entry);
});
index++;
}
ApplyResolvedCriticalities(entries, vulnerabilityCriticalityResolver);
summary.Lines.AddRange(entries
.OrderByDescending(entry => entry.SeverityPriority)
.ThenByDescending(entry => entry.CvssScore)
@@ -206,165 +179,6 @@ namespace winPEAS.Info.NetworkInfo
return summary;
}
private static void ApplyResolvedCriticalities(
List<PackageVulnerabilityEntry> entries,
Func<IEnumerable<string>, Dictionary<string, VulnerabilityCriticality>> vulnerabilityCriticalityResolver)
{
if (vulnerabilityCriticalityResolver == null || entries.Count == 0)
{
return;
}
var vulnerabilityIds = entries
.SelectMany(entry => entry.VulnerabilityIds)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (vulnerabilityIds.Count == 0)
{
return;
}
Dictionary<string, VulnerabilityCriticality> criticalities;
try
{
criticalities = vulnerabilityCriticalityResolver(vulnerabilityIds);
}
catch
{
return;
}
if (criticalities == null || criticalities.Count == 0)
{
return;
}
foreach (var entry in entries)
{
var resolved = entry.VulnerabilityIds
.Where(id => criticalities.ContainsKey(id))
.Select(id => criticalities[id])
.OrderByDescending(criticality => criticality.SeverityPriority)
.ThenByDescending(criticality => criticality.CvssScore)
.FirstOrDefault();
if (resolved == null || resolved.SeverityPriority <= entry.SeverityPriority)
{
continue;
}
entry.Severity = resolved.Severity;
entry.SeverityPriority = resolved.SeverityPriority;
entry.CvssScore = resolved.CvssScore;
entry.Line = AddSeverityToLine(entry.Line, resolved.Severity);
}
}
private static string AddSeverityToLine(string line, string severity)
{
severity = FormatSeverity(severity);
if (string.IsNullOrWhiteSpace(severity) || string.IsNullOrWhiteSpace(line))
{
return line;
}
var separator = line.IndexOf(": ", StringComparison.Ordinal);
if (separator < 0)
{
return line;
}
return line.Substring(0, separator) + $" [{severity}]" + line.Substring(separator);
}
private static Dictionary<string, VulnerabilityCriticality> ResolveVulnerabilityCriticalitiesFromOsv(IEnumerable<string> vulnerabilityIds)
{
var ids = vulnerabilityIds
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Take(MaxOsvVulnerabilityLookups)
.ToList();
var results = new Dictionary<string, VulnerabilityCriticality>(StringComparer.OrdinalIgnoreCase);
if (ids.Count == 0)
{
return results;
}
using (var httpClient = new HttpClient())
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(OsvTimeoutSeconds)))
{
httpClient.Timeout = TimeSpan.FromSeconds(OsvTimeoutSeconds);
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("winpeas");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var tasks = ids.Select(id => GetOsvCriticality(httpClient, cts.Token, id)).ToArray();
Task.WaitAll(tasks, TimeSpan.FromSeconds(OsvTimeoutSeconds));
foreach (var task in tasks)
{
if (task.Status == TaskStatus.RanToCompletion && task.Result.Value != null)
{
results[task.Result.Key] = task.Result.Value;
}
}
}
return results;
}
private static async Task<KeyValuePair<string, VulnerabilityCriticality>> GetOsvCriticality(
HttpClient httpClient,
CancellationToken cancellationToken,
string vulnerabilityId)
{
try
{
var url = OsvVulnerabilityUrl + Uri.EscapeDataString(vulnerabilityId);
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return new KeyValuePair<string, VulnerabilityCriticality>(vulnerabilityId, null);
}
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
using (var doc = JsonDocument.Parse(body))
{
return new KeyValuePair<string, VulnerabilityCriticality>(
vulnerabilityId,
GetOsvCriticality(doc.RootElement));
}
}
catch
{
return new KeyValuePair<string, VulnerabilityCriticality>(vulnerabilityId, null);
}
}
private static VulnerabilityCriticality GetOsvCriticality(JsonElement root)
{
var best = new VulnerabilityCriticality();
if (!root.TryGetProperty("severity", out var severities) || severities.ValueKind != JsonValueKind.Array)
{
return best;
}
foreach (var severity in severities.EnumerateArray())
{
var scoreText = GetString(severity, "score");
var cvssScore = GetCvssScore(scoreText);
var severityName = GetSeverityFromCvss(cvssScore);
var priority = GetSeverityPriority(severityName);
if (priority > best.SeverityPriority ||
(priority == best.SeverityPriority && cvssScore > best.CvssScore))
{
best.Severity = severityName;
best.SeverityPriority = priority;
best.CvssScore = cvssScore;
}
}
return best;
}
private static void EnsureStarted(bool includePackages)
{
Start(
@@ -530,51 +344,9 @@ namespace winPEAS.Info.NetworkInfo
}
}
if (package.TryGetProperty("severities", out var severities) && severities.ValueKind == JsonValueKind.Array)
{
foreach (var item in severities.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.String)
{
continue;
}
var candidate = item.GetString();
var candidatePriority = GetSeverityPriority(candidate);
if (candidatePriority > priority)
{
severity = candidate;
priority = candidatePriority;
}
}
}
if (package.TryGetProperty("vulns", out var vulns) && vulns.ValueKind == JsonValueKind.Array)
{
foreach (var vuln in vulns.EnumerateArray())
{
if (vuln.ValueKind != JsonValueKind.Object)
{
continue;
}
foreach (var propertyName in new[] { "severity", "max_severity", "criticality", "risk", "level" })
{
var candidate = GetString(vuln, propertyName);
var candidatePriority = GetSeverityPriority(candidate);
if (candidatePriority > priority)
{
severity = candidate;
priority = candidatePriority;
}
}
}
}
if (priority == 0)
{
var cvssScore = GetPackageCvssScore(package);
severity = GetSeverityFromCvss(cvssScore);
severity = GetSeverityFromCvss(GetPackageCvssScore(package));
}
return FormatSeverity(severity);
@@ -587,23 +359,6 @@ namespace winPEAS.Info.NetworkInfo
{
score = Math.Max(score, GetScore(package, propertyName));
}
if (package.TryGetProperty("vulns", out var vulns) && vulns.ValueKind == JsonValueKind.Array)
{
foreach (var vuln in vulns.EnumerateArray())
{
if (vuln.ValueKind != JsonValueKind.Object)
{
continue;
}
foreach (var propertyName in new[] { "cvss", "cvss_score", "cvssScore", "max_cvss", "max_cvss_score", "score" })
{
score = Math.Max(score, GetScore(vuln, propertyName));
}
}
}
return score;
}
@@ -648,7 +403,6 @@ namespace winPEAS.Info.NetworkInfo
{
return "Low";
}
return string.Empty;
}
@@ -659,7 +413,6 @@ namespace winPEAS.Info.NetworkInfo
{
return string.Empty;
}
return char.ToUpperInvariant(severity[0]) + severity.Substring(1).ToLowerInvariant();
}
@@ -669,160 +422,21 @@ namespace winPEAS.Info.NetworkInfo
{
return 0.0;
}
if (property.ValueKind == JsonValueKind.Number && property.TryGetDouble(out var value))
{
return value;
}
if (property.ValueKind == JsonValueKind.String)
{
return GetCvssScore(property.GetString());
}
return 0.0;
}
private static double GetCvssScore(string score)
{
score = (score ?? "").Trim();
if (double.TryParse(
score,
if (property.ValueKind == JsonValueKind.String && double.TryParse(
property.GetString(),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var numericScore))
out value))
{
return numericScore;
return value;
}
if (score.StartsWith("CVSS:3.", StringComparison.OrdinalIgnoreCase))
{
return GetCvssV3Score(score);
}
return 0.0;
}
private static double GetCvssV3Score(string vector)
{
var metrics = vector
.Split('/')
.Skip(1)
.Select(part => part.Split(':'))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1], StringComparer.OrdinalIgnoreCase);
if (!metrics.TryGetValue("AV", out var av) ||
!metrics.TryGetValue("AC", out var ac) ||
!metrics.TryGetValue("PR", out var pr) ||
!metrics.TryGetValue("UI", out var ui) ||
!metrics.TryGetValue("S", out var scope) ||
!metrics.TryGetValue("C", out var c) ||
!metrics.TryGetValue("I", out var i) ||
!metrics.TryGetValue("A", out var a))
{
return 0.0;
}
var scopeChanged = string.Equals(scope, "C", StringComparison.OrdinalIgnoreCase);
var impact = 1 - ((1 - GetCvssImpactValue(c)) * (1 - GetCvssImpactValue(i)) * (1 - GetCvssImpactValue(a)));
var impactSubScore = scopeChanged
? 7.52 * (impact - 0.029) - 3.25 * Math.Pow(impact - 0.02, 15)
: 6.42 * impact;
if (impactSubScore <= 0)
{
return 0.0;
}
var exploitability = 8.22 *
GetCvssAttackVectorValue(av) *
GetCvssAttackComplexityValue(ac) *
GetCvssPrivilegesRequiredValue(pr, scopeChanged) *
GetCvssUserInteractionValue(ui);
var rawScore = scopeChanged
? Math.Min(1.08 * (impactSubScore + exploitability), 10)
: Math.Min(impactSubScore + exploitability, 10);
return RoundUpCvss(rawScore);
}
private static double RoundUpCvss(double value)
{
return Math.Ceiling((value - 0.000001) * 10.0) / 10.0;
}
private static double GetCvssAttackVectorValue(string value)
{
switch ((value ?? "").ToUpperInvariant())
{
case "N":
return 0.85;
case "A":
return 0.62;
case "L":
return 0.55;
case "P":
return 0.20;
default:
return 0.0;
}
}
private static double GetCvssAttackComplexityValue(string value)
{
switch ((value ?? "").ToUpperInvariant())
{
case "L":
return 0.77;
case "H":
return 0.44;
default:
return 0.0;
}
}
private static double GetCvssPrivilegesRequiredValue(string value, bool scopeChanged)
{
switch ((value ?? "").ToUpperInvariant())
{
case "N":
return 0.85;
case "L":
return scopeChanged ? 0.68 : 0.62;
case "H":
return scopeChanged ? 0.50 : 0.27;
default:
return 0.0;
}
}
private static double GetCvssUserInteractionValue(string value)
{
switch ((value ?? "").ToUpperInvariant())
{
case "N":
return 0.85;
case "R":
return 0.62;
default:
return 0.0;
}
}
private static double GetCvssImpactValue(string value)
{
switch ((value ?? "").ToUpperInvariant())
{
case "H":
return 0.56;
case "L":
return 0.22;
case "N":
return 0.0;
default:
return 0.0;
}
}
private static List<string> GetStringArray(JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
@@ -832,27 +446,8 @@ namespace winPEAS.Info.NetworkInfo
return property
.EnumerateArray()
.Select(item =>
{
if (item.ValueKind == JsonValueKind.String)
{
return item.GetString();
}
if (item.ValueKind == JsonValueKind.Object)
{
foreach (var propertyNameCandidate in new[] { "id", "cve", "name", "vulnerability_id" })
{
var value = GetString(item, propertyNameCandidate);
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
}
return string.Empty;
})
.Where(item => item.ValueKind == JsonValueKind.String)
.Select(item => item.GetString())
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToList();
}