diff --git a/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryBuilder.cs b/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryBuilder.cs
index d3844e1e86..1f7c2cb7b0 100644
--- a/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryBuilder.cs
+++ b/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryBuilder.cs
@@ -4,11 +4,19 @@
using System;
using System.Linq;
using GitHubVulnerabilities2Db.GraphQL;
+using Newtonsoft.Json;
namespace GitHubVulnerabilities2Db.Collector
{
public class AdvisoryQueryBuilder : IAdvisoryQueryBuilder
{
+ private const string SecurityAdvisoryFields = @"databaseId
+ ghsaId
+ permalink
+ severity
+ withdrawnAt
+ updatedAt";
+
public int GetMaximumResultsPerRequest() => 100;
public string CreateSecurityAdvisoriesQuery(DateTimeOffset? updatedSince = null, string afterCursor = null)
@@ -20,27 +28,21 @@ public string CreateSecurityAdvisoriesQuery(DateTimeOffset? updatedSince = null,
edges {
cursor
node {
- databaseId
- permalink
- severity
- withdrawnAt
- updatedAt
+ " + SecurityAdvisoryFields + @"
" + CreateVulnerabilitiesConnectionQuery() + @"
}
}
}
}";
+ ///
+ /// Source: https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/queries#securityadvisory
+ ///
public string CreateSecurityAdvisoryQuery(SecurityAdvisory advisory)
=> @"
{
- securityAdvisory(databaseId: " + advisory.DatabaseId + @") {
- severity
- updatedAt
- identifiers {
- type
- value
- }
+ securityAdvisory(ghsaId: " + JsonConvert.SerializeObject(advisory.GhsaId) + @") {
+ " + SecurityAdvisoryFields + @"
" + CreateVulnerabilitiesConnectionQuery(advisory.Vulnerabilities?.Edges?.Last()?.Cursor) + @"
}
}";
diff --git a/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryService.cs b/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryService.cs
index 4183a0e6d4..303ad414da 100644
--- a/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryService.cs
+++ b/src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryService.cs
@@ -53,7 +53,7 @@ private async Task FetchAllVulnerabilitiesAsync(SecurityAdviso
var lastVulnerabilitiesFetchedCount = advisory.Vulnerabilities?.Edges?.Count() ?? 0;
while (lastVulnerabilitiesFetchedCount == _queryBuilder.GetMaximumResultsPerRequest())
{
- _logger.LogInformation("Fetching more vulnerabilities for advisory with database key {GitHubDatabaseKey}", advisory.DatabaseId);
+ _logger.LogInformation("Fetching more vulnerabilities for advisory with database key {GitHubDatabaseKey} / GHSA ID {GhsaId}", advisory.DatabaseId, advisory.GhsaId);
var queryForAdditionalVulnerabilities = _queryBuilder.CreateSecurityAdvisoryQuery(advisory);
var responseForAdditionalVulnerabilities = await _queryService.QueryAsync(queryForAdditionalVulnerabilities, token);
var advisoryWithAdditionalVulnerabilities = responseForAdditionalVulnerabilities.Data.SecurityAdvisory;
diff --git a/src/GitHubVulnerabilities2Db/GraphQL/QueryResponse.cs b/src/GitHubVulnerabilities2Db/GraphQL/QueryResponse.cs
index 7967d515b3..f74e1c8472 100644
--- a/src/GitHubVulnerabilities2Db/GraphQL/QueryResponse.cs
+++ b/src/GitHubVulnerabilities2Db/GraphQL/QueryResponse.cs
@@ -11,6 +11,16 @@ namespace GitHubVulnerabilities2Db.GraphQL
public class QueryResponse
{
public QueryResponseData Data { get; set; }
+ public List Errors { get; set; }
+ }
+
+ ///
+ /// The optional error details returned by the GraphQL endpoint.
+ /// See: https://www.apollographql.com/docs/react/data/error-handling/#graphql-errors
+ ///
+ public class QueryError
+ {
+ public string Message { get; set; }
}
///
diff --git a/src/GitHubVulnerabilities2Db/GraphQL/QueryService.cs b/src/GitHubVulnerabilities2Db/GraphQL/QueryService.cs
index 4f56c49693..6ac52834a5 100644
--- a/src/GitHubVulnerabilities2Db/GraphQL/QueryService.cs
+++ b/src/GitHubVulnerabilities2Db/GraphQL/QueryService.cs
@@ -39,7 +39,16 @@ public async Task QueryAsync(string query, CancellationToken toke
};
var response = await MakeWebRequestAsync(queryJObject.ToString(), token);
- return JsonConvert.DeserializeObject(response);
+ var queryResponse = JsonConvert.DeserializeObject(response);
+
+ if (queryResponse.Errors != null && queryResponse.Errors.Count > 0)
+ {
+ throw new InvalidOperationException(
+ "The GitHub GraphQL response returned errors in the response JSON. " +
+ $"Response body:{Environment.NewLine}{response}");
+ }
+
+ return queryResponse;
}
private async Task MakeWebRequestAsync(string query, CancellationToken token)
@@ -47,7 +56,15 @@ private async Task MakeWebRequestAsync(string query, CancellationToken t
using (var request = CreateRequest(query))
using (var response = await _client.SendAsync(request, token))
{
- return await response.Content.ReadAsStringAsync();
+ var responseBody = await response.Content.ReadAsStringAsync();
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new InvalidOperationException(
+ $"The GitHub GraphQL response returned status code {(int)response.StatusCode} {response.ReasonPhrase}. " +
+ $"Response body:{Environment.NewLine}{responseBody}");
+ }
+
+ return responseBody;
}
}
diff --git a/src/GitHubVulnerabilities2Db/GraphQL/SecurityAdvisory.cs b/src/GitHubVulnerabilities2Db/GraphQL/SecurityAdvisory.cs
index 6a3ce91874..8abba84abe 100644
--- a/src/GitHubVulnerabilities2Db/GraphQL/SecurityAdvisory.cs
+++ b/src/GitHubVulnerabilities2Db/GraphQL/SecurityAdvisory.cs
@@ -11,6 +11,7 @@ namespace GitHubVulnerabilities2Db.GraphQL
public class SecurityAdvisory : INode
{
public int DatabaseId { get; set; }
+ public string GhsaId { get; set; }
public string Permalink { get; set; }
public string Severity { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
diff --git a/src/GitHubVulnerabilities2Db/Job.cs b/src/GitHubVulnerabilities2Db/Job.cs
index c5fe62cda5..2b953cd3e1 100644
--- a/src/GitHubVulnerabilities2Db/Job.cs
+++ b/src/GitHubVulnerabilities2Db/Job.cs
@@ -34,7 +34,8 @@ public class Job : JsonConfigurationJob, IDisposable
public override async Task Run()
{
var collector = _serviceProvider.GetRequiredService();
- while (await collector.ProcessAsync(CancellationToken.None)) ;
+ while (await collector.ProcessAsync(CancellationToken.None));
+
}
protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot)
diff --git a/tests/GitHubVulnerabilities2Db.Facts/QueryServiceFacts.cs b/tests/GitHubVulnerabilities2Db.Facts/QueryServiceFacts.cs
index b35cecbd8d..b31fc39546 100644
--- a/tests/GitHubVulnerabilities2Db.Facts/QueryServiceFacts.cs
+++ b/tests/GitHubVulnerabilities2Db.Facts/QueryServiceFacts.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
+using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
@@ -56,6 +58,56 @@ public async Task Success()
Assert.True(_handler.WasCalled);
}
+ [Fact]
+ public async Task ErrorStatusCodeIsRejected()
+ {
+ // Arrange
+ var query = "someString";
+ _handler.ExpectedQueryContent = (new JObject { ["query"] = query }).ToString();
+
+ _handler.ExpectedEndpoint = new Uri("https://graphQL.net");
+ _configuration.GitHubGraphQLQueryEndpoint = _handler.ExpectedEndpoint;
+
+ _handler.ExpectedApiKey = "patpatpat";
+ _configuration.GitHubPersonalAccessToken = _handler.ExpectedApiKey;
+
+ var response = new QueryResponse();
+ _handler.ResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest)
+ {
+ Content = new StringContent(JsonConvert.SerializeObject(response))
+ };
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => _service.QueryAsync(query, new CancellationToken()));
+ Assert.True(_handler.WasCalled);
+ Assert.Equal("The GitHub GraphQL response returned status code 400 Bad Request. Response body:\r\n{\"Data\":null,\"Errors\":null}", ex.Message);
+ }
+
+ [Fact]
+ public async Task ErrorResponseJsonIsRejected()
+ {
+ // Arrange
+ var query = "someString";
+ _handler.ExpectedQueryContent = (new JObject { ["query"] = query }).ToString();
+
+ _handler.ExpectedEndpoint = new Uri("https://graphQL.net");
+ _configuration.GitHubGraphQLQueryEndpoint = _handler.ExpectedEndpoint;
+
+ _handler.ExpectedApiKey = "patpatpat";
+ _configuration.GitHubPersonalAccessToken = _handler.ExpectedApiKey;
+
+ var response = new QueryResponse { Errors = new List { new QueryError { Message = "Query = not great" } } };
+ _handler.ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonConvert.SerializeObject(response))
+ };
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => _service.QueryAsync(query, new CancellationToken()));
+ Assert.True(_handler.WasCalled);
+ Assert.Equal("The GitHub GraphQL response returned errors in the response JSON. Response body:\r\n{\"Data\":null,\"Errors\":[{\"Message\":\"Query = not great\"}]}", ex.Message);
+ }
+
private class QueryServiceHttpClientHandler : HttpClientHandler
{
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)