Skip to content

Commit 453da63

Browse files
committed
Nuget props detector
1 parent 2fd5d4f commit 453da63

File tree

6 files changed

+491
-0
lines changed

6 files changed

+491
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
namespace Microsoft.ComponentDetection.Detectors.NuGet;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using System.Xml;
9+
using System.Xml.Linq;
10+
using Microsoft.ComponentDetection.Contracts;
11+
using Microsoft.ComponentDetection.Contracts.Internal;
12+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
13+
using Microsoft.Extensions.Logging;
14+
15+
/// <summary>
16+
/// Detects NuGet packages in Central Package Management files (Directory.Packages.props, packages.props, package.props).
17+
/// </summary>
18+
public sealed class NuGetCentralPackageManagementDetector : FileComponentDetector, IExperimentalDetector
19+
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="NuGetCentralPackageManagementDetector"/> class.
22+
/// </summary>
23+
/// <param name="componentStreamEnumerableFactory">The factory for handing back component streams to File detectors.</param>
24+
/// <param name="walkerFactory">The factory for creating directory walkers.</param>
25+
/// <param name="logger">The logger to use.</param>
26+
public NuGetCentralPackageManagementDetector(
27+
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
28+
IObservableDirectoryWalkerFactory walkerFactory,
29+
ILogger<NuGetCentralPackageManagementDetector> logger)
30+
{
31+
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
32+
this.Scanner = walkerFactory;
33+
this.Logger = logger;
34+
}
35+
36+
/// <inheritdoc />
37+
public override IList<string> SearchPatterns => ["Directory.Packages.props", "packages.props", "package.props"];
38+
39+
/// <inheritdoc />
40+
public override string Id => "NuGetCentralPackageManagement";
41+
42+
/// <inheritdoc />
43+
public override IEnumerable<string> Categories =>
44+
[Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet)];
45+
46+
/// <inheritdoc />
47+
public override IEnumerable<ComponentType> SupportedComponentTypes => [ComponentType.NuGet];
48+
49+
/// <inheritdoc />
50+
public override int Version => 1;
51+
52+
/// <inheritdoc />
53+
protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
54+
{
55+
try
56+
{
57+
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
58+
var propsDocument = XDocument.Load(processRequest.ComponentStream.Stream);
59+
60+
// Check if this is a Central Package Management file
61+
if (!this.IsCentralPackageManagementFile(propsDocument))
62+
{
63+
this.Logger.LogDebug("File {File} is not a Central Package Management file", processRequest.ComponentStream.Location);
64+
return Task.CompletedTask;
65+
}
66+
67+
// Parse PackageVersion elements
68+
var packageVersionElements = propsDocument.Descendants("PackageVersion");
69+
foreach (var packageElement in packageVersionElements)
70+
{
71+
var packageId = packageElement.Attribute("Include")?.Value;
72+
var version = packageElement.Attribute("Version")?.Value;
73+
74+
if (string.IsNullOrWhiteSpace(packageId) || string.IsNullOrWhiteSpace(version))
75+
{
76+
this.Logger.LogDebug("Skipping PackageVersion element with missing Include or Version attribute in {File}", processRequest.ComponentStream.Location);
77+
continue;
78+
}
79+
80+
var detectedComponent = new DetectedComponent(
81+
new NuGetComponent(packageId, version));
82+
83+
// All packages in Central Package Management files are explicitly referenced
84+
// since they define the centrally managed versions
85+
singleFileComponentRecorder.RegisterUsage(detectedComponent, true, null, isDevelopmentDependency: false);
86+
87+
this.Logger.LogDebug(
88+
"Detected NuGet package {PackageId} version {Version} in Central Package Management file {File}",
89+
packageId,
90+
version,
91+
processRequest.ComponentStream.Location);
92+
}
93+
94+
// Parse GlobalPackageReference elements
95+
var globalPackageElements = propsDocument.Descendants("GlobalPackageReference");
96+
foreach (var packageElement in globalPackageElements)
97+
{
98+
var packageId = packageElement.Attribute("Include")?.Value;
99+
var version = packageElement.Attribute("Version")?.Value;
100+
101+
if (string.IsNullOrWhiteSpace(packageId) || string.IsNullOrWhiteSpace(version))
102+
{
103+
this.Logger.LogDebug("Skipping GlobalPackageReference element with missing Include or Version attribute in {File}", processRequest.ComponentStream.Location);
104+
continue;
105+
}
106+
107+
var detectedComponent = new DetectedComponent(
108+
new NuGetComponent(packageId, version));
109+
110+
// Global package references are explicitly referenced and typically development dependencies
111+
singleFileComponentRecorder.RegisterUsage(detectedComponent, true, null, isDevelopmentDependency: true);
112+
113+
this.Logger.LogDebug(
114+
"Detected global NuGet package {PackageId} version {Version} in Central Package Management file {File}",
115+
packageId,
116+
version,
117+
processRequest.ComponentStream.Location);
118+
}
119+
}
120+
catch (Exception e) when (e is XmlException)
121+
{
122+
this.Logger.LogWarning(e, "Failed to parse Central Package Management file {File}", processRequest.ComponentStream.Location);
123+
}
124+
125+
return Task.CompletedTask;
126+
}
127+
128+
/// <summary>
129+
/// Determines if the props file is a Central Package Management file by checking for the
130+
/// ManagePackageVersionsCentrally property or the presence of PackageVersion/GlobalPackageReference elements.
131+
/// </summary>
132+
/// <param name="propsDocument">The props file document to check.</param>
133+
/// <returns>True if this is a Central Package Management file, false otherwise.</returns>
134+
private bool IsCentralPackageManagementFile(XDocument propsDocument)
135+
{
136+
// Check for the ManagePackageVersionsCentrally property set to true
137+
var managePackageVersionsCentrally = propsDocument.Descendants("ManagePackageVersionsCentrally")
138+
.FirstOrDefault()?.Value?.Trim();
139+
140+
if (string.Equals(managePackageVersionsCentrally, "true", StringComparison.OrdinalIgnoreCase))
141+
{
142+
return true;
143+
}
144+
145+
// Check for the presence of PackageVersion or GlobalPackageReference elements
146+
var hasPackageVersionElements = propsDocument.Descendants("PackageVersion").Any();
147+
var hasGlobalPackageElements = propsDocument.Descendants("GlobalPackageReference").Any();
148+
149+
return hasPackageVersionElements || hasGlobalPackageElements;
150+
}
151+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;
2+
3+
using Microsoft.ComponentDetection.Contracts;
4+
using Microsoft.ComponentDetection.Detectors.NuGet;
5+
6+
/// <summary>
7+
/// Experiment to validate NuGetCentralPackageManagementDetector against NuGetComponentDetector.
8+
/// </summary>
9+
public class NuGetCentralPackageManagementDetectorExperiment : IExperimentConfiguration
10+
{
11+
/// <inheritdoc />
12+
public string Name => "NuGetCentralPackageManagementDetectorExperiment";
13+
14+
/// <inheritdoc />
15+
public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is NuGetComponentDetector;
16+
17+
/// <inheritdoc />
18+
public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is NuGetCentralPackageManagementDetector;
19+
20+
/// <inheritdoc />
21+
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
22+
}

src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
117117
services.AddSingleton<IComponentDetector, NuGetComponentDetector>();
118118
services.AddSingleton<IComponentDetector, NuGetPackagesConfigDetector>();
119119
services.AddSingleton<IComponentDetector, NuGetProjectModelProjectCentricComponentDetector>();
120+
services.AddSingleton<IComponentDetector, NuGetCentralPackageManagementDetector>();
120121

121122
// PIP
122123
services.AddSingleton<IPyPiClient, PyPiClient>();

0 commit comments

Comments
 (0)