tests fixes and sprints work

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("\"", "&quot;")
.Replace("'", "&apos;");
}
}
}

View File

@@ -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>