sprints completion. new product advisories prepared

This commit is contained in:
master
2026-01-16 16:30:03 +02:00
parent a927d924e3
commit 4ca3ce8fb4
255 changed files with 42434 additions and 1020 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View File

@@ -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
}