Skip to content

Commit

Permalink
Add tool to determine if a .nupkg has valid Microsoft metadata (NuGet…
Browse files Browse the repository at this point in the history
…#7834)

New version of NuGet#7000
  • Loading branch information
joelverhagen committed Feb 4, 2020
1 parent 62eb1e4 commit 4ac9939
Show file tree
Hide file tree
Showing 22 changed files with 1,924 additions and 110 deletions.
14 changes: 14 additions & 0 deletions NuGetGallery.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Entities", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Entities.Tests", "tests\NuGet.Services.Entities.Tests\NuGet.Services.Entities.Tests.csproj", "{79C831E9-7C88-4B98-B084-4DE940C73FC7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VerifyMicrosoftPackage", "src\VerifyMicrosoftPackage\VerifyMicrosoftPackage.csproj", "{C57292F4-9661-4AEB-AD89-C82CF1152260}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VerifyMicrosoftPackage.Facts", "tests\VerifyMicrosoftPackage.Facts\VerifyMicrosoftPackage.Facts.csproj", "{FFF38843-B8E1-40E7-95E4-E3346599E8D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseMigrationTools", "src\DatabaseMigrationTools\DatabaseMigrationTools.csproj", "{DCED7162-A24C-4579-96C8-544BFAF4C305}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.DatabaseMigration", "src\NuGet.Services.DatabaseMigration\NuGet.Services.DatabaseMigration.csproj", "{F4C8C34F-72A9-4773-A315-8FA3F2D5CE4E}"
Expand Down Expand Up @@ -80,6 +84,14 @@ Global
{79C831E9-7C88-4B98-B084-4DE940C73FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79C831E9-7C88-4B98-B084-4DE940C73FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79C831E9-7C88-4B98-B084-4DE940C73FC7}.Release|Any CPU.Build.0 = Release|Any CPU
{C57292F4-9661-4AEB-AD89-C82CF1152260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C57292F4-9661-4AEB-AD89-C82CF1152260}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C57292F4-9661-4AEB-AD89-C82CF1152260}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C57292F4-9661-4AEB-AD89-C82CF1152260}.Release|Any CPU.Build.0 = Release|Any CPU
{FFF38843-B8E1-40E7-95E4-E3346599E8D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FFF38843-B8E1-40E7-95E4-E3346599E8D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFF38843-B8E1-40E7-95E4-E3346599E8D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFF38843-B8E1-40E7-95E4-E3346599E8D5}.Release|Any CPU.Build.0 = Release|Any CPU
{DCED7162-A24C-4579-96C8-544BFAF4C305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCED7162-A24C-4579-96C8-544BFAF4C305}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCED7162-A24C-4579-96C8-544BFAF4C305}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -124,6 +136,8 @@ Global
{C5849063-8CDC-4561-BA5C-7D97BD905DC3} = {2204C510-A559-4ED7-9590-FDC09093575B}
{6262F4FC-29BE-4226-B676-DB391C89D396} = {155100FF-524B-4CAF-93C6-A57478B3DBAD}
{79C831E9-7C88-4B98-B084-4DE940C73FC7} = {39E54EC3-CBAA-453A-BE64-748FE1559A58}
{C57292F4-9661-4AEB-AD89-C82CF1152260} = {2204C510-A559-4ED7-9590-FDC09093575B}
{FFF38843-B8E1-40E7-95E4-E3346599E8D5} = {39E54EC3-CBAA-453A-BE64-748FE1559A58}
{DCED7162-A24C-4579-96C8-544BFAF4C305} = {2204C510-A559-4ED7-9590-FDC09093575B}
{F4C8C34F-72A9-4773-A315-8FA3F2D5CE4E} = {155100FF-524B-4CAF-93C6-A57478B3DBAD}
{082357A1-682E-4CCC-8FCD-FA250204CDB6} = {39E54EC3-CBAA-453A-BE64-748FE1559A58}
Expand Down
9 changes: 7 additions & 2 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ param (
[string]$PackageSuffix,
[string]$Branch,
[string]$CommitSHA,
[string]$BuildBranch = 'd298565f387e93995a179ef8ae6838f1be37904f'
[string]$BuildBranch = 'd298565f387e93995a179ef8ae6838f1be37904f',
[string]$VerifyMicrosoftPackageVersion = $null
)

Set-StrictMode -Version 1.0
Expand Down Expand Up @@ -85,7 +86,8 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' {
(Join-Path $PSScriptRoot "src\DatabaseMigrationTools\Properties\AssemblyInfo.g.cs"), `
(Join-Path $PSScriptRoot "src\AccountDeleter\Properties\AssemblyInfo.g.cs"), `
(Join-Path $PSScriptRoot "src\GitHubVulnerabilities2Db\Properties\AssemblyInfo.g.cs"), `
(Join-Path $PSScriptRoot "src\GalleryTools\Properties\AssemblyInfo.g.cs")
(Join-Path $PSScriptRoot "src\GalleryTools\Properties\AssemblyInfo.g.cs"), `
(Join-Path $PSScriptRoot "src\VerifyMicrosoftPackage\Properties\AssemblyInfo.g.cs")

