- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
331 lines
9.1 KiB
C#
331 lines
9.1 KiB
C#
using System.Collections.Immutable;
|
|
using System.Runtime.Serialization;
|
|
|
|
namespace StellaOps.Excititor.Core;
|
|
|
|
public sealed record VexClaim
|
|
{
|
|
public VexClaim(
|
|
string vulnerabilityId,
|
|
string providerId,
|
|
VexProduct product,
|
|
VexClaimStatus status,
|
|
VexClaimDocument document,
|
|
DateTimeOffset firstSeen,
|
|
DateTimeOffset lastSeen,
|
|
VexJustification? justification = null,
|
|
string? detail = null,
|
|
VexConfidence? confidence = null,
|
|
VexSignalSnapshot? signals = null,
|
|
ImmutableDictionary<string, string>? additionalMetadata = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(vulnerabilityId))
|
|
{
|
|
throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(providerId))
|
|
{
|
|
throw new ArgumentException("Provider id must be provided.", nameof(providerId));
|
|
}
|
|
|
|
if (lastSeen < firstSeen)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(lastSeen), "Last seen timestamp cannot be earlier than first seen.");
|
|
}
|
|
|
|
VulnerabilityId = vulnerabilityId.Trim();
|
|
ProviderId = providerId.Trim();
|
|
Product = product ?? throw new ArgumentNullException(nameof(product));
|
|
Status = status;
|
|
Document = document ?? throw new ArgumentNullException(nameof(document));
|
|
FirstSeen = firstSeen;
|
|
LastSeen = lastSeen;
|
|
Justification = justification;
|
|
Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
|
|
Confidence = confidence;
|
|
Signals = signals;
|
|
AdditionalMetadata = NormalizeMetadata(additionalMetadata);
|
|
}
|
|
|
|
public string VulnerabilityId { get; }
|
|
|
|
public string ProviderId { get; }
|
|
|
|
public VexProduct Product { get; }
|
|
|
|
public VexClaimStatus Status { get; }
|
|
|
|
public VexJustification? Justification { get; }
|
|
|
|
public string? Detail { get; }
|
|
|
|
public VexClaimDocument Document { get; }
|
|
|
|
public DateTimeOffset FirstSeen { get; }
|
|
|
|
public DateTimeOffset LastSeen { get; }
|
|
|
|
public VexConfidence? Confidence { get; }
|
|
|
|
public VexSignalSnapshot? Signals { get; }
|
|
|
|
public ImmutableSortedDictionary<string, string> AdditionalMetadata { get; }
|
|
|
|
private static ImmutableSortedDictionary<string, string> NormalizeMetadata(
|
|
ImmutableDictionary<string, string>? additionalMetadata)
|
|
{
|
|
if (additionalMetadata is null || additionalMetadata.Count == 0)
|
|
{
|
|
return ImmutableSortedDictionary<string, string>.Empty;
|
|
}
|
|
|
|
var builder = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
|
foreach (var (key, value) in additionalMetadata)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
builder[key.Trim()] = value?.Trim() ?? string.Empty;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
}
|
|
|
|
public sealed record VexProduct
|
|
{
|
|
public VexProduct(
|
|
string key,
|
|
string? name,
|
|
string? version = null,
|
|
string? purl = null,
|
|
string? cpe = null,
|
|
IEnumerable<string>? componentIdentifiers = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
throw new ArgumentException("Product key must be provided.", nameof(key));
|
|
}
|
|
|
|
Key = key.Trim();
|
|
Name = string.IsNullOrWhiteSpace(name) ? null : name.Trim();
|
|
Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim();
|
|
Purl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim();
|
|
Cpe = string.IsNullOrWhiteSpace(cpe) ? null : cpe.Trim();
|
|
ComponentIdentifiers = NormalizeComponentIdentifiers(componentIdentifiers);
|
|
}
|
|
|
|
public string Key { get; }
|
|
|
|
public string? Name { get; }
|
|
|
|
public string? Version { get; }
|
|
|
|
public string? Purl { get; }
|
|
|
|
public string? Cpe { get; }
|
|
|
|
public ImmutableArray<string> ComponentIdentifiers { get; }
|
|
|
|
private static ImmutableArray<string> NormalizeComponentIdentifiers(IEnumerable<string>? identifiers)
|
|
{
|
|
if (identifiers is null)
|
|
{
|
|
return ImmutableArray<string>.Empty;
|
|
}
|
|
|
|
var set = new SortedSet<string>(StringComparer.Ordinal);
|
|
foreach (var identifier in identifiers)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(identifier))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
set.Add(identifier.Trim());
|
|
}
|
|
|
|
return set.Count == 0 ? ImmutableArray<string>.Empty : set.ToImmutableArray();
|
|
}
|
|
}
|
|
|
|
public sealed record VexClaimDocument
|
|
{
|
|
public VexClaimDocument(
|
|
VexDocumentFormat format,
|
|
string digest,
|
|
Uri sourceUri,
|
|
string? revision = null,
|
|
VexSignatureMetadata? signature = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(digest))
|
|
{
|
|
throw new ArgumentException("Document digest must be provided.", nameof(digest));
|
|
}
|
|
|
|
Format = format;
|
|
Digest = digest.Trim();
|
|
SourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri));
|
|
Revision = string.IsNullOrWhiteSpace(revision) ? null : revision.Trim();
|
|
Signature = signature;
|
|
}
|
|
|
|
public VexDocumentFormat Format { get; }
|
|
|
|
public string Digest { get; }
|
|
|
|
public Uri SourceUri { get; }
|
|
|
|
public string? Revision { get; }
|
|
|
|
public VexSignatureMetadata? Signature { get; }
|
|
}
|
|
|
|
public sealed record VexSignatureMetadata
|
|
{
|
|
public VexSignatureMetadata(
|
|
string type,
|
|
string? subject = null,
|
|
string? issuer = null,
|
|
string? keyId = null,
|
|
DateTimeOffset? verifiedAt = null,
|
|
string? transparencyLogReference = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(type))
|
|
{
|
|
throw new ArgumentException("Signature type must be provided.", nameof(type));
|
|
}
|
|
|
|
Type = type.Trim();
|
|
Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim();
|
|
Issuer = string.IsNullOrWhiteSpace(issuer) ? null : issuer.Trim();
|
|
KeyId = string.IsNullOrWhiteSpace(keyId) ? null : keyId.Trim();
|
|
VerifiedAt = verifiedAt;
|
|
TransparencyLogReference = string.IsNullOrWhiteSpace(transparencyLogReference)
|
|
? null
|
|
: transparencyLogReference.Trim();
|
|
}
|
|
|
|
public string Type { get; }
|
|
|
|
public string? Subject { get; }
|
|
|
|
public string? Issuer { get; }
|
|
|
|
public string? KeyId { get; }
|
|
|
|
public DateTimeOffset? VerifiedAt { get; }
|
|
|
|
public string? TransparencyLogReference { get; }
|
|
}
|
|
|
|
public sealed record VexConfidence
|
|
{
|
|
public VexConfidence(string level, double? score = null, string? method = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(level))
|
|
{
|
|
throw new ArgumentException("Confidence level must be provided.", nameof(level));
|
|
}
|
|
|
|
if (score is not null && (double.IsNaN(score.Value) || double.IsInfinity(score.Value)))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(score), "Confidence score must be a finite number.");
|
|
}
|
|
|
|
Level = level.Trim();
|
|
Score = score;
|
|
Method = string.IsNullOrWhiteSpace(method) ? null : method.Trim();
|
|
}
|
|
|
|
public string Level { get; }
|
|
|
|
public double? Score { get; }
|
|
|
|
public string? Method { get; }
|
|
}
|
|
|
|
[DataContract]
|
|
public enum VexDocumentFormat
|
|
{
|
|
[EnumMember(Value = "csaf")]
|
|
Csaf,
|
|
|
|
[EnumMember(Value = "cyclonedx")]
|
|
CycloneDx,
|
|
|
|
[EnumMember(Value = "openvex")]
|
|
OpenVex,
|
|
|
|
[EnumMember(Value = "oci_attestation")]
|
|
OciAttestation,
|
|
}
|
|
|
|
[DataContract]
|
|
public enum VexClaimStatus
|
|
{
|
|
[EnumMember(Value = "affected")]
|
|
Affected,
|
|
|
|
[EnumMember(Value = "not_affected")]
|
|
NotAffected,
|
|
|
|
[EnumMember(Value = "fixed")]
|
|
Fixed,
|
|
|
|
[EnumMember(Value = "under_investigation")]
|
|
UnderInvestigation,
|
|
}
|
|
|
|
[DataContract]
|
|
public enum VexJustification
|
|
{
|
|
[EnumMember(Value = "component_not_present")]
|
|
ComponentNotPresent,
|
|
|
|
[EnumMember(Value = "component_not_configured")]
|
|
ComponentNotConfigured,
|
|
|
|
[EnumMember(Value = "vulnerable_code_not_present")]
|
|
VulnerableCodeNotPresent,
|
|
|
|
[EnumMember(Value = "vulnerable_code_not_in_execute_path")]
|
|
VulnerableCodeNotInExecutePath,
|
|
|
|
[EnumMember(Value = "vulnerable_code_cannot_be_controlled_by_adversary")]
|
|
VulnerableCodeCannotBeControlledByAdversary,
|
|
|
|
[EnumMember(Value = "inline_mitigations_already_exist")]
|
|
InlineMitigationsAlreadyExist,
|
|
|
|
[EnumMember(Value = "protected_by_mitigating_control")]
|
|
ProtectedByMitigatingControl,
|
|
|
|
[EnumMember(Value = "code_not_present")]
|
|
CodeNotPresent,
|
|
|
|
[EnumMember(Value = "code_not_reachable")]
|
|
CodeNotReachable,
|
|
|
|
[EnumMember(Value = "requires_configuration")]
|
|
RequiresConfiguration,
|
|
|
|
[EnumMember(Value = "requires_dependency")]
|
|
RequiresDependency,
|
|
|
|
[EnumMember(Value = "requires_environment")]
|
|
RequiresEnvironment,
|
|
|
|
[EnumMember(Value = "protected_by_compensating_control")]
|
|
ProtectedByCompensatingControl,
|
|
|
|
[EnumMember(Value = "protected_at_perimeter")]
|
|
ProtectedAtPerimeter,
|
|
|
|
[EnumMember(Value = "protected_at_runtime")]
|
|
ProtectedAtRuntime,
|
|
}
|