partly or unimplemented features - now implemented
This commit is contained in:
@@ -246,30 +246,130 @@ public sealed class AstraConnector : IFeedConnector
|
||||
/// Reference implementations:
|
||||
/// - OpenSCAP (C library with Python bindings)
|
||||
/// - OVAL Tools (Java)
|
||||
/// - Custom XPath/LINQ to XML parser
|
||||
/// - Custom XPath/LINQ to XML parser (implemented below)
|
||||
/// </remarks>
|
||||
private Task<IReadOnlyList<AstraVulnerabilityDefinition>> ParseOvalXmlAsync(
|
||||
string ovalXml,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Implement OVAL XML parsing
|
||||
// Placeholder return empty list
|
||||
_logger.LogWarning("OVAL XML parser not implemented");
|
||||
return Task.FromResult<IReadOnlyList<AstraVulnerabilityDefinition>>(Array.Empty<AstraVulnerabilityDefinition>());
|
||||
// Use the OvalParser to extract vulnerability definitions
|
||||
var parser = new Internal.OvalParser(
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger<Internal.OvalParser>.Instance);
|
||||
|
||||
var definitions = parser.Parse(ovalXml);
|
||||
|
||||
_logger.LogDebug("Parsed {Count} vulnerability definitions from OVAL XML", definitions.Count);
|
||||
|
||||
return Task.FromResult(definitions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps OVAL vulnerability definition to Concelier Advisory model.
|
||||
/// </summary>
|
||||
private Advisory MapToAdvisory(AstraVulnerabilityDefinition definition)
|
||||
private Advisory MapToAdvisory(AstraVulnerabilityDefinition definition, DateTimeOffset recordedAt)
|
||||
{
|
||||
// TODO: Implement mapping from OVAL definition to Advisory
|
||||
// This will use:
|
||||
// - Debian EVR version comparer (Astra is Debian-based)
|
||||
// - Trust vector for Astra (provenance: 0.95, coverage: 0.90, replayability: 0.85)
|
||||
// - Package naming from Debian ecosystem
|
||||
ArgumentNullException.ThrowIfNull(definition);
|
||||
|
||||
throw new NotImplementedException("OVAL to Advisory mapping not yet implemented");
|
||||
// Determine advisory key - prefer first CVE ID, fallback to definition ID
|
||||
var advisoryKey = definition.CveIds.Length > 0
|
||||
? definition.CveIds[0]
|
||||
: definition.DefinitionId;
|
||||
|
||||
// Get trust vector for Astra source
|
||||
var trustVector = AstraTrustDefaults.DefaultVector;
|
||||
|
||||
// Create base provenance record
|
||||
var baseProvenance = new AdvisoryProvenance(
|
||||
source: AstraOptions.SourceName,
|
||||
kind: "oval-definition",
|
||||
value: definition.DefinitionId,
|
||||
recordedAt: recordedAt,
|
||||
fieldMask: new[] { "advisoryKey", "title", "description", "severity", "published", "affectedPackages" });
|
||||
|
||||
// Map affected packages to canonical model
|
||||
var affectedPackages = MapAffectedPackages(definition.AffectedPackages, baseProvenance);
|
||||
|
||||
// Create the advisory
|
||||
return new Advisory(
|
||||
advisoryKey: advisoryKey,
|
||||
title: definition.Title,
|
||||
summary: null,
|
||||
language: "ru", // Astra Linux is primarily Russian
|
||||
published: definition.PublishedDate,
|
||||
modified: null,
|
||||
severity: definition.Severity,
|
||||
exploitKnown: false,
|
||||
aliases: definition.CveIds.Skip(1), // Additional CVEs as aliases
|
||||
credits: Array.Empty<AdvisoryCredit>(),
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: affectedPackages,
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { baseProvenance },
|
||||
description: definition.Description,
|
||||
cwes: null,
|
||||
canonicalMetricId: null,
|
||||
mergeHash: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps OVAL affected packages to canonical AffectedPackage model.
|
||||
/// </summary>
|
||||
private static IEnumerable<AffectedPackage> MapAffectedPackages(
|
||||
AstraAffectedPackage[] ovalPackages,
|
||||
AdvisoryProvenance provenance)
|
||||
{
|
||||
foreach (var pkg in ovalPackages)
|
||||
{
|
||||
// Create version range - Astra uses Debian EVR versioning
|
||||
var versionRange = new AffectedVersionRange(
|
||||
rangeKind: "evr", // Debian EVR (Epoch:Version-Release)
|
||||
introducedVersion: pkg.MinVersion,
|
||||
fixedVersion: pkg.FixedVersion,
|
||||
lastAffectedVersion: pkg.MaxVersion,
|
||||
rangeExpression: BuildRangeExpression(pkg),
|
||||
provenance: provenance);
|
||||
|
||||
yield return new AffectedPackage(
|
||||
type: AffectedPackageTypes.Deb, // Astra is Debian-based
|
||||
identifier: pkg.PackageName,
|
||||
platform: "astra-linux",
|
||||
versionRanges: new[] { versionRange },
|
||||
statuses: null,
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a human-readable range expression for the package.
|
||||
/// </summary>
|
||||
private static string? BuildRangeExpression(AstraAffectedPackage pkg)
|
||||
{
|
||||
if (pkg.FixedVersion is not null)
|
||||
{
|
||||
if (pkg.MinVersion is not null)
|
||||
{
|
||||
return $">={pkg.MinVersion}, <{pkg.FixedVersion}";
|
||||
}
|
||||
|
||||
return $"<{pkg.FixedVersion}";
|
||||
}
|
||||
|
||||
if (pkg.MaxVersion is not null)
|
||||
{
|
||||
if (pkg.MinVersion is not null)
|
||||
{
|
||||
return $">={pkg.MinVersion}, <={pkg.MaxVersion}";
|
||||
}
|
||||
|
||||
return $"<={pkg.MaxVersion}";
|
||||
}
|
||||
|
||||
if (pkg.MinVersion is not null)
|
||||
{
|
||||
return $">={pkg.MinVersion}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
// <copyright file="OvalParser.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
|
||||
// Sprint: SPRINT_20260208_034_Concelier_astra_linux_oval_feed_connector
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Astra.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// OVAL XML parser for Astra Linux vulnerability definitions.
|
||||
/// Parses OVAL (Open Vulnerability Assessment Language) databases into structured vulnerability definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// OVAL is an XML-based format for vulnerability definitions used by FSTEC-certified tools.
|
||||
/// This parser extracts:
|
||||
/// - Vulnerability definitions with CVE references
|
||||
/// - Affected package names and version constraints
|
||||
/// - Metadata (severity, published date, description)
|
||||
/// </remarks>
|
||||
public sealed class OvalParser
|
||||
{
|
||||
private static readonly XNamespace OvalDefsNs = "http://oval.mitre.org/XMLSchema/oval-definitions-5";
|
||||
private static readonly XNamespace DpkgNs = "http://oval.mitre.org/XMLSchema/oval-definitions-5#linux";
|
||||
|
||||
private readonly ILogger<OvalParser> _logger;
|
||||
|
||||
public OvalParser(ILogger<OvalParser> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses OVAL XML content into vulnerability definitions.
|
||||
/// </summary>
|
||||
/// <param name="ovalXml">The OVAL XML content as string.</param>
|
||||
/// <returns>List of parsed vulnerability definitions.</returns>
|
||||
public IReadOnlyList<AstraVulnerabilityDefinition> Parse(string ovalXml)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ovalXml))
|
||||
{
|
||||
_logger.LogWarning("Empty OVAL XML content provided");
|
||||
return Array.Empty<AstraVulnerabilityDefinition>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var doc = XDocument.Parse(ovalXml);
|
||||
var root = doc.Root;
|
||||
|
||||
if (root is null)
|
||||
{
|
||||
_logger.LogWarning("OVAL XML has no root element");
|
||||
return Array.Empty<AstraVulnerabilityDefinition>();
|
||||
}
|
||||
|
||||
// Extract definitions, tests, objects, and states
|
||||
var definitions = ExtractDefinitions(root);
|
||||
var tests = ExtractTests(root);
|
||||
var objects = ExtractObjects(root);
|
||||
var states = ExtractStates(root);
|
||||
|
||||
// Build lookup tables for efficient resolution
|
||||
var testLookup = tests.ToDictionary(t => t.Id, t => t);
|
||||
var objectLookup = objects.ToDictionary(o => o.Id, o => o);
|
||||
var stateLookup = states.ToDictionary(s => s.Id, s => s);
|
||||
|
||||
// Resolve definitions with affected packages
|
||||
var results = new List<AstraVulnerabilityDefinition>();
|
||||
|
||||
foreach (var def in definitions)
|
||||
{
|
||||
var affectedPackages = ResolveAffectedPackages(def, testLookup, objectLookup, stateLookup);
|
||||
|
||||
results.Add(new AstraVulnerabilityDefinition
|
||||
{
|
||||
DefinitionId = def.Id,
|
||||
Title = def.Title,
|
||||
Description = def.Description,
|
||||
CveIds = def.CveIds,
|
||||
Severity = def.Severity,
|
||||
PublishedDate = def.PublishedDate,
|
||||
AffectedPackages = affectedPackages.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogDebug("Parsed {Count} vulnerability definitions from OVAL XML", results.Count);
|
||||
return results;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse OVAL XML");
|
||||
throw new OvalParseException("Failed to parse OVAL XML document", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OvalDefinition> ExtractDefinitions(XElement root)
|
||||
{
|
||||
var definitions = new List<OvalDefinition>();
|
||||
|
||||
var defsElement = root.Element(OvalDefsNs + "definitions");
|
||||
if (defsElement is null)
|
||||
{
|
||||
_logger.LogDebug("No definitions element found in OVAL XML");
|
||||
return definitions;
|
||||
}
|
||||
|
||||
foreach (var defElement in defsElement.Elements(OvalDefsNs + "definition"))
|
||||
{
|
||||
var id = defElement.Attribute("id")?.Value;
|
||||
var classAttr = defElement.Attribute("class")?.Value;
|
||||
|
||||
// Only process vulnerability definitions
|
||||
if (string.IsNullOrEmpty(id) || classAttr != "vulnerability")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = defElement.Element(OvalDefsNs + "metadata");
|
||||
var criteria = defElement.Element(OvalDefsNs + "criteria");
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var title = metadata.Element(OvalDefsNs + "title")?.Value ?? string.Empty;
|
||||
var description = metadata.Element(OvalDefsNs + "description")?.Value;
|
||||
var severity = metadata.Element(OvalDefsNs + "advisory")?.Element(OvalDefsNs + "severity")?.Value;
|
||||
|
||||
// Extract CVE references
|
||||
var cveRefs = metadata
|
||||
.Elements(OvalDefsNs + "reference")
|
||||
.Where(r => r.Attribute("source")?.Value?.Equals("CVE", StringComparison.OrdinalIgnoreCase) == true)
|
||||
.Select(r => r.Attribute("ref_id")?.Value)
|
||||
.Where(c => !string.IsNullOrEmpty(c))
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
|
||||
// Extract issued date
|
||||
DateTimeOffset? publishedDate = null;
|
||||
var issuedElement = metadata.Element(OvalDefsNs + "advisory")?.Element(OvalDefsNs + "issued");
|
||||
if (issuedElement is not null)
|
||||
{
|
||||
var dateAttr = issuedElement.Attribute("date")?.Value;
|
||||
if (DateTimeOffset.TryParse(dateAttr, out var date))
|
||||
{
|
||||
publishedDate = date;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract test references from criteria
|
||||
var testRefs = ExtractTestReferences(criteria).ToList();
|
||||
|
||||
definitions.Add(new OvalDefinition
|
||||
{
|
||||
Id = id,
|
||||
Title = title,
|
||||
Description = description,
|
||||
Severity = severity,
|
||||
CveIds = cveRefs,
|
||||
PublishedDate = publishedDate,
|
||||
TestReferences = testRefs
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExtractTestReferences(XElement? criteria)
|
||||
{
|
||||
if (criteria is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Extract direct criterion references
|
||||
foreach (var criterion in criteria.Elements(OvalDefsNs + "criterion"))
|
||||
{
|
||||
var testRef = criterion.Attribute("test_ref")?.Value;
|
||||
if (!string.IsNullOrEmpty(testRef))
|
||||
{
|
||||
yield return testRef;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively extract from nested criteria
|
||||
foreach (var nestedCriteria in criteria.Elements(OvalDefsNs + "criteria"))
|
||||
{
|
||||
foreach (var testRef in ExtractTestReferences(nestedCriteria))
|
||||
{
|
||||
yield return testRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<OvalTest> ExtractTests(XElement root)
|
||||
{
|
||||
var tests = new List<OvalTest>();
|
||||
|
||||
var testsElement = root.Element(OvalDefsNs + "tests");
|
||||
if (testsElement is null)
|
||||
{
|
||||
return tests;
|
||||
}
|
||||
|
||||
// Look for dpkginfo_test elements (Debian/Astra package tests)
|
||||
foreach (var testElement in testsElement.Elements(DpkgNs + "dpkginfo_test"))
|
||||
{
|
||||
var id = testElement.Attribute("id")?.Value;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var objectRef = testElement.Element(DpkgNs + "object")?.Attribute("object_ref")?.Value;
|
||||
var stateRef = testElement.Element(DpkgNs + "state")?.Attribute("state_ref")?.Value;
|
||||
|
||||
tests.Add(new OvalTest
|
||||
{
|
||||
Id = id,
|
||||
ObjectRef = objectRef ?? string.Empty,
|
||||
StateRef = stateRef ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return tests;
|
||||
}
|
||||
|
||||
private List<OvalObject> ExtractObjects(XElement root)
|
||||
{
|
||||
var objects = new List<OvalObject>();
|
||||
|
||||
var objectsElement = root.Element(OvalDefsNs + "objects");
|
||||
if (objectsElement is null)
|
||||
{
|
||||
return objects;
|
||||
}
|
||||
|
||||
// Look for dpkginfo_object elements (package name references)
|
||||
foreach (var objElement in objectsElement.Elements(DpkgNs + "dpkginfo_object"))
|
||||
{
|
||||
var id = objElement.Attribute("id")?.Value;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var packageName = objElement.Element(DpkgNs + "name")?.Value ?? string.Empty;
|
||||
|
||||
objects.Add(new OvalObject
|
||||
{
|
||||
Id = id,
|
||||
PackageName = packageName
|
||||
});
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
private List<OvalState> ExtractStates(XElement root)
|
||||
{
|
||||
var states = new List<OvalState>();
|
||||
|
||||
var statesElement = root.Element(OvalDefsNs + "states");
|
||||
if (statesElement is null)
|
||||
{
|
||||
return states;
|
||||
}
|
||||
|
||||
// Look for dpkginfo_state elements (version constraints)
|
||||
foreach (var stateElement in statesElement.Elements(DpkgNs + "dpkginfo_state"))
|
||||
{
|
||||
var id = stateElement.Attribute("id")?.Value;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var evrElement = stateElement.Element(DpkgNs + "evr");
|
||||
var version = evrElement?.Value ?? string.Empty;
|
||||
var operation = evrElement?.Attribute("operation")?.Value ?? "less than";
|
||||
|
||||
states.Add(new OvalState
|
||||
{
|
||||
Id = id,
|
||||
Version = version,
|
||||
Operation = operation
|
||||
});
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private List<AstraAffectedPackage> ResolveAffectedPackages(
|
||||
OvalDefinition definition,
|
||||
Dictionary<string, OvalTest> testLookup,
|
||||
Dictionary<string, OvalObject> objectLookup,
|
||||
Dictionary<string, OvalState> stateLookup)
|
||||
{
|
||||
var packages = new List<AstraAffectedPackage>();
|
||||
|
||||
foreach (var testRef in definition.TestReferences)
|
||||
{
|
||||
if (!testLookup.TryGetValue(testRef, out var test))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!objectLookup.TryGetValue(test.ObjectRef, out var obj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? fixedVersion = null;
|
||||
string? maxVersion = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(test.StateRef) && stateLookup.TryGetValue(test.StateRef, out var state))
|
||||
{
|
||||
// Parse operation to determine if this is a fixed version or affected version range
|
||||
if (state.Operation.Contains("less than", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
fixedVersion = state.Version; // Versions less than this are affected
|
||||
}
|
||||
else
|
||||
{
|
||||
maxVersion = state.Version;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid duplicates
|
||||
if (!packages.Any(p => p.PackageName == obj.PackageName && p.FixedVersion == fixedVersion))
|
||||
{
|
||||
packages.Add(new AstraAffectedPackage
|
||||
{
|
||||
PackageName = obj.PackageName,
|
||||
FixedVersion = fixedVersion,
|
||||
MaxVersion = maxVersion,
|
||||
MinVersion = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
#region Internal OVAL Schema Models
|
||||
|
||||
private sealed record OvalDefinition
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public required string[] CveIds { get; init; }
|
||||
public DateTimeOffset? PublishedDate { get; init; }
|
||||
public required List<string> TestReferences { get; init; }
|
||||
}
|
||||
|
||||
private sealed record OvalTest
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ObjectRef { get; init; }
|
||||
public required string StateRef { get; init; }
|
||||
}
|
||||
|
||||
private sealed record OvalObject
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string PackageName { get; init; }
|
||||
}
|
||||
|
||||
private sealed record OvalState
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required string Operation { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when OVAL XML parsing fails.
|
||||
/// </summary>
|
||||
public sealed class OvalParseException : Exception
|
||||
{
|
||||
public OvalParseException(string message) : base(message) { }
|
||||
public OvalParseException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
Reference in New Issue
Block a user