sprints completion. new product advisories prepared
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AiCodeGuardSignalContextExtensions.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-001/005 - AI Code Guard signal context integration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Policy.AiCodeGuard;
|
||||
|
||||
namespace StellaOps.PolicyDsl;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for integrating AI Code Guard evidence with PolicyDsl SignalContext.
|
||||
/// </summary>
|
||||
public static class AiCodeGuardSignalContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds AI Code Guard evidence signals to the signal context.
|
||||
/// </summary>
|
||||
/// <param name="context">The signal context.</param>
|
||||
/// <param name="evidenceContext">The AI Code Guard evidence context.</param>
|
||||
/// <returns>The signal context for chaining.</returns>
|
||||
public static SignalContext WithAiCodeGuardEvidence(
|
||||
this SignalContext context,
|
||||
AiCodeGuardEvidenceContext evidenceContext)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(evidenceContext);
|
||||
|
||||
// Add flat signals
|
||||
var signals = AiCodeGuardSignalBinder.BindToSignals(evidenceContext);
|
||||
foreach (var (name, value) in signals)
|
||||
{
|
||||
context.SetSignal(name, value);
|
||||
}
|
||||
|
||||
// Add nested object for member access (guard.severity.high, etc.)
|
||||
var nested = AiCodeGuardSignalBinder.BindToNestedObject(evidenceContext);
|
||||
context.SetSignal("guard", nested);
|
||||
|
||||
// Add policy recommendation
|
||||
context.SetSignal("guard.recommendation", AiCodeGuardSignalBinder.GetRecommendation(evidenceContext));
|
||||
|
||||
// Add explain trace for deterministic auditing
|
||||
context.SetSignal("guard.explain_trace", AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext));
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds AI Code Guard evidence signals to the signal context builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The signal context builder.</param>
|
||||
/// <param name="evidenceContext">The AI Code Guard evidence context.</param>
|
||||
/// <returns>The builder for chaining.</returns>
|
||||
public static SignalContextBuilder WithAiCodeGuardEvidence(
|
||||
this SignalContextBuilder builder,
|
||||
AiCodeGuardEvidenceContext evidenceContext)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(evidenceContext);
|
||||
|
||||
// Add flat signals
|
||||
var signals = AiCodeGuardSignalBinder.BindToSignals(evidenceContext);
|
||||
foreach (var (name, value) in signals)
|
||||
{
|
||||
builder.WithSignal(name, value);
|
||||
}
|
||||
|
||||
// Add nested object for member access
|
||||
var nested = AiCodeGuardSignalBinder.BindToNestedObject(evidenceContext);
|
||||
builder.WithSignal("guard", nested);
|
||||
|
||||
// Add policy recommendation
|
||||
builder.WithSignal("guard.recommendation", AiCodeGuardSignalBinder.GetRecommendation(evidenceContext));
|
||||
|
||||
// Add explain trace
|
||||
builder.WithSignal("guard.explain_trace", AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds AI Code Guard evidence signals from a provider.
|
||||
/// </summary>
|
||||
/// <param name="builder">The signal context builder.</param>
|
||||
/// <param name="provider">The AI Code Guard evidence provider.</param>
|
||||
/// <returns>The builder for chaining.</returns>
|
||||
public static SignalContextBuilder WithAiCodeGuardEvidence(
|
||||
this SignalContextBuilder builder,
|
||||
IAiCodeGuardEvidenceProvider provider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
var context = new AiCodeGuardEvidenceContext(provider);
|
||||
return builder.WithAiCodeGuardEvidence(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a signal context builder with AI Code Guard evidence.
|
||||
/// </summary>
|
||||
/// <param name="evidenceContext">The AI Code Guard evidence context.</param>
|
||||
/// <returns>A new builder with guard signals.</returns>
|
||||
public static SignalContextBuilder CreateBuilderWithGuardEvidence(AiCodeGuardEvidenceContext evidenceContext)
|
||||
{
|
||||
return SignalContext.Builder().WithAiCodeGuardEvidence(evidenceContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a signal context with AI Code Guard evidence.
|
||||
/// </summary>
|
||||
/// <param name="evidenceContext">The AI Code Guard evidence context.</param>
|
||||
/// <returns>A new signal context with guard signals.</returns>
|
||||
public static SignalContext CreateContextWithGuardEvidence(AiCodeGuardEvidenceContext evidenceContext)
|
||||
{
|
||||
return CreateBuilderWithGuardEvidence(evidenceContext).Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds simplified AI Code Guard result signals for quick checks.
|
||||
/// This is useful when you have analysis results but not a full evidence provider.
|
||||
/// </summary>
|
||||
/// <param name="builder">The signal context builder.</param>
|
||||
/// <param name="status">The verdict status.</param>
|
||||
/// <param name="totalFindings">Total finding count.</param>
|
||||
/// <param name="criticalCount">Critical severity count.</param>
|
||||
/// <param name="highCount">High severity count.</param>
|
||||
/// <param name="mediumCount">Medium severity count.</param>
|
||||
/// <param name="aiPercentage">Optional AI-generated percentage.</param>
|
||||
/// <returns>The builder for chaining.</returns>
|
||||
public static SignalContextBuilder WithAiCodeGuardResult(
|
||||
this SignalContextBuilder builder,
|
||||
string status,
|
||||
int totalFindings,
|
||||
int criticalCount = 0,
|
||||
int highCount = 0,
|
||||
int mediumCount = 0,
|
||||
double? aiPercentage = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.WithSignal("guard.verdict", status.ToLowerInvariant());
|
||||
builder.WithSignal("guard.count", totalFindings);
|
||||
builder.WithSignal("guard.has_finding", totalFindings > 0);
|
||||
builder.WithSignal("guard.severity.critical", criticalCount > 0);
|
||||
builder.WithSignal("guard.severity.critical_count", criticalCount);
|
||||
builder.WithSignal("guard.severity.high", highCount > 0);
|
||||
builder.WithSignal("guard.severity.high_count", highCount);
|
||||
builder.WithSignal("guard.severity.medium", mediumCount > 0);
|
||||
builder.WithSignal("guard.severity.medium_count", mediumCount);
|
||||
builder.WithSignal("guard.ai_percentage", aiPercentage);
|
||||
|
||||
// Derive recommendation
|
||||
var recommendation = status.ToLowerInvariant() switch
|
||||
{
|
||||
"pass" => "allow",
|
||||
"passwithwarnings" or "pass_with_warnings" => "review",
|
||||
"fail" => "block",
|
||||
"error" => "block",
|
||||
_ => "review"
|
||||
};
|
||||
builder.WithSignal("guard.recommendation", recommendation);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AiCodeGuardEvidenceContext.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-001 - AI Code Guard evidence context
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.AiCodeGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Context for AI Code Guard evidence evaluation.
|
||||
/// Provides accessors for common policy signal patterns.
|
||||
/// </summary>
|
||||
public sealed class AiCodeGuardEvidenceContext
|
||||
{
|
||||
private readonly IAiCodeGuardEvidenceProvider _provider;
|
||||
private readonly ImmutableList<AiCodeGuardFinding> _activeFindings;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AI Code Guard evidence context.
|
||||
/// </summary>
|
||||
/// <param name="provider">The evidence provider.</param>
|
||||
public AiCodeGuardEvidenceContext(IAiCodeGuardEvidenceProvider provider)
|
||||
{
|
||||
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
// Filter out suppressed findings
|
||||
var suppressed = provider.Overrides
|
||||
.Where(o => o.Action.Equals("suppress", StringComparison.OrdinalIgnoreCase) ||
|
||||
o.Action.Equals("false-positive", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(o => o.FindingId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
_activeFindings = provider.Findings
|
||||
.Where(f => !suppressed.Contains(f.Id))
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all findings (including suppressed).
|
||||
/// </summary>
|
||||
public ImmutableList<AiCodeGuardFinding> AllFindings => _provider.Findings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets active findings (excluding suppressed).
|
||||
/// </summary>
|
||||
public ImmutableList<AiCodeGuardFinding> ActiveFindings => _activeFindings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all overrides.
|
||||
/// </summary>
|
||||
public ImmutableList<AiCodeGuardOverrideRecord> Overrides => _provider.Overrides;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are any findings.
|
||||
/// </summary>
|
||||
public bool HasAnyFinding => _provider.Findings.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are any active (non-suppressed) findings.
|
||||
/// </summary>
|
||||
public bool HasActiveFinding => _activeFindings.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total finding count.
|
||||
/// </summary>
|
||||
public int TotalFindingCount => _provider.Findings.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active finding count.
|
||||
/// </summary>
|
||||
public int ActiveFindingCount => _activeFindings.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the verdict status.
|
||||
/// </summary>
|
||||
public AiCodeGuardVerdictStatus VerdictStatus => _provider.VerdictStatus;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AI-generated code percentage.
|
||||
/// </summary>
|
||||
public double? AiGeneratedPercentage => _provider.AiGeneratedPercentage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner info.
|
||||
/// </summary>
|
||||
public AiCodeGuardScannerInfo? ScannerInfo => _provider.ScannerInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are active findings with the specified severity.
|
||||
/// </summary>
|
||||
public bool HasFindingWithSeverity(string severity)
|
||||
{
|
||||
return _activeFindings.Any(f =>
|
||||
f.Severity.Equals(severity, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of active findings with the specified severity.
|
||||
/// </summary>
|
||||
public int GetFindingCountBySeverity(string severity)
|
||||
{
|
||||
return _activeFindings.Count(f =>
|
||||
f.Severity.Equals(severity, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are active findings with the specified category.
|
||||
/// </summary>
|
||||
public bool HasFindingWithCategory(string category)
|
||||
{
|
||||
return _activeFindings.Any(f =>
|
||||
f.Category.Equals(category, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of active findings with the specified category.
|
||||
/// </summary>
|
||||
public int GetFindingCountByCategory(string category)
|
||||
{
|
||||
return _activeFindings.Count(f =>
|
||||
f.Category.Equals(category, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are active findings with confidence above threshold.
|
||||
/// </summary>
|
||||
public bool HasFindingWithConfidenceAbove(double threshold)
|
||||
{
|
||||
return _activeFindings.Any(f => f.Confidence >= threshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of active findings with confidence above threshold.
|
||||
/// </summary>
|
||||
public int GetFindingCountWithConfidenceAbove(double threshold)
|
||||
{
|
||||
return _activeFindings.Count(f => f.Confidence >= threshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the highest severity among active findings.
|
||||
/// </summary>
|
||||
public string? HighestSeverity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_activeFindings.Count == 0)
|
||||
return null;
|
||||
|
||||
var severityOrder = new[] { "critical", "high", "medium", "low", "info" };
|
||||
foreach (var severity in severityOrder)
|
||||
{
|
||||
if (HasFindingWithSeverity(severity))
|
||||
return severity;
|
||||
}
|
||||
return _activeFindings[0].Severity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the average confidence of active findings.
|
||||
/// </summary>
|
||||
public double? AverageConfidence
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_activeFindings.Count == 0)
|
||||
return null;
|
||||
return _activeFindings.Average(f => f.Confidence);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of active overrides.
|
||||
/// </summary>
|
||||
public int ActiveOverrideCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return _provider.Overrides.Count(o =>
|
||||
!o.ExpiresAt.HasValue || o.ExpiresAt.Value > now);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of expired overrides.
|
||||
/// </summary>
|
||||
public int ExpiredOverrideCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return _provider.Overrides.Count(o =>
|
||||
o.ExpiresAt.HasValue && o.ExpiresAt.Value <= now);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all findings in specified paths are suppressed.
|
||||
/// </summary>
|
||||
public bool AllFindingsInPathsSuppressed(IReadOnlyList<string> pathPatterns)
|
||||
{
|
||||
var matchingFindings = _provider.Findings
|
||||
.Where(f => pathPatterns.Any(p => MatchesGlob(f.FilePath, p)));
|
||||
|
||||
return matchingFindings.All(f =>
|
||||
_provider.Overrides.Any(o =>
|
||||
o.FindingId == f.Id &&
|
||||
(o.Action.Equals("suppress", StringComparison.OrdinalIgnoreCase) ||
|
||||
o.Action.Equals("false-positive", StringComparison.OrdinalIgnoreCase))));
|
||||
}
|
||||
|
||||
private static bool MatchesGlob(string path, string pattern)
|
||||
{
|
||||
// Simple glob matching for common patterns
|
||||
if (pattern == "*" || pattern == "**")
|
||||
return true;
|
||||
|
||||
if (pattern.StartsWith("**/", StringComparison.Ordinal))
|
||||
{
|
||||
var suffix = pattern[3..];
|
||||
return path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.Contains("/" + suffix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (pattern.EndsWith("/**", StringComparison.Ordinal))
|
||||
{
|
||||
var prefix = pattern[..^3];
|
||||
return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AiCodeGuardSignalBinder.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-001/002 - AI Code Guard signal binding for policy evaluation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Policy.AiCodeGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Binds AI Code Guard evidence to policy evaluation signals.
|
||||
/// This class converts AI code guard findings, verdicts, and override metadata
|
||||
/// into signals that can be evaluated by the PolicyDsl SignalContext.
|
||||
///
|
||||
/// <para>
|
||||
/// Available signals after binding:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>guard.has_finding</c> - true if any finding exists</item>
|
||||
/// <item><c>guard.has_active_finding</c> - true if any active (non-suppressed) finding exists</item>
|
||||
/// <item><c>guard.count</c> - total number of findings</item>
|
||||
/// <item><c>guard.active_count</c> - number of active findings</item>
|
||||
/// <item><c>guard.severity.critical</c> - true if any critical finding exists</item>
|
||||
/// <item><c>guard.severity.high</c> - true if any high severity finding exists</item>
|
||||
/// <item><c>guard.severity.medium</c> - true if any medium severity finding exists</item>
|
||||
/// <item><c>guard.severity.low</c> - true if any low severity finding exists</item>
|
||||
/// <item><c>guard.category.ai_generated</c> - true if any AI-generated finding exists</item>
|
||||
/// <item><c>guard.category.insecure_pattern</c> - true if any insecure pattern finding exists</item>
|
||||
/// <item><c>guard.category.hallucination</c> - true if any hallucination finding exists</item>
|
||||
/// <item><c>guard.category.license_risk</c> - true if any license risk finding exists</item>
|
||||
/// <item><c>guard.verdict</c> - the verdict status (pass, pass_with_warnings, fail, error)</item>
|
||||
/// <item><c>guard.ai_percentage</c> - estimated AI-generated code percentage</item>
|
||||
/// <item><c>guard.override.count</c> - number of overrides applied</item>
|
||||
/// <item><c>guard.override.expired_count</c> - number of expired overrides</item>
|
||||
/// <item><c>guard.scanner.version</c> - scanner version</item>
|
||||
/// <item><c>guard.scanner.confidence_threshold</c> - confidence threshold used</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class AiCodeGuardSignalBinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Signal name prefix for all AI Code Guard signals.
|
||||
/// </summary>
|
||||
public const string SignalPrefix = "guard";
|
||||
|
||||
/// <summary>
|
||||
/// Binds AI Code Guard evidence to a dictionary of signals.
|
||||
/// </summary>
|
||||
/// <param name="context">The AI Code Guard evidence context.</param>
|
||||
/// <returns>A dictionary of signal names to values.</returns>
|
||||
public static ImmutableDictionary<string, object?> BindToSignals(AiCodeGuardEvidenceContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var signals = ImmutableDictionary.CreateBuilder<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
// Core finding signals
|
||||
signals[$"{SignalPrefix}.has_finding"] = context.HasAnyFinding;
|
||||
signals[$"{SignalPrefix}.has_active_finding"] = context.HasActiveFinding;
|
||||
signals[$"{SignalPrefix}.count"] = context.TotalFindingCount;
|
||||
signals[$"{SignalPrefix}.active_count"] = context.ActiveFindingCount;
|
||||
|
||||
// Severity signals
|
||||
signals[$"{SignalPrefix}.severity.critical"] = context.HasFindingWithSeverity("critical");
|
||||
signals[$"{SignalPrefix}.severity.high"] = context.HasFindingWithSeverity("high");
|
||||
signals[$"{SignalPrefix}.severity.medium"] = context.HasFindingWithSeverity("medium");
|
||||
signals[$"{SignalPrefix}.severity.low"] = context.HasFindingWithSeverity("low");
|
||||
signals[$"{SignalPrefix}.severity.info"] = context.HasFindingWithSeverity("info");
|
||||
|
||||
// Severity counts
|
||||
signals[$"{SignalPrefix}.severity.critical_count"] = context.GetFindingCountBySeverity("critical");
|
||||
signals[$"{SignalPrefix}.severity.high_count"] = context.GetFindingCountBySeverity("high");
|
||||
signals[$"{SignalPrefix}.severity.medium_count"] = context.GetFindingCountBySeverity("medium");
|
||||
signals[$"{SignalPrefix}.severity.low_count"] = context.GetFindingCountBySeverity("low");
|
||||
signals[$"{SignalPrefix}.severity.info_count"] = context.GetFindingCountBySeverity("info");
|
||||
|
||||
// Category signals
|
||||
signals[$"{SignalPrefix}.category.ai_generated"] = context.HasFindingWithCategory("ai-generated") ||
|
||||
context.HasFindingWithCategory("AiGenerated");
|
||||
signals[$"{SignalPrefix}.category.insecure_pattern"] = context.HasFindingWithCategory("insecure-pattern") ||
|
||||
context.HasFindingWithCategory("InsecurePattern");
|
||||
signals[$"{SignalPrefix}.category.hallucination"] = context.HasFindingWithCategory("hallucination") ||
|
||||
context.HasFindingWithCategory("Hallucination");
|
||||
signals[$"{SignalPrefix}.category.license_risk"] = context.HasFindingWithCategory("license-risk") ||
|
||||
context.HasFindingWithCategory("LicenseRisk");
|
||||
signals[$"{SignalPrefix}.category.untrusted_dep"] = context.HasFindingWithCategory("untrusted-dep") ||
|
||||
context.HasFindingWithCategory("UntrustedDependency");
|
||||
signals[$"{SignalPrefix}.category.quality_issue"] = context.HasFindingWithCategory("quality-issue") ||
|
||||
context.HasFindingWithCategory("QualityIssue");
|
||||
|
||||
// Category counts
|
||||
signals[$"{SignalPrefix}.category.ai_generated_count"] = context.GetFindingCountByCategory("ai-generated") +
|
||||
context.GetFindingCountByCategory("AiGenerated");
|
||||
signals[$"{SignalPrefix}.category.insecure_pattern_count"] = context.GetFindingCountByCategory("insecure-pattern") +
|
||||
context.GetFindingCountByCategory("InsecurePattern");
|
||||
|
||||
// Verdict signals
|
||||
signals[$"{SignalPrefix}.verdict"] = context.VerdictStatus.ToString().ToLowerInvariant();
|
||||
signals[$"{SignalPrefix}.verdict.pass"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Pass;
|
||||
signals[$"{SignalPrefix}.verdict.pass_with_warnings"] = context.VerdictStatus == AiCodeGuardVerdictStatus.PassWithWarnings;
|
||||
signals[$"{SignalPrefix}.verdict.fail"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Fail;
|
||||
signals[$"{SignalPrefix}.verdict.error"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Error;
|
||||
|
||||
// AI percentage
|
||||
signals[$"{SignalPrefix}.ai_percentage"] = context.AiGeneratedPercentage;
|
||||
|
||||
// Confidence signals
|
||||
signals[$"{SignalPrefix}.highest_severity"] = context.HighestSeverity;
|
||||
signals[$"{SignalPrefix}.average_confidence"] = context.AverageConfidence;
|
||||
signals[$"{SignalPrefix}.high_confidence_count"] = context.GetFindingCountWithConfidenceAbove(0.8);
|
||||
|
||||
// Override signals
|
||||
signals[$"{SignalPrefix}.override.count"] = context.Overrides.Count;
|
||||
signals[$"{SignalPrefix}.override.active_count"] = context.ActiveOverrideCount;
|
||||
signals[$"{SignalPrefix}.override.expired_count"] = context.ExpiredOverrideCount;
|
||||
|
||||
// Scanner signals
|
||||
var scanner = context.ScannerInfo;
|
||||
if (scanner is not null)
|
||||
{
|
||||
signals[$"{SignalPrefix}.scanner.version"] = scanner.ScannerVersion;
|
||||
signals[$"{SignalPrefix}.scanner.model_version"] = scanner.ModelVersion;
|
||||
signals[$"{SignalPrefix}.scanner.confidence_threshold"] = scanner.ConfidenceThreshold;
|
||||
signals[$"{SignalPrefix}.scanner.category_count"] = scanner.EnabledCategories.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
signals[$"{SignalPrefix}.scanner.version"] = null;
|
||||
signals[$"{SignalPrefix}.scanner.model_version"] = null;
|
||||
signals[$"{SignalPrefix}.scanner.confidence_threshold"] = null;
|
||||
signals[$"{SignalPrefix}.scanner.category_count"] = 0;
|
||||
}
|
||||
|
||||
return signals.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds AI Code Guard evidence to a nested object suitable for member access in policies.
|
||||
/// This creates a hierarchical structure like:
|
||||
/// guard.severity.high, guard.verdict.pass, etc.
|
||||
/// </summary>
|
||||
/// <param name="context">The AI Code Guard evidence context.</param>
|
||||
/// <returns>A nested dictionary structure.</returns>
|
||||
public static ImmutableDictionary<string, object?> BindToNestedObject(AiCodeGuardEvidenceContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var severity = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["critical"] = context.HasFindingWithSeverity("critical"),
|
||||
["high"] = context.HasFindingWithSeverity("high"),
|
||||
["medium"] = context.HasFindingWithSeverity("medium"),
|
||||
["low"] = context.HasFindingWithSeverity("low"),
|
||||
["info"] = context.HasFindingWithSeverity("info"),
|
||||
["critical_count"] = context.GetFindingCountBySeverity("critical"),
|
||||
["high_count"] = context.GetFindingCountBySeverity("high"),
|
||||
["medium_count"] = context.GetFindingCountBySeverity("medium"),
|
||||
["low_count"] = context.GetFindingCountBySeverity("low"),
|
||||
["info_count"] = context.GetFindingCountBySeverity("info"),
|
||||
};
|
||||
|
||||
var category = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["ai_generated"] = context.HasFindingWithCategory("ai-generated") ||
|
||||
context.HasFindingWithCategory("AiGenerated"),
|
||||
["insecure_pattern"] = context.HasFindingWithCategory("insecure-pattern") ||
|
||||
context.HasFindingWithCategory("InsecurePattern"),
|
||||
["hallucination"] = context.HasFindingWithCategory("hallucination"),
|
||||
["license_risk"] = context.HasFindingWithCategory("license-risk") ||
|
||||
context.HasFindingWithCategory("LicenseRisk"),
|
||||
["untrusted_dep"] = context.HasFindingWithCategory("untrusted-dep") ||
|
||||
context.HasFindingWithCategory("UntrustedDependency"),
|
||||
["quality_issue"] = context.HasFindingWithCategory("quality-issue") ||
|
||||
context.HasFindingWithCategory("QualityIssue"),
|
||||
};
|
||||
|
||||
var verdict = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["status"] = context.VerdictStatus.ToString().ToLowerInvariant(),
|
||||
["pass"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Pass,
|
||||
["pass_with_warnings"] = context.VerdictStatus == AiCodeGuardVerdictStatus.PassWithWarnings,
|
||||
["fail"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Fail,
|
||||
["error"] = context.VerdictStatus == AiCodeGuardVerdictStatus.Error,
|
||||
};
|
||||
|
||||
var override_ = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["count"] = context.Overrides.Count,
|
||||
["active_count"] = context.ActiveOverrideCount,
|
||||
["expired_count"] = context.ExpiredOverrideCount,
|
||||
};
|
||||
|
||||
var scanner = context.ScannerInfo;
|
||||
var scannerDict = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["version"] = scanner?.ScannerVersion,
|
||||
["model_version"] = scanner?.ModelVersion,
|
||||
["confidence_threshold"] = scanner?.ConfidenceThreshold,
|
||||
["category_count"] = scanner?.EnabledCategories.Count ?? 0,
|
||||
};
|
||||
|
||||
return new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["has_finding"] = context.HasAnyFinding,
|
||||
["has_active_finding"] = context.HasActiveFinding,
|
||||
["count"] = context.TotalFindingCount,
|
||||
["active_count"] = context.ActiveFindingCount,
|
||||
["severity"] = severity,
|
||||
["category"] = category,
|
||||
["verdict"] = verdict,
|
||||
["override"] = override_,
|
||||
["scanner"] = scannerDict,
|
||||
["ai_percentage"] = context.AiGeneratedPercentage,
|
||||
["highest_severity"] = context.HighestSeverity,
|
||||
["average_confidence"] = context.AverageConfidence,
|
||||
}.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps verdict status to policy recommendation.
|
||||
/// </summary>
|
||||
/// <param name="context">The AI Code Guard evidence context.</param>
|
||||
/// <returns>Recommendation string (allow, review, block).</returns>
|
||||
public static string GetRecommendation(AiCodeGuardEvidenceContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
return context.VerdictStatus switch
|
||||
{
|
||||
AiCodeGuardVerdictStatus.Pass => "allow",
|
||||
AiCodeGuardVerdictStatus.PassWithWarnings => "review",
|
||||
AiCodeGuardVerdictStatus.Fail => "block",
|
||||
AiCodeGuardVerdictStatus.Error => "block",
|
||||
_ => "review"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates finding summary for policy explanation (deterministic, ASCII-only).
|
||||
/// </summary>
|
||||
/// <param name="context">The AI Code Guard evidence context.</param>
|
||||
/// <returns>A summary string for audit/explanation purposes.</returns>
|
||||
public static string CreateFindingSummary(AiCodeGuardEvidenceContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (!context.HasAnyFinding)
|
||||
{
|
||||
return "No AI code guard findings detected.";
|
||||
}
|
||||
|
||||
var findings = context.ActiveFindings;
|
||||
var severityCounts = findings
|
||||
.GroupBy(f => f.Severity, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.Count(), StringComparer.Ordinal);
|
||||
|
||||
var parts = new List<string>();
|
||||
if (severityCounts.TryGetValue("critical", out var critical) && critical > 0)
|
||||
{
|
||||
parts.Add($"{critical} critical");
|
||||
}
|
||||
if (severityCounts.TryGetValue("high", out var high) && high > 0)
|
||||
{
|
||||
parts.Add($"{high} high");
|
||||
}
|
||||
if (severityCounts.TryGetValue("medium", out var medium) && medium > 0)
|
||||
{
|
||||
parts.Add($"{medium} medium");
|
||||
}
|
||||
if (severityCounts.TryGetValue("low", out var low) && low > 0)
|
||||
{
|
||||
parts.Add($"{low} low");
|
||||
}
|
||||
if (severityCounts.TryGetValue("info", out var info) && info > 0)
|
||||
{
|
||||
parts.Add($"{info} info");
|
||||
}
|
||||
|
||||
var summary = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} AI code guard finding(s): {1}",
|
||||
findings.Count,
|
||||
string.Join(", ", parts));
|
||||
|
||||
if (context.AiGeneratedPercentage.HasValue)
|
||||
{
|
||||
summary += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" (AI-generated: {0:F1}%)",
|
||||
context.AiGeneratedPercentage.Value);
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates explain trace annotation for policy decisions.
|
||||
/// </summary>
|
||||
/// <param name="context">The AI Code Guard evidence context.</param>
|
||||
/// <returns>Deterministic trace annotation.</returns>
|
||||
public static string CreateExplainTrace(AiCodeGuardEvidenceContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"guard.verdict={context.VerdictStatus}",
|
||||
$"guard.total_findings={context.TotalFindingCount}",
|
||||
$"guard.active_findings={context.ActiveFindingCount}",
|
||||
$"guard.overrides={context.Overrides.Count}"
|
||||
};
|
||||
|
||||
if (context.AiGeneratedPercentage.HasValue)
|
||||
{
|
||||
lines.Add($"guard.ai_percentage={context.AiGeneratedPercentage.Value:F1}");
|
||||
}
|
||||
|
||||
if (context.HighestSeverity is not null)
|
||||
{
|
||||
lines.Add($"guard.highest_severity={context.HighestSeverity}");
|
||||
}
|
||||
|
||||
// Sort for determinism
|
||||
lines.Sort(StringComparer.Ordinal);
|
||||
|
||||
return string.Join(";", lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IAiCodeGuardEvidenceProvider.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-001 - AI Code Guard evidence provider interface
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.AiCodeGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Provides AI Code Guard evidence for policy evaluation.
|
||||
/// </summary>
|
||||
public interface IAiCodeGuardEvidenceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all AI Code Guard findings.
|
||||
/// </summary>
|
||||
ImmutableList<AiCodeGuardFinding> Findings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all policy overrides applied to findings.
|
||||
/// </summary>
|
||||
ImmutableList<AiCodeGuardOverrideRecord> Overrides { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the overall verdict status.
|
||||
/// </summary>
|
||||
AiCodeGuardVerdictStatus VerdictStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the estimated AI-generated code percentage (0-100).
|
||||
/// </summary>
|
||||
double? AiGeneratedPercentage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner configuration used.
|
||||
/// </summary>
|
||||
AiCodeGuardScannerInfo? ScannerInfo { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI Code Guard finding from analysis.
|
||||
/// </summary>
|
||||
public sealed record AiCodeGuardFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique finding identifier.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Finding category (e.g., "ai-generated", "insecure-pattern", "hallucination").
|
||||
/// </summary>
|
||||
public required string Category { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Finding severity (info, low, medium, high, critical).
|
||||
/// </summary>
|
||||
public required string Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detection confidence (0.0-1.0).
|
||||
/// </summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rule ID that triggered this finding.
|
||||
/// </summary>
|
||||
public required string RuleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File path where finding was detected.
|
||||
/// </summary>
|
||||
public required string FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start line number (1-based).
|
||||
/// </summary>
|
||||
public required int StartLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End line number (1-based).
|
||||
/// </summary>
|
||||
public required int EndLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description.
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Suggested remediation.
|
||||
/// </summary>
|
||||
public string? Remediation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI Code Guard override record.
|
||||
/// </summary>
|
||||
public sealed record AiCodeGuardOverrideRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Finding ID being overridden.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Override action (suppress, downgrade, accept-risk, false-positive).
|
||||
/// </summary>
|
||||
public required string Action { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for the override.
|
||||
/// </summary>
|
||||
public required string Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Who approved the override.
|
||||
/// </summary>
|
||||
public required string ApprovedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the override was approved.
|
||||
/// </summary>
|
||||
public required DateTimeOffset ApprovedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the override expires (optional).
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall verdict status.
|
||||
/// </summary>
|
||||
public enum AiCodeGuardVerdictStatus
|
||||
{
|
||||
/// <summary>Analysis passed.</summary>
|
||||
Pass,
|
||||
|
||||
/// <summary>Analysis passed with warnings.</summary>
|
||||
PassWithWarnings,
|
||||
|
||||
/// <summary>Analysis failed.</summary>
|
||||
Fail,
|
||||
|
||||
/// <summary>Analysis errored.</summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scanner configuration information.
|
||||
/// </summary>
|
||||
public sealed record AiCodeGuardScannerInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Scanner version.
|
||||
/// </summary>
|
||||
public required string ScannerVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detection model version.
|
||||
/// </summary>
|
||||
public required string ModelVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence threshold used.
|
||||
/// </summary>
|
||||
public required double ConfidenceThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Enabled detection categories.
|
||||
/// </summary>
|
||||
public required ImmutableList<string> EnabledCategories { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AiCodeGuardSignalContextExtensionsTests.cs
|
||||
// Sprint: SPRINT_20260112_010_POLICY_ai_code_guard_policy
|
||||
// Task: POLICY-AIGUARD-004 - Deterministic tests for AI Code Guard signal evaluation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.AiCodeGuard;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.PolicyDsl.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for AI Code Guard signal context extensions.
|
||||
/// </summary>
|
||||
public sealed class AiCodeGuardSignalContextExtensionsTests
|
||||
{
|
||||
#region Test Fixtures
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateEmptyProvider()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList<AiCodeGuardFinding>.Empty,
|
||||
Overrides = ImmutableList<AiCodeGuardOverrideRecord>.Empty,
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.Pass,
|
||||
AiGeneratedPercentage = null,
|
||||
ScannerInfo = null
|
||||
};
|
||||
}
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateProviderWithFindings()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList.Create(
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-1",
|
||||
Category = "InsecurePattern",
|
||||
Severity = "high",
|
||||
Confidence = 0.85,
|
||||
RuleId = "guard/sql-injection",
|
||||
FilePath = "src/database.cs",
|
||||
StartLine = 42,
|
||||
EndLine = 48,
|
||||
Description = "Potential SQL injection in AI-generated code"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-2",
|
||||
Category = "AiGenerated",
|
||||
Severity = "medium",
|
||||
Confidence = 0.92,
|
||||
RuleId = "guard/ai-detected",
|
||||
FilePath = "src/utils.cs",
|
||||
StartLine = 100,
|
||||
EndLine = 120,
|
||||
Description = "AI-generated code detected"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-3",
|
||||
Category = "Hallucination",
|
||||
Severity = "critical",
|
||||
Confidence = 0.78,
|
||||
RuleId = "guard/api-hallucination",
|
||||
FilePath = "src/api.cs",
|
||||
StartLine = 200,
|
||||
EndLine = 210,
|
||||
Description = "Reference to non-existent API method"
|
||||
}
|
||||
),
|
||||
Overrides = ImmutableList<AiCodeGuardOverrideRecord>.Empty,
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.Fail,
|
||||
AiGeneratedPercentage = 42.5,
|
||||
ScannerInfo = new AiCodeGuardScannerInfo
|
||||
{
|
||||
ScannerVersion = "1.0.0",
|
||||
ModelVersion = "2024.1",
|
||||
ConfidenceThreshold = 0.7,
|
||||
EnabledCategories = ImmutableList.Create("AiGenerated", "InsecurePattern", "Hallucination")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IAiCodeGuardEvidenceProvider CreateProviderWithOverrides()
|
||||
{
|
||||
return new TestAiCodeGuardEvidenceProvider
|
||||
{
|
||||
Findings = ImmutableList.Create(
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-1",
|
||||
Category = "InsecurePattern",
|
||||
Severity = "high",
|
||||
Confidence = 0.85,
|
||||
RuleId = "guard/sql-injection",
|
||||
FilePath = "src/database.cs",
|
||||
StartLine = 42,
|
||||
EndLine = 48,
|
||||
Description = "Potential SQL injection in AI-generated code"
|
||||
},
|
||||
new AiCodeGuardFinding
|
||||
{
|
||||
Id = "finding-2",
|
||||
Category = "AiGenerated",
|
||||
Severity = "low",
|
||||
Confidence = 0.92,
|
||||
RuleId = "guard/ai-detected",
|
||||
FilePath = "src/utils.cs",
|
||||
StartLine = 100,
|
||||
EndLine = 120,
|
||||
Description = "AI-generated code detected"
|
||||
}
|
||||
),
|
||||
Overrides = ImmutableList.Create(
|
||||
new AiCodeGuardOverrideRecord
|
||||
{
|
||||
FindingId = "finding-1",
|
||||
Action = "suppress",
|
||||
Justification = "False positive - parameterized query is safe",
|
||||
ApprovedBy = "security-team@example.com",
|
||||
ApprovedAt = DateTimeOffset.UtcNow.AddDays(-7),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(23)
|
||||
}
|
||||
),
|
||||
VerdictStatus = AiCodeGuardVerdictStatus.PassWithWarnings,
|
||||
AiGeneratedPercentage = 15.0,
|
||||
ScannerInfo = null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Signal Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_EmptyProvider_SetsCorrectSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.count"));
|
||||
Assert.Equal("pass", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("allow", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsSeveritySignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.Equal(3, context.GetSignal<int>("guard.count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.critical"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.high"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.medium"));
|
||||
Assert.False(context.GetSignal<bool>("guard.severity.low"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsCategorySignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetSignal<bool>("guard.category.insecure_pattern"));
|
||||
Assert.True(context.GetSignal<bool>("guard.category.ai_generated"));
|
||||
Assert.True(context.GetSignal<bool>("guard.category.hallucination"));
|
||||
Assert.False(context.GetSignal<bool>("guard.category.license_risk"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsVerdictSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("fail", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.True(context.GetSignal<bool>("guard.verdict.fail"));
|
||||
Assert.False(context.GetSignal<bool>("guard.verdict.pass"));
|
||||
Assert.Equal("block", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithFindings_SetsAiPercentage()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(42.5, context.GetSignal<double?>("guard.ai_percentage"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Override Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_FiltersActiveFindingsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, context.GetSignal<int>("guard.count")); // Total findings
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.active_count")); // After suppression
|
||||
Assert.True(context.GetSignal<bool>("guard.has_active_finding"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_SetsOverrideSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.override.count"));
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.override.active_count"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.override.expired_count"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithOverrides_SetsCorrectVerdict()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithOverrides();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("passwithwarnings", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.True(context.GetSignal<bool>("guard.verdict.pass_with_warnings"));
|
||||
Assert.Equal("review", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scanner Info Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_WithScannerInfo_SetsScannerSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("1.0.0", context.GetSignal<string>("guard.scanner.version"));
|
||||
Assert.Equal("2024.1", context.GetSignal<string>("guard.scanner.model_version"));
|
||||
Assert.Equal(0.7, context.GetSignal<double?>("guard.scanner.confidence_threshold"));
|
||||
Assert.Equal(3, context.GetSignal<int>("guard.scanner.category_count"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_NullScannerInfo_SetsNullScannerSignals()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.GetSignal<string>("guard.scanner.version"));
|
||||
Assert.Null(context.GetSignal<string>("guard.scanner.model_version"));
|
||||
Assert.Equal(0, context.GetSignal<int>("guard.scanner.category_count"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested Object Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardEvidence_SetsNestedGuardObject()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardEvidence(evidenceContext)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
var guard = context.GetSignal<IReadOnlyDictionary<string, object?>>("guard");
|
||||
Assert.NotNull(guard);
|
||||
Assert.True((bool)guard["has_finding"]!);
|
||||
Assert.Equal(3, guard["count"]);
|
||||
|
||||
var severity = guard["severity"] as IReadOnlyDictionary<string, object?>;
|
||||
Assert.NotNull(severity);
|
||||
Assert.True((bool)severity["critical"]!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateExplainTrace_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act - create trace multiple times
|
||||
var trace1 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
var trace2 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
var trace3 = AiCodeGuardSignalBinder.CreateExplainTrace(evidenceContext);
|
||||
|
||||
// Assert - all traces should be identical
|
||||
Assert.Equal(trace1, trace2);
|
||||
Assert.Equal(trace2, trace3);
|
||||
|
||||
// Verify trace contains expected content
|
||||
Assert.Contains("guard.verdict=Fail", trace1);
|
||||
Assert.Contains("guard.total_findings=3", trace1);
|
||||
Assert.Contains("guard.ai_percentage=42.5", trace1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFindingSummary_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProviderWithFindings();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var summary1 = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
var summary2 = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(summary1, summary2);
|
||||
Assert.Contains("3 AI code guard finding(s)", summary1);
|
||||
Assert.Contains("1 critical", summary1);
|
||||
Assert.Contains("1 high", summary1);
|
||||
Assert.Contains("1 medium", summary1);
|
||||
Assert.Contains("AI-generated: 42.5%", summary1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFindingSummary_EmptyFindings_ReturnsNoFindings()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateEmptyProvider();
|
||||
var evidenceContext = new AiCodeGuardEvidenceContext(provider);
|
||||
|
||||
// Act
|
||||
var summary = AiCodeGuardSignalBinder.CreateFindingSummary(evidenceContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("No AI code guard findings detected.", summary);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Simplified Result Tests
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_SetsBasicSignals()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "fail",
|
||||
totalFindings: 5,
|
||||
criticalCount: 1,
|
||||
highCount: 2,
|
||||
mediumCount: 2,
|
||||
aiPercentage: 25.0)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("fail", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal(5, context.GetSignal<int>("guard.count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.has_finding"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.critical"));
|
||||
Assert.Equal(1, context.GetSignal<int>("guard.severity.critical_count"));
|
||||
Assert.True(context.GetSignal<bool>("guard.severity.high"));
|
||||
Assert.Equal(2, context.GetSignal<int>("guard.severity.high_count"));
|
||||
Assert.Equal(25.0, context.GetSignal<double?>("guard.ai_percentage"));
|
||||
Assert.Equal("block", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_PassStatus_SetsAllowRecommendation()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "pass",
|
||||
totalFindings: 0)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pass", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("allow", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAiCodeGuardResult_WarningStatus_SetsReviewRecommendation()
|
||||
{
|
||||
// Act
|
||||
var context = SignalContext.Builder()
|
||||
.WithAiCodeGuardResult(
|
||||
status: "pass_with_warnings",
|
||||
totalFindings: 2,
|
||||
mediumCount: 2)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pass_with_warnings", context.GetSignal<string>("guard.verdict"));
|
||||
Assert.Equal("review", context.GetSignal<string>("guard.recommendation"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Provider Implementation
|
||||
|
||||
private sealed class TestAiCodeGuardEvidenceProvider : IAiCodeGuardEvidenceProvider
|
||||
{
|
||||
public ImmutableList<AiCodeGuardFinding> Findings { get; init; } = ImmutableList<AiCodeGuardFinding>.Empty;
|
||||
public ImmutableList<AiCodeGuardOverrideRecord> Overrides { get; init; } = ImmutableList<AiCodeGuardOverrideRecord>.Empty;
|
||||
public AiCodeGuardVerdictStatus VerdictStatus { get; init; }
|
||||
public double? AiGeneratedPercentage { get; init; }
|
||||
public AiCodeGuardScannerInfo? ScannerInfo { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user