Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dotnet list package --vulnerable uses AuditSources #6237

Merged
merged 15 commits into from
Feb 7, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class ListPackageArgs
public bool HighestPatch { get; }
public bool HighestMinor { get; }
public CancellationToken CancellationToken { get; }
public IReadOnlyList<PackageSource> AuditSources { get; }

/// <summary>
/// A constructor for the arguments of list package
Expand All @@ -41,6 +42,7 @@ internal class ListPackageArgs
/// <param name="prerelease"> Bool for --include-prerelease present </param>
/// <param name="highestPatch"> Bool for --highest-patch present </param>
/// <param name="highestMinor"> Bool for --highest-minor present </param>
/// <param name="auditSources"> A list of sources for performing vulnerability auditing</param>
/// <param name="logger"></param>
/// <param name="cancellationToken"></param>
public ListPackageArgs(
Expand All @@ -53,6 +55,7 @@ public ListPackageArgs(
bool prerelease,
bool highestPatch,
bool highestMinor,
IReadOnlyList<PackageSource> auditSources,
ILogger logger,
CancellationToken cancellationToken)
{
Expand All @@ -65,6 +68,7 @@ public ListPackageArgs(
Prerelease = prerelease;
HighestPatch = highestPatch;
HighestMinor = highestMinor;
AuditSources = auditSources;
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
CancellationToken = cancellationToken;
ArgumentText = GetReportParameters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public static void Register(
isVulnerable: vulnerableReport.HasValue());

IReportRenderer reportRenderer = GetOutputType(outputFormat.Value(), outputVersionOption: outputVersion.Value());

var provider = new PackageSourceProvider(settings);
var packageRefArgs = new ListPackageArgs(
path.Value,
packageSources,
Expand All @@ -140,6 +140,7 @@ public static void Register(
prerelease.HasValue(),
highestPatch.HasValue(),
highestMinor.HasValue(),
provider.LoadAuditSources(),
logger,
CancellationToken.None);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
using NuGet.ProjectModel;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Protocol.Model;
using NuGet.Protocol.Providers;
using NuGet.Protocol.Resources;
using NuGet.Versioning;

namespace NuGet.CommandLine.XPlat
Expand Down Expand Up @@ -101,105 +104,254 @@ private async Task GetProjectMetadataAsync(

var assetsPath = project.GetPropertyValue(ProjectAssetsFile);

if (!File.Exists(assetsPath))
if (!IsProjectAssetsFileValid(assetsPath, projectPath, projectModel, out LockFile assetsFile))
{
projectModel.AddProjectInformation(ProblemType.Error,
string.Format(CultureInfo.CurrentCulture, Strings.Error_AssetsFileNotFound, projectPath));
return;
}
else

List<FrameworkPackages> frameworks;

try
{
var lockFileFormat = new LockFileFormat();
LockFile assetsFile = lockFileFormat.Read(assetsPath);
frameworks = MSBuildAPIUtility.GetResolvedVersions(project, listPackageArgs.Frameworks, assetsFile, listPackageArgs.IncludeTransitive);
}
catch (InvalidOperationException ex)
{
projectModel.AddProjectInformation(ProblemType.Error, ex.Message);
return;
}

// Assets file validation
if (assetsFile.PackageSpec != null &&
assetsFile.Targets != null &&
assetsFile.Targets.Count != 0)
if (frameworks.Count > 0)
{
if (listPackageArgs.ReportType != ReportType.Default) // generic list package is offline -- no server lookups
{
// Get all the packages that are referenced in a project
List<FrameworkPackages> frameworks;
try
{
frameworks = MSBuildAPIUtility.GetResolvedVersions(project, listPackageArgs.Frameworks, assetsFile, listPackageArgs.IncludeTransitive);
}
catch (InvalidOperationException ex)
WarnForHttpSources(listPackageArgs, projectModel);

if (listPackageArgs.ReportType == ReportType.Vulnerable && listPackageArgs.AuditSources != null && listPackageArgs.AuditSources.Count > 0)
{
projectModel.AddProjectInformation(ProblemType.Error, ex.Message);
await GetVulnerabilitiesFromAuditSourcesAsync(listPackageArgs, listPackageReportModel, projectModel, frameworks);
return;
}

if (frameworks.Count > 0)
{
if (listPackageArgs.ReportType != ReportType.Default) // generic list package is offline -- no server lookups
{
WarnForHttpSources(listPackageArgs, projectModel);
var metadata = await GetPackageMetadataAsync(frameworks, listPackageArgs);
await UpdatePackagesWithSourceMetadata(frameworks, metadata, listPackageArgs);
}
var metadata = await GetPackageMetadataAsync(frameworks, listPackageArgs);
await UpdatePackagesWithSourceMetadata(frameworks, metadata, listPackageArgs);
}

bool printPackages = FilterPackages(frameworks, listPackageArgs);
printPackages = printPackages || ReportType.Default == listPackageArgs.ReportType;
if (printPackages)
{
var hasAutoReference = false;
List<ListPackageReportFrameworkPackage> projectFrameworkPackages = ProjectPackagesPrintUtility.GetPackagesMetadata(frameworks, listPackageArgs, ref hasAutoReference);
projectModel.TargetFrameworkPackages = projectFrameworkPackages;
projectModel.AutoReferenceFound = hasAutoReference;
}
else
{
projectModel.TargetFrameworkPackages = new List<ListPackageReportFrameworkPackage>();
}
}
bool printPackages = FilterPackages(frameworks, listPackageArgs) || ReportType.Default == listPackageArgs.ReportType;

if (printPackages)
{
var hasAutoReference = false;
List<ListPackageReportFrameworkPackage> projectFrameworkPackages = ProjectPackagesPrintUtility.GetPackagesMetadata(frameworks, listPackageArgs, ref hasAutoReference);
projectModel.TargetFrameworkPackages = projectFrameworkPackages;
projectModel.AutoReferenceFound = hasAutoReference;
}
else
{
projectModel.AddProjectInformation(ProblemType.Error,
string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsPath));
projectModel.TargetFrameworkPackages = new List<ListPackageReportFrameworkPackage>();
}
}
}

private static async Task GetVulnerabilitiesFromAuditSourcesAsync(
ListPackageArgs listPackageArgs,
ListPackageReportModel listPackageReportModel,
ListPackageProjectModel projectModel,
List<FrameworkPackages> frameworks)
{
List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities = await GetVulnerabilityData(
projectModel,
listPackageReportModel,
listPackageArgs.AuditSources,
listPackageArgs.Logger,
listPackageArgs.CancellationToken);

// Unload project
ProjectCollection.GlobalProjectCollection.UnloadProject(project);
foreach (var frameworkPackages in frameworks)
{
var frameworkPackage = new ListPackageReportFrameworkPackage(frameworkPackages.Framework)
{
TransitivePackages = new List<ListReportPackage>(),
TopLevelPackages = new List<ListReportPackage>()
};

ProcessPackages(frameworkPackages.TopLevelPackages, vulnerabilities, frameworkPackage.TopLevelPackages);
ProcessPackages(frameworkPackages.TransitivePackages, vulnerabilities, frameworkPackage.TransitivePackages);

projectModel.TargetFrameworkPackages ??= new List<ListPackageReportFrameworkPackage>();
projectModel.TargetFrameworkPackages.Add(frameworkPackage);
}
}

private static void WarnForHttpSources(ListPackageArgs listPackageArgs, ListPackageProjectModel projectModel)
private static void ProcessPackages(
IEnumerable<InstalledPackageReference> packages,
List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities,
List<ListReportPackage> reportPackages)
{
List<PackageSource> httpPackageSources = null;
foreach (PackageSource packageSource in listPackageArgs.PackageSources)
foreach (var package in packages)
{
if (packageSource.IsHttp && !packageSource.IsHttps && !packageSource.AllowInsecureConnections)
var vuln = GetPackageVulnerabilities(
vulnerabilities,
package.Name,
package.ResolvedPackageMetadata.Identity.Version.ToNormalizedString()
).ToList();

if (vuln != null && vuln.Count > 0)
{
if (httpPackageSources == null)
{
httpPackageSources = new();
}
httpPackageSources.Add(packageSource);
reportPackages.Add(
new ListReportPackage(
package.Name,
package.ResolvedPackageMetadata.Identity.Version.ToString(),
vuln));
}
}
}

if (httpPackageSources != null && httpPackageSources.Count != 0)
private static bool IsProjectAssetsFileValid(string assetsPath, string projectPath, ListPackageProjectModel projectModel, out LockFile assetsFile)
{
assetsFile = null;

if (!File.Exists(assetsPath))
{
if (httpPackageSources.Count == 1)
projectModel.AddProjectInformation(ProblemType.Error,
string.Format(CultureInfo.CurrentCulture, Strings.Error_AssetsFileNotFound, projectPath));
return false;
}
else
{
var lockFileFormat = new LockFileFormat();
assetsFile = lockFileFormat.Read(assetsPath);

// Assets file validation
if (assetsFile.PackageSpec == null ||
assetsFile.Targets == null ||
assetsFile.Targets.Count == 0)
{
projectModel.AddProjectInformation(
ProblemType.Warning,
string.Format(CultureInfo.CurrentCulture,
Strings.Warning_HttpServerUsage,
"list package",
httpPackageSources[0]));
projectModel.AddProjectInformation(ProblemType.Error,
string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsPath));
return false;
}
else
{
return true;
}
}
}

private static async Task<List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>>> GetVulnerabilityData(
ListPackageProjectModel projectModel,
ListPackageReportModel reportModel,
IReadOnlyList<PackageSource> sources,
ILogger logger,
CancellationToken cancellationToken)
{
var vulnerabilityInfo = new List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>>();

foreach (var source in sources)
{
if (!await TryAddSourceVulnerabilityInfo(source, reportModel, logger, cancellationToken, vulnerabilityInfo))
{
projectModel.AddProjectInformation(
ProblemType.Warning,
string.Format(CultureInfo.CurrentCulture,
Strings.Warning_HttpServerUsage_MultipleSources,
"list package",
Environment.NewLine + string.Join(Environment.NewLine, httpPackageSources.Select(e => e.Name))));
string.Format(CultureInfo.CurrentCulture, Strings.Warning_AuditSourceWithoutData, source.Name)
);
}
}

return vulnerabilityInfo;
}

private static async Task<bool> TryAddSourceVulnerabilityInfo(
PackageSource source,
ListPackageReportModel reportModel,
ILogger logger,
CancellationToken cancellationToken,
List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilityInfo)
{
var repository = Repository.Factory.GetCoreV3(source);
var vulnerabilityProvider = new VulnerabilityInfoResourceV3Provider();
var (isCreated, resource) = await vulnerabilityProvider.TryCreate(repository, cancellationToken);

if (!isCreated || resource is not VulnerabilityInfoResourceV3 vulnerabilityResource)
{
return false;
}

reportModel.SourcesUsed.Add(source);

var vulnerabilityInfoResult = await vulnerabilityResource.GetVulnerabilityInfoAsync(
new SourceCacheContext(),
logger,
cancellationToken
);

if (vulnerabilityInfoResult?.KnownVulnerabilities != null)
{
vulnerabilityInfo.AddRange(vulnerabilityInfoResult.KnownVulnerabilities);
}

return true;
}

private static IEnumerable<PackageVulnerabilityMetadata> GetPackageVulnerabilities(
IEnumerable<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities,
string id,
string version)
{
if (vulnerabilities == null)
{
return Enumerable.Empty<PackageVulnerabilityMetadata>();
}

var parsedVersion = new NuGetVersion(version);
foreach (var vulnFile in vulnerabilities)
{
if (vulnFile.TryGetValue(id, out IReadOnlyList<PackageVulnerabilityInfo> vulnPackages) && vulnPackages != null)
{
return vulnPackages
.Where(package => package.Versions.Satisfies(parsedVersion))
.Select(v => JsonExtensions.FromJson<PackageVulnerabilityMetadata>($"{{ \"AdvisoryUrl\": \"{v.Url}\", \"Severity\": \"{(int)v.Severity}\" }}"))
.ToList();
}
}

return Enumerable.Empty<PackageVulnerabilityMetadata>();
}

private static void WarnForHttpSources(
ListPackageArgs listPackageArgs,
ListPackageProjectModel projectModel)
{
var httpPackageSources = new List<PackageSource>();

AddHttpPackageSources(listPackageArgs.PackageSources, httpPackageSources);
AddHttpPackageSources(listPackageArgs.AuditSources, httpPackageSources);

if (httpPackageSources.Count == 0)
{
return;
}

string warningMessage = httpPackageSources.Count == 1
? string.Format(CultureInfo.CurrentCulture, Strings.Warning_HttpServerUsage, "list package", httpPackageSources[0])
: string.Format(CultureInfo.CurrentCulture, Strings.Warning_HttpServerUsage_MultipleSources, "list package", Environment.NewLine + string.Join(Environment.NewLine, httpPackageSources.Select(e => e.Name)));

projectModel.AddProjectInformation(ProblemType.Warning, warningMessage);
}

private static void AddHttpPackageSources(IEnumerable<PackageSource> packageSources, List<PackageSource> httpPackageSources)
{
if (packageSources == null)
{
return;
}

foreach (var packageSource in packageSources)
{
if (packageSource.IsHttp && !packageSource.IsHttps && !packageSource.AllowInsecureConnections)
{
httpPackageSources.Add(packageSource);
}
}
}

public static bool FilterPackages(IEnumerable<FrameworkPackages> packages, ListPackageArgs listPackageArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,18 @@ private void WriteToConsole(ListPackageReportModel listPackageReportModel)
return;
}

WriteSources(_consoleOut, listPackageReportModel.ListPackageArgs);
if (listPackageReportModel.ListPackageArgs.ReportType == ReportType.Vulnerable && listPackageReportModel.SourcesUsed.Count > 0)
{
_consoleOut.WriteLine();
_consoleOut.WriteLine(Strings.ListPkg_SourcesUsedDescription);
PrintSources(_consoleOut, listPackageReportModel.SourcesUsed);
_consoleOut.WriteLine();
}
else
{
WriteSources(_consoleOut, listPackageReportModel.ListPackageArgs);
}

WriteProjects(_consoleOut, _consoleError, listPackageReportModel.Projects, listPackageReportModel.ListPackageArgs);

// Print a legend message for auto-reference markers used
Expand Down
Loading
Loading