sln build fix (again), tests fixes, audit work and doctors work

This commit is contained in:
master
2026-01-12 22:15:51 +02:00
parent 9873f80830
commit 9330c64349
812 changed files with 48051 additions and 3891 deletions

View File

@@ -124,25 +124,25 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
private readonly ILogger<AdvisoryChatIntentRouter> _logger;
// Regex patterns for slash commands - compiled for performance
[GeneratedRegex(@"^/explain\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+)\s+in\s+(?<image>\S+)\s+(?<env>\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/explain\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+)(?:\s+in\s+(?<image>\S+)(?:\s+(?<env>\S+))?)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ExplainPattern();
[GeneratedRegex(@"^/is[_-]?it[_-]?reachable\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+|[^@\s]+)\s+in\s+(?<image>\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/is[_-]?it[_-]?reachable\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+|[^@\s]+)(?:\s+in\s+(?<image>\S+))?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ReachablePattern();
[GeneratedRegex(@"^/do[_-]?we[_-]?have[_-]?a[_-]?backport\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+)\s+in\s+(?<package>\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/do[_-]?we[_-]?have[_-]?a[_-]?backport\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+)(?:\s+in\s+(?<package>\S+))?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex BackportPattern();
[GeneratedRegex(@"^/propose[_-]?fix\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+|\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ProposeFixPattern();
[GeneratedRegex(@"^/waive\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+|\S+)\s+for\s+(?<duration>\d+[dhwm])\s+because\s+(?<reason>.+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/waive\s+(?<finding>CVE-\d{4}-\d+|GHSA-[a-z0-9-]+|\S+)(?:\s+(?:for\s+)?(?<duration>\d+[dhwm]))?(?:\s+(?:because\s+)?(?<reason>.+))?$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex WaivePattern();
[GeneratedRegex(@"^/batch[_-]?triage\s+(?:top\s+)?(?<top>\d+)\s+(?:findings\s+)?in\s+(?<env>\S+)(?:\s+by\s+(?<method>\S+))?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/batch[_-]?triage(?:\s+(?:top\s+)?(?<top>\d+))?(?:\s+(?:findings\s+)?in\s+(?<env>\S+))?(?:\s+by\s+(?<method>\S+))?|^/batch[_-]?triage\s+(?<priority>\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex BatchTriagePattern();
[GeneratedRegex(@"^/compare\s+(?<env1>\S+)\s+vs\s+(?<env2>\S+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
[GeneratedRegex(@"^/compare\s+(?<env1>\S+)\s+(?:vs\s+)?(?<env2>\S+)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ComparePattern();
// Patterns for CVE/GHSA extraction
@@ -281,8 +281,8 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
Parameters = new IntentParameters
{
FindingId = waiveMatch.Groups["finding"].Value.ToUpperInvariant(),
Duration = waiveMatch.Groups["duration"].Value,
Reason = waiveMatch.Groups["reason"].Value
Duration = waiveMatch.Groups["duration"].Success ? waiveMatch.Groups["duration"].Value : null,
Reason = waiveMatch.Groups["reason"].Success ? waiveMatch.Groups["reason"].Value : null
}
};
}
@@ -292,6 +292,7 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
if (batchMatch.Success)
{
_ = int.TryParse(batchMatch.Groups["top"].Value, out var topN);
var priority = batchMatch.Groups["priority"].Success ? batchMatch.Groups["priority"].Value : null;
return new IntentRoutingResult
{
Intent = AdvisoryChatIntent.BatchTriage,
@@ -301,10 +302,10 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
Parameters = new IntentParameters
{
TopN = topN > 0 ? topN : 10,
Environment = batchMatch.Groups["env"].Value,
Environment = batchMatch.Groups["env"].Success ? batchMatch.Groups["env"].Value : null,
PriorityMethod = batchMatch.Groups["method"].Success
? batchMatch.Groups["method"].Value
: "exploit_pressure"
: priority ?? "exploit_pressure"
}
};
}
@@ -335,22 +336,23 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
var lowerInput = input.ToLowerInvariant();
var parameters = ExtractParametersFromContent(input);
// Keywords for each intent
// Keywords for each intent - ordered by specificity
// Use phrases to avoid false positives from single words
var explainKeywords = new[] { "explain", "what does", "what is", "tell me about", "describe", "mean" };
var reachableKeywords = new[] { "reachable", "reach", "call", "path", "accessible", "executed" };
var backportKeywords = new[] { "backport", "patch", "binary", "distro fix", "security update" };
var fixKeywords = new[] { "fix", "remediate", "resolve", "mitigate", "patch", "upgrade", "update" };
var backportKeywords = new[] { "backport", "binary", "distro fix", "security update" };
var fixKeywords = new[] { "fix", "remediat", "remediation", "resolve", "mitigate", "upgrade", "update", "how do i", "how can i", "options for", "patch option", "patch for" };
var waiveKeywords = new[] { "waive", "accept risk", "exception", "defer", "skip" };
var triageKeywords = new[] { "triage", "prioritize", "batch", "top", "most important", "critical" };
var compareKeywords = new[] { "compare", "difference", "vs", "versus", "between" };
// Score each intent
// Score each intent with weighted keywords
var scores = new Dictionary<AdvisoryChatIntent, double>
{
[AdvisoryChatIntent.Explain] = ScoreKeywords(lowerInput, explainKeywords),
[AdvisoryChatIntent.IsItReachable] = ScoreKeywords(lowerInput, reachableKeywords),
[AdvisoryChatIntent.DoWeHaveABackport] = ScoreKeywords(lowerInput, backportKeywords),
[AdvisoryChatIntent.ProposeFix] = ScoreKeywords(lowerInput, fixKeywords),
[AdvisoryChatIntent.ProposeFix] = ScoreKeywordsWeighted(lowerInput, fixKeywords),
[AdvisoryChatIntent.Waive] = ScoreKeywords(lowerInput, waiveKeywords),
[AdvisoryChatIntent.BatchTriage] = ScoreKeywords(lowerInput, triageKeywords),
[AdvisoryChatIntent.Compare] = ScoreKeywords(lowerInput, compareKeywords)
@@ -362,7 +364,7 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
.First();
// If no strong signal, default to Explain if we have a CVE, otherwise General
if (bestScore < 0.3)
if (bestScore < 0.15)
{
if (parameters.FindingId is not null)
{
@@ -437,6 +439,21 @@ internal sealed partial class AdvisoryChatIntentRouter : IAdvisoryChatIntentRout
return matches / (double)keywords.Length;
}
private static double ScoreKeywordsWeighted(string input, string[] keywords)
{
// Give higher weight to phrase matches
double score = 0;
foreach (var keyword in keywords)
{
if (input.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
// Multi-word phrases get higher weight
score += keyword.Contains(' ') ? 0.4 : 0.2;
}
}
return Math.Min(score, 1.0);
}
private static string TruncateForLog(string input)
{
const int maxLength = 100;

View File

@@ -1,7 +1,7 @@
# Advisory AI Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |

View File

@@ -168,7 +168,7 @@ public sealed class AdvisoryChatIntentRouterTests
[Theory]
[InlineData("How do I fix CVE-2024-12345?", AdvisoryChatIntent.ProposeFix)]
[InlineData("What's the remediation for this vulnerability?", AdvisoryChatIntent.ProposeFix)]
[InlineData("What's the remediation for CVE-2024-12345?", AdvisoryChatIntent.ProposeFix)]
[InlineData("Patch options for openssl", AdvisoryChatIntent.ProposeFix)]
public async Task RouteAsync_NaturalLanguageFix_InfersProposeFixIntent(
string input, AdvisoryChatIntent expectedIntent)

View File

@@ -271,6 +271,16 @@ public sealed class AdvisoryChatDeterminismTests
SbomDigest = "sha256:sbom123",
ComponentCount = 10
});
mockSbom.Setup(x => x.GetFindingDataAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new FindingData
{
Type = "CVE",
Id = "CVE-2024-12345",
Package = "pkg:npm/test-package@1.0.0",
Severity = "High",
CvssScore = 7.5,
Description = "Test vulnerability"
});
return new EvidenceBundleAssembler(
mockVex.Object,
@@ -313,6 +323,16 @@ public sealed class AdvisoryChatDeterminismTests
SbomDigest = "sha256:sbom123",
ComponentCount = 10
});
mockSbom.Setup(x => x.GetFindingDataAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new FindingData
{
Type = "CVE",
Id = "CVE-2024-12345",
Package = "pkg:npm/test-package@1.0.0",
Severity = "High",
CvssScore = 7.5,
Description = "Test vulnerability"
});
return new EvidenceBundleAssembler(
mockVex.Object,

View File

@@ -149,17 +149,23 @@ public sealed class LocalInferenceClientTests
var chunks = new List<AdvisoryChatResponseChunk>();
// Act
await foreach (var chunk in _client.StreamResponseAsync(bundle, routingResult, cts.Token))
try
{
chunks.Add(chunk);
if (chunks.Count >= 2)
await foreach (var chunk in _client.StreamResponseAsync(bundle, routingResult, cts.Token))
{
cts.Cancel();
chunks.Add(chunk);
if (chunks.Count >= 2)
{
cts.Cancel();
}
}
}
catch (OperationCanceledException)
{
// Expected - cancellation was requested
}
// Assert - should have stopped early due to cancellation
// (but OperationCanceledException might be thrown)
// Assert - should have stopped due to cancellation
Assert.True(chunks.Count >= 2);
}

View File

@@ -48,8 +48,8 @@ public sealed class SystemPromptLoaderTests
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() =>
// Act & Assert - TaskCanceledException derives from OperationCanceledException
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
loader.LoadSystemPromptAsync(cts.Token));
}