sprints completion. new product advisories prepared
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user