partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

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

View File

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