Skip to content

Commit b3c174e

Browse files
authored
Merge pull request #8147 from NuGet/dev
[ReleasePrep][2020.08.07] RI of dev into master
2 parents de753f5 + 6c5734e commit b3c174e

26 files changed

Lines changed: 663 additions & 140 deletions
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Data.Entity;
7+
using System.Data.SqlClient;
8+
using System.Linq;
9+
using System.Net.Http;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using GitHubVulnerabilities2Db.Collector;
13+
using GitHubVulnerabilities2Db.Configuration;
14+
using GitHubVulnerabilities2Db.GraphQL;
15+
using GitHubVulnerabilities2Db.Ingest;
16+
using Microsoft.Extensions.CommandLineUtils;
17+
using Microsoft.Extensions.Logging.Abstractions;
18+
using NuGet.Services.Entities;
19+
using NuGet.Versioning;
20+
using NuGetGallery;
21+
22+
namespace GalleryTools.Commands
23+
{
24+
/// <summary>
25+
/// This command verifies that the <see cref="PackageVulnerability"/> and <see cref="VulnerablePackageVersionRange"/> entities in the
26+
/// database match the <see cref="SecurityAdvisory"/> and <see cref="SecurityVulnerability"/> entities in GitHub's V4 GraphQL API.
27+
/// </summary>
28+
/// <remarks>
29+
/// The verification only expects that advisories that are present in the GitHub API have the same metadata and contain the same ranges in the DB.
30+
/// It intentionally does not require that all vulnerabilities in the DB come from GitHub, or that the set of ranges in the DB match the set of ranges in the GitHub API.
31+
/// This is so that we can add some additional vulnerabilities or ranges for testing or administrative purposes.
32+
/// </remarks>
33+
public static class VerifyGitHubVulnerabilitiesCommand
34+
{
35+
public static void Configure(CommandLineApplication config)
36+
{
37+
config.Description = "Verify that the gallery database's vulnerability information matches GitHub's feed.";
38+
config.HelpOption("-? | -h | --help");
39+
40+
var gitHubPersonalAccessTokenOption = config.Option(
41+
"--token | -t",
42+
"The personal access token to use to authenticate with GitHub.",
43+
CommandOptionType.SingleValue);
44+
45+
var connectionStringOption = config.Option(
46+
"--connectionstring | -c",
47+
"The SQL connectionstring of the target NuGetGallery database.",
48+
CommandOptionType.SingleValue);
49+
50+
config.OnExecute(async () => await ExecuteAsync(
51+
connectionStringOption,
52+
gitHubPersonalAccessTokenOption));
53+
}
54+
55+
private static async Task<int> ExecuteAsync(
56+
CommandOption connectionStringOption,
57+
CommandOption gitHubPersonalAccessTokenOption)
58+
{
59+
if (!connectionStringOption.HasValue())
60+
{
61+
Console.Error.WriteLine($"The {connectionStringOption.Template} option is required.");
62+
return 1;
63+
}
64+
65+
if (!gitHubPersonalAccessTokenOption.HasValue())
66+
{
67+
Console.Error.WriteLine($"The {connectionStringOption.Template} option is required.");
68+
return 1;
69+
}
70+
71+
try
72+
{
73+
var advisories = await FetchAdvisories(gitHubPersonalAccessTokenOption.Value());
74+
await VerifyPackageVulnerabilities(
75+
connectionStringOption.Value(),
76+
advisories);
77+
78+
Console.WriteLine("DONE");
79+
return 0;
80+
}
81+
catch (Exception e)
82+
{
83+
Console.WriteLine(" FAILED");
84+
Console.Error.WriteLine(e.Message);
85+
return 1;
86+
}
87+
}
88+
89+
private static async Task<IReadOnlyList<SecurityAdvisory>> FetchAdvisories(
90+
string token)
91+
{
92+
Console.Write("Fetching vulnerabilities from GitHub...");
93+
94+
var config = new GitHubVulnerabilities2DbConfiguration
95+
{
96+
GitHubPersonalAccessToken = token
97+
};
98+
99+
var queryService = new QueryService(
100+
config,
101+
new HttpClient());
102+
103+
var advisoryQueryService = new AdvisoryQueryService(
104+
queryService,
105+
new AdvisoryQueryBuilder(),
106+
NullLogger<AdvisoryQueryService>.Instance);
107+
108+
var advisories = await advisoryQueryService.GetAdvisoriesSinceAsync(DateTimeOffset.MinValue, CancellationToken.None);
109+
Console.WriteLine($" FOUND {advisories.Count} advisories.");
110+
return advisories;
111+
}
112+
113+
private static async Task VerifyPackageVulnerabilities(
114+
string connectionString,
115+
IReadOnlyList<SecurityAdvisory> advisories)
116+
{
117+
Console.WriteLine("Fetching vulnerabilities from DB...");
118+
119+
using (var sqlConnection = new SqlConnection(connectionString))
120+
{
121+
await sqlConnection.OpenAsync();
122+
using (var entitiesContext = new EntitiesContext(sqlConnection, readOnly: false))
123+
{
124+
var verifier = new PackageVulnerabilityServiceVerifier(entitiesContext);
125+
var ingestor = new AdvisoryIngestor(verifier, new GitHubVersionRangeParser());
126+
await ingestor.IngestAsync(advisories);
127+
128+
if (verifier.HasErrors)
129+
{
130+
throw new Exception("DB does not match GitHub API!");
131+
}
132+
133+
Console.WriteLine("DB matches GitHub API!");
134+
}
135+
}
136+
}
137+
138+
public class PackageVulnerabilityServiceVerifier : IPackageVulnerabilityService
139+
{
140+
private readonly IEntitiesContext _entitiesContext;
141+
142+
public PackageVulnerabilityServiceVerifier(
143+
IEntitiesContext entitiesContext)
144+
{
145+
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
146+
}
147+
148+
public void ApplyExistingVulnerabilitiesToPackage(Package package)
149+
{
150+
throw new NotImplementedException();
151+
}
152+
153+
public Task UpdateVulnerabilityAsync(PackageVulnerability vulnerability, bool withdrawn)
154+
{
155+
Console.WriteLine($"Verifying vulnerability {vulnerability.GitHubDatabaseKey}.");
156+
var existingVulnerability = _entitiesContext.Vulnerabilities
157+
.Include(v => v.AffectedRanges)
158+
.SingleOrDefault(v => v.GitHubDatabaseKey == vulnerability.GitHubDatabaseKey);
159+
160+
if (withdrawn || !vulnerability.AffectedRanges.Any())
161+
{
162+
if (existingVulnerability != null)
163+
{
164+
Console.Error.WriteLine(withdrawn ?
165+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey} was withdrawn and should not be in DB!" :
166+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey} affects no packages and should not be in DB!");
167+
HasErrors = true;
168+
}
169+
170+
return Task.CompletedTask;
171+
}
172+
173+
if (existingVulnerability == null)
174+
{
175+
Console.Error.WriteLine($"Cannot find vulnerability {vulnerability.GitHubDatabaseKey} in DB!");
176+
HasErrors = true;
177+
return Task.CompletedTask;
178+
}
179+
180+
if (existingVulnerability.Severity != vulnerability.Severity)
181+
{
182+
Console.Error.WriteLine(
183+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey
184+
}, severity does not match! GitHub: {vulnerability.Severity}, DB: {existingVulnerability.Severity}");
185+
HasErrors = true;
186+
}
187+
188+
if (existingVulnerability.AdvisoryUrl != vulnerability.AdvisoryUrl)
189+
{
190+
Console.Error.WriteLine(
191+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey
192+
}, advisory URL does not match! GitHub: {vulnerability.AdvisoryUrl}, DB: { existingVulnerability.AdvisoryUrl}");
193+
HasErrors = true;
194+
}
195+
196+
foreach (var range in vulnerability.AffectedRanges)
197+
{
198+
Console.WriteLine($"Verifying range affecting {range.PackageId} {range.PackageVersionRange}.");
199+
var existingRange = existingVulnerability.AffectedRanges
200+
.SingleOrDefault(r => r.PackageId == range.PackageId && r.PackageVersionRange == range.PackageVersionRange);
201+
202+
if (existingRange == null)
203+
{
204+
Console.Error.WriteLine(
205+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey
206+
}, cannot find range {range.PackageId} {range.PackageVersionRange} in DB!");
207+
HasErrors = true;
208+
continue;
209+
}
210+
211+
if (existingRange.FirstPatchedPackageVersion != range.FirstPatchedPackageVersion)
212+
{
213+
Console.Error.WriteLine(
214+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey
215+
}, range {range.PackageId} {range.PackageVersionRange}, first patched version does not match! GitHub: {
216+
range.FirstPatchedPackageVersion}, DB: {range.FirstPatchedPackageVersion}");
217+
HasErrors = true;
218+
}
219+
220+
var packages = _entitiesContext.Packages
221+
.Where(p => p.PackageRegistration.Id == range.PackageId)
222+
.Include(p => p.Vulnerabilities)
223+
.ToList();
224+
225+
var versionRange = VersionRange.Parse(range.PackageVersionRange);
226+
foreach (var package in packages)
227+
{
228+
var version = NuGetVersion.Parse(package.NormalizedVersion);
229+
if (versionRange.Satisfies(version) != package.Vulnerabilities.Contains(existingRange))
230+
{
231+
Console.Error.WriteLine(
232+
$@"Vulnerability advisory {vulnerability.GitHubDatabaseKey
233+
}, range {range.PackageId} {range.PackageVersionRange}, package {package.NormalizedVersion
234+
} is not properly marked vulnerable to vulnerability!");
235+
HasErrors = true;
236+
}
237+
}
238+
}
239+
240+
return Task.CompletedTask;
241+
}
242+
243+
public bool HasErrors { get; private set; }
244+
}
245+
}
246+
}

