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();
        }
Esempio n. 2
0
            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();
            }
Esempio n. 3
0
            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();
            }
Esempio n. 4
0
        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
     }
 };
Esempio n. 7
0
 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));
        }
Esempio n. 10
0
            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();
            }
Esempio n. 11
0
            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();
        }