release orchestration strengthening
This commit is contained in:
@@ -319,10 +319,17 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
// Best single hypothesis
|
||||
var bestHint = sorted[0];
|
||||
|
||||
var bestKey = GetAgreementKey(bestHint) ?? bestHint.Hypothesis;
|
||||
|
||||
// If we have multiple high-confidence hints that agree, boost confidence
|
||||
var agreeing = sorted
|
||||
.Where(h => h.Confidence >= 0.5)
|
||||
.GroupBy(GetAgreementKey)
|
||||
.Select(h => new
|
||||
{
|
||||
Hint = h,
|
||||
Key = GetAgreementKey(h) ?? bestKey
|
||||
})
|
||||
.GroupBy(x => x.Key)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.FirstOrDefault();
|
||||
|
||||
@@ -330,7 +337,7 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
{
|
||||
// Multiple hints agree - combine confidence
|
||||
var combinedConfidence = Math.Min(0.99,
|
||||
agreeing.Max(h => h.Confidence) + (agreeing.Count() - 1) * 0.1);
|
||||
agreeing.Max(x => x.Hint.Confidence) + (agreeing.Count() - 1) * 0.1);
|
||||
|
||||
return (
|
||||
$"{agreeing.Key} (confirmed by {agreeing.Count()} evidence sources)",
|
||||
@@ -360,7 +367,7 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetAgreementKey(ProvenanceHint hint)
|
||||
private static string? GetAgreementKey(ProvenanceHint hint)
|
||||
{
|
||||
var evidence = hint.Evidence;
|
||||
var key = evidence.BuildId?.MatchedPackage
|
||||
@@ -370,7 +377,7 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
?? ExtractPackageFromVersion(evidence.CorpusMatch?.MatchedEntry)
|
||||
?? ExtractPackageFromHypothesis(hint.Hypothesis);
|
||||
|
||||
return string.IsNullOrWhiteSpace(key) ? hint.Hypothesis : key;
|
||||
return string.IsNullOrWhiteSpace(key) ? null : key;
|
||||
}
|
||||
|
||||
private static string? BestMatchPackage(IReadOnlyList<FingerprintMatch>? matches)
|
||||
@@ -395,7 +402,12 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
var token = trimmed.Split([' ', '/', '\t'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||
if (!trimmed.Contains(' ') && !trimmed.Contains('/'))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = trimmed.Split([' ', '/', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||
return string.IsNullOrWhiteSpace(token) ? null : token;
|
||||
}
|
||||
|
||||
@@ -419,11 +431,11 @@ public sealed partial class ProvenanceHintBuilder : IProvenanceHintBuilder
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractPackageFromHypothesis(string hypothesis)
|
||||
private static string? ExtractPackageFromHypothesis(string hypothesis)
|
||||
{
|
||||
// Simple extraction - match "matches <package>" or "from <package>"
|
||||
var match = PackageExtractionRegex().Match(hypothesis);
|
||||
return match.Success ? match.Groups[1].Value : hypothesis;
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?:matches?|from)\s+(\S+)")]
|
||||
|
||||
@@ -19,6 +19,10 @@ public sealed class NativeUnknownClassifier
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly string _createdBy;
|
||||
private static readonly JsonSerializerOptions ContextJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public NativeUnknownClassifier(TimeProvider timeProvider, string createdBy = "unknowns")
|
||||
{
|
||||
@@ -243,16 +247,7 @@ public sealed class NativeUnknownClassifier
|
||||
|
||||
private static JsonDocument SerializeContext(NativeUnknownContext context)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(context, NativeUnknownContextJsonContext.Default.NativeUnknownContext);
|
||||
var json = JsonSerializer.Serialize(context, ContextJsonOptions);
|
||||
return JsonDocument.Parse(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON context for NativeUnknownContext serialization.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonSourceGenerationOptions(PropertyNamingPolicy = System.Text.Json.JsonKnownNamingPolicy.CamelCase)]
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(NativeUnknownContext))]
|
||||
internal partial class NativeUnknownContextJsonContext : System.Text.Json.Serialization.JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -277,11 +277,10 @@ public sealed class ProvenanceHintSerializationTests
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
|
||||
// Assert - JSON is parseable
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
parsed.RootElement.GetProperty("hint_id").GetString().Should().StartWith("hint:sha256:");
|
||||
parsed.RootElement.GetProperty("type").GetString().Should().NotBeNullOrEmpty();
|
||||
parsed.RootElement.GetProperty("type").GetInt32().Should().Be((int)ProvenanceHintType.BuildIdMatch);
|
||||
parsed.RootElement.GetProperty("confidence").GetDouble().Should().BeInRange(0, 1);
|
||||
parsed.RootElement.GetProperty("evidence").GetProperty("build_id").GetProperty("catalog_source")
|
||||
.GetString().Should().Be("debian-security");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using StellaOps.Unknowns.Core.Repositories;
|
||||
@@ -24,10 +25,24 @@ public sealed class UnknownsEndpointsTests : IClassFixture<WebApplicationFactory
|
||||
|
||||
public UnknownsEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(
|
||||
"ConnectionStrings__UnknownsDb",
|
||||
"Host=localhost;Database=unknowns_test;Username=test;Password=test");
|
||||
|
||||
_mockRepository = Substitute.For<IUnknownRepository>();
|
||||
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:UnknownsDb"] =
|
||||
"Host=localhost;Database=unknowns_test;Username=test;Password=test"
|
||||
};
|
||||
config.AddInMemoryCollection(settings);
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Remove existing repository registration
|
||||
@@ -336,20 +351,35 @@ public sealed class UnknownsEndpointsTests : IClassFixture<WebApplicationFactory
|
||||
ProvenanceHints = [
|
||||
new ProvenanceHint
|
||||
{
|
||||
Id = $"hint:{Guid.NewGuid():N}",
|
||||
HintId = $"hint:sha256:{Guid.NewGuid():N}",
|
||||
Type = ProvenanceHintType.BuildIdMatch,
|
||||
Confidence = 0.85,
|
||||
ConfidenceLevel = HintConfidence.High,
|
||||
Summary = "Build-ID match from Debian",
|
||||
Hypothesis = "Likely debian:bookworm backport",
|
||||
Evidence = new ProvenanceEvidence
|
||||
{
|
||||
BuildId = new BuildIdEvidence
|
||||
{
|
||||
BuildId = "deadbeef0123456789abcdef",
|
||||
BuildIdType = "sha256",
|
||||
MatchedPackage = "openssl",
|
||||
MatchedVersion = "3.0.0",
|
||||
MatchedDistro = "debian",
|
||||
CatalogSource = "debian-security"
|
||||
}
|
||||
},
|
||||
SuggestedActions = [
|
||||
new SuggestedAction
|
||||
{
|
||||
Action = "Verify debian package version",
|
||||
Priority = 1,
|
||||
Effort = "low",
|
||||
Description = "Check against Debian security tracker"
|
||||
}
|
||||
],
|
||||
GeneratedAt = now.AddDays(-3)
|
||||
GeneratedAt = now.AddDays(-3),
|
||||
Source = "BuildIdAnalyzer"
|
||||
}
|
||||
],
|
||||
BestHypothesis = "Likely debian:bookworm backport",
|
||||
|
||||
Reference in New Issue
Block a user