public async Task OneResult() { // Arrange var vulnerability = new SecurityVulnerability(); var advisory = new SecurityAdvisory { Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = new[] { new Edge <SecurityVulnerability> { Node = vulnerability } } } }; var response = CreateResponseFromEdges(new[] { new Edge <SecurityAdvisory> { Node = advisory } }); SetupFirstQueryResult(response); // Act var results = await _service.GetAdvisoriesSinceAsync( _lastUpdated, _token); // Assert Assert.Single(results, advisory); _queryBuilderMock.Verify(); _queryServiceMock.Verify(); }
public async Task IngestsAdvisoryWithoutVulnerability(bool withdrawn) { // Arrange var advisory = new SecurityAdvisory { DatabaseId = 1, GhsaId = "ghsa", Severity = "MODERATE", References = new[] { new SecurityAdvisoryReference { Url = "https://vulnerable" } }, WithdrawnAt = withdrawn ? new DateTime() : (DateTime?)null }; PackageVulnerabilityServiceMock .Setup(x => x.UpdateVulnerabilityAsync(It.IsAny <PackageVulnerability>(), withdrawn)) .Callback <PackageVulnerability, bool>((vulnerability, wasWithdrawn) => { Assert.Equal(advisory.DatabaseId, vulnerability.GitHubDatabaseKey); Assert.Equal(PackageVulnerabilitySeverity.Moderate, vulnerability.Severity); Assert.Equal(advisory.References.Single().Url, vulnerability.ReferenceUrl); }) .Returns(Task.CompletedTask) .Verifiable(); // Act await Ingestor.IngestAsync(new[] { advisory }); // Assert PackageVulnerabilityServiceMock.Verify(); }
public async Task IngestsAdvisoryWithoutVulnerability(bool withdrawn) { // Arrange var advisory = new SecurityAdvisory { DatabaseId = 1, Permalink = "https://example/advisories/GHSA-3456-abcd-7890", Severity = "MODERATE", WithdrawnAt = withdrawn ? new DateTimeOffset() : (DateTimeOffset?)null }; PackageVulnerabilityServiceMock .Setup(x => x.UpdateVulnerabilityAsync(It.IsAny <PackageVulnerability>(), withdrawn)) .Callback <PackageVulnerability, bool>((vulnerability, wasWithdrawn) => { Assert.Equal(advisory.DatabaseId, vulnerability.GitHubDatabaseKey); Assert.Equal(PackageVulnerabilitySeverity.Moderate, vulnerability.Severity); Assert.Equal(advisory.Permalink, vulnerability.AdvisoryUrl); }) .Returns(Task.CompletedTask) .Verifiable(); // Act await Ingestor.IngestAsync(new[] { advisory }); // Assert PackageVulnerabilityServiceMock.Verify(); }
private async Task <SecurityAdvisory> FetchAllVulnerabilitiesAsync(SecurityAdvisory advisory, CancellationToken token) { // If the last time we fetched this advisory, it returned the maximum amount of vulnerabilities, query again to fetch the next batch. var lastVulnerabilitiesFetchedCount = advisory.Vulnerabilities?.Edges?.Count() ?? 0; while (lastVulnerabilitiesFetchedCount == _queryBuilder.GetMaximumResultsPerRequest()) { _logger.LogInformation("Fetching more vulnerabilities for advisory with database key {GitHubDatabaseKey}", advisory.DatabaseId); var queryForAdditionalVulnerabilities = _queryBuilder.CreateSecurityAdvisoryQuery(advisory); var responseForAdditionalVulnerabilities = await _queryService.QueryAsync(queryForAdditionalVulnerabilities, token); var advisoryWithAdditionalVulnerabilities = responseForAdditionalVulnerabilities.Data.SecurityAdvisory; lastVulnerabilitiesFetchedCount = advisoryWithAdditionalVulnerabilities.Vulnerabilities?.Edges?.Count() ?? 0; advisory = MergeAdvisories(advisory, advisoryWithAdditionalVulnerabilities); } // We have seen some duplicate ranges (same ID and version range) returned by the API before, so make sure to dedupe the ranges. var comparer = new VulnerabilityForSameAdvisoryComparer(); if (advisory.Vulnerabilities?.Edges != null) { advisory.Vulnerabilities.Edges = advisory.Vulnerabilities.Edges.Distinct(comparer); } return(advisory); }
public async Task DedupesIdenticalVulnerabilities() { // Arrange var id = "identical"; var range = "(,)"; var firstVulnerability = new SecurityVulnerability { Package = new SecurityVulnerabilityPackage { Name = id }, VulnerableVersionRange = range }; var secondVulnerability = new SecurityVulnerability { Package = new SecurityVulnerabilityPackage { Name = id }, VulnerableVersionRange = range }; var advisory = new SecurityAdvisory { Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = new[] { new Edge <SecurityVulnerability> { Node = firstVulnerability }, new Edge <SecurityVulnerability> { Node = secondVulnerability } } } }; var response = CreateResponseFromEdges(new[] { new Edge <SecurityAdvisory> { Node = advisory } }); SetupFirstQueryResult(response); // Act var results = await _service.GetAdvisoriesSinceAsync( _cursorMock.Object, _token); // Assert Assert.Single(results, advisory); Assert.Single(results.Single().Vulnerabilities.Edges); var node = results.Single().Vulnerabilities.Edges.Single().Node; Assert.Equal(id, node.Package.Name); Assert.Equal(range, node.VulnerableVersionRange); _cursorMock.Verify(); _queryBuilderMock.Verify(); _queryServiceMock.Verify(); }
private QueryResponse CreateResponseFromAdvisory(SecurityAdvisory advisory) => new QueryResponse { Data = new QueryResponseData { SecurityAdvisory = advisory } };
private SecurityAdvisory MergeAdvisories(SecurityAdvisory advisory, SecurityAdvisory nextAdvisory) { // We want to keep the next advisory's data, but prepend the existing vulnerabilities that were returned in previous queries. nextAdvisory.Vulnerabilities.Edges = advisory.Vulnerabilities.Edges.Concat( nextAdvisory.Vulnerabilities.Edges ?? Enumerable.Empty <Edge <SecurityVulnerability> >()); // We are not querying the advisories feed at this time so we do not want to advance the advisory cursor past what it was originally. nextAdvisory.UpdatedAt = advisory.UpdatedAt; return(nextAdvisory); }
public string CreateSecurityAdvisoryQuery(SecurityAdvisory advisory) => @" { securityAdvisory(ghsaId: " + advisory.GhsaId + @") { severity updatedAt identifiers { type value } " + CreateVulnerabilitiesConnectionQuery(advisory.Vulnerabilities?.Edges?.Last()?.Cursor) + @" } }";
private Tuple <PackageVulnerability, bool> FromAdvisory(SecurityAdvisory advisory) { var vulnerability = new PackageVulnerability { GitHubDatabaseKey = advisory.DatabaseId, Severity = (PackageVulnerabilitySeverity)Enum.Parse(typeof(PackageVulnerabilitySeverity), advisory.Severity, ignoreCase: true), AdvisoryUrl = advisory.Permalink }; foreach (var securityVulnerability in advisory.Vulnerabilities?.Edges?.Select(e => e.Node) ?? Enumerable.Empty <SecurityVulnerability>()) { var packageVulnerability = FromVulnerability(vulnerability, securityVulnerability); vulnerability.AffectedRanges.Add(packageVulnerability); } return(Tuple.Create(vulnerability, advisory.WithdrawnAt != null)); }
public async Task IngestsAdvisory(bool withdrawn) { // Arrange var securityVulnerability = new SecurityVulnerability { Package = new SecurityVulnerabilityPackage { Name = "crested.gecko" }, VulnerableVersionRange = "homeOnTheRange" }; var advisory = new SecurityAdvisory { DatabaseId = 1, GhsaId = "ghsa", Severity = "CRITICAL", References = new[] { new SecurityAdvisoryReference { Url = "https://vulnerable" } }, WithdrawnAt = withdrawn ? new DateTime() : (DateTime?)null, Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = new[] { new Edge <SecurityVulnerability> { Node = securityVulnerability } } } }; securityVulnerability.Advisory = advisory; var versionRange = VersionRange.Parse("[1.0.0, 1.0.0]"); GitHubVersionRangeParserMock .Setup(x => x.ToNuGetVersionRange(securityVulnerability.VulnerableVersionRange)) .Returns(versionRange); PackageVulnerabilityServiceMock .Setup(x => x.UpdateVulnerabilityAsync(It.IsAny <PackageVulnerability>(), withdrawn)) .Callback <PackageVulnerability, bool>((vulnerability, wasWithdrawn) => { Assert.Equal(advisory.DatabaseId, vulnerability.GitHubDatabaseKey); Assert.Equal(PackageVulnerabilitySeverity.Critical, vulnerability.Severity); Assert.Equal(advisory.References.Single().Url, vulnerability.ReferenceUrl); var packageVulnerability = vulnerability.AffectedRanges.Single(); Assert.Equal(securityVulnerability.Package.Name, packageVulnerability.PackageId); Assert.Equal(versionRange.ToNormalizedString(), packageVulnerability.PackageVersionRange); }) .Returns(Task.CompletedTask) .Verifiable(); // Act await Ingestor.IngestAsync(new[] { advisory }); // Assert PackageVulnerabilityServiceMock.Verify(); }
public async Task IngestsAdvisory(bool withdrawn, bool vulnerabilityHasFirstPatchedVersion) { // Arrange var securityVulnerability = new SecurityVulnerability { Package = new SecurityVulnerabilityPackage { Name = "crested.gecko" }, VulnerableVersionRange = "homeOnTheRange", FirstPatchedVersion = vulnerabilityHasFirstPatchedVersion ? new SecurityVulnerabilityPackageVersion { Identifier = "1.2.3" } : null }; var advisory = new SecurityAdvisory { DatabaseId = 1, Permalink = "https://example/advisories/GHSA-6543-dcba-0987", Severity = "CRITICAL", WithdrawnAt = withdrawn ? new DateTimeOffset() : (DateTimeOffset?)null, Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = new[] { new Edge <SecurityVulnerability> { Node = securityVulnerability } } } }; securityVulnerability.Advisory = advisory; var versionRange = VersionRange.Parse("[1.0.0, 1.0.0]"); GitHubVersionRangeParserMock .Setup(x => x.ToNuGetVersionRange(securityVulnerability.VulnerableVersionRange)) .Returns(versionRange); PackageVulnerabilityServiceMock .Setup(x => x.UpdateVulnerabilityAsync(It.IsAny <PackageVulnerability>(), withdrawn)) .Callback <PackageVulnerability, bool>((vulnerability, wasWithdrawn) => { Assert.Equal(advisory.DatabaseId, vulnerability.GitHubDatabaseKey); Assert.Equal(PackageVulnerabilitySeverity.Critical, vulnerability.Severity); Assert.Equal(advisory.Permalink, vulnerability.AdvisoryUrl); var range = vulnerability.AffectedRanges.Single(); Assert.Equal(securityVulnerability.Package.Name, range.PackageId); Assert.Equal(versionRange.ToNormalizedString(), range.PackageVersionRange); Assert.Equal(securityVulnerability.FirstPatchedVersion?.Identifier, range.FirstPatchedPackageVersion); }) .Returns(Task.CompletedTask) .Verifiable(); // Act await Ingestor.IngestAsync(new[] { advisory }); // Assert PackageVulnerabilityServiceMock.Verify(); }
public async Task OneResultWithManyVulnerabilitiesShouldPage() { // Arrange var firstVulnerabilityEdges = Enumerable .Range(0, _maxResultsPerQuery) .Select(i => new Edge <SecurityVulnerability> { Cursor = i.ToString(), Node = new SecurityVulnerability { VulnerableVersionRange = i.ToString() } }); var firstAdvisoryEdge = new Edge <SecurityAdvisory> { Node = new SecurityAdvisory { Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = firstVulnerabilityEdges } } }; var firstResponse = CreateResponseFromEdges(new[] { firstAdvisoryEdge }); SetupFirstQueryResult(firstResponse); var secondVulnerabilityEdges = Enumerable .Range(_maxResultsPerQuery, _maxResultsPerQuery) .Select(i => new Edge <SecurityVulnerability> { Cursor = i.ToString(), Node = new SecurityVulnerability { VulnerableVersionRange = i.ToString() } }); var secondAdvisory = new SecurityAdvisory { Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = secondVulnerabilityEdges } }; var secondResponse = CreateResponseFromAdvisory(secondAdvisory); SetupAdditionalVulnerabilitiesQueryResponse((_maxResultsPerQuery - 1).ToString(), secondResponse); var thirdAdvisory = new SecurityAdvisory { Vulnerabilities = new ConnectionResponseData <SecurityVulnerability> { Edges = Enumerable.Empty <Edge <SecurityVulnerability> >() } }; var thirdResponse = CreateResponseFromAdvisory(thirdAdvisory); SetupAdditionalVulnerabilitiesQueryResponse((_maxResultsPerQuery * 2 - 1).ToString(), thirdResponse); // Act var results = await _service.GetAdvisoriesSinceAsync( _lastUpdated, _token); // Assert Assert.Equal(_maxResultsPerQuery * 2, results.Single().Vulnerabilities.Edges.Count()); Assert.Equal((_maxResultsPerQuery * 2 - 1).ToString(), results.Single().Vulnerabilities.Edges.Last().Cursor); _queryBuilderMock.Verify(); _queryServiceMock.Verify(); }