Rename Vexer to Excititor
This commit is contained in:
23
src/StellaOps.Excititor.Formats.CSAF/AGENTS.md
Normal file
23
src/StellaOps.Excititor.Formats.CSAF/AGENTS.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# AGENTS
|
||||
## Role
|
||||
Normalize CSAF VEX profile documents into Excititor claims and provide CSAF export adapters.
|
||||
## Scope
|
||||
- CSAF ingestion helpers: provider metadata parsing, document revision handling, vulnerability/action mappings.
|
||||
- Normalizer implementation fulfilling `INormalizer` for CSAF sources (Red Hat, Cisco, SUSE, MSRC, Oracle, Ubuntu).
|
||||
- Export adapters producing CSAF-compliant output slices from consensus data.
|
||||
- Schema/version compatibility checks (CSAF 2.0 profile validation).
|
||||
## Participants
|
||||
- Connectors deliver raw CSAF documents to this module for normalization.
|
||||
- Export module leverages adapters when producing CSAF exports.
|
||||
- Policy engine consumes normalized justification/status fields for gating.
|
||||
## Interfaces & contracts
|
||||
- Parser/normalizer classes, helper utilities for `product_tree`, `vulnerabilities`, and `notes`.
|
||||
- Export writer interfaces for per-provider/per-product CSAF packaging.
|
||||
## In/Out of scope
|
||||
In: CSAF parsing/normalization/export, schema validation, mapping to canonical claims.
|
||||
Out: HTTP fetching (connectors), storage persistence, attestation logic.
|
||||
## Observability & security expectations
|
||||
- Emit structured diagnostics when CSAF documents fail schema validation, including source URI and revision.
|
||||
- Provide counters for normalization outcomes (status distribution, justification coverage).
|
||||
## Tests
|
||||
- Fixture-driven parsing/export tests will live in `../StellaOps.Excititor.Formats.CSAF.Tests` using real CSAF samples.
|
||||
532
src/StellaOps.Excititor.Formats.CSAF/CsafNormalizer.cs
Normal file
532
src/StellaOps.Excititor.Formats.CSAF/CsafNormalizer.cs
Normal file
@@ -0,0 +1,532 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Formats.CSAF;
|
||||
|
||||
public sealed class CsafNormalizer : IVexNormalizer
|
||||
{
|
||||
private static readonly ImmutableDictionary<VexClaimStatus, int> StatusPrecedence = new Dictionary<VexClaimStatus, int>
|
||||
{
|
||||
[VexClaimStatus.UnderInvestigation] = 0,
|
||||
[VexClaimStatus.Affected] = 1,
|
||||
[VexClaimStatus.NotAffected] = 2,
|
||||
[VexClaimStatus.Fixed] = 3,
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private readonly ILogger<CsafNormalizer> _logger;
|
||||
|
||||
public CsafNormalizer(ILogger<CsafNormalizer> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string Format => VexDocumentFormat.Csaf.ToString().ToLowerInvariant();
|
||||
|
||||
public bool CanHandle(VexRawDocument document)
|
||||
=> document is not null && document.Format == VexDocumentFormat.Csaf;
|
||||
|
||||
public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, VexProvider provider, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var result = CsafParser.Parse(document);
|
||||
var claims = ImmutableArray.CreateBuilder<VexClaim>(result.Claims.Length);
|
||||
foreach (var entry in result.Claims)
|
||||
{
|
||||
var product = new VexProduct(
|
||||
entry.Product.ProductId,
|
||||
entry.Product.Name,
|
||||
entry.Product.Version,
|
||||
entry.Product.Purl,
|
||||
entry.Product.Cpe);
|
||||
|
||||
var claimDocument = new VexClaimDocument(
|
||||
VexDocumentFormat.Csaf,
|
||||
document.Digest,
|
||||
document.SourceUri,
|
||||
result.Revision,
|
||||
signature: null);
|
||||
|
||||
var claim = new VexClaim(
|
||||
entry.VulnerabilityId,
|
||||
provider.Id,
|
||||
product,
|
||||
entry.Status,
|
||||
claimDocument,
|
||||
result.FirstRelease,
|
||||
result.LastRelease,
|
||||
justification: null,
|
||||
detail: entry.Detail,
|
||||
confidence: null,
|
||||
additionalMetadata: result.Metadata);
|
||||
|
||||
claims.Add(claim);
|
||||
}
|
||||
|
||||
var orderedClaims = claims
|
||||
.ToImmutable()
|
||||
.OrderBy(static claim => claim.VulnerabilityId, StringComparer.Ordinal)
|
||||
.ThenBy(static claim => claim.Product.Key, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Normalized CSAF document {Source} into {ClaimCount} claim(s).",
|
||||
document.SourceUri,
|
||||
orderedClaims.Length);
|
||||
|
||||
return ValueTask.FromResult(new VexClaimBatch(
|
||||
document,
|
||||
orderedClaims,
|
||||
ImmutableDictionary<string, string>.Empty));
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse CSAF document {SourceUri}", document.SourceUri);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CsafParser
|
||||
{
|
||||
public static CsafParseResult Parse(VexRawDocument document)
|
||||
{
|
||||
using var json = JsonDocument.Parse(document.Content.ToArray());
|
||||
var root = json.RootElement;
|
||||
|
||||
var tracking = root.TryGetProperty("document", out var documentElement) &&
|
||||
documentElement.ValueKind == JsonValueKind.Object &&
|
||||
documentElement.TryGetProperty("tracking", out var trackingElement)
|
||||
? trackingElement
|
||||
: default;
|
||||
|
||||
var firstRelease = ParseDate(tracking, "initial_release_date") ?? document.RetrievedAt;
|
||||
var lastRelease = ParseDate(tracking, "current_release_date") ?? firstRelease;
|
||||
|
||||
if (lastRelease < firstRelease)
|
||||
{
|
||||
lastRelease = firstRelease;
|
||||
}
|
||||
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
AddIfPresent(metadataBuilder, tracking, "id", "csaf.tracking.id");
|
||||
AddIfPresent(metadataBuilder, tracking, "version", "csaf.tracking.version");
|
||||
AddIfPresent(metadataBuilder, tracking, "status", "csaf.tracking.status");
|
||||
AddPublisherMetadata(metadataBuilder, documentElement);
|
||||
|
||||
var revision = TryGetString(tracking, "revision");
|
||||
|
||||
var productCatalog = CollectProducts(root);
|
||||
var claimsBuilder = ImmutableArray.CreateBuilder<CsafClaimEntry>();
|
||||
|
||||
if (root.TryGetProperty("vulnerabilities", out var vulnerabilitiesElement) &&
|
||||
vulnerabilitiesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var vulnerability in vulnerabilitiesElement.EnumerateArray())
|
||||
{
|
||||
var vulnerabilityId = ResolveVulnerabilityId(vulnerability);
|
||||
if (string.IsNullOrWhiteSpace(vulnerabilityId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var detail = ResolveDetail(vulnerability);
|
||||
var productClaims = BuildClaimsForVulnerability(
|
||||
vulnerabilityId,
|
||||
vulnerability,
|
||||
productCatalog,
|
||||
detail);
|
||||
|
||||
claimsBuilder.AddRange(productClaims);
|
||||
}
|
||||
}
|
||||
|
||||
return new CsafParseResult(
|
||||
firstRelease,
|
||||
lastRelease,
|
||||
revision,
|
||||
metadataBuilder.ToImmutable(),
|
||||
claimsBuilder.ToImmutable());
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CsafClaimEntry> BuildClaimsForVulnerability(
|
||||
string vulnerabilityId,
|
||||
JsonElement vulnerability,
|
||||
IReadOnlyDictionary<string, CsafProductInfo> productCatalog,
|
||||
string? detail)
|
||||
{
|
||||
if (!vulnerability.TryGetProperty("product_status", out var statusElement) ||
|
||||
statusElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return Array.Empty<CsafClaimEntry>();
|
||||
}
|
||||
|
||||
var claims = new Dictionary<string, CsafClaimEntryBuilder>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var statusProperty in statusElement.EnumerateObject())
|
||||
{
|
||||
var status = MapStatus(statusProperty.Name);
|
||||
if (status is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statusProperty.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var productIdElement in statusProperty.Value.EnumerateArray())
|
||||
{
|
||||
var productId = productIdElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(productId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var product = ResolveProduct(productCatalog, productId);
|
||||
UpdateClaim(claims, product, status.Value, detail);
|
||||
}
|
||||
}
|
||||
|
||||
if (claims.Count == 0)
|
||||
{
|
||||
return Array.Empty<CsafClaimEntry>();
|
||||
}
|
||||
|
||||
return claims.Values
|
||||
.Select(builder => new CsafClaimEntry(
|
||||
vulnerabilityId,
|
||||
builder.Product,
|
||||
builder.Status,
|
||||
builder.Detail))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static void UpdateClaim(
|
||||
IDictionary<string, CsafClaimEntryBuilder> claims,
|
||||
CsafProductInfo product,
|
||||
VexClaimStatus status,
|
||||
string? detail)
|
||||
{
|
||||
if (!claims.TryGetValue(product.ProductId, out var existing) ||
|
||||
StatusPrecedence[status] > StatusPrecedence[existing.Status])
|
||||
{
|
||||
claims[product.ProductId] = new CsafClaimEntryBuilder(product, status, detail);
|
||||
}
|
||||
}
|
||||
|
||||
private static CsafProductInfo ResolveProduct(
|
||||
IReadOnlyDictionary<string, CsafProductInfo> catalog,
|
||||
string productId)
|
||||
{
|
||||
if (catalog.TryGetValue(productId, out var product))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
|
||||
return new CsafProductInfo(productId, productId, null, null, null);
|
||||
}
|
||||
|
||||
private static string ResolveVulnerabilityId(JsonElement vulnerability)
|
||||
{
|
||||
var id = TryGetString(vulnerability, "cve")
|
||||
?? TryGetString(vulnerability, "id")
|
||||
?? TryGetString(vulnerability, "vuln_id");
|
||||
|
||||
return string.IsNullOrWhiteSpace(id) ? string.Empty : id.Trim();
|
||||
}
|
||||
|
||||
private static string? ResolveDetail(JsonElement vulnerability)
|
||||
{
|
||||
var title = TryGetString(vulnerability, "title");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
return title.Trim();
|
||||
}
|
||||
|
||||
if (vulnerability.TryGetProperty("notes", out var notesElement) &&
|
||||
notesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var note in notesElement.EnumerateArray())
|
||||
{
|
||||
if (note.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var category = TryGetString(note, "category");
|
||||
if (!string.IsNullOrWhiteSpace(category) &&
|
||||
!string.Equals(category, "description", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = TryGetString(note, "text");
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return text.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, CsafProductInfo> CollectProducts(JsonElement root)
|
||||
{
|
||||
var products = new Dictionary<string, CsafProductInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!root.TryGetProperty("product_tree", out var productTree) ||
|
||||
productTree.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return products;
|
||||
}
|
||||
|
||||
if (productTree.TryGetProperty("full_product_names", out var fullNames) &&
|
||||
fullNames.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var productEntry in fullNames.EnumerateArray())
|
||||
{
|
||||
var product = ParseProduct(productEntry, parentBranchName: null);
|
||||
if (product is not null)
|
||||
{
|
||||
AddOrUpdate(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (productTree.TryGetProperty("branches", out var branches) &&
|
||||
branches.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var branch in branches.EnumerateArray())
|
||||
{
|
||||
VisitBranch(branch, parentBranchName: null);
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
|
||||
void VisitBranch(JsonElement branch, string? parentBranchName)
|
||||
{
|
||||
if (branch.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var branchName = TryGetString(branch, "name") ?? parentBranchName;
|
||||
|
||||
if (branch.TryGetProperty("product", out var productElement))
|
||||
{
|
||||
var product = ParseProduct(productElement, branchName);
|
||||
if (product is not null)
|
||||
{
|
||||
AddOrUpdate(product);
|
||||
}
|
||||
}
|
||||
|
||||
if (branch.TryGetProperty("branches", out var childBranches) &&
|
||||
childBranches.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var childBranch in childBranches.EnumerateArray())
|
||||
{
|
||||
VisitBranch(childBranch, branchName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddOrUpdate(CsafProductInfo product)
|
||||
{
|
||||
if (products.TryGetValue(product.ProductId, out var existing))
|
||||
{
|
||||
products[product.ProductId] = MergeProducts(existing, product);
|
||||
}
|
||||
else
|
||||
{
|
||||
products[product.ProductId] = product;
|
||||
}
|
||||
}
|
||||
|
||||
static CsafProductInfo MergeProducts(CsafProductInfo existing, CsafProductInfo incoming)
|
||||
{
|
||||
static string ChooseName(string incoming, string fallback)
|
||||
=> string.IsNullOrWhiteSpace(incoming) ? fallback : incoming;
|
||||
|
||||
static string? ChooseOptional(string? incoming, string? fallback)
|
||||
=> string.IsNullOrWhiteSpace(incoming) ? fallback : incoming;
|
||||
|
||||
return new CsafProductInfo(
|
||||
existing.ProductId,
|
||||
ChooseName(incoming.Name, existing.Name),
|
||||
ChooseOptional(incoming.Version, existing.Version),
|
||||
ChooseOptional(incoming.Purl, existing.Purl),
|
||||
ChooseOptional(incoming.Cpe, existing.Cpe));
|
||||
}
|
||||
}
|
||||
|
||||
private static CsafProductInfo? ParseProduct(JsonElement element, string? parentBranchName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonElement productElement = element;
|
||||
if (!element.TryGetProperty("product_id", out var idElement) &&
|
||||
element.TryGetProperty("product", out var nestedProduct) &&
|
||||
nestedProduct.ValueKind == JsonValueKind.Object &&
|
||||
nestedProduct.TryGetProperty("product_id", out idElement))
|
||||
{
|
||||
productElement = nestedProduct;
|
||||
}
|
||||
|
||||
var productId = idElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(productId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = TryGetString(productElement, "name")
|
||||
?? TryGetString(element, "name")
|
||||
?? parentBranchName
|
||||
?? productId;
|
||||
|
||||
var version = TryGetString(productElement, "product_version")
|
||||
?? TryGetString(productElement, "version")
|
||||
?? TryGetString(element, "product_version");
|
||||
|
||||
string? cpe = null;
|
||||
string? purl = null;
|
||||
if (productElement.TryGetProperty("product_identification_helper", out var helper) &&
|
||||
helper.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
cpe = TryGetString(helper, "cpe");
|
||||
purl = TryGetString(helper, "purl");
|
||||
}
|
||||
|
||||
return new CsafProductInfo(productId.Trim(), name.Trim(), version?.Trim(), purl?.Trim(), cpe?.Trim());
|
||||
}
|
||||
|
||||
private static VexClaimStatus? MapStatus(string statusName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(statusName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return statusName switch
|
||||
{
|
||||
"known_affected" or "fixed_after_release" or "first_affected" or "last_affected" => VexClaimStatus.Affected,
|
||||
"known_not_affected" or "last_not_affected" or "first_not_affected" => VexClaimStatus.NotAffected,
|
||||
"fixed" or "first_fixed" or "last_fixed" => VexClaimStatus.Fixed,
|
||||
"under_investigation" or "investigating" => VexClaimStatus.UnderInvestigation,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDate(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!element.TryGetProperty(propertyName, out var dateElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = dateElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryGetString(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!element.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return property.ValueKind == JsonValueKind.String ? property.GetString() : null;
|
||||
}
|
||||
|
||||
private static void AddIfPresent(
|
||||
ImmutableDictionary<string, string>.Builder builder,
|
||||
JsonElement element,
|
||||
string propertyName,
|
||||
string metadataKey)
|
||||
{
|
||||
var value = TryGetString(element, propertyName);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
builder[metadataKey] = value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPublisherMetadata(
|
||||
ImmutableDictionary<string, string>.Builder builder,
|
||||
JsonElement documentElement)
|
||||
{
|
||||
if (documentElement.ValueKind != JsonValueKind.Object ||
|
||||
!documentElement.TryGetProperty("publisher", out var publisher) ||
|
||||
publisher.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddIfPresent(builder, publisher, "name", "csaf.publisher.name");
|
||||
AddIfPresent(builder, publisher, "category", "csaf.publisher.category");
|
||||
}
|
||||
|
||||
private readonly record struct CsafClaimEntryBuilder(
|
||||
CsafProductInfo Product,
|
||||
VexClaimStatus Status,
|
||||
string? Detail);
|
||||
}
|
||||
|
||||
private sealed record CsafParseResult(
|
||||
DateTimeOffset FirstRelease,
|
||||
DateTimeOffset LastRelease,
|
||||
string? Revision,
|
||||
ImmutableDictionary<string, string> Metadata,
|
||||
ImmutableArray<CsafClaimEntry> Claims);
|
||||
|
||||
private sealed record CsafClaimEntry(
|
||||
string VulnerabilityId,
|
||||
CsafProductInfo Product,
|
||||
VexClaimStatus Status,
|
||||
string? Detail);
|
||||
|
||||
private sealed record CsafProductInfo(
|
||||
string ProductId,
|
||||
string Name,
|
||||
string? Version,
|
||||
string? Purl,
|
||||
string? Cpe);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Formats.CSAF;
|
||||
|
||||
public static class CsafFormatsServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCsafNormalizer(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
services.AddSingleton<IVexNormalizer, CsafNormalizer>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
7
src/StellaOps.Excititor.Formats.CSAF/TASKS.md
Normal file
7
src/StellaOps.Excititor.Formats.CSAF/TASKS.md
Normal file
@@ -0,0 +1,7 @@
|
||||
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
|
||||
# TASKS
|
||||
| Task | Owner(s) | Depends on | Notes |
|
||||
|---|---|---|---|
|
||||
|EXCITITOR-FMT-CSAF-01-001 – CSAF normalizer foundation|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** – Implemented CSAF normalizer + DI hook, parsing tracking metadata, product tree branches/full names, and mapping product statuses into canonical `VexClaim`s with baseline precedence. Regression added in `CsafNormalizerTests`.|
|
||||
|EXCITITOR-FMT-CSAF-01-002 – Status/justification mapping|Team Excititor Formats|EXCITITOR-FMT-CSAF-01-001, EXCITITOR-POLICY-01-001|TODO – Normalize CSAF `product_status` + `justification` values into policy-aware enums with audit diagnostics for unsupported codes.|
|
||||
|EXCITITOR-FMT-CSAF-01-003 – CSAF export adapter|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CSAF-01-001|TODO – Provide CSAF export writer producing deterministic documents (per vuln/product) and manifest metadata for attestation.|
|
||||
Reference in New Issue
Block a user