Foreach ($Path in $Paths) {
Set-VersionInfo -Path $Path -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA
Expand Down Expand Up @@ -114,6 +116,9 @@ Invoke-BuildStep 'Creating artifacts' { `
New-Package (Join-Path $PSScriptRoot "src\AccountDeleter\Gallery.AccountDeleter.nuspec") -Configuration $Configuration -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch -MSBuildVersion "15"
New-Package (Join-Path $PSScriptRoot "src\GitHubVulnerabilities2Db\GitHubVulnerabilities2Db.nuspec") -Configuration $Configuration -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch -MSBuildVersion "15"
New-Package (Join-Path $PSScriptRoot "src\GalleryTools\Gallery.GalleryTools.nuspec") -Configuration $Configuration -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch -MSBuildVersion "15"

if (!$VerifyMicrosoftPackageVersion) { $VerifyMicrosoftPackageVersion = $SemanticVersion }
New-Package (Join-Path $PSScriptRoot "src\VerifyMicrosoftPackage\VerifyMicrosoftPackage.nuspec") -Configuration $Configuration -BuildNumber $BuildNumber -Version $VerifyMicrosoftPackageVersion -Branch $Branch -MSBuildVersion "15"
} `
-ev +BuildErrors

Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery.Services/NuGetGallery.Services.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@
<Compile Include="Security\RequireMinProtocolVersionForPushPolicy.cs" />
<Compile Include="Security\RequireOrganizationTenantPolicy.cs" />
<Compile Include="Security\RequirePackageMetadataCompliancePolicy.cs" />
<Compile Include="Security\RequirePackageMetadataComplianceUtility.cs" />
<Compile Include="Security\RequirePackageMetadataState.cs" />
<Compile Include="Security\RequirePackageVerifyScopePolicy.cs" />
<Compile Include="Security\SecurityPolicyAction.cs" />
<Compile Include="Security\SecurityPolicyHandler.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override async Task<SecurityPolicyResult> EvaluateAsync(PackageSecurityPo

// This particular package policy assumes the existence of a particular user.
// Succeed silently (effectively ignoring this policy when enabled) when that user does not exist.
var state = GetState(context);
var state = RequirePackageMetadataComplianceUtility.DeserializeState(context.Policies);
var requiredCoOwner = context.UserService.FindByUsername(state.RequiredCoOwnerUsername);
if (requiredCoOwner == null)
{
Expand All @@ -49,7 +49,7 @@ public override async Task<SecurityPolicyResult> EvaluateAsync(PackageSecurityPo
}

// Evaluate package metadata validations
if (!IsPackageMetadataCompliant(context.Package, state, out var complianceFailures))
if (!RequirePackageMetadataComplianceUtility.IsPackageMetadataCompliant(context.Package, state, out var complianceFailures))
{
context.TelemetryService.TrackPackageMetadataComplianceError(
context.Package.Id,
Expand Down Expand Up @@ -101,7 +101,7 @@ public static UserSecurityPolicy CreatePolicy(
bool isProjectUrlRequired,
string errorMessageFormat)
{
var value = JsonConvert.SerializeObject(new State()
var value = JsonConvert.SerializeObject(new RequirePackageMetadataState
{
RequiredCoOwnerUsername = requiredCoOwnerUsername,
AllowedCopyrightNotices = allowedCopyrightNotices,
Expand All @@ -113,109 +113,5 @@ public static UserSecurityPolicy CreatePolicy(

return new UserSecurityPolicy(PolicyName, subscription, value);
}

private bool IsPackageMetadataCompliant(Package package, State state, out IList<string> complianceFailures)
{
complianceFailures = new List<string>();

// Author validation
ValidatePackageAuthors(package, state, complianceFailures);

// Copyright validation
if (!state.AllowedCopyrightNotices.Contains(package.Copyright))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_CopyrightNotCompliant);
}

// LicenseUrl validation
if (state.IsLicenseUrlRequired && string.IsNullOrWhiteSpace(package.LicenseUrl))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_RequiredLicenseUrlMissing);
}

// ProjectUrl validation
if (state.IsProjectUrlRequired && string.IsNullOrWhiteSpace(package.ProjectUrl))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_RequiredProjectUrlMissing);
}

return !complianceFailures.Any();
}

private static void ValidatePackageAuthors(Package package, State state, IList<string> complianceFailures)
{
var packageAuthors = package.FlattenedAuthors
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToList();

// Check for duplicate entries
var duplicateAuthors = packageAuthors
.GroupBy(x => x)
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToList();

if (duplicateAuthors.Any())
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_PackageAuthorDuplicatesNotAllowed, string.Join(",", duplicateAuthors)));
}
else
{
if (state.AllowedAuthors?.Length > 0)
{
foreach (var packageAuthor in packageAuthors)
{
if (!state.AllowedAuthors.Contains(packageAuthor))
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_PackageAuthorNotAllowed, packageAuthor));
}
}
}
else
{
// No list of allowed authors is defined for this policy.
// We require the required co-owner to be defined as the only package author.
if (packageAuthors.Count() > 1 || packageAuthors.Single() != state.RequiredCoOwnerUsername)
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_RequiredAuthorMissing, state.RequiredCoOwnerUsername));
}
}
}
}

