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,5 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
@@ -99,7 +99,7 @@ public sealed class OsvConflictFixtureTests
|
||||
SourceName: OsvConnectorPlugin.SourceName,
|
||||
Format: "osv.v1",
|
||||
SchemaVersion: "osv.v1",
|
||||
Payload: new BsonDocument("id", dto.Id),
|
||||
Payload: new DocumentObject("id", dto.Id),
|
||||
CreatedAt: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
|
||||
ValidatedAt: new DateTimeOffset(2025, 3, 6, 12, 5, 0, TimeSpan.Zero));
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,240 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_NormalizesAliasesReferencesAndRanges()
|
||||
{
|
||||
var published = DateTimeOffset.UtcNow.AddDays(-2);
|
||||
var modified = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
|
||||
using var databaseSpecificJson = JsonDocument.Parse("{}");
|
||||
using var ecosystemSpecificJson = JsonDocument.Parse("{}");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-2025-TEST",
|
||||
Summary = "Test summary",
|
||||
Details = "Longer details for the advisory.",
|
||||
Published = published,
|
||||
Modified = modified,
|
||||
Aliases = new[] { "CVE-2025-0001", "CVE-2025-0001", "GHSA-xxxx" },
|
||||
Related = new[] { "CVE-2025-0002" },
|
||||
References = new[]
|
||||
{
|
||||
new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = "https://example.com/patch", Type = "PATCH" },
|
||||
},
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
|
||||
},
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = "PyPI",
|
||||
Name = "example",
|
||||
Purl = "pkg:pypi/example",
|
||||
},
|
||||
Ranges = new[]
|
||||
{
|
||||
new OsvRangeDto
|
||||
{
|
||||
Type = "SEMVER",
|
||||
Events = new[]
|
||||
{
|
||||
new OsvEventDto { Introduced = "0" },
|
||||
new OsvEventDto { Fixed = "1.0.1" },
|
||||
}
|
||||
}
|
||||
},
|
||||
EcosystemSpecific = ecosystemSpecificJson.RootElement,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
"https://osv.dev/vulnerability/OSV-2025-TEST",
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = "PyPI",
|
||||
},
|
||||
null,
|
||||
modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = BsonDocument.Parse(JsonSerializer.Serialize(dto, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
}));
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, DateTimeOffset.UtcNow);
|
||||
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.Equal(dto.Id, advisory.AdvisoryKey);
|
||||
Assert.Contains("CVE-2025-0002", advisory.Aliases);
|
||||
Assert.Equal(4, advisory.Aliases.Length);
|
||||
|
||||
Assert.Equal(2, advisory.References.Length);
|
||||
Assert.Equal("https://example.com/advisory", advisory.References[0].Url);
|
||||
Assert.Equal("https://example.com/patch", advisory.References[1].Url);
|
||||
|
||||
Assert.Single(advisory.AffectedPackages);
|
||||
var affected = advisory.AffectedPackages[0];
|
||||
Assert.Equal(AffectedPackageTypes.SemVer, affected.Type);
|
||||
Assert.Single(affected.VersionRanges);
|
||||
Assert.Equal("0", affected.VersionRanges[0].IntroducedVersion);
|
||||
Assert.Equal("1.0.1", affected.VersionRanges[0].FixedVersion);
|
||||
var semver = affected.VersionRanges[0].Primitives?.SemVer;
|
||||
Assert.NotNull(semver);
|
||||
Assert.Equal("0", semver!.Introduced);
|
||||
Assert.True(semver.IntroducedInclusive);
|
||||
Assert.Equal("1.0.1", semver.Fixed);
|
||||
Assert.False(semver.FixedInclusive);
|
||||
|
||||
Assert.Single(advisory.CvssMetrics);
|
||||
Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
|
||||
{
|
||||
using var databaseSpecificJson = JsonDocument.Parse("""
|
||||
{
|
||||
"severity": "MODERATE",
|
||||
"cwe_ids": ["CWE-290"]
|
||||
}
|
||||
""");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-CVSS4",
|
||||
Summary = "Severity-only advisory",
|
||||
Details = "OSV entry that lacks a parsable CVSS vector.",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-10),
|
||||
Modified = DateTimeOffset.UtcNow.AddDays(-5),
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto
|
||||
{
|
||||
Type = "CVSS_V4",
|
||||
Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.True(advisory.CvssMetrics.IsEmpty);
|
||||
Assert.Equal("medium", advisory.Severity);
|
||||
Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);
|
||||
|
||||
var weakness = Assert.Single(advisory.Cwes);
|
||||
var provenance = Assert.Single(weakness.Provenance);
|
||||
Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
|
||||
[InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
|
||||
[InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")]
|
||||
[InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")]
|
||||
[InlineData("crates", "serde", "pkg:cargo/serde")]
|
||||
public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier)
|
||||
{
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-{ecosystem}-PURL",
|
||||
Summary = "Test advisory",
|
||||
Details = "Details",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Modified = DateTimeOffset.UtcNow,
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = null,
|
||||
},
|
||||
Ranges = null,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical));
|
||||
Assert.Equal(expectedIdentifier, canonical);
|
||||
}
|
||||
|
||||
var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string;
|
||||
Assert.Equal(expectedIdentifier, directIdentifier);
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem);
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
|
||||
var affected = Assert.Single(advisory.AffectedPackages);
|
||||
Assert.Equal(expectedIdentifier, affected.Identifier);
|
||||
}
|
||||
|
||||
private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem)
|
||||
{
|
||||
var recordedAt = DateTimeOffset.UtcNow;
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/{dto.Id}",
|
||||
recordedAt,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
null,
|
||||
dto.Modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = new BsonDocument("id", dto.Id);
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt);
|
||||
return (document, dtoRecord);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_NormalizesAliasesReferencesAndRanges()
|
||||
{
|
||||
var published = DateTimeOffset.UtcNow.AddDays(-2);
|
||||
var modified = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
|
||||
using var databaseSpecificJson = JsonDocument.Parse("{}");
|
||||
using var ecosystemSpecificJson = JsonDocument.Parse("{}");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-2025-TEST",
|
||||
Summary = "Test summary",
|
||||
Details = "Longer details for the advisory.",
|
||||
Published = published,
|
||||
Modified = modified,
|
||||
Aliases = new[] { "CVE-2025-0001", "CVE-2025-0001", "GHSA-xxxx" },
|
||||
Related = new[] { "CVE-2025-0002" },
|
||||
References = new[]
|
||||
{
|
||||
new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = "https://example.com/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = "https://example.com/patch", Type = "PATCH" },
|
||||
},
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
|
||||
},
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = "PyPI",
|
||||
Name = "example",
|
||||
Purl = "pkg:pypi/example",
|
||||
},
|
||||
Ranges = new[]
|
||||
{
|
||||
new OsvRangeDto
|
||||
{
|
||||
Type = "SEMVER",
|
||||
Events = new[]
|
||||
{
|
||||
new OsvEventDto { Introduced = "0" },
|
||||
new OsvEventDto { Fixed = "1.0.1" },
|
||||
}
|
||||
}
|
||||
},
|
||||
EcosystemSpecific = ecosystemSpecificJson.RootElement,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
"https://osv.dev/vulnerability/OSV-2025-TEST",
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = "PyPI",
|
||||
},
|
||||
null,
|
||||
modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = DocumentObject.Parse(JsonSerializer.Serialize(dto, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
}));
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, DateTimeOffset.UtcNow);
|
||||
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.Equal(dto.Id, advisory.AdvisoryKey);
|
||||
Assert.Contains("CVE-2025-0002", advisory.Aliases);
|
||||
Assert.Equal(4, advisory.Aliases.Length);
|
||||
|
||||
Assert.Equal(2, advisory.References.Length);
|
||||
Assert.Equal("https://example.com/advisory", advisory.References[0].Url);
|
||||
Assert.Equal("https://example.com/patch", advisory.References[1].Url);
|
||||
|
||||
Assert.Single(advisory.AffectedPackages);
|
||||
var affected = advisory.AffectedPackages[0];
|
||||
Assert.Equal(AffectedPackageTypes.SemVer, affected.Type);
|
||||
Assert.Single(affected.VersionRanges);
|
||||
Assert.Equal("0", affected.VersionRanges[0].IntroducedVersion);
|
||||
Assert.Equal("1.0.1", affected.VersionRanges[0].FixedVersion);
|
||||
var semver = affected.VersionRanges[0].Primitives?.SemVer;
|
||||
Assert.NotNull(semver);
|
||||
Assert.Equal("0", semver!.Introduced);
|
||||
Assert.True(semver.IntroducedInclusive);
|
||||
Assert.Equal("1.0.1", semver.Fixed);
|
||||
Assert.False(semver.FixedInclusive);
|
||||
|
||||
Assert.Single(advisory.CvssMetrics);
|
||||
Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
|
||||
{
|
||||
using var databaseSpecificJson = JsonDocument.Parse("""
|
||||
{
|
||||
"severity": "MODERATE",
|
||||
"cwe_ids": ["CWE-290"]
|
||||
}
|
||||
""");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-CVSS4",
|
||||
Summary = "Severity-only advisory",
|
||||
Details = "OSV entry that lacks a parsable CVSS vector.",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-10),
|
||||
Modified = DateTimeOffset.UtcNow.AddDays(-5),
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto
|
||||
{
|
||||
Type = "CVSS_V4",
|
||||
Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.True(advisory.CvssMetrics.IsEmpty);
|
||||
Assert.Equal("medium", advisory.Severity);
|
||||
Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);
|
||||
|
||||
var weakness = Assert.Single(advisory.Cwes);
|
||||
var provenance = Assert.Single(weakness.Provenance);
|
||||
Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
|
||||
[InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
|
||||
[InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")]
|
||||
[InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")]
|
||||
[InlineData("crates", "serde", "pkg:cargo/serde")]
|
||||
public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier)
|
||||
{
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-{ecosystem}-PURL",
|
||||
Summary = "Test advisory",
|
||||
Details = "Details",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Modified = DateTimeOffset.UtcNow,
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = null,
|
||||
},
|
||||
Ranges = null,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical));
|
||||
Assert.Equal(expectedIdentifier, canonical);
|
||||
}
|
||||
|
||||
var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string;
|
||||
Assert.Equal(expectedIdentifier, directIdentifier);
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem);
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
|
||||
var affected = Assert.Single(advisory.AffectedPackages);
|
||||
Assert.Equal(expectedIdentifier, affected.Identifier);
|
||||
}
|
||||
|
||||
private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem)
|
||||
{
|
||||
var recordedAt = DateTimeOffset.UtcNow;
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/{dto.Id}",
|
||||
recordedAt,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
null,
|
||||
dto.Modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = new DocumentObject("id", dto.Id);
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt);
|
||||
return (document, dtoRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvSnapshotTests
|
||||
{
|
||||
private static readonly DateTimeOffset BaselinePublished = new(2025, 1, 5, 12, 0, 0, TimeSpan.Zero);
|
||||
private static readonly DateTimeOffset BaselineModified = new(2025, 1, 8, 6, 30, 0, TimeSpan.Zero);
|
||||
private static readonly DateTimeOffset BaselineFetched = new(2025, 1, 8, 7, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public OsvSnapshotTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PyPI", "pkg:pypi/requests", "requests", "osv-pypi.snapshot.json")]
|
||||
[InlineData("npm", "pkg:npm/%40scope%2Fleft-pad", "@scope/left-pad", "osv-npm.snapshot.json")]
|
||||
public void Map_ProducesExpectedSnapshot(string ecosystem, string purl, string packageName, string snapshotFile)
|
||||
{
|
||||
var dto = CreateDto(ecosystem, purl, packageName);
|
||||
var document = CreateDocumentRecord(ecosystem);
|
||||
var dtoRecord = CreateDtoRecord(document, dto);
|
||||
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
var actual = SnapshotSerializer.ToSnapshot(advisory).Trim();
|
||||
|
||||
var snapshotPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", snapshotFile);
|
||||
var expected = File.Exists(snapshotPath) ? File.ReadAllText(snapshotPath).Trim() : string.Empty;
|
||||
|
||||
if (!string.Equals(actual, expected, StringComparison.Ordinal))
|
||||
{
|
||||
_output.WriteLine(actual);
|
||||
}
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(expected), $"Snapshot '{snapshotFile}' not found or empty.");
|
||||
|
||||
using var expectedJson = JsonDocument.Parse(expected);
|
||||
using var actualJson = JsonDocument.Parse(actual);
|
||||
Assert.True(JsonElement.DeepEquals(actualJson.RootElement, expectedJson.RootElement), "OSV snapshot mismatch.");
|
||||
}
|
||||
|
||||
private static OsvVulnerabilityDto CreateDto(string ecosystem, string purl, string packageName)
|
||||
{
|
||||
return new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-2025-{ecosystem}-0001",
|
||||
Summary = $"{ecosystem} package vulnerability",
|
||||
Details = $"Detailed description for {ecosystem} package {packageName}.",
|
||||
Published = BaselinePublished,
|
||||
Modified = BaselineModified,
|
||||
Aliases = new[] { $"CVE-2025-11{ecosystem.Length}", $"GHSA-{ecosystem.Length}abc-{ecosystem.Length}def-{ecosystem.Length}ghi" },
|
||||
Related = new[] { $"OSV-RELATED-{ecosystem}-42" },
|
||||
References = new[]
|
||||
{
|
||||
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/fix", Type = "FIX" },
|
||||
},
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
|
||||
},
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = purl,
|
||||
},
|
||||
Ranges = new[]
|
||||
{
|
||||
new OsvRangeDto
|
||||
{
|
||||
Type = "SEMVER",
|
||||
Events = new[]
|
||||
{
|
||||
new OsvEventDto { Introduced = "0" },
|
||||
new OsvEventDto { Fixed = "2.0.0" },
|
||||
}
|
||||
}
|
||||
},
|
||||
Versions = new[] { "1.0.0", "1.5.0" },
|
||||
EcosystemSpecific = ParseElement("{\"severity\":\"high\"}"),
|
||||
}
|
||||
},
|
||||
DatabaseSpecific = ParseElement("{\"source\":\"osv.dev\"}"),
|
||||
};
|
||||
}
|
||||
|
||||
private static DocumentRecord CreateDocumentRecord(string ecosystem)
|
||||
=> new(
|
||||
Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/OSV-2025-{ecosystem}-0001",
|
||||
BaselineFetched,
|
||||
"sha256-osv-snapshot",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
"\"osv-etag\"",
|
||||
BaselineModified,
|
||||
null,
|
||||
null);
|
||||
|
||||
private static DtoRecord CreateDtoRecord(DocumentRecord document, OsvVulnerabilityDto dto)
|
||||
{
|
||||
var payload = BsonDocument.Parse(JsonSerializer.Serialize(dto, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
}));
|
||||
|
||||
return new DtoRecord(Guid.Parse("22222222-2222-2222-2222-222222222222"), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, BaselineModified);
|
||||
}
|
||||
|
||||
private static JsonElement ParseElement(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvSnapshotTests
|
||||
{
|
||||
private static readonly DateTimeOffset BaselinePublished = new(2025, 1, 5, 12, 0, 0, TimeSpan.Zero);
|
||||
private static readonly DateTimeOffset BaselineModified = new(2025, 1, 8, 6, 30, 0, TimeSpan.Zero);
|
||||
private static readonly DateTimeOffset BaselineFetched = new(2025, 1, 8, 7, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public OsvSnapshotTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PyPI", "pkg:pypi/requests", "requests", "osv-pypi.snapshot.json")]
|
||||
[InlineData("npm", "pkg:npm/%40scope%2Fleft-pad", "@scope/left-pad", "osv-npm.snapshot.json")]
|
||||
public void Map_ProducesExpectedSnapshot(string ecosystem, string purl, string packageName, string snapshotFile)
|
||||
{
|
||||
var dto = CreateDto(ecosystem, purl, packageName);
|
||||
var document = CreateDocumentRecord(ecosystem);
|
||||
var dtoRecord = CreateDtoRecord(document, dto);
|
||||
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
var actual = SnapshotSerializer.ToSnapshot(advisory).Trim();
|
||||
|
||||
var snapshotPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", snapshotFile);
|
||||
var expected = File.Exists(snapshotPath) ? File.ReadAllText(snapshotPath).Trim() : string.Empty;
|
||||
|
||||
if (!string.Equals(actual, expected, StringComparison.Ordinal))
|
||||
{
|
||||
_output.WriteLine(actual);
|
||||
}
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(expected), $"Snapshot '{snapshotFile}' not found or empty.");
|
||||
|
||||
using var expectedJson = JsonDocument.Parse(expected);
|
||||
using var actualJson = JsonDocument.Parse(actual);
|
||||
Assert.True(JsonElement.DeepEquals(actualJson.RootElement, expectedJson.RootElement), "OSV snapshot mismatch.");
|
||||
}
|
||||
|
||||
private static OsvVulnerabilityDto CreateDto(string ecosystem, string purl, string packageName)
|
||||
{
|
||||
return new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-2025-{ecosystem}-0001",
|
||||
Summary = $"{ecosystem} package vulnerability",
|
||||
Details = $"Detailed description for {ecosystem} package {packageName}.",
|
||||
Published = BaselinePublished,
|
||||
Modified = BaselineModified,
|
||||
Aliases = new[] { $"CVE-2025-11{ecosystem.Length}", $"GHSA-{ecosystem.Length}abc-{ecosystem.Length}def-{ecosystem.Length}ghi" },
|
||||
Related = new[] { $"OSV-RELATED-{ecosystem}-42" },
|
||||
References = new[]
|
||||
{
|
||||
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/advisory", Type = "ADVISORY" },
|
||||
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/fix", Type = "FIX" },
|
||||
},
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
|
||||
},
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = purl,
|
||||
},
|
||||
Ranges = new[]
|
||||
{
|
||||
new OsvRangeDto
|
||||
{
|
||||
Type = "SEMVER",
|
||||
Events = new[]
|
||||
{
|
||||
new OsvEventDto { Introduced = "0" },
|
||||
new OsvEventDto { Fixed = "2.0.0" },
|
||||
}
|
||||
}
|
||||
},
|
||||
Versions = new[] { "1.0.0", "1.5.0" },
|
||||
EcosystemSpecific = ParseElement("{\"severity\":\"high\"}"),
|
||||
}
|
||||
},
|
||||
DatabaseSpecific = ParseElement("{\"source\":\"osv.dev\"}"),
|
||||
};
|
||||
}
|
||||
|
||||
private static DocumentRecord CreateDocumentRecord(string ecosystem)
|
||||
=> new(
|
||||
Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/OSV-2025-{ecosystem}-0001",
|
||||
BaselineFetched,
|
||||
"sha256-osv-snapshot",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
"\"osv-etag\"",
|
||||
BaselineModified,
|
||||
null,
|
||||
null);
|
||||
|
||||
private static DtoRecord CreateDtoRecord(DocumentRecord document, OsvVulnerabilityDto dto)
|
||||
{
|
||||
var payload = DocumentObject.Parse(JsonSerializer.Serialize(dto, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
}));
|
||||
|
||||
return new DtoRecord(Guid.Parse("22222222-2222-2222-2222-222222222222"), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, BaselineModified);
|
||||
}
|
||||
|
||||
private static JsonElement ParseElement(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user