save progress
This commit is contained in:
@@ -6,7 +6,13 @@ public sealed class GhsaOptions
|
||||
{
|
||||
public static string HttpClientName => "source.ghsa";
|
||||
|
||||
public Uri BaseEndpoint { get; set; } = new("https://api.github.com/", UriKind.Absolute);
|
||||
private Uri _baseEndpoint = new("https://api.github.com/", UriKind.Absolute);
|
||||
|
||||
public Uri BaseEndpoint
|
||||
{
|
||||
get => _baseEndpoint;
|
||||
set => _baseEndpoint = EnsureHttps(value);
|
||||
}
|
||||
|
||||
public string ApiToken { get; set; } = string.Empty;
|
||||
|
||||
@@ -72,4 +78,29 @@ public sealed class GhsaOptions
|
||||
throw new InvalidOperationException("SecondaryRateLimitBackoff must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Uri EnsureHttps(Uri? endpoint)
|
||||
{
|
||||
if (endpoint is null)
|
||||
{
|
||||
return new Uri("https://api.github.com/", UriKind.Absolute);
|
||||
}
|
||||
|
||||
if (!endpoint.IsAbsoluteUri)
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
if (string.Equals(endpoint.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var builder = new UriBuilder(endpoint)
|
||||
{
|
||||
Scheme = Uri.UriSchemeHttps,
|
||||
Port = -1
|
||||
};
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory job store for development and tests when no persistent store is configured.
|
||||
/// </summary>
|
||||
public sealed class InMemoryJobStore : IJobStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, JobRunSnapshot> _runs = new();
|
||||
|
||||
public Task<JobRunSnapshot> CreateAsync(JobRunCreateRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var run = new JobRunSnapshot(
|
||||
Guid.NewGuid(),
|
||||
request.Kind,
|
||||
JobRunStatus.Pending,
|
||||
request.CreatedAt,
|
||||
null,
|
||||
null,
|
||||
request.Trigger,
|
||||
request.ParametersHash,
|
||||
null,
|
||||
request.Timeout,
|
||||
request.LeaseDuration,
|
||||
request.Parameters);
|
||||
|
||||
_runs[run.RunId] = run;
|
||||
return Task.FromResult(run);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryStartAsync(Guid runId, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_runs.TryGetValue(runId, out var run))
|
||||
{
|
||||
var updated = run with { Status = JobRunStatus.Running, StartedAt = startedAt };
|
||||
_runs[runId] = updated;
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryCompleteAsync(Guid runId, JobRunCompletion completion, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_runs.TryGetValue(runId, out var run))
|
||||
{
|
||||
var updated = run with
|
||||
{
|
||||
Status = completion.Status,
|
||||
CompletedAt = completion.CompletedAt,
|
||||
Error = completion.Error
|
||||
};
|
||||
_runs[runId] = updated;
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> FindAsync(Guid runId, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(_runs.TryGetValue(runId, out var run) ? run : null);
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _runs.Values.AsEnumerable();
|
||||
if (!string.IsNullOrWhiteSpace(kind))
|
||||
{
|
||||
query = query.Where(r => string.Equals(r.Kind, kind, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
var list = query
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(list);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = _runs.Values
|
||||
.Where(r => r.Status is JobRunStatus.Pending or JobRunStatus.Running)
|
||||
.ToArray();
|
||||
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(list);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken)
|
||||
{
|
||||
var run = _runs.Values
|
||||
.Where(r => string.Equals(r.Kind, kind, StringComparison.Ordinal))
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult<JobRunSnapshot?>(run);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new Dictionary<string, JobRunSnapshot>(StringComparer.Ordinal);
|
||||
foreach (var kind in kinds.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var run = _runs.Values
|
||||
.Where(r => string.Equals(r.Kind, kind, StringComparison.Ordinal))
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (run is not null)
|
||||
{
|
||||
results[kind] = run;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(results);
|
||||
}
|
||||
}
|
||||
@@ -233,5 +233,6 @@ public sealed record Advisory
|
||||
/// Semantic merge hash for provenance-scoped deduplication.
|
||||
/// Nullable during migration; computed from (CVE + PURL + version-range + CWE + patch-lineage).
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? MergeHash { get; }
|
||||
}
|
||||
|
||||
@@ -269,16 +269,18 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
|
||||
}
|
||||
}
|
||||
|
||||
var normalizedVersions = BuildNormalizedVersions(versionRanges);
|
||||
var (platform, normalizedVersions) = ReadDatabaseSpecific(a.DatabaseSpecific);
|
||||
var effectivePlatform = platform ?? ResolvePlatformFromRanges(versionRanges);
|
||||
var resolvedNormalizedVersions = normalizedVersions ?? BuildNormalizedVersions(versionRanges);
|
||||
|
||||
return new AffectedPackage(
|
||||
MapEcosystemToType(a.Ecosystem),
|
||||
a.PackageName,
|
||||
null,
|
||||
effectivePlatform,
|
||||
versionRanges,
|
||||
Array.Empty<AffectedPackageStatus>(),
|
||||
Array.Empty<AdvisoryProvenance>(),
|
||||
normalizedVersions);
|
||||
resolvedNormalizedVersions);
|
||||
}).ToArray();
|
||||
|
||||
// Parse provenance if available
|
||||
@@ -391,7 +393,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
|
||||
"pub" => "semver",
|
||||
"rpm" => "rpm",
|
||||
"deb" => "deb",
|
||||
"apk" => "semver",
|
||||
"apk" => "apk",
|
||||
"cpe" => "cpe",
|
||||
"vendor" => "vendor",
|
||||
"ics" => "ics-vendor",
|
||||
@@ -399,4 +401,75 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
|
||||
_ => "semver"
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Platform, IReadOnlyList<NormalizedVersionRule>? NormalizedVersions) ReadDatabaseSpecific(string? databaseSpecific)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(databaseSpecific) || databaseSpecific == "{}")
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(databaseSpecific);
|
||||
var root = document.RootElement;
|
||||
|
||||
string? platform = null;
|
||||
if (root.TryGetProperty("platform", out var platformValue) && platformValue.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
platform = platformValue.GetString();
|
||||
}
|
||||
|
||||
IReadOnlyList<NormalizedVersionRule>? normalizedVersions = null;
|
||||
if (root.TryGetProperty("normalizedVersions", out var normalizedValue) && normalizedValue.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
normalizedVersions = JsonSerializer.Deserialize<NormalizedVersionRule[]>(normalizedValue.GetRawText(), JsonOptions);
|
||||
}
|
||||
|
||||
return (platform, normalizedVersions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolvePlatformFromRanges(IEnumerable<AffectedVersionRange> ranges)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var extensions = range.Primitives?.VendorExtensions;
|
||||
if (extensions is null || extensions.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extensions.TryGetValue("debian.release", out var debRelease) && !string.IsNullOrWhiteSpace(debRelease))
|
||||
{
|
||||
return debRelease;
|
||||
}
|
||||
|
||||
if (extensions.TryGetValue("ubuntu.release", out var ubuntuRelease) && !string.IsNullOrWhiteSpace(ubuntuRelease))
|
||||
{
|
||||
return ubuntuRelease;
|
||||
}
|
||||
|
||||
if (extensions.TryGetValue("alpine.distroversion", out var alpineRelease) && !string.IsNullOrWhiteSpace(alpineRelease))
|
||||
{
|
||||
if (extensions.TryGetValue("alpine.repo", out var alpineRepo) && !string.IsNullOrWhiteSpace(alpineRepo))
|
||||
{
|
||||
return $"{alpineRelease}/{alpineRepo}";
|
||||
}
|
||||
|
||||
return alpineRelease;
|
||||
}
|
||||
|
||||
if (extensions.TryGetValue("suse.platform", out var susePlatform) && !string.IsNullOrWhiteSpace(susePlatform))
|
||||
{
|
||||
return susePlatform;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public sealed class AdvisoryConverter
|
||||
{
|
||||
var ecosystem = MapTypeToEcosystem(pkg.Type);
|
||||
var versionRangeJson = JsonSerializer.Serialize(pkg.VersionRanges, JsonOptions);
|
||||
var databaseSpecificJson = BuildDatabaseSpecific(pkg);
|
||||
|
||||
affectedEntities.Add(new AdvisoryAffectedEntity
|
||||
{
|
||||
@@ -110,7 +111,7 @@ public sealed class AdvisoryConverter
|
||||
VersionRange = versionRangeJson,
|
||||
VersionsAffected = null,
|
||||
VersionsFixed = ExtractFixedVersions(pkg.VersionRanges),
|
||||
DatabaseSpecific = null,
|
||||
DatabaseSpecific = databaseSpecificJson,
|
||||
CreatedAt = now
|
||||
});
|
||||
}
|
||||
@@ -245,6 +246,29 @@ public sealed class AdvisoryConverter
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string? BuildDatabaseSpecific(AffectedPackage package)
|
||||
{
|
||||
if (package is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
if (!string.IsNullOrWhiteSpace(package.Platform))
|
||||
{
|
||||
payload["platform"] = package.Platform;
|
||||
}
|
||||
|
||||
if (!package.NormalizedVersions.IsEmpty)
|
||||
{
|
||||
payload["normalizedVersions"] = package.NormalizedVersions;
|
||||
}
|
||||
|
||||
return payload.Count == 0
|
||||
? null
|
||||
: JsonSerializer.Serialize(payload, JsonOptions);
|
||||
}
|
||||
|
||||
private static string[]? ExtractFixedVersions(IEnumerable<AffectedVersionRange> ranges)
|
||||
{
|
||||
var fixedVersions = ranges
|
||||
|
||||
@@ -271,10 +271,10 @@ public static partial class ChangelogParser
|
||||
[GeneratedRegex(@"^\* (.+) - (.+)")]
|
||||
private static partial Regex RpmHeaderRegex();
|
||||
|
||||
[GeneratedRegex(@" ([\d\.\-]+):")]
|
||||
[GeneratedRegex(@"^\s{2}([0-9A-Za-z\.\-_+]+):")]
|
||||
private static partial Regex AlpineVersionRegex();
|
||||
|
||||
[GeneratedRegex(@"CVE-\d{4}-\d{4,}")]
|
||||
[GeneratedRegex(@"CVE-\d{4}-[0-9A-Za-z]{4,}")]
|
||||
private static partial Regex CvePatternRegex();
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,11 @@ public static partial class PatchHeaderParser
|
||||
|
||||
private static double CalculateConfidence(int cveCount, string description, string origin)
|
||||
{
|
||||
if (cveCount == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Base confidence for patch header CVE mention
|
||||
var confidence = 0.80;
|
||||
|
||||
@@ -137,7 +142,7 @@ public static partial class PatchHeaderParser
|
||||
return Math.Min(confidence, 0.95);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"CVE-\d{4}-\d{4,}")]
|
||||
[GeneratedRegex(@"CVE-\d{4}-[0-9A-Za-z]{4,}")]
|
||||
private static partial Regex CvePatternRegex();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user