mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
normalize CPE strings
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using AutopilotMonitor.Functions.Services.Vulnerability;
|
||||
|
||||
namespace AutopilotMonitor.Functions.Tests;
|
||||
|
||||
public class CpeUriNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Normalize_UrlEncodedPlus_LowerCase_BecomesBackslashQuoted()
|
||||
{
|
||||
Assert.Equal(
|
||||
@"cpe:2.3:a:microsoft:visual_c\+\+",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:microsoft:visual_c%2b%2b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_UrlEncodedPlus_UpperCase_BecomesBackslashQuoted()
|
||||
{
|
||||
Assert.Equal(
|
||||
@"cpe:2.3:a:microsoft:visual_c\+\+",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:microsoft:visual_c%2B%2B"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_BarePlus_BecomesBackslashQuoted()
|
||||
{
|
||||
Assert.Equal(
|
||||
@"cpe:2.3:a:microsoft:visual_c\+\+",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:microsoft:visual_c++"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AlreadyQuoted_IsIdempotent()
|
||||
{
|
||||
var canonical = @"cpe:2.3:a:microsoft:visual_c\+\+";
|
||||
Assert.Equal(canonical, CpeUriNormalizer.Normalize(canonical));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_NoSpecialChars_PassesThrough()
|
||||
{
|
||||
Assert.Equal(
|
||||
"cpe:2.3:a:adobe:acrobat_dc",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:adobe:acrobat_dc"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_NullOrEmpty_ReturnsEmpty()
|
||||
{
|
||||
Assert.Equal("", CpeUriNormalizer.Normalize(null));
|
||||
Assert.Equal("", CpeUriNormalizer.Normalize(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_OtherPercentSequences_AreNotTouched()
|
||||
{
|
||||
// %20 (space), %2F (slash) etc. must NOT be rewritten — only %2B/%2b.
|
||||
Assert.Equal(
|
||||
"cpe:2.3:a:vendor:foo%20bar",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:vendor:foo%20bar"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_Wildcard_IsNotTouched()
|
||||
{
|
||||
// '*' is the CPE 2.3 ANY wildcard and must remain unquoted.
|
||||
Assert.Equal(
|
||||
"cpe:2.3:a:microsoft:windows:*:*",
|
||||
CpeUriNormalizer.Normalize("cpe:2.3:a:microsoft:windows:*:*"));
|
||||
}
|
||||
}
|
||||
+4
@@ -52,6 +52,10 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
|
||||
return badReq;
|
||||
}
|
||||
|
||||
// Normalize CPE 2.3 quoting (%2b/%2b/bare '+' -> '\+'). See CpeUriNormalizer for rationale.
|
||||
mapping.CpeUri = CpeUriNormalizer.Normalize(mapping.CpeUri);
|
||||
mapping.CpeProduct = CpeUriNormalizer.Normalize(mapping.CpeProduct);
|
||||
|
||||
var rowKey = $"{(mapping.NormalizedVendor ?? "unknown")}:{mapping.NormalizedProduct}".ToLowerInvariant();
|
||||
// Sanitize RowKey (Table Storage doesn't allow /, \, #, ?)
|
||||
rowKey = rowKey.Replace("/", "_").Replace("\\", "_").Replace("#", "_").Replace("?", "_");
|
||||
|
||||
@@ -864,8 +864,8 @@ namespace AutopilotMonitor.Functions.Services
|
||||
{ "NormalizedVendor", mapping.NormalizedVendor ?? "" },
|
||||
{ "NormalizedProduct", mapping.NormalizedProduct ?? "" },
|
||||
{ "CpeVendor", mapping.CpeVendor ?? "" },
|
||||
{ "CpeProduct", mapping.CpeProduct ?? "" },
|
||||
{ "CpeUri", mapping.CpeUri ?? "" },
|
||||
{ "CpeProduct", CpeUriNormalizer.Normalize(mapping.CpeProduct) },
|
||||
{ "CpeUri", CpeUriNormalizer.Normalize(mapping.CpeUri) },
|
||||
{ "Category", mapping.Category ?? "" },
|
||||
{ "DisplayNamePatternsJson", System.Text.Json.JsonSerializer.Serialize(mapping.DisplayNamePatterns ?? new List<string>()) },
|
||||
{ "ExcludePatternsJson", System.Text.Json.JsonSerializer.Serialize(mapping.ExcludePatterns ?? new List<string>()) },
|
||||
|
||||
+7
@@ -417,6 +417,13 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability
|
||||
var mapping = CpeMatchScorer.BuildMappingFromNvdResult(
|
||||
cpeEntry, normalizedVendor, normalizedName, displayName);
|
||||
|
||||
// Defense-in-depth: ensure CPE 2.3 quoting is canonical before we
|
||||
// persist or pass this mapping to the NVD API. NVD itself returns
|
||||
// the backslash form already, so this is normally a no-op, but
|
||||
// protects against any upstream that returns URL-encoded variants.
|
||||
mapping.CpeUri = CpeUriNormalizer.Normalize(mapping.CpeUri);
|
||||
mapping.CpeProduct = CpeUriNormalizer.Normalize(mapping.CpeProduct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"CPE auto-resolve HIT: '{DisplayName}' -> {CpeUri} (score={Score:F2}, candidates={CandidateCount}, cpeName={CpeName})",
|
||||
displayName, mapping.CpeUri, score, candidateCount, cpeEntry.CpeName);
|
||||
|
||||
Reference in New Issue
Block a user