tests fixes and sprints work
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user