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:
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
196
src/AdvisoryAI/StellaOps.AdvisoryAI/PolicyStudio/PolicyIntent.cs
Normal file
196
src/AdvisoryAI/StellaOps.AdvisoryAI/PolicyStudio/PolicyIntent.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user