Skip to content

Commit

Permalink
Merge pull request #9296 from NuGet/jver-merge
Browse files Browse the repository at this point in the history
Merge branch "main" into dev
  • Loading branch information
joelverhagen authored Nov 3, 2022
2 parents a3ccd39 + 60a2f80 commit 6ce4702
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 16 deletions.
26 changes: 14 additions & 12 deletions src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -20,27 +28,21 @@ public string CreateSecurityAdvisoriesQuery(DateTimeOffset? updatedSince = null,
edges {
cursor
node {
databaseId
permalink
severity
withdrawnAt
updatedAt
" + SecurityAdvisoryFields + @"
" + CreateVulnerabilitiesConnectionQuery() + @"
}
}
}
}";

/// <summary>
/// Source: https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/queries#securityadvisory
/// </summary>
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) + @"
}
}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private async Task<SecurityAdvisory> 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;
Expand Down
10 changes: 10 additions & 0 deletions src/GitHubVulnerabilities2Db/GraphQL/QueryResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ namespace GitHubVulnerabilities2Db.GraphQL
public class QueryResponse
{
public QueryResponseData Data { get; set; }
public List<QueryError> Errors { get; set; }
}

/// <summary>
/// The optional error details returned by the GraphQL endpoint.
/// See: https://www.apollographql.com/docs/react/data/error-handling/#graphql-errors
/// </summary>
public class QueryError
{
public string Message { get; set; }
}

/// <summary>
Expand Down
21 changes: 19 additions & 2 deletions src/GitHubVulnerabilities2Db/GraphQL/QueryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,32 @@ public async Task<QueryResponse> QueryAsync(string query, CancellationToken toke
};

var response = await MakeWebRequestAsync(queryJObject.ToString(), token);
return JsonConvert.DeserializeObject<QueryResponse>(response);
var queryResponse = JsonConvert.DeserializeObject<QueryResponse>(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<string> MakeWebRequestAsync(string query, CancellationToken token)
{
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;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/GitHubVulnerabilities2Db/GraphQL/SecurityAdvisory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
3 changes: 2 additions & 1 deletion src/GitHubVulnerabilities2Db/Job.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public class Job : JsonConfigurationJob, IDisposable
public override async Task Run()
{
var collector = _serviceProvider.GetRequiredService<IAdvisoryCollector>();
while (await collector.ProcessAsync(CancellationToken.None)) ;
while (await collector.ProcessAsync(CancellationToken.None));

}

protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot)
Expand Down
52 changes: 52 additions & 0 deletions tests/GitHubVulnerabilities2Db.Facts/QueryServiceFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<InvalidOperationException>(() => _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<QueryError> { 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<InvalidOperationException>(() => _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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand Down

0 comments on commit 6ce4702

Please sign in to comment.