src/GalleryTools/GalleryTools.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<Compile Include="Commands\ReflowCommand.cs" />
5353
<Compile Include="Commands\UpdateIsLatestCommand.cs" />
5454
<Compile Include="Commands\VerifyApiKeyCommand.cs" />
55+
<Compile Include="Commands\VerifyGitHubVulnerabilitiesCommand.cs" />
5556
<Compile Include="Program.cs" />
5657
<Compile Include="Properties\AssemblyInfo.cs" />
5758
<Compile Include="Properties\AssemblyInfo.*.cs" />
@@ -61,6 +62,10 @@
6162
<None Include="App.config" />
6263
</ItemGroup>
6364
<ItemGroup>
65+
<ProjectReference Include="..\GitHubVulnerabilities2Db\GitHubVulnerabilities2Db.csproj">
66+
<Project>{26bb718a-e1c1-4e70-9008-fb8ee7a7f7e5}</Project>
67+
<Name>GitHubVulnerabilities2Db</Name>
68+
</ProjectReference>
6469
<ProjectReference Include="..\NuGet.Services.Entities\NuGet.Services.Entities.csproj">
6570
<Project>{6262f4fc-29be-4226-b676-db391c89d396}</Project>
6671
<Name>NuGet.Services.Entities</Name>

src/GalleryTools/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static int Main(params string[] args)
2121
commandLineApplication.Command("filldevdeps", BackfillDevelopmentDependencyCommand.Configure);
2222
commandLineApplication.Command("verifyapikey", VerifyApiKeyCommand.Configure);
2323
commandLineApplication.Command("updateIsLatest", UpdateIsLatestCommand.Configure);
24+
commandLineApplication.Command("verifyVulnerabilities", VerifyGitHubVulnerabilitiesCommand.Configure);
2425

