Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.AiMlSecurity/Analyzers/ModelProvenanceVerifier.cs
2026-02-01 21:37:40 +02:00

190 lines
6.6 KiB
C#

using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.AiMlSecurity.Models;
using StellaOps.Scanner.AiMlSecurity.Policy;
using System.Collections.Immutable;
namespace StellaOps.Scanner.AiMlSecurity.Analyzers;
public sealed class ModelProvenanceVerifier : IAiMlSecurityCheck
{
public Task<AiMlSecurityResult> AnalyzeAsync(AiMlSecurityContext context, CancellationToken ct = default)
{
var findings = new List<AiSecurityFinding>();
var provenancePolicy = context.Policy.ProvenanceRequirements;
foreach (var component in context.ModelComponents)
{
ct.ThrowIfCancellationRequested();
if (context.IsExempted(component))
{
continue;
}
var hasHash = !component.Hashes.IsDefaultOrEmpty;
var hasSignature = HasSignature(component);
var source = ResolveSource(component);
var hasTrustedSource = HasTrustedSource(source, provenancePolicy);
if ((provenancePolicy.RequireHash && !hasHash)
|| (provenancePolicy.RequireSignature && !hasSignature)
|| (!provenancePolicy.TrustedSources.IsDefaultOrEmpty && !hasTrustedSource))
{
findings.Add(new AiSecurityFinding
{
Type = AiSecurityFindingType.UnverifiedModelProvenance,
Severity = provenancePolicy.RequireSignature ? Severity.High : Severity.Medium,
Title = "Unverified model provenance",
Description = "Model provenance does not meet policy requirements.",
Remediation = "Provide hashes/signatures and trusted source references.",
ComponentName = component.Name,
ComponentBomRef = component.BomRef,
ModelName = component.Name,
Metadata = ImmutableDictionary<string, string>.Empty
.Add("hasHash", hasHash.ToString())
.Add("hasSignature", hasSignature.ToString())
.Add("source", source ?? string.Empty)
});
}
if (component.Modified || HasLineage(component))
{
findings.Add(new AiSecurityFinding
{
Type = AiSecurityFindingType.ModelDriftRisk,
Severity = Severity.Medium,
Title = "Model drift risk",
Description = "Model indicates modifications or fine-tuning lineage.",
Remediation = "Review fine-tuning lineage and validate drift monitoring.",
ComponentName = component.Name,
ComponentBomRef = component.BomRef,
ModelName = component.Name
});
}
if (IsAdversarialVulnerable(component))
{
findings.Add(new AiSecurityFinding
{
Type = AiSecurityFindingType.AdversarialVulnerability,
Severity = Severity.High,
Title = "Adversarial vulnerability flagged",
Description = "Model indicates adversarial robustness concerns.",
Remediation = "Perform adversarial testing and mitigation.",
ComponentName = component.Name,
ComponentBomRef = component.BomRef,
ModelName = component.Name
});
}
}
return Task.FromResult(new AiMlSecurityResult
{
Findings = findings.ToImmutableArray()
});
}
private static bool HasSignature(ParsedComponent component)
{
if (component.ExternalReferences.Any(reference =>
(reference.Type ?? string.Empty).Contains("signature", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
foreach (var pair in component.Properties)
{
if (pair.Key.Contains("signature", StringComparison.OrdinalIgnoreCase)
&& IsTruthy(pair.Value))
{
return true;
}
}
return false;
}
private static string? ResolveSource(ParsedComponent component)
{
if (!string.IsNullOrWhiteSpace(component.Publisher))
{
return component.Publisher;
}
if (!string.IsNullOrWhiteSpace(component.Supplier?.Name))
{
return component.Supplier?.Name;
}
var external = component.ExternalReferences
.Select(reference => reference.Url)
.FirstOrDefault(url => !string.IsNullOrWhiteSpace(url));
return external;
}
private static bool HasTrustedSource(string? source, AiProvenanceRequirements policy)
{
if (string.IsNullOrWhiteSpace(source))
{
return false;
}
var normalized = source.ToLowerInvariant();
return policy.TrustedSources.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase))
|| policy.KnownModelHubs.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
private static bool HasLineage(ParsedComponent component)
{
var pedigree = component.Pedigree;
if (pedigree is null)
{
return false;
}
return !pedigree.Ancestors.IsDefaultOrEmpty || !pedigree.Variants.IsDefaultOrEmpty;
}
private static bool IsAdversarialVulnerable(ParsedComponent component)
{
if (component.Properties.TryGetValue("ai:adversarialVulnerability", out var value)
&& IsTruthy(value))
{
return true;
}
if (component.Properties.TryGetValue("ai:adversarial", out var shorthand)
&& IsTruthy(shorthand))
{
return true;
}
if (component.ModelCard?.Considerations?.TechnicalLimitations is { } limitations)
{
foreach (var limitation in limitations)
{
if (limitation.Contains("adversarial", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
private static bool IsTruthy(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
return value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase)
|| value.Equals("1", StringComparison.OrdinalIgnoreCase);
}
}