up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,33 +1,33 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
|
||||
public sealed class KevOptions
|
||||
{
|
||||
public static string HttpClientName => "source.kev";
|
||||
|
||||
/// <summary>
|
||||
/// Official CISA Known Exploited Vulnerabilities JSON feed.
|
||||
/// </summary>
|
||||
public Uri FeedUri { get; set; } = new("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", UriKind.Absolute);
|
||||
|
||||
/// <summary>
|
||||
/// Timeout applied to KEV feed requests.
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
[MemberNotNull(nameof(FeedUri))]
|
||||
public void Validate()
|
||||
{
|
||||
if (FeedUri is null || !FeedUri.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("FeedUri must be an absolute URI.");
|
||||
}
|
||||
|
||||
if (RequestTimeout <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("RequestTimeout must be greater than zero.");
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
|
||||
public sealed class KevOptions
|
||||
{
|
||||
public static string HttpClientName => "source.kev";
|
||||
|
||||
/// <summary>
|
||||
/// Official CISA Known Exploited Vulnerabilities JSON feed.
|
||||
/// </summary>
|
||||
public Uri FeedUri { get; set; } = new("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", UriKind.Absolute);
|
||||
|
||||
/// <summary>
|
||||
/// Timeout applied to KEV feed requests.
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
[MemberNotNull(nameof(FeedUri))]
|
||||
public void Validate()
|
||||
{
|
||||
if (FeedUri is null || !FeedUri.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("FeedUri must be an absolute URI.");
|
||||
}
|
||||
|
||||
if (RequestTimeout <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("RequestTimeout must be greater than zero.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal sealed record KevCatalogDto
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
[JsonPropertyName("catalogVersion")]
|
||||
public string? CatalogVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("dateReleased")]
|
||||
public DateTimeOffset? DateReleased { get; init; }
|
||||
|
||||
[JsonPropertyName("count")]
|
||||
public int Count { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerabilities")]
|
||||
public IReadOnlyList<KevVulnerabilityDto> Vulnerabilities { get; init; } = Array.Empty<KevVulnerabilityDto>();
|
||||
}
|
||||
|
||||
internal sealed record KevVulnerabilityDto
|
||||
{
|
||||
[JsonPropertyName("cveID")]
|
||||
public string? CveId { get; init; }
|
||||
|
||||
[JsonPropertyName("vendorProject")]
|
||||
public string? VendorProject { get; init; }
|
||||
|
||||
[JsonPropertyName("product")]
|
||||
public string? Product { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerabilityName")]
|
||||
public string? VulnerabilityName { get; init; }
|
||||
|
||||
[JsonPropertyName("dateAdded")]
|
||||
public string? DateAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("shortDescription")]
|
||||
public string? ShortDescription { get; init; }
|
||||
|
||||
[JsonPropertyName("requiredAction")]
|
||||
public string? RequiredAction { get; init; }
|
||||
|
||||
[JsonPropertyName("dueDate")]
|
||||
public string? DueDate { get; init; }
|
||||
|
||||
[JsonPropertyName("knownRansomwareCampaignUse")]
|
||||
public string? KnownRansomwareCampaignUse { get; init; }
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
[JsonPropertyName("cwes")]
|
||||
public IReadOnlyList<string> Cwes { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal sealed record KevCatalogDto
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
[JsonPropertyName("catalogVersion")]
|
||||
public string? CatalogVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("dateReleased")]
|
||||
public DateTimeOffset? DateReleased { get; init; }
|
||||
|
||||
[JsonPropertyName("count")]
|
||||
public int Count { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerabilities")]
|
||||
public IReadOnlyList<KevVulnerabilityDto> Vulnerabilities { get; init; } = Array.Empty<KevVulnerabilityDto>();
|
||||
}
|
||||
|
||||
internal sealed record KevVulnerabilityDto
|
||||
{
|
||||
[JsonPropertyName("cveID")]
|
||||
public string? CveId { get; init; }
|
||||
|
||||
[JsonPropertyName("vendorProject")]
|
||||
public string? VendorProject { get; init; }
|
||||
|
||||
[JsonPropertyName("product")]
|
||||
public string? Product { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerabilityName")]
|
||||
public string? VulnerabilityName { get; init; }
|
||||
|
||||
[JsonPropertyName("dateAdded")]
|
||||
public string? DateAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("shortDescription")]
|
||||
public string? ShortDescription { get; init; }
|
||||
|
||||
[JsonPropertyName("requiredAction")]
|
||||
public string? RequiredAction { get; init; }
|
||||
|
||||
[JsonPropertyName("dueDate")]
|
||||
public string? DueDate { get; init; }
|
||||
|
||||
[JsonPropertyName("knownRansomwareCampaignUse")]
|
||||
public string? KnownRansomwareCampaignUse { get; init; }
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
[JsonPropertyName("cwes")]
|
||||
public IReadOnlyList<string> Cwes { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Bson;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal sealed record KevCursor(
|
||||
string? CatalogVersion,
|
||||
DateTimeOffset? CatalogReleased,
|
||||
IReadOnlyCollection<Guid> PendingDocuments,
|
||||
IReadOnlyCollection<Guid> PendingMappings)
|
||||
{
|
||||
public static KevCursor Empty { get; } = new(null, null, Array.Empty<Guid>(), Array.Empty<Guid>());
|
||||
|
||||
public BsonDocument ToBsonDocument()
|
||||
{
|
||||
var document = new BsonDocument
|
||||
{
|
||||
["pendingDocuments"] = new BsonArray(PendingDocuments.Select(static id => id.ToString())),
|
||||
["pendingMappings"] = new BsonArray(PendingMappings.Select(static id => id.ToString())),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(CatalogVersion))
|
||||
{
|
||||
document["catalogVersion"] = CatalogVersion;
|
||||
}
|
||||
|
||||
if (CatalogReleased.HasValue)
|
||||
{
|
||||
document["catalogReleased"] = CatalogReleased.Value.UtcDateTime;
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public static KevCursor FromBson(BsonDocument? document)
|
||||
{
|
||||
if (document is null || document.ElementCount == 0)
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var version = document.TryGetValue("catalogVersion", out var versionValue)
|
||||
? versionValue.AsString
|
||||
: null;
|
||||
|
||||
var released = document.TryGetValue("catalogReleased", out var releasedValue)
|
||||
? ParseDate(releasedValue)
|
||||
: null;
|
||||
|
||||
return new KevCursor(
|
||||
version,
|
||||
released,
|
||||
ReadGuidArray(document, "pendingDocuments"),
|
||||
ReadGuidArray(document, "pendingMappings"));
|
||||
}
|
||||
|
||||
public KevCursor WithCatalogMetadata(string? version, DateTimeOffset? released)
|
||||
=> this with
|
||||
{
|
||||
CatalogVersion = string.IsNullOrWhiteSpace(version) ? null : version.Trim(),
|
||||
CatalogReleased = released?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
public KevCursor WithPendingDocuments(IEnumerable<Guid> ids)
|
||||
=> this with { PendingDocuments = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
|
||||
|
||||
public KevCursor WithPendingMappings(IEnumerable<Guid> ids)
|
||||
=> this with { PendingMappings = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
|
||||
|
||||
private static DateTimeOffset? ParseDate(BsonValue value)
|
||||
=> value.BsonType switch
|
||||
{
|
||||
BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
|
||||
BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static IReadOnlyCollection<Guid> ReadGuidArray(BsonDocument document, string field)
|
||||
{
|
||||
if (!document.TryGetValue(field, out var value) || value is not BsonArray array)
|
||||
{
|
||||
return Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
var results = new List<Guid>(array.Count);
|
||||
foreach (var element in array)
|
||||
{
|
||||
if (element is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(element.ToString(), out var guid))
|
||||
{
|
||||
results.Add(guid);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Documents;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal sealed record KevCursor(
|
||||
string? CatalogVersion,
|
||||
DateTimeOffset? CatalogReleased,
|
||||
IReadOnlyCollection<Guid> PendingDocuments,
|
||||
IReadOnlyCollection<Guid> PendingMappings)
|
||||
{
|
||||
public static KevCursor Empty { get; } = new(null, null, Array.Empty<Guid>(), Array.Empty<Guid>());
|
||||
|
||||
public DocumentObject ToDocumentObject()
|
||||
{
|
||||
var document = new DocumentObject
|
||||
{
|
||||
["pendingDocuments"] = new DocumentArray(PendingDocuments.Select(static id => id.ToString())),
|
||||
["pendingMappings"] = new DocumentArray(PendingMappings.Select(static id => id.ToString())),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(CatalogVersion))
|
||||
{
|
||||
document["catalogVersion"] = CatalogVersion;
|
||||
}
|
||||
|
||||
if (CatalogReleased.HasValue)
|
||||
{
|
||||
document["catalogReleased"] = CatalogReleased.Value.UtcDateTime;
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public static KevCursor FromBson(DocumentObject? document)
|
||||
{
|
||||
if (document is null || document.ElementCount == 0)
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var version = document.TryGetValue("catalogVersion", out var versionValue)
|
||||
? versionValue.AsString
|
||||
: null;
|
||||
|
||||
var released = document.TryGetValue("catalogReleased", out var releasedValue)
|
||||
? ParseDate(releasedValue)
|
||||
: null;
|
||||
|
||||
return new KevCursor(
|
||||
version,
|
||||
released,
|
||||
ReadGuidArray(document, "pendingDocuments"),
|
||||
ReadGuidArray(document, "pendingMappings"));
|
||||
}
|
||||
|
||||
public KevCursor WithCatalogMetadata(string? version, DateTimeOffset? released)
|
||||
=> this with
|
||||
{
|
||||
CatalogVersion = string.IsNullOrWhiteSpace(version) ? null : version.Trim(),
|
||||
CatalogReleased = released?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
public KevCursor WithPendingDocuments(IEnumerable<Guid> ids)
|
||||
=> this with { PendingDocuments = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
|
||||
|
||||
public KevCursor WithPendingMappings(IEnumerable<Guid> ids)
|
||||
=> this with { PendingMappings = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
|
||||
|
||||
private static DateTimeOffset? ParseDate(DocumentValue value)
|
||||
=> value.DocumentType switch
|
||||
{
|
||||
DocumentType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
|
||||
DocumentType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static IReadOnlyCollection<Guid> ReadGuidArray(DocumentObject document, string field)
|
||||
{
|
||||
if (!document.TryGetValue(field, out var value) || value is not DocumentArray array)
|
||||
{
|
||||
return Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
var results = new List<Guid>(array.Count);
|
||||
foreach (var element in array)
|
||||
{
|
||||
if (element is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(element.ToString(), out var guid))
|
||||
{
|
||||
results.Add(guid);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
public sealed class KevDiagnostics : IDisposable
|
||||
{
|
||||
public const string MeterName = "StellaOps.Concelier.Connector.Kev";
|
||||
private const string MeterVersion = "1.0.0";
|
||||
|
||||
private readonly Meter _meter;
|
||||
private readonly Counter<long> _fetchAttempts;
|
||||
private readonly Counter<long> _fetchSuccess;
|
||||
private readonly Counter<long> _fetchFailures;
|
||||
private readonly Counter<long> _fetchUnchanged;
|
||||
private readonly Counter<long> _parsedEntries;
|
||||
private readonly Counter<long> _parseFailures;
|
||||
private readonly Counter<long> _parseAnomalies;
|
||||
private readonly Counter<long> _mappedAdvisories;
|
||||
|
||||
public KevDiagnostics()
|
||||
{
|
||||
_meter = new Meter(MeterName, MeterVersion);
|
||||
_fetchAttempts = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.attempts",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts performed.");
|
||||
_fetchSuccess = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.success",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts that produced new catalog content.");
|
||||
_fetchFailures = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.failures",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts that failed.");
|
||||
_fetchUnchanged = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.unchanged",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts returning HTTP 304 / unchanged catalog.");
|
||||
_parsedEntries = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.entries",
|
||||
unit: "entries",
|
||||
description: "Number of KEV vulnerabilities parsed from the catalog.");
|
||||
_parseFailures = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.failures",
|
||||
unit: "documents",
|
||||
description: "Number of KEV catalog parse operations that failed or were quarantined.");
|
||||
_parseAnomalies = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.anomalies",
|
||||
unit: "entries",
|
||||
description: "Number of KEV entries skipped or flagged during parsing due to anomalies.");
|
||||
_mappedAdvisories = _meter.CreateCounter<long>(
|
||||
name: "kev.map.advisories",
|
||||
unit: "advisories",
|
||||
description: "Number of KEV advisories emitted during mapping.");
|
||||
}
|
||||
|
||||
public void FetchAttempt() => _fetchAttempts.Add(1);
|
||||
|
||||
public void FetchSuccess() => _fetchSuccess.Add(1);
|
||||
|
||||
public void FetchFailure() => _fetchFailures.Add(1);
|
||||
|
||||
public void FetchUnchanged() => _fetchUnchanged.Add(1);
|
||||
|
||||
public void CatalogParsed(string? catalogVersion, int entryCount)
|
||||
{
|
||||
if (entryCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedEntries.Add(entryCount, new KeyValuePair<string, object?>("catalogVersion", catalogVersion ?? string.Empty));
|
||||
}
|
||||
|
||||
public void ParseFailure(string reason, string? catalogVersion = null)
|
||||
{
|
||||
var tags = string.IsNullOrWhiteSpace(catalogVersion)
|
||||
? new[] { new KeyValuePair<string, object?>("reason", reason) }
|
||||
: new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("reason", reason),
|
||||
new KeyValuePair<string, object?>("catalogVersion", catalogVersion)
|
||||
};
|
||||
|
||||
_parseFailures.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordAnomaly(string reason, string? catalogVersion = null)
|
||||
{
|
||||
var tags = string.IsNullOrWhiteSpace(catalogVersion)
|
||||
? new[] { new KeyValuePair<string, object?>("reason", reason) }
|
||||
: new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("reason", reason),
|
||||
new KeyValuePair<string, object?>("catalogVersion", catalogVersion)
|
||||
};
|
||||
|
||||
_parseAnomalies.Add(1, tags);
|
||||
}
|
||||
|
||||
public void AdvisoriesMapped(string? catalogVersion, int advisoryCount)
|
||||
{
|
||||
if (advisoryCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mappedAdvisories.Add(advisoryCount, new KeyValuePair<string, object?>("catalogVersion", catalogVersion ?? string.Empty));
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.Dispose();
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
public sealed class KevDiagnostics : IDisposable
|
||||
{
|
||||
public const string MeterName = "StellaOps.Concelier.Connector.Kev";
|
||||
private const string MeterVersion = "1.0.0";
|
||||
|
||||
private readonly Meter _meter;
|
||||
private readonly Counter<long> _fetchAttempts;
|
||||
private readonly Counter<long> _fetchSuccess;
|
||||
private readonly Counter<long> _fetchFailures;
|
||||
private readonly Counter<long> _fetchUnchanged;
|
||||
private readonly Counter<long> _parsedEntries;
|
||||
private readonly Counter<long> _parseFailures;
|
||||
private readonly Counter<long> _parseAnomalies;
|
||||
private readonly Counter<long> _mappedAdvisories;
|
||||
|
||||
public KevDiagnostics()
|
||||
{
|
||||
_meter = new Meter(MeterName, MeterVersion);
|
||||
_fetchAttempts = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.attempts",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts performed.");
|
||||
_fetchSuccess = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.success",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts that produced new catalog content.");
|
||||
_fetchFailures = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.failures",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts that failed.");
|
||||
_fetchUnchanged = _meter.CreateCounter<long>(
|
||||
name: "kev.fetch.unchanged",
|
||||
unit: "operations",
|
||||
description: "Number of KEV fetch attempts returning HTTP 304 / unchanged catalog.");
|
||||
_parsedEntries = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.entries",
|
||||
unit: "entries",
|
||||
description: "Number of KEV vulnerabilities parsed from the catalog.");
|
||||
_parseFailures = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.failures",
|
||||
unit: "documents",
|
||||
description: "Number of KEV catalog parse operations that failed or were quarantined.");
|
||||
_parseAnomalies = _meter.CreateCounter<long>(
|
||||
name: "kev.parse.anomalies",
|
||||
unit: "entries",
|
||||
description: "Number of KEV entries skipped or flagged during parsing due to anomalies.");
|
||||
_mappedAdvisories = _meter.CreateCounter<long>(
|
||||
name: "kev.map.advisories",
|
||||
unit: "advisories",
|
||||
description: "Number of KEV advisories emitted during mapping.");
|
||||
}
|
||||
|
||||
public void FetchAttempt() => _fetchAttempts.Add(1);
|
||||
|
||||
public void FetchSuccess() => _fetchSuccess.Add(1);
|
||||
|
||||
public void FetchFailure() => _fetchFailures.Add(1);
|
||||
|
||||
public void FetchUnchanged() => _fetchUnchanged.Add(1);
|
||||
|
||||
public void CatalogParsed(string? catalogVersion, int entryCount)
|
||||
{
|
||||
if (entryCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedEntries.Add(entryCount, new KeyValuePair<string, object?>("catalogVersion", catalogVersion ?? string.Empty));
|
||||
}
|
||||
|
||||
public void ParseFailure(string reason, string? catalogVersion = null)
|
||||
{
|
||||
var tags = string.IsNullOrWhiteSpace(catalogVersion)
|
||||
? new[] { new KeyValuePair<string, object?>("reason", reason) }
|
||||
: new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("reason", reason),
|
||||
new KeyValuePair<string, object?>("catalogVersion", catalogVersion)
|
||||
};
|
||||
|
||||
_parseFailures.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordAnomaly(string reason, string? catalogVersion = null)
|
||||
{
|
||||
var tags = string.IsNullOrWhiteSpace(catalogVersion)
|
||||
? new[] { new KeyValuePair<string, object?>("reason", reason) }
|
||||
: new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("reason", reason),
|
||||
new KeyValuePair<string, object?>("catalogVersion", catalogVersion)
|
||||
};
|
||||
|
||||
_parseAnomalies.Add(1, tags);
|
||||
}
|
||||
|
||||
public void AdvisoriesMapped(string? catalogVersion, int advisoryCount)
|
||||
{
|
||||
if (advisoryCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mappedAdvisories.Add(advisoryCount, new KeyValuePair<string, object?>("catalogVersion", catalogVersion ?? string.Empty));
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.Dispose();
|
||||
}
|
||||
|
||||
@@ -1,373 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal static class KevMapper
|
||||
{
|
||||
public static IReadOnlyList<Advisory> Map(
|
||||
KevCatalogDto catalog,
|
||||
string sourceName,
|
||||
Uri feedUri,
|
||||
DateTimeOffset fetchedAt,
|
||||
DateTimeOffset validatedAt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(sourceName);
|
||||
ArgumentNullException.ThrowIfNull(feedUri);
|
||||
|
||||
var advisories = new List<Advisory>();
|
||||
var fetchProvenance = new AdvisoryProvenance(sourceName, "document", feedUri.ToString(), fetchedAt);
|
||||
var mappingProvenance = new AdvisoryProvenance(
|
||||
sourceName,
|
||||
"mapping",
|
||||
catalog.CatalogVersion ?? feedUri.ToString(),
|
||||
validatedAt);
|
||||
|
||||
if (catalog.Vulnerabilities is null || catalog.Vulnerabilities.Count == 0)
|
||||
{
|
||||
return advisories;
|
||||
}
|
||||
|
||||
foreach (var entry in catalog.Vulnerabilities)
|
||||
{
|
||||
if (entry is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cveId = Normalize(entry.CveId);
|
||||
if (string.IsNullOrEmpty(cveId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var advisoryKey = $"kev/{cveId.ToLowerInvariant()}";
|
||||
var title = Normalize(entry.VulnerabilityName) ?? cveId;
|
||||
var summary = Normalize(entry.ShortDescription);
|
||||
var published = ParseDate(entry.DateAdded);
|
||||
var dueDate = ParseDate(entry.DueDate);
|
||||
|
||||
var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { cveId };
|
||||
|
||||
var references = BuildReferences(entry, sourceName, mappingProvenance, feedUri, cveId).ToArray();
|
||||
|
||||
var affectedPackages = BuildAffectedPackages(
|
||||
entry,
|
||||
catalog,
|
||||
sourceName,
|
||||
mappingProvenance,
|
||||
published,
|
||||
dueDate).ToArray();
|
||||
|
||||
var provenance = new[]
|
||||
{
|
||||
fetchProvenance,
|
||||
mappingProvenance
|
||||
};
|
||||
|
||||
advisories.Add(new Advisory(
|
||||
advisoryKey,
|
||||
title,
|
||||
summary,
|
||||
language: "en",
|
||||
published,
|
||||
modified: catalog.DateReleased?.ToUniversalTime(),
|
||||
severity: null,
|
||||
exploitKnown: true,
|
||||
aliases,
|
||||
references,
|
||||
affectedPackages,
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance));
|
||||
}
|
||||
|
||||
return advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AdvisoryReference> BuildReferences(
|
||||
KevVulnerabilityDto entry,
|
||||
string sourceName,
|
||||
AdvisoryProvenance mappingProvenance,
|
||||
Uri feedUri,
|
||||
string cveId)
|
||||
{
|
||||
var references = new List<AdvisoryReference>();
|
||||
var provenance = new AdvisoryProvenance(sourceName, "reference", cveId, mappingProvenance.RecordedAt);
|
||||
|
||||
var catalogUrl = BuildCatalogSearchUrl(cveId);
|
||||
if (catalogUrl is not null)
|
||||
{
|
||||
TryAddReference(references, catalogUrl, "advisory", "cisa-kev", provenance);
|
||||
}
|
||||
|
||||
TryAddReference(references, feedUri.ToString(), "reference", "cisa-kev-feed", provenance);
|
||||
|
||||
foreach (var url in ExtractUrls(entry.Notes))
|
||||
{
|
||||
TryAddReference(references, url, "reference", "kev.notes", provenance);
|
||||
}
|
||||
|
||||
return references
|
||||
.GroupBy(static r => r.Url, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(static group => group
|
||||
.OrderBy(static r => r.Kind, StringComparer.Ordinal)
|
||||
.ThenBy(static r => r.SourceTag, StringComparer.Ordinal)
|
||||
.First())
|
||||
.OrderBy(static r => r.Kind, StringComparer.Ordinal)
|
||||
.ThenBy(static r => r.Url, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static void TryAddReference(
|
||||
ICollection<AdvisoryReference> references,
|
||||
string? url,
|
||||
string kind,
|
||||
string? sourceTag,
|
||||
AdvisoryProvenance provenance)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed)
|
||||
|| (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
references.Add(new AdvisoryReference(parsed.ToString(), kind, sourceTag, null, provenance));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Ignore invalid references while leaving traceability via diagnostics elsewhere.
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BuildCatalogSearchUrl(string cveId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cveId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder("https://www.cisa.gov/known-exploited-vulnerabilities-catalog?search=");
|
||||
builder.Append(Uri.EscapeDataString(cveId));
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static IEnumerable<AffectedPackage> BuildAffectedPackages(
|
||||
KevVulnerabilityDto entry,
|
||||
KevCatalogDto catalog,
|
||||
string sourceName,
|
||||
AdvisoryProvenance mappingProvenance,
|
||||
DateTimeOffset? published,
|
||||
DateTimeOffset? dueDate)
|
||||
{
|
||||
var identifier = BuildIdentifier(entry) ?? entry.CveId ?? "kev";
|
||||
var rangeExtensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void TryAddExtension(string key, string? value, int maxLength = 512)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.Length > maxLength)
|
||||
{
|
||||
trimmed = trimmed[..maxLength].Trim();
|
||||
}
|
||||
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
rangeExtensions[key] = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
TryAddExtension("kev.vendorProject", entry.VendorProject, 256);
|
||||
TryAddExtension("kev.product", entry.Product, 256);
|
||||
TryAddExtension("kev.requiredAction", entry.RequiredAction);
|
||||
TryAddExtension("kev.knownRansomwareCampaignUse", entry.KnownRansomwareCampaignUse, 64);
|
||||
TryAddExtension("kev.notes", entry.Notes);
|
||||
TryAddExtension("kev.catalogVersion", catalog.CatalogVersion, 64);
|
||||
|
||||
if (catalog.DateReleased.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.catalogReleased", catalog.DateReleased.Value.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (published.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.dateAdded", published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (dueDate.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.dueDate", dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (entry.Cwes is { Count: > 0 })
|
||||
{
|
||||
TryAddExtension("kev.cwe", string.Join(",", entry.Cwes.Where(static cwe => !string.IsNullOrWhiteSpace(cwe)).OrderBy(static cwe => cwe, StringComparer.Ordinal)));
|
||||
}
|
||||
|
||||
if (rangeExtensions.Count == 0)
|
||||
{
|
||||
return Array.Empty<AffectedPackage>();
|
||||
}
|
||||
|
||||
var rangeProvenance = new AdvisoryProvenance(sourceName, "kev-range", identifier, mappingProvenance.RecordedAt);
|
||||
var range = new AffectedVersionRange(
|
||||
rangeKind: AffectedPackageTypes.Vendor,
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: null,
|
||||
provenance: rangeProvenance,
|
||||
primitives: new RangePrimitives(null, null, null, rangeExtensions));
|
||||
|
||||
var normalizedVersions = BuildNormalizedVersions(identifier, catalog, published, dueDate);
|
||||
|
||||
var affectedPackage = new AffectedPackage(
|
||||
AffectedPackageTypes.Vendor,
|
||||
identifier,
|
||||
platform: null,
|
||||
versionRanges: new[] { range },
|
||||
statuses: Array.Empty<AffectedPackageStatus>(),
|
||||
provenance: new[] { mappingProvenance },
|
||||
normalizedVersions: normalizedVersions);
|
||||
|
||||
return new[] { affectedPackage };
|
||||
}
|
||||
|
||||
private static string? BuildIdentifier(KevVulnerabilityDto entry)
|
||||
{
|
||||
var vendor = Normalize(entry.VendorProject);
|
||||
var product = Normalize(entry.Product);
|
||||
|
||||
if (!string.IsNullOrEmpty(vendor) && !string.IsNullOrEmpty(product))
|
||||
{
|
||||
return $"{vendor}::{product}";
|
||||
}
|
||||
|
||||
return vendor ?? product;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExtractUrls(string? notes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(notes))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var tokens = notes.Split(new[] { ';', ',', ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var results = new List<string>();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var trimmed = token.Trim().TrimEnd('.', ')', ';', ',');
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
results.Add(uri.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return results.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: results.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(static value => value, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return trimmed.Length == 0 ? null : trimmed;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDate(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
|
||||
{
|
||||
return parsed.ToUniversalTime();
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date))
|
||||
{
|
||||
return new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
|
||||
string identifier,
|
||||
KevCatalogDto catalog,
|
||||
DateTimeOffset? published,
|
||||
DateTimeOffset? dueDate)
|
||||
{
|
||||
var rules = new List<NormalizedVersionRule>();
|
||||
var notes = Validation.TrimToNull(identifier);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(catalog.CatalogVersion))
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.catalog",
|
||||
type: NormalizedVersionRuleTypes.Exact,
|
||||
value: catalog.CatalogVersion.Trim(),
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
if (published.HasValue)
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.date-added",
|
||||
type: NormalizedVersionRuleTypes.Exact,
|
||||
value: published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
if (dueDate.HasValue)
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.due-date",
|
||||
type: NormalizedVersionRuleTypes.LessThanOrEqual,
|
||||
max: dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
maxInclusive: true,
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
return rules.Count == 0
|
||||
? Array.Empty<NormalizedVersionRule>()
|
||||
: rules
|
||||
.OrderBy(static rule => rule.Scheme, StringComparer.Ordinal)
|
||||
.ThenBy(static rule => rule.Type, StringComparer.Ordinal)
|
||||
.ThenBy(static rule => rule.Value ?? rule.Max ?? string.Empty, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal static class KevMapper
|
||||
{
|
||||
public static IReadOnlyList<Advisory> Map(
|
||||
KevCatalogDto catalog,
|
||||
string sourceName,
|
||||
Uri feedUri,
|
||||
DateTimeOffset fetchedAt,
|
||||
DateTimeOffset validatedAt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(sourceName);
|
||||
ArgumentNullException.ThrowIfNull(feedUri);
|
||||
|
||||
var advisories = new List<Advisory>();
|
||||
var fetchProvenance = new AdvisoryProvenance(sourceName, "document", feedUri.ToString(), fetchedAt);
|
||||
var mappingProvenance = new AdvisoryProvenance(
|
||||
sourceName,
|
||||
"mapping",
|
||||
catalog.CatalogVersion ?? feedUri.ToString(),
|
||||
validatedAt);
|
||||
|
||||
if (catalog.Vulnerabilities is null || catalog.Vulnerabilities.Count == 0)
|
||||
{
|
||||
return advisories;
|
||||
}
|
||||
|
||||
foreach (var entry in catalog.Vulnerabilities)
|
||||
{
|
||||
if (entry is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cveId = Normalize(entry.CveId);
|
||||
if (string.IsNullOrEmpty(cveId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var advisoryKey = $"kev/{cveId.ToLowerInvariant()}";
|
||||
var title = Normalize(entry.VulnerabilityName) ?? cveId;
|
||||
var summary = Normalize(entry.ShortDescription);
|
||||
var published = ParseDate(entry.DateAdded);
|
||||
var dueDate = ParseDate(entry.DueDate);
|
||||
|
||||
var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { cveId };
|
||||
|
||||
var references = BuildReferences(entry, sourceName, mappingProvenance, feedUri, cveId).ToArray();
|
||||
|
||||
var affectedPackages = BuildAffectedPackages(
|
||||
entry,
|
||||
catalog,
|
||||
sourceName,
|
||||
mappingProvenance,
|
||||
published,
|
||||
dueDate).ToArray();
|
||||
|
||||
var provenance = new[]
|
||||
{
|
||||
fetchProvenance,
|
||||
mappingProvenance
|
||||
};
|
||||
|
||||
advisories.Add(new Advisory(
|
||||
advisoryKey,
|
||||
title,
|
||||
summary,
|
||||
language: "en",
|
||||
published,
|
||||
modified: catalog.DateReleased?.ToUniversalTime(),
|
||||
severity: null,
|
||||
exploitKnown: true,
|
||||
aliases,
|
||||
references,
|
||||
affectedPackages,
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance));
|
||||
}
|
||||
|
||||
return advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AdvisoryReference> BuildReferences(
|
||||
KevVulnerabilityDto entry,
|
||||
string sourceName,
|
||||
AdvisoryProvenance mappingProvenance,
|
||||
Uri feedUri,
|
||||
string cveId)
|
||||
{
|
||||
var references = new List<AdvisoryReference>();
|
||||
var provenance = new AdvisoryProvenance(sourceName, "reference", cveId, mappingProvenance.RecordedAt);
|
||||
|
||||
var catalogUrl = BuildCatalogSearchUrl(cveId);
|
||||
if (catalogUrl is not null)
|
||||
{
|
||||
TryAddReference(references, catalogUrl, "advisory", "cisa-kev", provenance);
|
||||
}
|
||||
|
||||
TryAddReference(references, feedUri.ToString(), "reference", "cisa-kev-feed", provenance);
|
||||
|
||||
foreach (var url in ExtractUrls(entry.Notes))
|
||||
{
|
||||
TryAddReference(references, url, "reference", "kev.notes", provenance);
|
||||
}
|
||||
|
||||
return references
|
||||
.GroupBy(static r => r.Url, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(static group => group
|
||||
.OrderBy(static r => r.Kind, StringComparer.Ordinal)
|
||||
.ThenBy(static r => r.SourceTag, StringComparer.Ordinal)
|
||||
.First())
|
||||
.OrderBy(static r => r.Kind, StringComparer.Ordinal)
|
||||
.ThenBy(static r => r.Url, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static void TryAddReference(
|
||||
ICollection<AdvisoryReference> references,
|
||||
string? url,
|
||||
string kind,
|
||||
string? sourceTag,
|
||||
AdvisoryProvenance provenance)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed)
|
||||
|| (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
references.Add(new AdvisoryReference(parsed.ToString(), kind, sourceTag, null, provenance));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Ignore invalid references while leaving traceability via diagnostics elsewhere.
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BuildCatalogSearchUrl(string cveId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cveId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder("https://www.cisa.gov/known-exploited-vulnerabilities-catalog?search=");
|
||||
builder.Append(Uri.EscapeDataString(cveId));
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static IEnumerable<AffectedPackage> BuildAffectedPackages(
|
||||
KevVulnerabilityDto entry,
|
||||
KevCatalogDto catalog,
|
||||
string sourceName,
|
||||
AdvisoryProvenance mappingProvenance,
|
||||
DateTimeOffset? published,
|
||||
DateTimeOffset? dueDate)
|
||||
{
|
||||
var identifier = BuildIdentifier(entry) ?? entry.CveId ?? "kev";
|
||||
var rangeExtensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void TryAddExtension(string key, string? value, int maxLength = 512)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.Length > maxLength)
|
||||
{
|
||||
trimmed = trimmed[..maxLength].Trim();
|
||||
}
|
||||
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
rangeExtensions[key] = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
TryAddExtension("kev.vendorProject", entry.VendorProject, 256);
|
||||
TryAddExtension("kev.product", entry.Product, 256);
|
||||
TryAddExtension("kev.requiredAction", entry.RequiredAction);
|
||||
TryAddExtension("kev.knownRansomwareCampaignUse", entry.KnownRansomwareCampaignUse, 64);
|
||||
TryAddExtension("kev.notes", entry.Notes);
|
||||
TryAddExtension("kev.catalogVersion", catalog.CatalogVersion, 64);
|
||||
|
||||
if (catalog.DateReleased.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.catalogReleased", catalog.DateReleased.Value.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (published.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.dateAdded", published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (dueDate.HasValue)
|
||||
{
|
||||
TryAddExtension("kev.dueDate", dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (entry.Cwes is { Count: > 0 })
|
||||
{
|
||||
TryAddExtension("kev.cwe", string.Join(",", entry.Cwes.Where(static cwe => !string.IsNullOrWhiteSpace(cwe)).OrderBy(static cwe => cwe, StringComparer.Ordinal)));
|
||||
}
|
||||
|
||||
if (rangeExtensions.Count == 0)
|
||||
{
|
||||
return Array.Empty<AffectedPackage>();
|
||||
}
|
||||
|
||||
var rangeProvenance = new AdvisoryProvenance(sourceName, "kev-range", identifier, mappingProvenance.RecordedAt);
|
||||
var range = new AffectedVersionRange(
|
||||
rangeKind: AffectedPackageTypes.Vendor,
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: null,
|
||||
provenance: rangeProvenance,
|
||||
primitives: new RangePrimitives(null, null, null, rangeExtensions));
|
||||
|
||||
var normalizedVersions = BuildNormalizedVersions(identifier, catalog, published, dueDate);
|
||||
|
||||
var affectedPackage = new AffectedPackage(
|
||||
AffectedPackageTypes.Vendor,
|
||||
identifier,
|
||||
platform: null,
|
||||
versionRanges: new[] { range },
|
||||
statuses: Array.Empty<AffectedPackageStatus>(),
|
||||
provenance: new[] { mappingProvenance },
|
||||
normalizedVersions: normalizedVersions);
|
||||
|
||||
return new[] { affectedPackage };
|
||||
}
|
||||
|
||||
private static string? BuildIdentifier(KevVulnerabilityDto entry)
|
||||
{
|
||||
var vendor = Normalize(entry.VendorProject);
|
||||
var product = Normalize(entry.Product);
|
||||
|
||||
if (!string.IsNullOrEmpty(vendor) && !string.IsNullOrEmpty(product))
|
||||
{
|
||||
return $"{vendor}::{product}";
|
||||
}
|
||||
|
||||
return vendor ?? product;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExtractUrls(string? notes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(notes))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var tokens = notes.Split(new[] { ';', ',', ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var results = new List<string>();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var trimmed = token.Trim().TrimEnd('.', ')', ';', ',');
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
results.Add(uri.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return results.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: results.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(static value => value, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return trimmed.Length == 0 ? null : trimmed;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDate(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
|
||||
{
|
||||
return parsed.ToUniversalTime();
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date))
|
||||
{
|
||||
return new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
|
||||
string identifier,
|
||||
KevCatalogDto catalog,
|
||||
DateTimeOffset? published,
|
||||
DateTimeOffset? dueDate)
|
||||
{
|
||||
var rules = new List<NormalizedVersionRule>();
|
||||
var notes = Validation.TrimToNull(identifier);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(catalog.CatalogVersion))
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.catalog",
|
||||
type: NormalizedVersionRuleTypes.Exact,
|
||||
value: catalog.CatalogVersion.Trim(),
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
if (published.HasValue)
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.date-added",
|
||||
type: NormalizedVersionRuleTypes.Exact,
|
||||
value: published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
if (dueDate.HasValue)
|
||||
{
|
||||
rules.Add(new NormalizedVersionRule(
|
||||
scheme: "kev.due-date",
|
||||
type: NormalizedVersionRuleTypes.LessThanOrEqual,
|
||||
max: dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
maxInclusive: true,
|
||||
notes: notes));
|
||||
}
|
||||
|
||||
return rules.Count == 0
|
||||
? Array.Empty<NormalizedVersionRule>()
|
||||
: rules
|
||||
.OrderBy(static rule => rule.Scheme, StringComparer.Ordinal)
|
||||
.ThenBy(static rule => rule.Type, StringComparer.Ordinal)
|
||||
.ThenBy(static rule => rule.Value ?? rule.Max ?? string.Empty, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Json.Schema;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal static class KevSchemaProvider
|
||||
{
|
||||
private const string ResourceName = "StellaOps.Concelier.Connector.Kev.Schemas.kev-catalog.schema.json";
|
||||
|
||||
private static readonly Lazy<JsonSchema> CachedSchema = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
public static JsonSchema Schema => CachedSchema.Value;
|
||||
|
||||
private static JsonSchema LoadSchema()
|
||||
{
|
||||
var assembly = typeof(KevSchemaProvider).GetTypeInfo().Assembly;
|
||||
using var stream = assembly.GetManifestResourceStream(ResourceName)
|
||||
?? throw new InvalidOperationException($"Embedded schema '{ResourceName}' was not found.");
|
||||
using var reader = new StreamReader(stream);
|
||||
var schemaJson = reader.ReadToEnd();
|
||||
return JsonSchema.FromText(schemaJson);
|
||||
}
|
||||
}
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Json.Schema;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
internal static class KevSchemaProvider
|
||||
{
|
||||
private const string ResourceName = "StellaOps.Concelier.Connector.Kev.Schemas.kev-catalog.schema.json";
|
||||
|
||||
private static readonly Lazy<JsonSchema> CachedSchema = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
public static JsonSchema Schema => CachedSchema.Value;
|
||||
|
||||
private static JsonSchema LoadSchema()
|
||||
{
|
||||
var assembly = typeof(KevSchemaProvider).GetTypeInfo().Assembly;
|
||||
using var stream = assembly.GetManifestResourceStream(ResourceName)
|
||||
?? throw new InvalidOperationException($"Embedded schema '{ResourceName}' was not found.");
|
||||
using var reader = new StreamReader(stream);
|
||||
var schemaJson = reader.ReadToEnd();
|
||||
return JsonSchema.FromText(schemaJson);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
internal static class KevJobKinds
|
||||
{
|
||||
public const string Fetch = "source:kev:fetch";
|
||||
public const string Parse = "source:kev:parse";
|
||||
public const string Map = "source:kev:map";
|
||||
}
|
||||
|
||||
internal sealed class KevFetchJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevFetchJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.FetchAsync(context.Services, cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class KevParseJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevParseJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.ParseAsync(context.Services, cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class KevMapJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevMapJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.MapAsync(context.Services, cancellationToken);
|
||||
}
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
internal static class KevJobKinds
|
||||
{
|
||||
public const string Fetch = "source:kev:fetch";
|
||||
public const string Parse = "source:kev:parse";
|
||||
public const string Map = "source:kev:map";
|
||||
}
|
||||
|
||||
internal sealed class KevFetchJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevFetchJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.FetchAsync(context.Services, cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class KevParseJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevParseJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.ParseAsync(context.Services, cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class KevMapJob : IJob
|
||||
{
|
||||
private readonly KevConnector _connector;
|
||||
|
||||
public KevMapJob(KevConnector connector)
|
||||
=> _connector = connector ?? throw new ArgumentNullException(nameof(connector));
|
||||
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
=> _connector.MapAsync(context.Services, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
@@ -262,7 +262,7 @@ public sealed class KevConnector : IFeedConnector
|
||||
try
|
||||
{
|
||||
var payloadJson = JsonSerializer.Serialize(catalog, SerializerOptions);
|
||||
var payload = BsonDocument.Parse(payloadJson);
|
||||
var payload = DocumentObject.Parse(payloadJson);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Parsed KEV catalog document {DocumentId} (version={CatalogVersion}, released={Released}, entries={EntryCount})",
|
||||
@@ -334,9 +334,9 @@ public sealed class KevConnector : IFeedConnector
|
||||
KevCatalogDto? catalog;
|
||||
try
|
||||
{
|
||||
var dtoJson = dtoRecord.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings
|
||||
var dtoJson = dtoRecord.Payload.ToJson(new StellaOps.Concelier.Documents.IO.JsonWriterSettings
|
||||
{
|
||||
OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson,
|
||||
OutputMode = StellaOps.Concelier.Documents.IO.JsonOutputMode.RelaxedExtendedJson,
|
||||
});
|
||||
|
||||
catalog = JsonSerializer.Deserialize<KevCatalogDto>(dtoJson, SerializerOptions);
|
||||
@@ -391,7 +391,7 @@ public sealed class KevConnector : IFeedConnector
|
||||
|
||||
private Task UpdateCursorAsync(KevCursor cursor, CancellationToken cancellationToken)
|
||||
{
|
||||
return _stateRepository.UpdateCursorAsync(SourceName, cursor.ToBsonDocument(), _timeProvider.GetUtcNow(), cancellationToken);
|
||||
return _stateRepository.UpdateCursorAsync(SourceName, cursor.ToDocumentObject(), _timeProvider.GetUtcNow(), cancellationToken);
|
||||
}
|
||||
|
||||
private void RecordCatalogAnomalies(KevCatalogDto catalog)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Plugin;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public sealed class KevConnectorPlugin : IConnectorPlugin
|
||||
{
|
||||
public const string SourceName = "kev";
|
||||
|
||||
public string Name => SourceName;
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||
|
||||
public IFeedConnector Create(IServiceProvider services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
return ActivatorUtilities.CreateInstance<KevConnector>(services);
|
||||
}
|
||||
}
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Plugin;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public sealed class KevConnectorPlugin : IConnectorPlugin
|
||||
{
|
||||
public const string SourceName = "kev";
|
||||
|
||||
public string Name => SourceName;
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||
|
||||
public IFeedConnector Create(IServiceProvider services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
return ActivatorUtilities.CreateInstance<KevConnector>(services);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public sealed class KevDependencyInjectionRoutine : IDependencyInjectionRoutine
|
||||
{
|
||||
private const string ConfigurationSection = "concelier:sources:kev";
|
||||
|
||||
public IServiceCollection Register(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddKevConnector(options =>
|
||||
{
|
||||
configuration.GetSection(ConfigurationSection).Bind(options);
|
||||
options.Validate();
|
||||
});
|
||||
|
||||
services.AddTransient<KevFetchJob>();
|
||||
services.AddTransient<KevParseJob>();
|
||||
services.AddTransient<KevMapJob>();
|
||||
|
||||
services.PostConfigure<JobSchedulerOptions>(options =>
|
||||
{
|
||||
EnsureJob(options, KevJobKinds.Fetch, typeof(KevFetchJob));
|
||||
EnsureJob(options, KevJobKinds.Parse, typeof(KevParseJob));
|
||||
EnsureJob(options, KevJobKinds.Map, typeof(KevMapJob));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void EnsureJob(JobSchedulerOptions options, string kind, Type jobType)
|
||||
{
|
||||
if (options.Definitions.ContainsKey(kind))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.Definitions[kind] = new JobDefinition(
|
||||
kind,
|
||||
jobType,
|
||||
options.DefaultTimeout,
|
||||
options.DefaultLeaseDuration,
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public sealed class KevDependencyInjectionRoutine : IDependencyInjectionRoutine
|
||||
{
|
||||
private const string ConfigurationSection = "concelier:sources:kev";
|
||||
|
||||
public IServiceCollection Register(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddKevConnector(options =>
|
||||
{
|
||||
configuration.GetSection(ConfigurationSection).Bind(options);
|
||||
options.Validate();
|
||||
});
|
||||
|
||||
services.AddTransient<KevFetchJob>();
|
||||
services.AddTransient<KevParseJob>();
|
||||
services.AddTransient<KevMapJob>();
|
||||
|
||||
services.PostConfigure<JobSchedulerOptions>(options =>
|
||||
{
|
||||
EnsureJob(options, KevJobKinds.Fetch, typeof(KevFetchJob));
|
||||
EnsureJob(options, KevJobKinds.Parse, typeof(KevParseJob));
|
||||
EnsureJob(options, KevJobKinds.Map, typeof(KevMapJob));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void EnsureJob(JobSchedulerOptions options, string kind, Type jobType)
|
||||
{
|
||||
if (options.Definitions.ContainsKey(kind))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.Definitions[kind] = new JobDefinition(
|
||||
kind,
|
||||
jobType,
|
||||
options.DefaultTimeout,
|
||||
options.DefaultLeaseDuration,
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
using StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public static class KevServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddKevConnector(this IServiceCollection services, Action<KevOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
services.AddOptions<KevOptions>()
|
||||
.Configure(configure)
|
||||
.PostConfigure(static options => options.Validate());
|
||||
|
||||
services.AddSourceHttpClient(KevOptions.HttpClientName, (provider, clientOptions) =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<IOptions<KevOptions>>().Value;
|
||||
clientOptions.BaseAddress = opts.FeedUri;
|
||||
clientOptions.Timeout = opts.RequestTimeout;
|
||||
clientOptions.UserAgent = "StellaOps.Concelier.Kev/1.0";
|
||||
clientOptions.AllowedHosts.Clear();
|
||||
clientOptions.AllowedHosts.Add(opts.FeedUri.Host);
|
||||
clientOptions.DefaultRequestHeaders["Accept"] = "application/json";
|
||||
});
|
||||
|
||||
services.TryAddSingleton<KevDiagnostics>();
|
||||
services.AddTransient<KevConnector>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
using StellaOps.Concelier.Connector.Kev.Internal;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev;
|
||||
|
||||
public static class KevServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddKevConnector(this IServiceCollection services, Action<KevOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
services.AddOptions<KevOptions>()
|
||||
.Configure(configure)
|
||||
.PostConfigure(static options => options.Validate());
|
||||
|
||||
services.AddSourceHttpClient(KevOptions.HttpClientName, (provider, clientOptions) =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<IOptions<KevOptions>>().Value;
|
||||
clientOptions.BaseAddress = opts.FeedUri;
|
||||
clientOptions.Timeout = opts.RequestTimeout;
|
||||
clientOptions.UserAgent = "StellaOps.Concelier.Kev/1.0";
|
||||
clientOptions.AllowedHosts.Clear();
|
||||
clientOptions.AllowedHosts.Add(opts.FeedUri.Host);
|
||||
clientOptions.DefaultRequestHeaders["Accept"] = "application/json";
|
||||
});
|
||||
|
||||
services.TryAddSingleton<KevDiagnostics>();
|
||||
services.AddTransient<KevConnector>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user