2526
try
2627
{

src/GitHubVulnerabilities2Db/Collector/AdvisoryCollector.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public AdvisoryCollector(
3232

3333
public async Task<bool> ProcessAsync(CancellationToken token)
3434
{
35-
var advisories = await _queryService.GetAdvisoriesSinceAsync(_cursor, token);
35+
await _cursor.Load(token);
36+
var lastUpdated = _cursor.Value;
37+
var advisories = await _queryService.GetAdvisoriesSinceAsync(lastUpdated, token);
3638
var hasAdvisories = advisories != null && advisories.Any();
3739
_logger.LogInformation("Found {AdvisoryCount} new advisories to process", advisories?.Count() ?? 0);
3840
if (hasAdvisories)

src/GitHubVulnerabilities2Db/Collector/AdvisoryQueryService.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Threading.Tasks;
99
using GitHubVulnerabilities2Db.GraphQL;
1010
using Microsoft.Extensions.Logging;
11-
using NuGet.Services.Cursor;
1211

1312
namespace GitHubVulnerabilities2Db.Collector
1413
{
@@ -28,10 +27,8 @@ public AdvisoryQueryService(
2827
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
2928
}
3029

31-
public async Task<IReadOnlyList<SecurityAdvisory>> GetAdvisoriesSinceAsync(ReadCursor<DateTimeOffset> cursor, CancellationToken token)
30+
public async Task<IReadOnlyList<SecurityAdvisory>> GetAdvisoriesSinceAsync(DateTimeOffset lastUpdated, CancellationToken token)
3231
{
33-
await cursor.Load(token);
34-
var lastUpdated = cursor.Value;
3532
_logger.LogInformation("Fetching advisories updated since {LastUpdated}", lastUpdated);
3633
var firstQuery = _queryBuilder.CreateSecurityAdvisoriesQuery(lastUpdated);
3734
var firstResponse = await _queryService.QueryAsync(firstQuery, token);

src/GitHubVulnerabilities2Db/Collector/IAdvisoryQueryService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ namespace GitHubVulnerabilities2Db.Collector
1515
/// </summary>
1616
public interface IAdvisoryQueryService
1717
{
18-
Task<IReadOnlyList<SecurityAdvisory>> GetAdvisoriesSinceAsync(ReadCursor<DateTimeOffset> cursor, CancellationToken token);
18+
Task<IReadOnlyList<SecurityAdvisory>> GetAdvisoriesSinceAsync(DateTimeOffset lastUpdated, CancellationToken token);
1919
}
2020
}

src/GitHubVulnerabilities2Db/Job.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
using System.Net.Http;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using System.Web;
89
using Autofac;
910
using GitHubVulnerabilities2Db.Collector;
1011
using GitHubVulnerabilities2Db.Configuration;
1112
using GitHubVulnerabilities2Db.Gallery;
1213
using GitHubVulnerabilities2Db.GraphQL;
1314
using GitHubVulnerabilities2Db.Ingest;
15+
using Microsoft.ApplicationInsights.Extensibility;
1416
using Microsoft.Extensions.Configuration;
1517
using Microsoft.Extensions.DependencyInjection;
1618
using Microsoft.Extensions.Options;
@@ -21,6 +23,8 @@
2123
using NuGet.Services.Storage;
2224
using NuGetGallery;
2325
using NuGetGallery.Auditing;
26+
using NuGetGallery.Configuration;
27+
using NuGetGallery.Diagnostics;
2428
using NuGetGallery.Security;
2529

2630
namespace GitHubVulnerabilities2Db
@@ -109,6 +113,19 @@ protected void ConfigureGalleryServices(ContainerBuilder containerBuilder)
109113
containerBuilder
110114
.RegisterType<PackageUpdateService>()
111115
.As<IPackageUpdateService>();
116+
117+
containerBuilder.RegisterType<AppConfiguration>()
118+
.As<IAppConfiguration>()
119+
.SingleInstance();
120+
121+
var contentService = new FakeContentService();
122+
containerBuilder.RegisterInstance(contentService)
123+
.As<IContentService>()
124+
.SingleInstance();
125+
126+
containerBuilder.RegisterType<ContentObjectService>()
127+
.As<IContentObjectService>()
128+
.SingleInstance();
112129
}
113130

114131
protected void ConfigureQueryServices(ContainerBuilder containerBuilder)
@@ -165,4 +182,18 @@ private DurableCursor CreateCursor(IComponentContext ctx, Func<GitHubVulnerabili
165182
return new DurableCursor(storage.ResolveUri(getBlobName(config)), storage, DateTimeOffset.MinValue);
166183
}
167184
}
185+
186+
public class FakeContentService : IContentService
187+
{
188+
public void ClearCache()
189+
{
190+
//no-op
191+
}
192+
193+
public Task<IHtmlString> GetContentItemAsync(string name, TimeSpan expiresIn)
194+
{
195+
// no-op
196+
return Task.FromResult((IHtmlString)null);
197+
}
198+
}
168199
}

0 commit comments

Comments
 (0)