tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -0,0 +1,206 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class AlgorithmStrengthAnalyzer : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
var findings = new List<CryptoFinding>();
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Algorithm)
{
continue;
}
var algorithm = CryptoAlgorithmCatalog.ResolveAlgorithmName(component);
if (string.IsNullOrWhiteSpace(algorithm))
{
continue;
}
if (context.IsExempted(component, algorithm))
{
continue;
}
var strength = ClassifyStrength(algorithm, crypto);
if (strength is AlgorithmStrength.Broken or AlgorithmStrength.Weak or AlgorithmStrength.Legacy)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.WeakAlgorithm,
Severity = MapStrengthSeverity(strength),
Title = $"Weak cryptographic algorithm detected ({algorithm})",
Description = $"Component {component.Name ?? component.BomRef} uses {algorithm}, which is classified as {strength.ToString().ToLowerInvariant()}.",
Remediation = "Replace with a modern, approved algorithm (AES-GCM, SHA-256+, or post-quantum where required).",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto)
});
}
if (IsProhibited(context, algorithm))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.WeakAlgorithm,
Severity = Severity.High,
Title = $"Prohibited algorithm detected ({algorithm})",
Description = $"Policy prohibits {algorithm} but it appears in component {component.Name ?? component.BomRef}.",
Remediation = "Remove the prohibited algorithm or add a scoped exemption with expiration.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto)
});
}
var keySize = crypto.AlgorithmProperties?.KeySize;
var family = CryptoAlgorithmCatalog.GetAlgorithmFamily(algorithm);
if (keySize.HasValue && !string.IsNullOrWhiteSpace(family))
{
if (context.Policy.MinimumKeyLengths.TryGetValue(family, out var minimum)
&& keySize.Value < minimum)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.ShortKeyLength,
Severity = Severity.High,
Title = $"Key length below policy minimum ({algorithm})",
Description = $"{algorithm} key length {keySize} is below required minimum {minimum}.",
Remediation = "Rotate keys to meet policy minimum key length.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto, ("keySize", keySize.Value.ToString()), ("minimumKeySize", minimum.ToString()))
});
}
}
if (crypto.AlgorithmProperties?.Mode == CryptoMode.Ecb)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.InsecureMode,
Severity = Severity.High,
Title = $"Insecure cipher mode detected ({algorithm})",
Description = "ECB mode does not provide semantic security and should be avoided.",
Remediation = "Use GCM or CTR mode with authenticated encryption.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto, ("mode", "ECB"))
});
}
if (context.Policy.RequiredFeatures.AuthenticatedEncryption
&& crypto.AlgorithmProperties?.CryptoFunctions is { Length: > 0 } functions
&& functions.Any(f => f.Contains("encrypt", StringComparison.OrdinalIgnoreCase))
&& !functions.Any(f => f.Contains("mac", StringComparison.OrdinalIgnoreCase)
|| f.Contains("auth", StringComparison.OrdinalIgnoreCase)
|| f.Contains("integrity", StringComparison.OrdinalIgnoreCase)))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.MissingIntegrity,
Severity = Severity.Medium,
Title = $"Missing integrity protection ({algorithm})",
Description = "Encryption functions were declared without authenticated integrity protection.",
Remediation = "Ensure authenticated encryption (e.g., AES-GCM) or add MAC/HMAC coverage.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto)
});
}
}
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray()
});
}
private static AlgorithmStrength ClassifyStrength(string algorithm, ParsedCryptoProperties properties)
{
if (CryptoAlgorithmCatalog.IsPostQuantum(algorithm))
{
return AlgorithmStrength.PostQuantum;
}
if (CryptoAlgorithmCatalog.IsWeakAlgorithm(algorithm))
{
return AlgorithmStrength.Weak;
}
if (!string.IsNullOrWhiteSpace(properties.AlgorithmProperties?.Curve)
&& properties.AlgorithmProperties?.Curve?.Contains("secp256", StringComparison.OrdinalIgnoreCase) == true)
{
return AlgorithmStrength.Strong;
}
return AlgorithmStrength.Acceptable;
}
private static Severity MapStrengthSeverity(AlgorithmStrength strength)
{
return strength switch
{
AlgorithmStrength.Broken => Severity.Critical,
AlgorithmStrength.Weak => Severity.High,
AlgorithmStrength.Legacy => Severity.Medium,
AlgorithmStrength.Acceptable => Severity.Low,
AlgorithmStrength.Strong => Severity.Low,
AlgorithmStrength.PostQuantum => Severity.Low,
_ => Severity.Unknown
};
}
private static bool IsProhibited(CryptoAnalysisContext context, string algorithm)
{
if (context.Policy.ProhibitedAlgorithms.IsDefaultOrEmpty)
{
return false;
}
return context.Policy.ProhibitedAlgorithms.Any(entry =>
entry.Equals(algorithm, StringComparison.OrdinalIgnoreCase)
|| algorithm.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
private static ImmutableDictionary<string, string> BuildMetadata(
ParsedCryptoProperties properties,
params (string Key, string Value)[] additions)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(properties.Oid))
{
metadata["oid"] = properties.Oid!;
}
foreach (var (key, value) in additions)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
metadata[key] = value;
}
return metadata.Count == 0
? ImmutableDictionary<string, string>.Empty
: metadata.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,122 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class CertificateAnalyzer : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
var findings = new List<CryptoFinding>();
var warningDays = context.Policy.Certificates.ExpirationWarningDays;
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Certificate)
{
continue;
}
var certificate = crypto.CertificateProperties;
if (certificate is null)
{
continue;
}
if (certificate.NotValidAfter is { } notAfter)
{
var daysRemaining = (notAfter - context.NowUtc).TotalDays;
if (daysRemaining < 0)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.ExpiredCertificate,
Severity = Severity.High,
Title = "Expired certificate detected",
Description = $"Certificate for {component.Name ?? component.BomRef} expired on {notAfter:O}.",
Remediation = "Rotate the certificate and update the CBOM metadata.",
Certificate = certificate.SubjectName,
Metadata = BuildMetadata(certificate, ("daysRemaining", daysRemaining.ToString("0")))
});
}
else if (daysRemaining <= warningDays)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.ExpiredCertificate,
Severity = Severity.Medium,
Title = "Certificate nearing expiration",
Description = $"Certificate for {component.Name ?? component.BomRef} expires on {notAfter:O}.",
Remediation = "Schedule rotation before expiry window closes.",
Certificate = certificate.SubjectName,
Metadata = BuildMetadata(certificate, ("daysRemaining", daysRemaining.ToString("0")))
});
}
}
var signatureAlgorithm = certificate.SignatureAlgorithmRef ?? component.Name;
if (!string.IsNullOrWhiteSpace(signatureAlgorithm)
&& CryptoAlgorithmCatalog.IsWeakAlgorithm(signatureAlgorithm))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.WeakAlgorithm,
Severity = Severity.High,
Title = "Weak certificate signature algorithm",
Description = $"Certificate uses {signatureAlgorithm}, which is considered weak.",
Remediation = "Use SHA-256+ with RSA/ECDSA or regional approved algorithms.",
Certificate = certificate.SubjectName,
Algorithm = signatureAlgorithm,
Metadata = BuildMetadata(certificate)
});
}
}
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray()
});
}
private static ImmutableDictionary<string, string> BuildMetadata(
ParsedCertificateProperties certificate,
params (string Key, string Value)[] additions)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(certificate.SubjectName))
{
metadata["subject"] = certificate.SubjectName!;
}
if (!string.IsNullOrWhiteSpace(certificate.IssuerName))
{
metadata["issuer"] = certificate.IssuerName!;
}
foreach (var (key, value) in additions)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
metadata[key] = value;
}
return metadata.Count == 0
? ImmutableDictionary<string, string>.Empty
: metadata.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,261 @@
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public static class CryptoAlgorithmCatalog
{
private static readonly Dictionary<string, string> OidToAlgorithm = new(StringComparer.OrdinalIgnoreCase)
{
["1.2.840.113549.1.1.1"] = "RSA",
["1.2.840.113549.1.1.11"] = "SHA256withRSA",
["1.2.840.10045.4.3.2"] = "ECDSA-SHA256",
["1.2.840.10045.4.3.3"] = "ECDSA-SHA384",
["1.2.840.10045.4.3.4"] = "ECDSA-SHA512",
["2.16.840.1.101.3.4.2.1"] = "SHA256",
["2.16.840.1.101.3.4.2.2"] = "SHA384",
["2.16.840.1.101.3.4.2.3"] = "SHA512",
["1.2.643.7.1.1.1.1"] = "GOST3410-2012-256",
["1.2.643.7.1.1.1.2"] = "GOST3410-2012-512",
["1.2.643.7.1.1.2.2"] = "GOST3411-2012-256",
["1.2.643.7.1.1.2.3"] = "GOST3411-2012-512",
["1.2.156.10197.1.301"] = "SM2",
["1.2.156.10197.1.401"] = "SM3",
["1.2.156.10197.1.104.1"] = "SM4"
};
private static readonly HashSet<string> WeakAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"MD2",
"MD4",
"MD5",
"SHA1",
"SHA-1",
"DES",
"3DES",
"TRIPLEDES",
"RC2",
"RC4",
"BLOWFISH"
};
private static readonly HashSet<string> QuantumVulnerableAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"RSA",
"DSA",
"DH",
"DIFFIE-HELLMAN",
"ECDSA",
"ECDH",
"ECC",
"ED25519",
"ED448",
"EDDSA"
};
private static readonly HashSet<string> PostQuantumAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"KYBER",
"DILITHIUM",
"SPHINCS+",
"SPHINCS",
"FALCON",
"CLASSICMCELIECE"
};
private static readonly HashSet<string> FipsApprovedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"AES",
"HMAC",
"SHA256",
"SHA384",
"SHA512",
"RSA",
"ECDSA",
"ECDH",
"HKDF",
"PBKDF2",
"CTR",
"GCM",
"CBC",
"OAEP",
"PKCS1"
};
private static readonly HashSet<string> EidasAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"RSA",
"ECDSA",
"SHA256",
"SHA384",
"SHA512"
};
private static readonly HashSet<string> GostAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"GOST",
"GOST3410",
"GOST3411",
"GOST28147",
"GOST3410-2012-256",
"GOST3410-2012-512",
"GOST3411-2012-256",
"GOST3411-2012-512"
};
private static readonly HashSet<string> SmAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"SM2",
"SM3",
"SM4"
};
public static string? ResolveAlgorithmName(ParsedComponent component)
{
if (!string.IsNullOrWhiteSpace(component.Name))
{
return component.Name.Trim();
}
var crypto = component.CryptoProperties;
if (crypto is null)
{
return null;
}
var byOid = MapOidToAlgorithm(crypto.Oid);
if (!string.IsNullOrWhiteSpace(byOid))
{
return byOid;
}
var parameters = crypto.AlgorithmProperties?.ParameterSetIdentifier;
if (!string.IsNullOrWhiteSpace(parameters))
{
return parameters.Trim();
}
var curve = crypto.AlgorithmProperties?.Curve;
if (!string.IsNullOrWhiteSpace(curve))
{
return curve.Trim();
}
return null;
}
public static string? MapOidToAlgorithm(string? oid)
{
if (string.IsNullOrWhiteSpace(oid))
{
return null;
}
return OidToAlgorithm.TryGetValue(oid.Trim(), out var algorithm)
? algorithm
: null;
}
public static string Normalize(string? algorithm)
{
return string.IsNullOrWhiteSpace(algorithm)
? string.Empty
: algorithm.Trim().ToUpperInvariant();
}
public static bool IsWeakAlgorithm(string algorithm)
{
var normalized = Normalize(algorithm);
if (WeakAlgorithms.Contains(normalized))
{
return true;
}
return WeakAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsPostQuantum(string algorithm)
{
var normalized = Normalize(algorithm);
return PostQuantumAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsQuantumVulnerable(string algorithm)
{
var normalized = Normalize(algorithm);
return QuantumVulnerableAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsFipsApproved(string algorithm)
{
var normalized = Normalize(algorithm);
if (FipsApprovedAlgorithms.Contains(normalized))
{
return true;
}
return FipsApprovedAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsEidasAlgorithm(string algorithm)
{
var normalized = Normalize(algorithm);
return EidasAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsGostAlgorithm(string algorithm)
{
var normalized = Normalize(algorithm);
return GostAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static bool IsSmAlgorithm(string algorithm)
{
var normalized = Normalize(algorithm);
return SmAlgorithms.Any(entry => normalized.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
public static string? GetAlgorithmFamily(string algorithm)
{
var normalized = Normalize(algorithm);
if (normalized.Contains("RSA", StringComparison.OrdinalIgnoreCase))
{
return "RSA";
}
if (normalized.Contains("ECDSA", StringComparison.OrdinalIgnoreCase))
{
return "ECDSA";
}
if (normalized.Contains("ECDH", StringComparison.OrdinalIgnoreCase))
{
return "ECDH";
}
if (normalized.Contains("ED25519", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("ED448", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("EDDSA", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("ECC", StringComparison.OrdinalIgnoreCase))
{
return "ECC";
}
if (normalized.Contains("DSA", StringComparison.OrdinalIgnoreCase))
{
return "DSA";
}
if (normalized.Contains("DH", StringComparison.OrdinalIgnoreCase))
{
return "DH";
}
if (normalized.Contains("AES", StringComparison.OrdinalIgnoreCase))
{
return "AES";
}
return null;
}
}

View File

@@ -0,0 +1,117 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Policy;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class CryptoAnalysisContext
{
private readonly RegexCache _regexCache = new();
private CryptoAnalysisContext(
CryptoPolicy policy,
ImmutableArray<ParsedComponent> components,
DateTimeOffset nowUtc)
{
Policy = policy;
Components = components;
NowUtc = nowUtc;
}
public CryptoPolicy Policy { get; }
public ImmutableArray<ParsedComponent> Components { get; }
public DateTimeOffset NowUtc { get; }
public static CryptoAnalysisContext Create(
IReadOnlyList<ParsedComponent> components,
CryptoPolicy policy,
TimeProvider timeProvider)
{
var sorted = (components ?? Array.Empty<ParsedComponent>())
.Where(component => component.CryptoProperties is not null)
.OrderBy(component => component.BomRef, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var now = timeProvider.GetUtcNow();
return new CryptoAnalysisContext(policy, sorted, now);
}
public bool IsExempted(ParsedComponent component, string? algorithm)
{
if (Policy.Exemptions.IsDefaultOrEmpty || string.IsNullOrWhiteSpace(algorithm))
{
return false;
}
foreach (var exemption in Policy.Exemptions)
{
if (IsExemptionExpired(exemption))
{
continue;
}
if (!MatchesPattern(component.Name, exemption.ComponentPattern)
&& !MatchesPattern(component.BomRef, exemption.ComponentPattern))
{
continue;
}
if (exemption.Algorithms.IsDefaultOrEmpty)
{
return true;
}
if (exemption.Algorithms.Any(entry => entry.Equals(algorithm, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
private bool IsExemptionExpired(CryptoPolicyExemption exemption)
{
if (exemption.ExpirationDate is null)
{
return false;
}
return exemption.ExpirationDate.Value < NowUtc;
}
private bool MatchesPattern(string? value, string? pattern)
{
if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(pattern))
{
return false;
}
var regex = _regexCache.Get(pattern);
return regex.IsMatch(value);
}
private sealed class RegexCache
{
private readonly Dictionary<string, Regex> _cache = new(StringComparer.OrdinalIgnoreCase);
public Regex Get(string pattern)
{
if (_cache.TryGetValue(pattern, out var cached))
{
return cached;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".")
+ "$";
var regex = new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
_cache[pattern] = regex;
return regex;
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Immutable;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed record CryptoAnalysisResult
{
public static CryptoAnalysisResult Empty { get; } = new();
public ImmutableArray<CryptoFinding> Findings { get; init; } = [];
public CryptoInventory? Inventory { get; init; }
public PostQuantumReadiness? QuantumReadiness { get; init; }
}

View File

@@ -0,0 +1,124 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class CryptoInventoryGenerator : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
var algorithms = new List<CryptoAlgorithmUsage>();
var certificates = new List<CryptoCertificateUsage>();
var protocols = new List<CryptoProtocolUsage>();
var keyMaterials = new List<CryptoKeyMaterial>();
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null)
{
continue;
}
switch (crypto.AssetType)
{
case CryptoAssetType.Algorithm:
{
var properties = crypto.AlgorithmProperties;
algorithms.Add(new CryptoAlgorithmUsage
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Algorithm = CryptoAlgorithmCatalog.ResolveAlgorithmName(component),
AlgorithmIdentifier = crypto.Oid,
Primitive = properties?.Primitive?.ToString(),
Mode = properties?.Mode?.ToString(),
Padding = properties?.Padding?.ToString(),
KeySize = properties?.KeySize,
Curve = properties?.Curve,
ExecutionEnvironment = properties?.ExecutionEnvironment?.ToString(),
CertificationLevel = properties?.CertificationLevel?.ToString(),
CryptoFunctions = properties?.CryptoFunctions ?? []
});
break;
}
case CryptoAssetType.Certificate:
{
var properties = crypto.CertificateProperties;
certificates.Add(new CryptoCertificateUsage
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
SubjectName = properties?.SubjectName,
IssuerName = properties?.IssuerName,
NotValidBefore = properties?.NotValidBefore,
NotValidAfter = properties?.NotValidAfter,
SignatureAlgorithmRef = properties?.SignatureAlgorithmRef,
SubjectPublicKeyRef = properties?.SubjectPublicKeyRef,
CertificateFormat = properties?.CertificateFormat,
CertificateExtension = properties?.CertificateExtension
});
break;
}
case CryptoAssetType.Protocol:
{
var properties = crypto.ProtocolProperties;
protocols.Add(new CryptoProtocolUsage
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = properties?.Type,
Version = properties?.Version,
CipherSuites = properties?.CipherSuites ?? [],
IkeV2TransformTypes = properties?.IkeV2TransformTypes ?? [],
CryptoRefArray = properties?.CryptoRefArray ?? []
});
break;
}
case CryptoAssetType.RelatedCryptoMaterial:
{
var properties = crypto.RelatedCryptoMaterial;
keyMaterials.Add(new CryptoKeyMaterial
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = properties?.Type,
Reference = properties?.Reference,
MaterialRefs = properties?.MaterialRefs ?? []
});
break;
}
}
}
var inventory = new CryptoInventory
{
Algorithms = algorithms
.OrderBy(entry => entry.ComponentBomRef, StringComparer.OrdinalIgnoreCase)
.ThenBy(entry => entry.Algorithm ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray(),
Certificates = certificates
.OrderBy(entry => entry.ComponentBomRef, StringComparer.OrdinalIgnoreCase)
.ThenBy(entry => entry.SubjectName ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray(),
Protocols = protocols
.OrderBy(entry => entry.ComponentBomRef, StringComparer.OrdinalIgnoreCase)
.ThenBy(entry => entry.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray(),
KeyMaterials = keyMaterials
.OrderBy(entry => entry.ComponentBomRef, StringComparer.OrdinalIgnoreCase)
.ThenBy(entry => entry.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray()
};
return Task.FromResult(new CryptoAnalysisResult
{
Inventory = inventory
});
}
}

View File

@@ -0,0 +1,146 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class FipsComplianceChecker : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
if (!RequiresFips(context.Policy))
{
return Task.FromResult(CryptoAnalysisResult.Empty);
}
var findings = new List<CryptoFinding>();
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Algorithm)
{
continue;
}
var algorithm = CryptoAlgorithmCatalog.ResolveAlgorithmName(component);
if (string.IsNullOrWhiteSpace(algorithm))
{
continue;
}
if (context.IsExempted(component, algorithm))
{
continue;
}
if (!IsApprovedAlgorithm(context.Policy, algorithm))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.NonFipsCompliant,
Severity = Severity.High,
Title = $"Non-FIPS algorithm detected ({algorithm})",
Description = $"Component {component.Name ?? component.BomRef} uses {algorithm}, which is not approved by FIPS policy.",
Remediation = "Replace with an approved FIPS algorithm or apply an approved exemption.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto)
});
}
var mode = crypto.AlgorithmProperties?.Mode;
if (mode == CryptoMode.Ecb)
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.InsecureMode,
Severity = Severity.High,
Title = $"Non-FIPS cipher mode detected ({algorithm})",
Description = "ECB mode is not permitted under FIPS guidance for confidentiality.",
Remediation = "Use GCM, CTR, or CBC with appropriate padding.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto, ("mode", "ECB"))
});
}
if (crypto.AlgorithmProperties?.Padding == CryptoPadding.None
&& !string.IsNullOrWhiteSpace(algorithm)
&& algorithm.Contains("RSA", StringComparison.OrdinalIgnoreCase))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.InsecureMode,
Severity = Severity.High,
Title = "Missing padding on RSA operation",
Description = "RSA operations without padding are non-compliant and insecure.",
Remediation = "Use OAEP or PKCS1 padding under FIPS-approved profiles.",
Algorithm = algorithm,
Metadata = BuildMetadata(crypto, ("padding", "None"))
});
}
}
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray()
});
}
private static bool RequiresFips(Policy.CryptoPolicy policy)
{
if (!policy.ComplianceFrameworks.IsDefaultOrEmpty
&& policy.ComplianceFrameworks.Any(framework => framework.Contains("FIPS", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
return !string.IsNullOrWhiteSpace(policy.ComplianceFramework)
&& policy.ComplianceFramework.Contains("FIPS", StringComparison.OrdinalIgnoreCase);
}
private static bool IsApprovedAlgorithm(Policy.CryptoPolicy policy, string algorithm)
{
if (!policy.ApprovedAlgorithms.IsDefaultOrEmpty)
{
return policy.ApprovedAlgorithms.Any(entry =>
entry.Equals(algorithm, StringComparison.OrdinalIgnoreCase)
|| algorithm.Contains(entry, StringComparison.OrdinalIgnoreCase));
}
return CryptoAlgorithmCatalog.IsFipsApproved(algorithm);
}
private static ImmutableDictionary<string, string> BuildMetadata(
ParsedCryptoProperties properties,
params (string Key, string Value)[] additions)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(properties.Oid))
{
metadata["oid"] = properties.Oid!;
}
foreach (var (key, value) in additions)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
metadata[key] = value;
}
return metadata.Count == 0
? ImmutableDictionary<string, string>.Empty
: metadata.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,144 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class PostQuantumAnalyzer : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
if (!context.Policy.PostQuantum.Enabled)
{
return Task.FromResult(CryptoAnalysisResult.Empty);
}
var findings = new List<CryptoFinding>();
var totalAlgorithms = 0;
var pqcAlgorithms = 0;
var hybridAlgorithms = 0;
var vulnerableAlgorithms = 0;
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Algorithm)
{
continue;
}
var algorithm = CryptoAlgorithmCatalog.ResolveAlgorithmName(component);
if (string.IsNullOrWhiteSpace(algorithm))
{
continue;
}
totalAlgorithms++;
if (algorithm.Contains("hybrid", StringComparison.OrdinalIgnoreCase))
{
hybridAlgorithms++;
}
if (CryptoAlgorithmCatalog.IsPostQuantum(algorithm))
{
pqcAlgorithms++;
continue;
}
if (!CryptoAlgorithmCatalog.IsQuantumVulnerable(algorithm))
{
continue;
}
vulnerableAlgorithms++;
var severity = context.Policy.PostQuantum.RequireHybridForLongLived
? Severity.High
: Severity.Medium;
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.QuantumVulnerable,
Severity = severity,
Title = $"Quantum-vulnerable algorithm detected ({algorithm})",
Description = $"{algorithm} is vulnerable to future quantum attacks and should be migrated.",
Remediation = "Adopt a hybrid or post-quantum algorithm (Kyber, Dilithium, SPHINCS+) for long-lived data.",
Algorithm = algorithm
});
}
var score = CalculateReadinessScore(totalAlgorithms, pqcAlgorithms, hybridAlgorithms, vulnerableAlgorithms);
var recommendations = BuildRecommendations(vulnerableAlgorithms, pqcAlgorithms, context);
var readiness = new PostQuantumReadiness
{
Score = score,
TotalAlgorithms = totalAlgorithms,
QuantumVulnerableAlgorithms = vulnerableAlgorithms,
PostQuantumAlgorithms = pqcAlgorithms,
HybridAlgorithms = hybridAlgorithms,
MigrationRecommendations = recommendations
};
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray(),
QuantumReadiness = readiness
});
}
private static int CalculateReadinessScore(
int totalAlgorithms,
int pqcAlgorithms,
int hybridAlgorithms,
int vulnerableAlgorithms)
{
if (totalAlgorithms == 0)
{
return 100;
}
var resilient = pqcAlgorithms + hybridAlgorithms;
var score = resilient / (double)totalAlgorithms * 100d;
if (vulnerableAlgorithms == 0 && resilient == 0)
{
score = 100d;
}
return (int)Math.Round(score, MidpointRounding.AwayFromZero);
}
private static ImmutableArray<string> BuildRecommendations(
int vulnerableAlgorithms,
int pqcAlgorithms,
CryptoAnalysisContext context)
{
if (vulnerableAlgorithms == 0)
{
return ImmutableArray<string>.Empty;
}
var recommendations = new List<string>
{
"Prioritize migration from RSA/ECC to hybrid or post-quantum algorithms."
};
if (context.Policy.PostQuantum.RequireHybridForLongLived)
{
recommendations.Add("Adopt hybrid PQC for long-lived data flows per policy.");
}
if (pqcAlgorithms == 0)
{
recommendations.Add("Introduce Kyber and Dilithium pilots to validate PQC readiness.");
}
return recommendations.ToImmutableArray();
}
}