/// <summary>
/// Retrieve the policy state.
/// </summary>
private State GetState(UserSecurityPolicyEvaluationContext context)
{
var policyStates = context.Policies
.Where(p => !string.IsNullOrEmpty(p.Value))
.Select(p => JsonConvert.DeserializeObject<State>(p.Value));

// TODO: what if there are multiple?
return policyStates.First();
}

public class State
{
[JsonProperty("u")]
public string RequiredCoOwnerUsername { get; set; }

[JsonProperty("copy")]
public string[] AllowedCopyrightNotices { get; set; }

[JsonProperty("authors")]
public string[] AllowedAuthors { get; set; }

[JsonProperty("licUrlReq")]
public bool IsLicenseUrlRequired { get; set; }

[JsonProperty("projUrlReq")]
public bool IsProjectUrlRequired { get; set; }

[JsonProperty("error")]
public string ErrorMessageFormat { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Globalization;
using System.Linq;
using Newtonsoft.Json;
using NuGet.Services.Entities;

namespace NuGetGallery.Security
{
public static class RequirePackageMetadataComplianceUtility
{
/// <summary>
/// Retrieve the policy state.
/// </summary>
public static RequirePackageMetadataState DeserializeState(IEnumerable<UserSecurityPolicy> policies)
{
var policyStates = policies
.Where(p => !string.IsNullOrEmpty(p.Value))
.Select(p => JsonConvert.DeserializeObject<RequirePackageMetadataState>(p.Value));

// TODO: what if there are multiple?
return policyStates.First();
}

public static bool IsPackageMetadataCompliant(Package package, RequirePackageMetadataState state, out IList<string> complianceFailures)
{
complianceFailures = new List<string>();

// Author validation
ValidatePackageAuthors(package, state, complianceFailures);

// Copyright validation
if (!state.AllowedCopyrightNotices.Contains(package.Copyright))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_CopyrightNotCompliant);
}

// LicenseUrl validation
if (state.IsLicenseUrlRequired && string.IsNullOrWhiteSpace(package.LicenseUrl))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_RequiredLicenseUrlMissing);
}

// ProjectUrl validation
if (state.IsProjectUrlRequired && string.IsNullOrWhiteSpace(package.ProjectUrl))
{
complianceFailures.Add(ServicesStrings.SecurityPolicy_RequiredProjectUrlMissing);
}

return !complianceFailures.Any();
}

private static void ValidatePackageAuthors(Package package, RequirePackageMetadataState state, IList<string> complianceFailures)
{
var packageAuthors = package.FlattenedAuthors
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToList();

// Check for duplicate entries
var duplicateAuthors = packageAuthors
.GroupBy(x => x)
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToList();

if (duplicateAuthors.Any())
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_PackageAuthorDuplicatesNotAllowed, string.Join(",", duplicateAuthors)));
}
else
{
if (state.AllowedAuthors?.Length > 0)
{
foreach (var packageAuthor in packageAuthors)
{
if (!state.AllowedAuthors.Contains(packageAuthor))
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_PackageAuthorNotAllowed, packageAuthor));
}
}
}
else
{
// No list of allowed authors is defined for this policy.
// We require the required co-owner to be defined as the only package author.
if (packageAuthors.Count() > 1 || packageAuthors.Single() != state.RequiredCoOwnerUsername)
{
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, ServicesStrings.SecurityPolicy_RequiredAuthorMissing, state.RequiredCoOwnerUsername));
}
}
}
}
}
}
28 changes: 28 additions & 0 deletions src/NuGetGallery.Services/Security/RequirePackageMetadataState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Newtonsoft.Json;

namespace NuGetGallery.Security
{
public class RequirePackageMetadataState
{
[JsonProperty("u")]
public string RequiredCoOwnerUsername { get; set; }

[JsonProperty("copy")]
public string[] AllowedCopyrightNotices { get; set; }

[JsonProperty("authors")]
public string[] AllowedAuthors { get; set; }

[JsonProperty("licUrlReq")]
public bool IsLicenseUrlRequired { get; set; }

[JsonProperty("projUrlReq")]
public bool IsProjectUrlRequired { get; set; }

[JsonProperty("error")]
public string ErrorMessageFormat { get; set; }
}
}
6 changes: 6 additions & 0 deletions src/VerifyMicrosoftPackage/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
</startup>
</configuration>
Loading

0 comments on commit 4ac9939

Please sign in to comment.