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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Analyzers;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Policy;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis;
|
||||
|
||||
public interface ICryptoAnalyzer
|
||||
{
|
||||
Task<CryptoAnalysisReport> AnalyzeAsync(
|
||||
IReadOnlyList<ParsedComponent> componentsWithCrypto,
|
||||
CryptoPolicy policy,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class CryptoAnalysisAnalyzer : ICryptoAnalyzer
|
||||
{
|
||||
private readonly IReadOnlyList<ICryptoCheck> _checks;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public CryptoAnalysisAnalyzer(
|
||||
IEnumerable<ICryptoCheck> checks,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_checks = (checks ?? Array.Empty<ICryptoCheck>()).ToList();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<CryptoAnalysisReport> AnalyzeAsync(
|
||||
IReadOnlyList<ParsedComponent> componentsWithCrypto,
|
||||
CryptoPolicy policy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var context = CryptoAnalysisContext.Create(componentsWithCrypto, policy, _timeProvider);
|
||||
var findings = new List<CryptoFinding>();
|
||||
CryptoInventory? inventory = null;
|
||||
PostQuantumReadiness? quantumReadiness = null;
|
||||
|
||||
foreach (var check in _checks)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var result = await check.AnalyzeAsync(context, ct).ConfigureAwait(false);
|
||||
|
||||
if (!result.Findings.IsDefaultOrEmpty)
|
||||
{
|
||||
findings.AddRange(result.Findings);
|
||||
}
|
||||
|
||||
inventory ??= result.Inventory;
|
||||
quantumReadiness ??= result.QuantumReadiness;
|
||||
}
|
||||
|
||||
var summary = BuildSummary(findings);
|
||||
var complianceStatus = BuildComplianceStatus(policy, findings, _timeProvider.GetUtcNow());
|
||||
|
||||
return new CryptoAnalysisReport
|
||||
{
|
||||
Inventory = inventory ?? CryptoInventory.Empty,
|
||||
Findings = findings.ToImmutableArray(),
|
||||
ComplianceStatus = complianceStatus,
|
||||
QuantumReadiness = quantumReadiness ?? PostQuantumReadiness.Empty,
|
||||
Summary = summary,
|
||||
GeneratedAtUtc = _timeProvider.GetUtcNow(),
|
||||
PolicyVersion = policy.Version
|
||||
};
|
||||
}
|
||||
|
||||
private static CryptoSummary BuildSummary(IReadOnlyList<CryptoFinding> findings)
|
||||
{
|
||||
if (findings.Count == 0)
|
||||
{
|
||||
return CryptoSummary.Empty;
|
||||
}
|
||||
|
||||
var bySeverity = findings
|
||||
.GroupBy(f => f.Severity)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
var byType = findings
|
||||
.GroupBy(f => f.Type)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
return new CryptoSummary
|
||||
{
|
||||
TotalFindings = findings.Count,
|
||||
FindingsBySeverity = bySeverity,
|
||||
FindingsByType = byType
|
||||
};
|
||||
}
|
||||
|
||||
private static CryptoComplianceStatus BuildComplianceStatus(
|
||||
CryptoPolicy policy,
|
||||
IReadOnlyList<CryptoFinding> findings,
|
||||
DateTimeOffset generatedAtUtc)
|
||||
{
|
||||
var frameworks = GetFrameworks(policy);
|
||||
var violations = findings
|
||||
.Select(f => $"{f.Type}:{f.ComponentName ?? f.ComponentBomRef}")
|
||||
.ToImmutableArray();
|
||||
var isCompliant = violations.Length == 0;
|
||||
|
||||
var frameworkStatuses = frameworks
|
||||
.Select(framework => new ComplianceFrameworkStatus
|
||||
{
|
||||
Framework = framework,
|
||||
IsCompliant = isCompliant,
|
||||
ViolationCount = violations.Length
|
||||
})
|
||||
.ToImmutableArray();
|
||||
|
||||
return new CryptoComplianceStatus
|
||||
{
|
||||
Frameworks = frameworkStatuses,
|
||||
IsCompliant = isCompliant,
|
||||
Violations = violations,
|
||||
Attestation = new CryptoComplianceAttestation
|
||||
{
|
||||
Frameworks = frameworks,
|
||||
IsCompliant = isCompliant,
|
||||
GeneratedAtUtc = generatedAtUtc,
|
||||
EvidenceNote = isCompliant ? "All crypto checks passed." : "Crypto findings require review."
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetFrameworks(CryptoPolicy policy)
|
||||
{
|
||||
var frameworks = new List<string>();
|
||||
|
||||
if (!policy.ComplianceFrameworks.IsDefaultOrEmpty)
|
||||
{
|
||||
frameworks.AddRange(policy.ComplianceFrameworks
|
||||
.Where(f => !string.IsNullOrWhiteSpace(f))
|
||||
.Select(f => f.Trim()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(policy.ComplianceFramework))
|
||||
{
|
||||
var framework = policy.ComplianceFramework!.Trim();
|
||||
if (!frameworks.Any(existing => existing.Equals(framework, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
frameworks.Add(framework);
|
||||
}
|
||||
}
|
||||
|
||||
if (frameworks.Count == 0)
|
||||
{
|
||||
frameworks.Add("custom");
|
||||
}
|
||||
|
||||
return frameworks
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICryptoCheck
|
||||
{
|
||||
Task<CryptoAnalysisResult> AnalyzeAsync(
|
||||
CryptoAnalysisContext context,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Analyzers;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Policy;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Reporting;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis;
|
||||
|
||||
public static class CryptoAnalysisServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCryptoAnalysis(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.AddSingleton<ICryptoPolicyLoader, CryptoPolicyLoader>();
|
||||
services.AddSingleton<ICryptoCheck, CryptoInventoryGenerator>();
|
||||
services.AddSingleton<ICryptoCheck, AlgorithmStrengthAnalyzer>();
|
||||
services.AddSingleton<ICryptoCheck, FipsComplianceChecker>();
|
||||
services.AddSingleton<ICryptoCheck, RegionalComplianceChecker>();
|
||||
services.AddSingleton<ICryptoCheck, PostQuantumAnalyzer>();
|
||||
services.AddSingleton<ICryptoCheck, CertificateAnalyzer>();
|
||||
services.AddSingleton<ICryptoCheck, ProtocolAnalyzer>();
|
||||
services.AddSingleton<ICryptoAnalyzer, CryptoAnalysisAnalyzer>();
|
||||
services.AddSingleton<CryptoAnalysisSarifExporter>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
|
||||
public sealed record CryptoAnalysisReport
|
||||
{
|
||||
public CryptoInventory Inventory { get; init; } = CryptoInventory.Empty;
|
||||
public ImmutableArray<CryptoFinding> Findings { get; init; } = [];
|
||||
public CryptoComplianceStatus ComplianceStatus { get; init; } = CryptoComplianceStatus.Empty;
|
||||
public PostQuantumReadiness QuantumReadiness { get; init; } = PostQuantumReadiness.Empty;
|
||||
public CryptoSummary Summary { get; init; } = CryptoSummary.Empty;
|
||||
public DateTimeOffset GeneratedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string? PolicyVersion { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoInventory
|
||||
{
|
||||
public static CryptoInventory Empty { get; } = new();
|
||||
|
||||
public ImmutableArray<CryptoAlgorithmUsage> Algorithms { get; init; } = [];
|
||||
public ImmutableArray<CryptoCertificateUsage> Certificates { get; init; } = [];
|
||||
public ImmutableArray<CryptoProtocolUsage> Protocols { get; init; } = [];
|
||||
public ImmutableArray<CryptoKeyMaterial> KeyMaterials { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CryptoAlgorithmUsage
|
||||
{
|
||||
public required string ComponentBomRef { get; init; }
|
||||
public string? ComponentName { get; init; }
|
||||
public string? Algorithm { get; init; }
|
||||
public string? AlgorithmIdentifier { get; init; }
|
||||
public string? Primitive { get; init; }
|
||||
public string? Mode { get; init; }
|
||||
public string? Padding { get; init; }
|
||||
public int? KeySize { get; init; }
|
||||
public string? Curve { get; init; }
|
||||
public string? ExecutionEnvironment { get; init; }
|
||||
public string? CertificationLevel { get; init; }
|
||||
public ImmutableArray<string> CryptoFunctions { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CryptoCertificateUsage
|
||||
{
|
||||
public required string ComponentBomRef { get; init; }
|
||||
public string? ComponentName { get; init; }
|
||||
public string? SubjectName { get; init; }
|
||||
public string? IssuerName { get; init; }
|
||||
public DateTimeOffset? NotValidBefore { get; init; }
|
||||
public DateTimeOffset? NotValidAfter { get; init; }
|
||||
public string? SignatureAlgorithmRef { get; init; }
|
||||
public string? SubjectPublicKeyRef { get; init; }
|
||||
public string? CertificateFormat { get; init; }
|
||||
public string? CertificateExtension { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoProtocolUsage
|
||||
{
|
||||
public required string ComponentBomRef { get; init; }
|
||||
public string? ComponentName { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string? Version { get; init; }
|
||||
public ImmutableArray<string> CipherSuites { get; init; } = [];
|
||||
public ImmutableArray<string> IkeV2TransformTypes { get; init; } = [];
|
||||
public ImmutableArray<string> CryptoRefArray { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CryptoKeyMaterial
|
||||
{
|
||||
public required string ComponentBomRef { get; init; }
|
||||
public string? ComponentName { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string? Reference { get; init; }
|
||||
public ImmutableArray<string> MaterialRefs { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CryptoFinding
|
||||
{
|
||||
public required string ComponentBomRef { get; init; }
|
||||
public string? ComponentName { get; init; }
|
||||
public required CryptoFindingType Type { get; init; }
|
||||
public required Severity Severity { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public string? Remediation { get; init; }
|
||||
public string? Algorithm { get; init; }
|
||||
public string? Protocol { get; init; }
|
||||
public string? Certificate { get; init; }
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
public enum CryptoFindingType
|
||||
{
|
||||
WeakAlgorithm,
|
||||
ShortKeyLength,
|
||||
DeprecatedProtocol,
|
||||
NonFipsCompliant,
|
||||
QuantumVulnerable,
|
||||
ExpiredCertificate,
|
||||
WeakCipherSuite,
|
||||
InsecureMode,
|
||||
MissingIntegrity
|
||||
}
|
||||
|
||||
public enum Severity
|
||||
{
|
||||
Unknown,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical
|
||||
}
|
||||
|
||||
public enum AlgorithmStrength
|
||||
{
|
||||
Broken,
|
||||
Weak,
|
||||
Legacy,
|
||||
Acceptable,
|
||||
Strong,
|
||||
PostQuantum
|
||||
}
|
||||
|
||||
public sealed record CryptoComplianceStatus
|
||||
{
|
||||
public static CryptoComplianceStatus Empty { get; } = new();
|
||||
|
||||
public ImmutableArray<ComplianceFrameworkStatus> Frameworks { get; init; } = [];
|
||||
public bool IsCompliant { get; init; }
|
||||
public ImmutableArray<string> Violations { get; init; } = [];
|
||||
public CryptoComplianceAttestation? Attestation { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ComplianceFrameworkStatus
|
||||
{
|
||||
public required string Framework { get; init; }
|
||||
public bool IsCompliant { get; init; }
|
||||
public int ViolationCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoComplianceAttestation
|
||||
{
|
||||
public ImmutableArray<string> Frameworks { get; init; } = [];
|
||||
public bool IsCompliant { get; init; }
|
||||
public DateTimeOffset GeneratedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string? EvidenceNote { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PostQuantumReadiness
|
||||
{
|
||||
public static PostQuantumReadiness Empty { get; } = new();
|
||||
|
||||
public int Score { get; init; }
|
||||
public int TotalAlgorithms { get; init; }
|
||||
public int QuantumVulnerableAlgorithms { get; init; }
|
||||
public int PostQuantumAlgorithms { get; init; }
|
||||
public int HybridAlgorithms { get; init; }
|
||||
public ImmutableArray<string> MigrationRecommendations { get; init; } = [];
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoSummary
|
||||
{
|
||||
public static CryptoSummary Empty { get; } = new()
|
||||
{
|
||||
TotalFindings = 0,
|
||||
FindingsBySeverity = ImmutableDictionary<Severity, int>.Empty,
|
||||
FindingsByType = ImmutableDictionary<CryptoFindingType, int>.Empty
|
||||
};
|
||||
|
||||
public int TotalFindings { get; init; }
|
||||
public ImmutableDictionary<Severity, int> FindingsBySeverity { get; init; } =
|
||||
ImmutableDictionary<Severity, int>.Empty;
|
||||
public ImmutableDictionary<CryptoFindingType, int> FindingsByType { get; init; } =
|
||||
ImmutableDictionary<CryptoFindingType, int>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis.Policy;
|
||||
|
||||
public sealed record CryptoPolicy
|
||||
{
|
||||
public string? ComplianceFramework { get; init; }
|
||||
public ImmutableArray<string> ComplianceFrameworks { get; init; } = [];
|
||||
public ImmutableDictionary<string, int> MinimumKeyLengths { get; init; } =
|
||||
ImmutableDictionary<string, int>.Empty;
|
||||
public ImmutableArray<string> ProhibitedAlgorithms { get; init; } = [];
|
||||
public ImmutableArray<string> ApprovedAlgorithms { get; init; } = [];
|
||||
public CryptoRequiredFeatures RequiredFeatures { get; init; } = new();
|
||||
public PostQuantumPolicy PostQuantum { get; init; } = new();
|
||||
public CertificatePolicy Certificates { get; init; } = new();
|
||||
public RegionalCryptoPolicy RegionalRequirements { get; init; } = new();
|
||||
public ImmutableArray<CryptoPolicyExemption> Exemptions { get; init; } = [];
|
||||
public string? Version { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoRequiredFeatures
|
||||
{
|
||||
public bool PerfectForwardSecrecy { get; init; }
|
||||
public bool AuthenticatedEncryption { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PostQuantumPolicy
|
||||
{
|
||||
public bool Enabled { get; init; }
|
||||
public bool RequireHybridForLongLived { get; init; }
|
||||
public int LongLivedDataThresholdYears { get; init; } = 10;
|
||||
}
|
||||
|
||||
public sealed record CertificatePolicy
|
||||
{
|
||||
public int ExpirationWarningDays { get; init; } = 90;
|
||||
public string? MinimumSignatureAlgorithm { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RegionalCryptoPolicy
|
||||
{
|
||||
public bool Eidas { get; init; }
|
||||
public bool Gost { get; init; }
|
||||
public bool Sm { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CryptoPolicyExemption
|
||||
{
|
||||
public required string ComponentPattern { get; init; }
|
||||
public ImmutableArray<string> Algorithms { get; init; } = [];
|
||||
public string? Reason { get; init; }
|
||||
public DateTimeOffset? ExpirationDate { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis.Policy;
|
||||
|
||||
public interface ICryptoPolicyLoader
|
||||
{
|
||||
Task<CryptoPolicy> LoadAsync(string? path, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public static class CryptoPolicyDefaults
|
||||
{
|
||||
public static CryptoPolicy Default { get; } = new()
|
||||
{
|
||||
MinimumKeyLengths = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["RSA"] = 2048,
|
||||
["DSA"] = 2048,
|
||||
["ECDSA"] = 256,
|
||||
["ECDH"] = 256,
|
||||
["ECC"] = 256,
|
||||
["AES"] = 128
|
||||
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase),
|
||||
ProhibitedAlgorithms = ["MD5", "SHA1", "DES", "3DES", "RC4"],
|
||||
RequiredFeatures = new CryptoRequiredFeatures
|
||||
{
|
||||
PerfectForwardSecrecy = false,
|
||||
AuthenticatedEncryption = false
|
||||
},
|
||||
Certificates = new CertificatePolicy
|
||||
{
|
||||
ExpirationWarningDays = 90,
|
||||
MinimumSignatureAlgorithm = "SHA256"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class CryptoPolicyLoader : ICryptoPolicyLoader
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
|
||||
|
||||
private readonly IDeserializer _yamlDeserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
public async Task<CryptoPolicy> LoadAsync(string? path, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
return CryptoPolicyDefaults.Default;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path).ToLowerInvariant();
|
||||
await using var stream = File.OpenRead(path);
|
||||
|
||||
return extension switch
|
||||
{
|
||||
".yaml" or ".yml" => LoadFromYaml(stream),
|
||||
_ => await LoadFromJsonAsync(stream, ct).ConfigureAwait(false)
|
||||
};
|
||||
}
|
||||
|
||||
private CryptoPolicy LoadFromYaml(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
var yamlObject = _yamlDeserializer.Deserialize(reader);
|
||||
if (yamlObject is null)
|
||||
{
|
||||
return CryptoPolicyDefaults.Default;
|
||||
}
|
||||
|
||||
var payload = JsonSerializer.Serialize(yamlObject);
|
||||
using var document = JsonDocument.Parse(payload);
|
||||
return ExtractPolicy(document.RootElement);
|
||||
}
|
||||
|
||||
private static async Task<CryptoPolicy> LoadFromJsonAsync(Stream stream, CancellationToken ct)
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct)
|
||||
.ConfigureAwait(false);
|
||||
return ExtractPolicy(document.RootElement);
|
||||
}
|
||||
|
||||
private static CryptoPolicy ExtractPolicy(JsonElement root)
|
||||
{
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& root.TryGetProperty("cryptoPolicy", out var policyElement))
|
||||
{
|
||||
return JsonSerializer.Deserialize<CryptoPolicy>(policyElement, JsonOptions)
|
||||
?? CryptoPolicyDefaults.Default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<CryptoPolicy>(root, JsonOptions)
|
||||
?? CryptoPolicyDefaults.Default;
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateJsonOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
options.Converters.Add(new FlexibleBooleanConverter());
|
||||
return options;
|
||||
}
|
||||
|
||||
private sealed class FlexibleBooleanConverter : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.String when bool.TryParse(reader.GetString(), out var value) => value,
|
||||
_ => throw new JsonException($"Expected boolean value or boolean string, got {reader.TokenType}.")
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
using StellaOps.Scanner.Sarif;
|
||||
using CryptoSeverity = StellaOps.Scanner.CryptoAnalysis.Models.Severity;
|
||||
using SarifSeverity = StellaOps.Scanner.Sarif.Severity;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis.Reporting;
|
||||
|
||||
public static class CryptoAnalysisReportFormatter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static byte[] ToJsonBytes(CryptoAnalysisReport report)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(report, JsonOptions);
|
||||
}
|
||||
|
||||
public static string ToText(CryptoAnalysisReport report)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("Crypto Analysis Report");
|
||||
builder.AppendLine($"Findings: {report.Summary.TotalFindings}");
|
||||
|
||||
foreach (var severityGroup in report.Summary.FindingsBySeverity.OrderByDescending(kvp => kvp.Key))
|
||||
{
|
||||
builder.AppendLine($" {severityGroup.Key}: {severityGroup.Value}");
|
||||
}
|
||||
|
||||
if (!report.ComplianceStatus.Frameworks.IsDefaultOrEmpty)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("Compliance:");
|
||||
foreach (var framework in report.ComplianceStatus.Frameworks)
|
||||
{
|
||||
builder.AppendLine($" {framework.Framework}: {(framework.IsCompliant ? "Compliant" : "Non-compliant")} ({framework.ViolationCount} violations)");
|
||||
}
|
||||
}
|
||||
|
||||
if (report.QuantumReadiness.TotalAlgorithms > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendLine($"Post-Quantum Readiness Score: {report.QuantumReadiness.Score}");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
foreach (var finding in report.Findings)
|
||||
{
|
||||
builder.AppendLine($"- [{finding.Severity}] {finding.Title} ({finding.ComponentName ?? finding.ComponentBomRef})");
|
||||
if (!string.IsNullOrWhiteSpace(finding.Description))
|
||||
{
|
||||
builder.AppendLine($" {finding.Description}");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(finding.Remediation))
|
||||
{
|
||||
builder.AppendLine($" Remediation: {finding.Remediation}");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static byte[] ToPdfBytes(CryptoAnalysisReport report)
|
||||
{
|
||||
return SimplePdfBuilder.Build(ToText(report));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CryptoAnalysisSarifExporter
|
||||
{
|
||||
private readonly ISarifExportService _sarifExporter;
|
||||
|
||||
public CryptoAnalysisSarifExporter(ISarifExportService sarifExporter)
|
||||
{
|
||||
_sarifExporter = sarifExporter ?? throw new ArgumentNullException(nameof(sarifExporter));
|
||||
}
|
||||
|
||||
public async Task<object?> ExportAsync(CryptoAnalysisReport report, CancellationToken ct = default)
|
||||
{
|
||||
if (report.Findings.IsDefaultOrEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var inputs = report.Findings.Select(MapToFindingInput).ToList();
|
||||
var options = new SarifExportOptions
|
||||
{
|
||||
ToolName = "StellaOps Scanner",
|
||||
ToolVersion = "1.0.0",
|
||||
Category = "crypto-analysis",
|
||||
IncludeEvidenceUris = false,
|
||||
IncludeReachability = false,
|
||||
IncludeVexStatus = false
|
||||
};
|
||||
|
||||
return await _sarifExporter.ExportAsync(inputs, options, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static FindingInput MapToFindingInput(CryptoFinding finding)
|
||||
{
|
||||
return new FindingInput
|
||||
{
|
||||
Type = FindingType.Configuration,
|
||||
VulnerabilityId = finding.Algorithm,
|
||||
ComponentName = finding.ComponentName,
|
||||
Severity = MapSeverity(finding.Severity),
|
||||
Title = finding.Title,
|
||||
Description = finding.Description,
|
||||
Recommendation = finding.Remediation,
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
["componentBomRef"] = finding.ComponentBomRef,
|
||||
["findingType"] = finding.Type.ToString(),
|
||||
["algorithm"] = finding.Algorithm ?? string.Empty,
|
||||
["protocol"] = finding.Protocol ?? string.Empty,
|
||||
["certificate"] = finding.Certificate ?? string.Empty
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static SarifSeverity MapSeverity(CryptoSeverity severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
CryptoSeverity.Critical => SarifSeverity.Critical,
|
||||
CryptoSeverity.High => SarifSeverity.High,
|
||||
CryptoSeverity.Medium => SarifSeverity.Medium,
|
||||
CryptoSeverity.Low => SarifSeverity.Low,
|
||||
_ => SarifSeverity.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static class SimplePdfBuilder
|
||||
{
|
||||
public static byte[] Build(string text)
|
||||
{
|
||||
var lines = text.Replace("\r", string.Empty).Split('\n');
|
||||
var contentStream = BuildContentStream(lines);
|
||||
var objects = new List<string>
|
||||
{
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
|
||||
$"<< /Length {contentStream.Length} >>\\nstream\\n{contentStream}\\nendstream",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"
|
||||
};
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
WriteLine(stream, "%PDF-1.4");
|
||||
|
||||
var offsets = new List<long> { 0 };
|
||||
for (var i = 0; i < objects.Count; i++)
|
||||
{
|
||||
offsets.Add(stream.Position);
|
||||
WriteLine(stream, $"{i + 1} 0 obj");
|
||||
WriteLine(stream, objects[i]);
|
||||
WriteLine(stream, "endobj");
|
||||
}
|
||||
|
||||
var xrefStart = stream.Position;
|
||||
WriteLine(stream, "xref");
|
||||
WriteLine(stream, $"0 {objects.Count + 1}");
|
||||
WriteLine(stream, "0000000000 65535 f ");
|
||||
for (var i = 1; i < offsets.Count; i++)
|
||||
{
|
||||
WriteLine(stream, $"{offsets[i]:0000000000} 00000 n ");
|
||||
}
|
||||
|
||||
WriteLine(stream, "trailer");
|
||||
WriteLine(stream, $"<< /Size {objects.Count + 1} /Root 1 0 R >>");
|
||||
WriteLine(stream, "startxref");
|
||||
WriteLine(stream, xrefStart.ToString());
|
||||
WriteLine(stream, "%%EOF");
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static string BuildContentStream(IEnumerable<string> lines)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("BT");
|
||||
builder.AppendLine("/F1 10 Tf");
|
||||
var y = 760;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var escaped = EscapeText(line);
|
||||
builder.AppendLine($"72 {y} Td ({escaped}) Tj");
|
||||
y -= 14;
|
||||
if (y < 60)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
builder.AppendLine("ET");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeText(string value)
|
||||
{
|
||||
return value.Replace("\\\\", "\\\\\\\\")
|
||||
.Replace("(", "\\\\(")
|
||||
.Replace(")", "\\\\)");
|
||||
}
|
||||
|
||||
private static void WriteLine(Stream stream, string line)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(line + "\\n");
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
|
||||
namespace StellaOps.Scanner.CryptoAnalysis.Reporting;
|
||||
|
||||
public enum CryptoInventoryFormat
|
||||
{
|
||||
Json,
|
||||
Csv,
|
||||
Xlsx
|
||||
}
|
||||
|
||||
public static class CryptoInventoryExporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static byte[] Export(CryptoInventory inventory, CryptoInventoryFormat format)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inventory);
|
||||
|
||||
return format switch
|
||||
{
|
||||
CryptoInventoryFormat.Json => JsonSerializer.SerializeToUtf8Bytes(inventory, JsonOptions),
|
||||
CryptoInventoryFormat.Csv => ExportCsv(inventory),
|
||||
CryptoInventoryFormat.Xlsx => ExportXlsx(inventory),
|
||||
_ => JsonSerializer.SerializeToUtf8Bytes(inventory, JsonOptions)
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] ExportCsv(CryptoInventory inventory)
|
||||
{
|
||||
var (headers, rows) = BuildRows(inventory);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine(string.Join(',', headers.Select(EscapeCsv)));
|
||||
foreach (var row in rows)
|
||||
{
|
||||
builder.AppendLine(string.Join(',', row.Select(EscapeCsv)));
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(builder.ToString());
|
||||
}
|
||||
|
||||
private static byte[] ExportXlsx(CryptoInventory inventory)
|
||||
{
|
||||
var (headers, rows) = BuildRows(inventory);
|
||||
return XlsxExporter.Export(headers, rows);
|
||||
}
|
||||
|
||||
private static (string[] Headers, List<string[]> Rows) BuildRows(CryptoInventory inventory)
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
"assetType",
|
||||
"componentBomRef",
|
||||
"componentName",
|
||||
"algorithm",
|
||||
"algorithmIdentifier",
|
||||
"keySize",
|
||||
"mode",
|
||||
"padding",
|
||||
"certificateSubject",
|
||||
"certificateIssuer",
|
||||
"certificateNotValidAfter",
|
||||
"protocolType",
|
||||
"protocolVersion",
|
||||
"cipherSuites",
|
||||
"keyMaterialType",
|
||||
"keyMaterialReference"
|
||||
};
|
||||
|
||||
var rows = new List<string[]>();
|
||||
foreach (var algorithm in inventory.Algorithms)
|
||||
{
|
||||
rows.Add(new[]
|
||||
{
|
||||
"algorithm",
|
||||
algorithm.ComponentBomRef,
|
||||
algorithm.ComponentName ?? string.Empty,
|
||||
algorithm.Algorithm ?? string.Empty,
|
||||
algorithm.AlgorithmIdentifier ?? string.Empty,
|
||||
algorithm.KeySize?.ToString() ?? string.Empty,
|
||||
algorithm.Mode ?? string.Empty,
|
||||
algorithm.Padding ?? string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var certificate in inventory.Certificates)
|
||||
{
|
||||
rows.Add(new[]
|
||||
{
|
||||
"certificate",
|
||||
certificate.ComponentBomRef,
|
||||
certificate.ComponentName ?? string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
certificate.SubjectName ?? string.Empty,
|
||||
certificate.IssuerName ?? string.Empty,
|
||||
certificate.NotValidAfter?.ToString("O") ?? string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var protocol in inventory.Protocols)
|
||||
{
|
||||
rows.Add(new[]
|
||||
{
|
||||
"protocol",
|
||||
protocol.ComponentBomRef,
|
||||
protocol.ComponentName ?? string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
protocol.Type ?? string.Empty,
|
||||
protocol.Version ?? string.Empty,
|
||||
string.Join(';', protocol.CipherSuites),
|
||||
string.Empty,
|
||||
string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var material in inventory.KeyMaterials)
|
||||
{
|
||||
rows.Add(new[]
|
||||
{
|
||||
"key-material",
|
||||
material.ComponentBomRef,
|
||||
material.ComponentName ?? string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
material.Type ?? string.Empty,
|
||||
material.Reference ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return (headers, rows);
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
var sanitized = value ?? string.Empty;
|
||||
if (sanitized.Contains('"') || sanitized.Contains(',') || sanitized.Contains('\n'))
|
||||
{
|
||||
sanitized = '"' + sanitized.Replace("\"", "\"\"") + '"';
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static class XlsxExporter
|
||||
{
|
||||
public static byte[] Export(string[] headers, IReadOnlyList<string[]> rows)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
AddEntry(archive, "[Content_Types].xml", BuildContentTypes());
|
||||
AddEntry(archive, "_rels/.rels", BuildRootRels());
|
||||
AddEntry(archive, "xl/workbook.xml", BuildWorkbook());
|
||||
AddEntry(archive, "xl/_rels/workbook.xml.rels", BuildWorkbookRels());
|
||||
AddEntry(archive, "xl/styles.xml", BuildStyles());
|
||||
AddEntry(archive, "xl/worksheets/sheet1.xml", BuildSheet(headers, rows));
|
||||
}
|
||||
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
private static void AddEntry(ZipArchive archive, string path, string content)
|
||||
{
|
||||
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.Write(content);
|
||||
}
|
||||
|
||||
private static string BuildContentTypes() =>
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">" +
|
||||
"<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>" +
|
||||
"<Default Extension=\"xml\" ContentType=\"application/xml\"/>" +
|
||||
"<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>" +
|
||||
"<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>" +
|
||||
"<Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>" +
|
||||
"</Types>";
|
||||
|
||||
private static string BuildRootRels() =>
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">" +
|
||||
"<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>" +
|
||||
"</Relationships>";
|
||||
|
||||
private static string BuildWorkbook() =>
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
|
||||
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
|
||||
"<sheets><sheet name=\"CryptoInventory\" sheetId=\"1\" r:id=\"rId1\"/></sheets>" +
|
||||
"</workbook>";
|
||||
|
||||
private static string BuildWorkbookRels() =>
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">" +
|
||||
"<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>" +
|
||||
"<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>" +
|
||||
"</Relationships>";
|
||||
|
||||
private static string BuildStyles() =>
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">" +
|
||||
"<fonts count=\"1\"><font><sz val=\"11\"/><name val=\"Calibri\"/></font></fonts>" +
|
||||
"<fills count=\"1\"><fill><patternFill patternType=\"none\"/></fill></fills>" +
|
||||
"<borders count=\"1\"><border/></borders>" +
|
||||
"<cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs>" +
|
||||
"<cellXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/></cellXfs>" +
|
||||
"</styleSheet>";
|
||||
|
||||
private static string BuildSheet(string[] headers, IReadOnlyList<string[]> rows)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
builder.Append("<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">");
|
||||
builder.Append("<sheetData>");
|
||||
|
||||
AppendRow(builder, 1, headers);
|
||||
var rowIndex = 2;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
AppendRow(builder, rowIndex, row);
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
builder.Append("</sheetData></worksheet>");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendRow(StringBuilder builder, int rowIndex, IReadOnlyList<string> values)
|
||||
{
|
||||
builder.Append("<row r=\"").Append(rowIndex).Append("\">");
|
||||
for (var colIndex = 0; colIndex < values.Count; colIndex++)
|
||||
{
|
||||
var cellRef = GetCellReference(colIndex, rowIndex);
|
||||
var value = EscapeXml(values[colIndex] ?? string.Empty);
|
||||
builder.Append("<c r=\"").Append(cellRef).Append("\" t=\"inlineStr\"><is><t>")
|
||||
.Append(value).Append("</t></is></c>");
|
||||
}
|
||||
builder.Append("</row>");
|
||||
}
|
||||
|
||||
private static string GetCellReference(int columnIndex, int rowIndex)
|
||||
{
|
||||
return ColumnName(columnIndex) + rowIndex.ToString();
|
||||
}
|
||||
|
||||
private static string ColumnName(int index)
|
||||
{
|
||||
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
var dividend = index + 1;
|
||||
var columnName = string.Empty;
|
||||
while (dividend > 0)
|
||||
{
|
||||
var modulo = (dividend - 1) % 26;
|
||||
columnName = alphabet[modulo] + columnName;
|
||||
dividend = (dividend - modulo - 1) / 26;
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
private static string EscapeXml(string value)
|
||||
{
|
||||
return value.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.cs" Exclude="obj\**;bin\**" />
|
||||
<EmbeddedResource Include="**\*.json" Exclude="obj\**;bin\**" />
|
||||
<None Include="**\*" Exclude="**\*.cs;**\*.json;bin\**;obj\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Scanner.Sarif/StellaOps.Scanner.Sarif.csproj" />
|
||||
<ProjectReference Include="../../../Concelier/__Libraries/StellaOps.Concelier.SbomIntegration/StellaOps.Concelier.SbomIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user