Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Prompting;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Guardrails;
|
||||
|
||||
public interface IAdvisoryGuardrailPipeline
|
||||
{
|
||||
Task<AdvisoryGuardrailResult> EvaluateAsync(AdvisoryPrompt prompt, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record AdvisoryGuardrailResult(
|
||||
bool Blocked,
|
||||
string SanitizedPrompt,
|
||||
ImmutableArray<AdvisoryGuardrailViolation> Violations,
|
||||
ImmutableDictionary<string, string> Metadata)
|
||||
{
|
||||
public static AdvisoryGuardrailResult Allowed(string sanitizedPrompt, ImmutableDictionary<string, string>? metadata = null)
|
||||
=> new(false, sanitizedPrompt, ImmutableArray<AdvisoryGuardrailViolation>.Empty, metadata ?? ImmutableDictionary<string, string>.Empty);
|
||||
|
||||
public static AdvisoryGuardrailResult Blocked(string sanitizedPrompt, IEnumerable<AdvisoryGuardrailViolation> violations, ImmutableDictionary<string, string>? metadata = null)
|
||||
=> new(true, sanitizedPrompt, violations.ToImmutableArray(), metadata ?? ImmutableDictionary<string, string>.Empty);
|
||||
}
|
||||
|
||||
public sealed record AdvisoryGuardrailViolation(string Code, string Message);
|
||||
|
||||
public sealed class AdvisoryGuardrailOptions
|
||||
{
|
||||
private static readonly string[] DefaultBlockedPhrases =
|
||||
{
|
||||
"ignore previous instructions",
|
||||
"disregard earlier instructions",
|
||||
"you are now the system",
|
||||
"override the system prompt",
|
||||
"please jailbreak"
|
||||
};
|
||||
|
||||
public int MaxPromptLength { get; set; } = 16000;
|
||||
|
||||
public bool RequireCitations { get; set; } = true;
|
||||
|
||||
public List<string> BlockedPhrases { get; } = new(DefaultBlockedPhrases);
|
||||
}
|
||||
|
||||
internal sealed class AdvisoryGuardrailPipeline : IAdvisoryGuardrailPipeline
|
||||
{
|
||||
private readonly AdvisoryGuardrailOptions _options;
|
||||
private readonly ILogger<AdvisoryGuardrailPipeline>? _logger;
|
||||
private readonly IReadOnlyList<RedactionRule> _redactionRules;
|
||||
private readonly string[] _blockedPhraseCache;
|
||||
|
||||
public AdvisoryGuardrailPipeline(
|
||||
IOptions<AdvisoryGuardrailOptions> options,
|
||||
ILogger<AdvisoryGuardrailPipeline>? logger = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.Value ?? new AdvisoryGuardrailOptions();
|
||||
_logger = logger;
|
||||
|
||||
_redactionRules = new[]
|
||||
{
|
||||
new RedactionRule(
|
||||
new Regex(@"(?i)(aws_secret_access_key\s*[:=]\s*)([A-Za-z0-9\/+=]{40,})", RegexOptions.CultureInvariant | RegexOptions.Compiled),
|
||||
match => $"{match.Groups[1].Value}[REDACTED_AWS_SECRET]"),
|
||||
new RedactionRule(
|
||||
new Regex(@"(?i)(token|apikey|password)\s*[:=]\s*([A-Za-z0-9\-_/]{16,})", RegexOptions.CultureInvariant | RegexOptions.Compiled),
|
||||
match => $"{match.Groups[1].Value}: [REDACTED_CREDENTIAL]"),
|
||||
new RedactionRule(
|
||||
new Regex(@"(?is)-----BEGIN [^-]+ PRIVATE KEY-----.*?-----END [^-]+ PRIVATE KEY-----", RegexOptions.CultureInvariant | RegexOptions.Compiled),
|
||||
_ => "[REDACTED_PRIVATE_KEY]")
|
||||
};
|
||||
|
||||
_blockedPhraseCache = _options.BlockedPhrases
|
||||
.Where(phrase => !string.IsNullOrWhiteSpace(phrase))
|
||||
.Select(phrase => phrase.Trim().ToLowerInvariant())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Task<AdvisoryGuardrailResult> EvaluateAsync(AdvisoryPrompt prompt, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prompt);
|
||||
|
||||
var sanitized = prompt.Prompt ?? string.Empty;
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
var violations = ImmutableArray.CreateBuilder<AdvisoryGuardrailViolation>();
|
||||
|
||||
var redactionCount = ApplyRedactions(ref sanitized);
|
||||
metadataBuilder["prompt_length"] = sanitized.Length.ToString(CultureInfo.InvariantCulture);
|
||||
metadataBuilder["redaction_count"] = redactionCount.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var blocked = false;
|
||||
|
||||
if (_options.RequireCitations && prompt.Citations.IsDefaultOrEmpty)
|
||||
{
|
||||
blocked = true;
|
||||
violations.Add(new AdvisoryGuardrailViolation("citation_missing", "At least one citation is required."));
|
||||
}
|
||||
|
||||
if (!prompt.Citations.IsDefaultOrEmpty)
|
||||
{
|
||||
foreach (var citation in prompt.Citations)
|
||||
{
|
||||
if (citation.Index <= 0 || string.IsNullOrWhiteSpace(citation.DocumentId) || string.IsNullOrWhiteSpace(citation.ChunkId))
|
||||
{
|
||||
blocked = true;
|
||||
violations.Add(new AdvisoryGuardrailViolation("citation_invalid", "Citation index or identifiers are missing."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.MaxPromptLength > 0 && sanitized.Length > _options.MaxPromptLength)
|
||||
{
|
||||
blocked = true;
|
||||
violations.Add(new AdvisoryGuardrailViolation("prompt_too_long", $"Prompt length {sanitized.Length} exceeds {_options.MaxPromptLength}."));
|
||||
}
|
||||
|
||||
if (_blockedPhraseCache.Length > 0)
|
||||
{
|
||||
var lowered = sanitized.ToLowerInvariant();
|
||||
var phraseHits = 0;
|
||||
foreach (var phrase in _blockedPhraseCache)
|
||||
{
|
||||
if (lowered.Contains(phrase))
|
||||
{
|
||||
phraseHits++;
|
||||
violations.Add(new AdvisoryGuardrailViolation("prompt_injection", $"Detected blocked phrase '{phrase}'"));
|
||||
}
|
||||
}
|
||||
|
||||
if (phraseHits > 0)
|
||||
{
|
||||
blocked = true;
|
||||
metadataBuilder["blocked_phrase_count"] = phraseHits.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
var metadata = metadataBuilder.ToImmutable();
|
||||
|
||||
if (blocked)
|
||||
{
|
||||
_logger?.LogWarning("Guardrail blocked prompt for cache key {CacheKey}", prompt.CacheKey);
|
||||
return Task.FromResult(AdvisoryGuardrailResult.Blocked(sanitized, violations, metadata));
|
||||
}
|
||||
|
||||
return Task.FromResult(AdvisoryGuardrailResult.Allowed(sanitized, metadata));
|
||||
}
|
||||
|
||||
private int ApplyRedactions(ref string sanitized)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
foreach (var rule in _redactionRules)
|
||||
{
|
||||
sanitized = rule.Regex.Replace(sanitized, match =>
|
||||
{
|
||||
count++;
|
||||
return rule.Replacement(match);
|
||||
});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private sealed record RedactionRule(Regex Regex, Func<Match, string> Replacement);
|
||||
}
|
||||
|
||||
internal sealed class NoOpAdvisoryGuardrailPipeline : IAdvisoryGuardrailPipeline
|
||||
{
|
||||
private readonly ILogger<NoOpAdvisoryGuardrailPipeline>? _logger;
|
||||
|
||||
public NoOpAdvisoryGuardrailPipeline(ILogger<NoOpAdvisoryGuardrailPipeline>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<AdvisoryGuardrailResult> EvaluateAsync(AdvisoryPrompt prompt, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prompt);
|
||||
_logger?.LogDebug("No-op guardrail pipeline invoked for cache key {CacheKey}", prompt.CacheKey);
|
||||
return Task.FromResult(AdvisoryGuardrailResult.Allowed(prompt.Prompt ?? string.Empty));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user