View File

@@ -0,0 +1,174 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class ProtocolAnalyzer : ICryptoCheck
{
private static readonly string[] WeakCipherMarkers =
[
"NULL",
"EXPORT",
"RC4",
"DES",
"3DES",
"MD5",
"SHA1"
];
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
var findings = new List<CryptoFinding>();
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Protocol)
{
continue;
}
var protocol = crypto.ProtocolProperties;
if (protocol is null)
{
continue;
}
var protocolType = protocol.Type ?? component.Name ?? "protocol";
if (IsDeprecatedProtocol(protocolType, protocol.Version))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.DeprecatedProtocol,
Severity = Severity.High,
Title = $"Deprecated protocol version detected ({protocolType} {protocol.Version})",
Description = "Deprecated protocol versions should be upgraded to TLS 1.2+ or equivalent.",
Remediation = "Upgrade the protocol version and remove legacy cipher support.",
Protocol = protocolType,
Metadata = BuildMetadata(protocol)
});
}
if (!protocol.CipherSuites.IsDefaultOrEmpty)
{
foreach (var suite in protocol.CipherSuites)
{
if (string.IsNullOrWhiteSpace(suite))
{
continue;
}
if (WeakCipherMarkers.Any(marker => suite.Contains(marker, StringComparison.OrdinalIgnoreCase)))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.WeakCipherSuite,
Severity = Severity.High,
Title = $"Weak cipher suite detected ({suite})",
Description = "Cipher suite includes weak or deprecated algorithms.",
Remediation = "Remove weak cipher suites and enforce modern TLS profiles.",
Protocol = protocolType,
Metadata = BuildMetadata(protocol, ("cipherSuite", suite))
});
}
}
if (context.Policy.RequiredFeatures.PerfectForwardSecrecy
&& !protocol.CipherSuites.Any(suite => suite.Contains("DHE", StringComparison.OrdinalIgnoreCase)))
{
findings.Add(new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.WeakCipherSuite,
Severity = Severity.Medium,
Title = "Perfect forward secrecy not detected",
Description = "No cipher suites with (EC)DHE were declared, which weakens forward secrecy guarantees.",
Remediation = "Prefer ECDHE/DHE cipher suites for forward secrecy.",
Protocol = protocolType,
Metadata = BuildMetadata(protocol)
});
}
}
}
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray()
});
}
private static bool IsDeprecatedProtocol(string protocolType, string? version)
{
if (string.IsNullOrWhiteSpace(version))
{
return false;
}
var normalizedType = protocolType.Trim().ToLowerInvariant();
if (!normalizedType.Contains("tls", StringComparison.OrdinalIgnoreCase)
&& !normalizedType.Contains("ssl", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!Version.TryParse(NormalizeVersion(version), out var parsed))
{
return false;
}
return parsed.Major < 1 || (parsed.Major == 1 && parsed.Minor < 2);
}
private static string NormalizeVersion(string version)
{
var trimmed = version.Trim();
if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
trimmed = trimmed[1..];
}
return trimmed.Replace("TLS", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("SSL", string.Empty, StringComparison.OrdinalIgnoreCase)
.Trim();
}
private static ImmutableDictionary<string, string> BuildMetadata(
ParsedProtocolProperties protocol,
params (string Key, string Value)[] additions)
{
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(protocol.Type))
{
metadata["type"] = protocol.Type!;
}
if (!string.IsNullOrWhiteSpace(protocol.Version))
{
metadata["version"] = protocol.Version!;
}
foreach (var (key, value) in additions)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
metadata[key] = value;
}
return metadata.Count == 0
? ImmutableDictionary<string, string>.Empty
: metadata.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Scanner.CryptoAnalysis.Models;
namespace StellaOps.Scanner.CryptoAnalysis.Analyzers;
public sealed class RegionalComplianceChecker : ICryptoCheck
{
public Task<CryptoAnalysisResult> AnalyzeAsync(
CryptoAnalysisContext context,
CancellationToken ct = default)
{
if (!RequiresRegionalCompliance(context.Policy))
{
return Task.FromResult(CryptoAnalysisResult.Empty);
}
var findings = new List<CryptoFinding>();
foreach (var component in context.Components)
{
ct.ThrowIfCancellationRequested();
var crypto = component.CryptoProperties;
if (crypto is null || crypto.AssetType != CryptoAssetType.Algorithm)
{
continue;
}
var algorithm = CryptoAlgorithmCatalog.ResolveAlgorithmName(component);
if (string.IsNullOrWhiteSpace(algorithm))
{
continue;
}
if (context.IsExempted(component, algorithm))
{
continue;
}
if (context.Policy.RegionalRequirements.Eidas && !CryptoAlgorithmCatalog.IsEidasAlgorithm(algorithm))
{
findings.Add(BuildFinding(component, algorithm, "eIDAS"));
}
if (context.Policy.RegionalRequirements.Gost && !CryptoAlgorithmCatalog.IsGostAlgorithm(algorithm))
{
findings.Add(BuildFinding(component, algorithm, "GOST"));
}
if (context.Policy.RegionalRequirements.Sm && !CryptoAlgorithmCatalog.IsSmAlgorithm(algorithm))
{
findings.Add(BuildFinding(component, algorithm, "SM"));
}
}
return Task.FromResult(new CryptoAnalysisResult
{
Findings = findings.ToImmutableArray()
});
}
private static bool RequiresRegionalCompliance(Policy.CryptoPolicy policy)
{
return policy.RegionalRequirements.Eidas
|| policy.RegionalRequirements.Gost
|| policy.RegionalRequirements.Sm;
}
private static CryptoFinding BuildFinding(ParsedComponent component, string algorithm, string region)
{
return new CryptoFinding
{
ComponentBomRef = component.BomRef,
ComponentName = component.Name,
Type = CryptoFindingType.NonFipsCompliant,
Severity = Severity.Medium,
Title = $"{region} compliance gap detected",
Description = $"Algorithm {algorithm} is not recognized as {region}-approved for component {component.Name ?? component.BomRef}.",
Remediation = "Select a region-approved algorithm or document an exemption with expiration.",
Algorithm = algorithm,
Metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["region"] = region
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)
};
}
}