Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism

- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency.
- Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling.
- Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies.
- Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification.
- Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -0,0 +1,273 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.PolicyStudio;
/// <summary>
/// AI-powered implementation of policy intent parser.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-03
/// </summary>
public sealed class AiPolicyIntentParser : IPolicyIntentParser
{
private readonly IPolicyPromptService _promptService;
private readonly IPolicyInferenceClient _inferenceClient;
private readonly IPolicyIntentStore _intentStore;
private static readonly string[] FewShotExamples = new[]
{
"Input: Block all critical vulnerabilities in production\nIntent: OverrideRule | Conditions: [severity=critical, scope=production] | Actions: [set_verdict=block]",
"Input: Allow log4j vulnerabilities in dev if not reachable\nIntent: ExceptionCondition | Conditions: [vuln_id contains log4j, scope=dev, reachable=false] | Actions: [set_verdict=allow]",
"Input: Escalate any CVE with EPSS score above 0.9\nIntent: EscalationRule | Conditions: [epss_score > 0.9] | Actions: [escalate, notify=security-team]",
"Input: Override to pass if vendor VEX says not_affected\nIntent: OverrideRule | Conditions: [vex_status=not_affected, vex_source=vendor] | Actions: [set_verdict=pass]",
"Input: Require approval for any major version bump\nIntent: ThresholdRule | Conditions: [upgrade_type=major] | Actions: [require_approval]"
};
public AiPolicyIntentParser(
IPolicyPromptService promptService,
IPolicyInferenceClient inferenceClient,
IPolicyIntentStore intentStore)
{
_promptService = promptService;
_inferenceClient = inferenceClient;
_intentStore = intentStore;
}
public async Task<PolicyParseResult> ParseAsync(
string naturalLanguageInput,
PolicyParseContext? context = null,
CancellationToken cancellationToken = default)
{
// Build prompt with few-shot examples
var prompt = await _promptService.BuildParsePromptAsync(
naturalLanguageInput,
FewShotExamples,
context,
cancellationToken);
// Generate via LLM
var inferenceResult = await _inferenceClient.ParseIntentAsync(prompt, cancellationToken);
// Parse LLM response into structured intent
var intent = ParseIntentFromResponse(naturalLanguageInput, inferenceResult);
// Store for clarification workflow
await _intentStore.StoreAsync(intent, cancellationToken);
return new PolicyParseResult
{
Intent = intent,
Success = intent.Confidence >= 0.7,
ErrorMessage = intent.Confidence < 0.7 ? "Ambiguous input - clarification needed" : null,
ModelId = inferenceResult.ModelId,
ParsedAt = DateTime.UtcNow.ToString("O")
};
}
public async Task<PolicyParseResult> ClarifyAsync(
string intentId,
string clarification,
CancellationToken cancellationToken = default)
{
var original = await _intentStore.GetAsync(intentId, cancellationToken)
?? throw new InvalidOperationException($"Intent {intentId} not found");
// Build clarification prompt
var prompt = await _promptService.BuildClarificationPromptAsync(
original,
clarification,
cancellationToken);
// Generate clarified intent
var inferenceResult = await _inferenceClient.ParseIntentAsync(prompt, cancellationToken);
// Parse updated intent
var clarifiedIntent = ParseIntentFromResponse(original.OriginalInput, inferenceResult);
// Update store
await _intentStore.StoreAsync(clarifiedIntent, cancellationToken);
return new PolicyParseResult
{
Intent = clarifiedIntent,
Success = clarifiedIntent.Confidence >= 0.8,
ModelId = inferenceResult.ModelId,
ParsedAt = DateTime.UtcNow.ToString("O")
};
}
private static PolicyIntent ParseIntentFromResponse(string originalInput, PolicyInferenceResult result)
{
// Parse the structured response from LLM
// In a real implementation, this would parse the actual LLM output format
var intentId = $"intent:{ComputeHash(originalInput)[..12]}";
var intentType = ExtractIntentType(result.Content);
var conditions = ExtractConditions(result.Content);
var actions = ExtractActions(result.Content);
var clarifyingQuestions = ExtractClarifyingQuestions(result.Content);
return new PolicyIntent
{
IntentId = intentId,
IntentType = intentType,
OriginalInput = originalInput,
Conditions = conditions,
Actions = actions,
Scope = "all",
Priority = 100,
Confidence = result.Confidence,
ClarifyingQuestions = clarifyingQuestions.Count > 0 ? clarifyingQuestions : null
};
}
private static PolicyIntentType ExtractIntentType(string content)
{
if (content.Contains("override", StringComparison.OrdinalIgnoreCase))
return PolicyIntentType.OverrideRule;
if (content.Contains("escalat", StringComparison.OrdinalIgnoreCase))
return PolicyIntentType.EscalationRule;
if (content.Contains("exception", StringComparison.OrdinalIgnoreCase))
return PolicyIntentType.ExceptionCondition;
if (content.Contains("precedence", StringComparison.OrdinalIgnoreCase))
return PolicyIntentType.MergePrecedence;
if (content.Contains("threshold", StringComparison.OrdinalIgnoreCase))
return PolicyIntentType.ThresholdRule;
return PolicyIntentType.OverrideRule;
}
private static IReadOnlyList<PolicyCondition> ExtractConditions(string content)
{
var conditions = new List<PolicyCondition>();
// Simplified extraction - real implementation would parse structured output
if (content.Contains("severity", StringComparison.OrdinalIgnoreCase))
{
conditions.Add(new PolicyCondition
{
Field = "severity",
Operator = "equals",
Value = "critical"
});
}
if (content.Contains("reachable", StringComparison.OrdinalIgnoreCase))
{
conditions.Add(new PolicyCondition
{
Field = "reachable",
Operator = "equals",
Value = content.Contains("not reachable", StringComparison.OrdinalIgnoreCase) ? false : true,
Connector = conditions.Count > 0 ? "and" : null
});
}
return conditions;
}
private static IReadOnlyList<PolicyAction> ExtractActions(string content)
{
var actions = new List<PolicyAction>();
if (content.Contains("block", StringComparison.OrdinalIgnoreCase))
{
actions.Add(new PolicyAction
{
ActionType = "set_verdict",
Parameters = new Dictionary<string, object> { { "verdict", "block" } }
});
}
if (content.Contains("allow", StringComparison.OrdinalIgnoreCase) ||
content.Contains("pass", StringComparison.OrdinalIgnoreCase))
{
actions.Add(new PolicyAction
{
ActionType = "set_verdict",
Parameters = new Dictionary<string, object> { { "verdict", "pass" } }
});
}
if (content.Contains("escalat", StringComparison.OrdinalIgnoreCase))
{
actions.Add(new PolicyAction
{
ActionType = "escalate",
Parameters = new Dictionary<string, object> { { "notify", "security-team" } }
});
}
return actions;
}
private static IReadOnlyList<string> ExtractClarifyingQuestions(string content)
{
var questions = new List<string>();
if (content.Contains("?"))
{
// Extract questions from content
var lines = content.Split('\n');
foreach (var line in lines)
{
if (line.TrimEnd().EndsWith('?'))
{
questions.Add(line.Trim());
}
}
}
return questions;
}
private static string ComputeHash(string content)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return Convert.ToHexStringLower(bytes);
}
}
/// <summary>
/// Prompt for policy parsing.
/// </summary>
public sealed record PolicyPrompt
{
public required string Content { get; init; }
public required string TemplateVersion { get; init; }
}
/// <summary>
/// Inference result for policy parsing.
/// </summary>
public sealed record PolicyInferenceResult
{
public required string Content { get; init; }
public required double Confidence { get; init; }
public required string ModelId { get; init; }
}
/// <summary>
/// Service for building policy prompts.
/// </summary>
public interface IPolicyPromptService
{
Task<PolicyPrompt> BuildParsePromptAsync(
string input,
string[] examples,
PolicyParseContext? context,
CancellationToken cancellationToken = default);
Task<PolicyPrompt> BuildClarificationPromptAsync(
PolicyIntent original,
string clarification,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Client for policy inference.
/// </summary>
public interface IPolicyInferenceClient
{
Task<PolicyInferenceResult> ParseIntentAsync(PolicyPrompt prompt, CancellationToken cancellationToken = default);
}
/// <summary>
/// Store for policy intents.
/// </summary>
public interface IPolicyIntentStore
{
Task StoreAsync(PolicyIntent intent, CancellationToken cancellationToken = default);
Task<PolicyIntent?> GetAsync(string intentId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,59 @@
namespace StellaOps.AdvisoryAI.PolicyStudio;
/// <summary>
/// Service for parsing natural language into policy intents.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-02
/// </summary>
public interface IPolicyIntentParser
{
/// <summary>
/// Parse natural language input into a policy intent.
/// </summary>
/// <param name="naturalLanguageInput">The natural language description of the policy.</param>
/// <param name="context">Optional context about the policy scope.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Parsed policy intent with confidence score.</returns>
Task<PolicyParseResult> ParseAsync(
string naturalLanguageInput,
PolicyParseContext? context = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Clarify an ambiguous intent with additional information.
/// </summary>
/// <param name="intentId">The intent to clarify.</param>
/// <param name="clarification">User's clarification response.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Updated parsed policy intent.</returns>
Task<PolicyParseResult> ClarifyAsync(
string intentId,
string clarification,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Context for policy parsing.
/// </summary>
public sealed record PolicyParseContext
{
/// <summary>
/// Default scope for the policy.
/// </summary>
public string? DefaultScope { get; init; }
/// <summary>
/// Organization or team context.
/// </summary>
public string? OrganizationId { get; init; }
/// <summary>
/// Existing policies for conflict detection.
/// </summary>
public IReadOnlyList<string>? ExistingPolicyIds { get; init; }
/// <summary>
/// Preferred policy language (yaml, json).
/// </summary>
public string? PreferredFormat { get; init; }
}

View File

@@ -0,0 +1,196 @@
namespace StellaOps.AdvisoryAI.PolicyStudio;
/// <summary>
/// Type of policy intent.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-01
/// </summary>
public enum PolicyIntentType
{
/// <summary>
/// Override default verdict for specific conditions.
/// </summary>
OverrideRule,
/// <summary>
/// Escalate findings under specific conditions.
/// </summary>
EscalationRule,
/// <summary>
/// Define exception conditions that bypass normal rules.
/// </summary>
ExceptionCondition,
/// <summary>
/// Define precedence when multiple rules match.
/// </summary>
MergePrecedence,
/// <summary>
/// Set thresholds for automatic verdicts.
/// </summary>
ThresholdRule,
/// <summary>
/// Define scope restrictions for rules.
/// </summary>
ScopeRestriction
}
/// <summary>
/// Condition in a policy rule.
/// </summary>
public sealed record PolicyCondition
{
/// <summary>
/// Field to evaluate (severity, cvss_score, reachable, has_vex, etc.).
/// </summary>
public required string Field { get; init; }
/// <summary>
/// Operator (equals, greater_than, less_than, contains, in, not_in).
/// </summary>
public required string Operator { get; init; }
/// <summary>
/// Value to compare against.
/// </summary>
public required object Value { get; init; }
/// <summary>
/// Logical connector to next condition (and, or).
/// </summary>
public string? Connector { get; init; }
}
/// <summary>
/// Action to take when conditions match.
/// </summary>
public sealed record PolicyAction
{
/// <summary>
/// Action type (set_verdict, escalate, notify, block, allow).
/// </summary>
public required string ActionType { get; init; }
/// <summary>
/// Action parameters.
/// </summary>
public required IReadOnlyDictionary<string, object> Parameters { get; init; }
}
/// <summary>
/// Authority level of the policy draft.
/// </summary>
public enum PolicyDraftAuthority
{
/// <summary>
/// AI suggestion requiring review.
/// </summary>
Suggestion,
/// <summary>
/// Validated draft ready for approval.
/// </summary>
Validated,
/// <summary>
/// Approved and ready for production.
/// </summary>
Approved
}
/// <summary>
/// A parsed policy intent from natural language.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-04
/// </summary>
public sealed record PolicyIntent
{
/// <summary>
/// Unique intent ID.
/// </summary>
public required string IntentId { get; init; }
/// <summary>
/// Type of intent.
/// </summary>
public required PolicyIntentType IntentType { get; init; }
/// <summary>
/// Original natural language input.
/// </summary>
public required string OriginalInput { get; init; }
/// <summary>
/// Conditions for the rule.
/// </summary>
public required IReadOnlyList<PolicyCondition> Conditions { get; init; }
/// <summary>
/// Actions to take when conditions match.
/// </summary>
public required IReadOnlyList<PolicyAction> Actions { get; init; }
/// <summary>
/// Scope of the rule (all, service, team, project).
/// </summary>
public required string Scope { get; init; }
/// <summary>
/// Scope identifier.
/// </summary>
public string? ScopeId { get; init; }
/// <summary>
/// Rule priority (higher = evaluated first).
/// </summary>
public required int Priority { get; init; }
/// <summary>
/// Confidence in the parsing (0.0-1.0).
/// </summary>
public required double Confidence { get; init; }
/// <summary>
/// Alternative interpretations if ambiguous.
/// </summary>
public IReadOnlyList<PolicyIntent>? Alternatives { get; init; }
/// <summary>
/// Clarifying questions if ambiguous.
/// </summary>
public IReadOnlyList<string>? ClarifyingQuestions { get; init; }
}
/// <summary>
/// Result of parsing natural language to policy intent.
/// </summary>
public sealed record PolicyParseResult
{
/// <summary>
/// Primary parsed intent.
/// </summary>
public required PolicyIntent Intent { get; init; }
/// <summary>
/// Whether parsing was successful.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// Error message if parsing failed.
/// </summary>
public string? ErrorMessage { get; init; }
/// <summary>
/// Model ID used for parsing.
/// </summary>
public required string ModelId { get; init; }
/// <summary>
/// Parsed timestamp.
/// </summary>
public required string ParsedAt { get; init; }
}