winpeas: use KB supersedence graph for windows version vuln filtering

This commit is contained in:
Carlos Polop
2026-02-26 01:52:42 +01:00
parent 75a67b4511
commit 1bb9b22958
3 changed files with 53 additions and 82 deletions
+18 -8
View File
@@ -34,18 +34,27 @@ def build_definitions(definitions_zip: Path) -> dict:
rows.extend(_read_csv_from_zip(zf, "Custom_")) rows.extend(_read_csv_from_zip(zf, "Custom_"))
products: dict[str, dict[str, dict[str, str]]] = defaultdict(dict) products: dict[str, dict[str, dict[str, str]]] = defaultdict(dict)
kb_supersedes: dict[str, set[str]] = defaultdict(set)
for row in rows: for row in rows:
if not (row.get("Exploits") or "").strip():
continue
product = (row.get("AffectedProduct") or "").strip() product = (row.get("AffectedProduct") or "").strip()
if not product:
continue
if "windows" not in product.lower(): if "windows" not in product.lower():
continue continue
cve = (row.get("CVE") or "").strip()
kb = (row.get("BulletinKB") or "").strip() kb = (row.get("BulletinKB") or "").strip()
supersedes = (row.get("Supersedes") or "").strip()
if kb and supersedes:
for item in supersedes.split(";"):
item = item.strip()
if item:
kb_supersedes[kb].add(item)
if not (row.get("Exploits") or "").strip():
continue
if not product:
continue
cve = (row.get("CVE") or "").strip()
vuln_key = cve or f"KB{kb}" vuln_key = cve or f"KB{kb}"
if not vuln_key: if not vuln_key:
continue continue
@@ -58,13 +67,14 @@ def build_definitions(definitions_zip: Path) -> dict:
"kb": kb, "kb": kb,
"severity": (row.get("Severity") or "").strip(), "severity": (row.get("Severity") or "").strip(),
"impact": (row.get("Impact") or "").strip(), "impact": (row.get("Impact") or "").strip(),
"supersedes": (row.get("Supersedes") or "").strip(),
} }
data = {"generated": generated, "products": {}} data = {"generated": generated, "products": {}, "kb_supersedes": {}}
for product in sorted(products): for product in sorted(products):
entries = [products[product][key] for key in sorted(products[product])] entries = [products[product][key] for key in sorted(products[product])]
data["products"][product] = entries data["products"][product] = entries
for kb in sorted(kb_supersedes):
data["kb_supersedes"][kb] = sorted(kb_supersedes[kb])
return data return data
File diff suppressed because one or more lines are too long
@@ -62,7 +62,7 @@ namespace winPEAS.Info.SystemInfo
report.MatchedProducts = matchedProducts.OrderBy(p => p).ToList(); report.MatchedProducts = matchedProducts.OrderBy(p => p).ToList();
report.TotalMatchedBeforeFiltering = matchedEntries.Count; report.TotalMatchedBeforeFiltering = matchedEntries.Count;
var filteredVulns = FilterPatchedVulnerabilities(matchedEntries, installedHotfixes); var filteredVulns = FilterPatchedVulnerabilities(matchedEntries, installedHotfixes, definitions.kb_supersedes);
var vulnById = new Dictionary<string, WindowsVersionVulnEntry>(StringComparer.OrdinalIgnoreCase); var vulnById = new Dictionary<string, WindowsVersionVulnEntry>(StringComparer.OrdinalIgnoreCase);
AddEntries(filteredVulns, vulnById); AddEntries(filteredVulns, vulnById);
@@ -168,89 +168,56 @@ namespace winPEAS.Info.SystemInfo
return hotfixes; return hotfixes;
} }
private static List<WindowsVersionVulnEntry> FilterPatchedVulnerabilities(List<WindowsVersionVulnEntry> entries, HashSet<string> installedHotfixes) private static List<WindowsVersionVulnEntry> FilterPatchedVulnerabilities(
List<WindowsVersionVulnEntry> entries,
HashSet<string> installedHotfixes,
Dictionary<string, List<string>> kbSupersedes)
{ {
if (entries.Count == 0) if (entries.Count == 0)
{ {
return entries; return entries;
} }
var relevant = entries.Select(e => new RelevantVuln var suppressed = ExpandSupersededHotfixes(installedHotfixes, kbSupersedes);
return entries
.Where(entry =>
{ {
Entry = e, if (string.IsNullOrWhiteSpace(entry.kb))
Relevant = true
}).ToList();
var initialHotfixes = new HashSet<string>(installedHotfixes, StringComparer.OrdinalIgnoreCase);
// This mirrors WES behavior to allow recursive supersedence pruning.
foreach (var rv in relevant)
{ {
foreach (var ss in ParseSupersedes(rv.Entry.supersedes)) return true;
{
initialHotfixes.Add(ss);
} }
return !suppressed.Contains(entry.kb);
})
.ToList();
} }
MarkSupersededHotfixes(relevant, initialHotfixes, new HashSet<string>(StringComparer.OrdinalIgnoreCase)); private static HashSet<string> ExpandSupersededHotfixes(HashSet<string> installedHotfixes, Dictionary<string, List<string>> kbSupersedes)
var supersedes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var rv in relevant.Where(r => r.Relevant))
{ {
foreach (var ss in ParseSupersedes(rv.Entry.supersedes)) var suppressed = new HashSet<string>(installedHotfixes, StringComparer.OrdinalIgnoreCase);
if (kbSupersedes == null || kbSupersedes.Count == 0)
{ {
supersedes.Add(ss); return suppressed;
}
} }
foreach (var rv in relevant.Where(r => r.Relevant)) var queue = new Queue<string>(installedHotfixes);
while (queue.Count > 0)
{ {
if (!string.IsNullOrWhiteSpace(rv.Entry.kb) && supersedes.Contains(rv.Entry.kb)) var current = queue.Dequeue();
if (!kbSupersedes.TryGetValue(current, out var children) || children == null)
{ {
rv.Relevant = false; continue;
}
} }
return relevant.Where(r => r.Relevant).Select(r => r.Entry).ToList(); foreach (var child in children)
}
private static void MarkSupersededHotfixes(List<RelevantVuln> relevant, HashSet<string> hotfixes, HashSet<string> visited)
{ {
foreach (var hotfix in hotfixes) if (suppressed.Add(child))
{ {
MarkHotfixAndChildren(relevant, hotfix, visited); queue.Enqueue(child);
}
}
private static void MarkHotfixAndChildren(List<RelevantVuln> relevant, string hotfix, HashSet<string> visited)
{
if (string.IsNullOrWhiteSpace(hotfix) || visited.Contains(hotfix))
{
return;
}
visited.Add(hotfix);
foreach (var rv in relevant.Where(r => r.Relevant && string.Equals(r.Entry.kb, hotfix, StringComparison.OrdinalIgnoreCase)))
{
rv.Relevant = false;
foreach (var child in ParseSupersedes(rv.Entry.supersedes))
{
MarkHotfixAndChildren(relevant, child, visited);
} }
} }
} }
private static IEnumerable<string> ParseSupersedes(string supersedes) return suppressed;
{
if (string.IsNullOrWhiteSpace(supersedes))
{
return Enumerable.Empty<string>();
}
return supersedes
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrWhiteSpace(s));
} }
private static List<string> BuildCandidateProducts(Dictionary<string, string> basicInfo) private static List<string> BuildCandidateProducts(Dictionary<string, string> basicInfo)
@@ -457,6 +424,7 @@ namespace winPEAS.Info.SystemInfo
{ {
public string generated { get; set; } public string generated { get; set; }
public Dictionary<string, List<WindowsVersionVulnEntry>> products { get; set; } public Dictionary<string, List<WindowsVersionVulnEntry>> products { get; set; }
public Dictionary<string, List<string>> kb_supersedes { get; set; }
} }
internal class WindowsVersionVulnEntry internal class WindowsVersionVulnEntry
@@ -465,12 +433,5 @@ namespace winPEAS.Info.SystemInfo
public string kb { get; set; } public string kb { get; set; }
public string severity { get; set; } public string severity { get; set; }
public string impact { get; set; } public string impact { get; set; }
public string supersedes { get; set; }
}
internal class RelevantVuln
{
public WindowsVersionVulnEntry Entry { get; set; }
public bool Relevant { get; set; }
} }
} }