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

feat: add a new detector MvnPomCliComponentDetector #544

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Microsoft.ComponentDetection.Detectors.Maven;

using Microsoft.ComponentDetection.Contracts.Internal;

public interface IMavenFileParserService
{
void ParseDependenciesFile(ProcessRequest processRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Microsoft.ComponentDetection.Detectors.Maven;

using System;
using System.Xml;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;

public class MavenFileParserService : IMavenFileParserService
{
private readonly ILogger<MavenFileParserService> logger;

public MavenFileParserService(
ILogger<MavenFileParserService> logger) => this.logger = logger;

public void ParseDependenciesFile(ProcessRequest processRequest)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var stream = processRequest.ComponentStream;

try
{
var doc = new XmlDocument();
doc.Load(stream.Location);

var nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("ns", "http://maven.apache.org/POM/4.0.0");

var dependencies = doc.SelectSingleNode("//ns:project/ns:dependencies", nsmgr);
if (dependencies == null)
{
return;
}

foreach (XmlNode node in dependencies.ChildNodes)
{
this.RegisterComponent(node, nsmgr, singleFileComponentRecorder);
}
}
catch (Exception e)
{
// If something went wrong, just ignore the component
this.logger.LogError(e, "Error parsing pom maven component from {PomLocation}", stream.Location);
singleFileComponentRecorder.RegisterPackageParseFailure(stream.Location);
}
}

private void RegisterComponent(XmlNode node, XmlNamespaceManager nsmgr, ISingleFileComponentRecorder singleFileComponentRecorder)
{
var groupIdNode = node.SelectSingleNode("ns:groupId", nsmgr);
var artifactIdNode = node.SelectSingleNode("ns:artifactId", nsmgr);
var versionNode = node.SelectSingleNode("ns:version", nsmgr);

if (groupIdNode == null || artifactIdNode == null || versionNode == null)
{
this.logger.LogInformation("{XmlNode} doesn't have groupId, artifactId or version information", node.InnerText);
return;
}

var groupId = groupIdNode.InnerText;
var artifactId = artifactIdNode.InnerText;
var version = versionNode.InnerText;
var dependencyScope = DependencyScope.MavenCompile;

var component = new MavenComponent(groupId, artifactId, version);

singleFileComponentRecorder.RegisterUsage(
new DetectedComponent(component),
isDevelopmentDependency: null,
dependencyScope: dependencyScope);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Microsoft.ComponentDetection.Detectors.Maven;

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;

public class MavenPomComponentDetector : FileComponentDetector, IDefaultOffComponentDetector
{
private readonly IMavenFileParserService mavenFileParserService;

public MavenPomComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
IMavenFileParserService mavenFileParserService,
ILogger<MavenPomComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.mavenFileParserService = mavenFileParserService;
this.Logger = logger;
}

public override string Id => "MvnPom";

public override IList<string> SearchPatterns => new List<string>() { "*.pom" };

public override IEnumerable<string> Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) };

public override IEnumerable<ComponentType> SupportedComponentTypes => new[] { ComponentType.Maven };

public override int Version => 2;

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs)
{
await this.ProcessFileAsync(processRequest);
}

private async Task ProcessFileAsync(ProcessRequest processRequest)
{
this.mavenFileParserService.ParseDependenciesFile(processRequest);

await Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
namespace Microsoft.ComponentDetection.Detectors.Maven;

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;

public class MvnPomCliComponentDetector : FileComponentDetector
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also implement IDefaultOffComponentDetector? We don't want a new detector to be enabled by default (only explicitly opted-in) before we've had the chance to fully verify it correctly works.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MavenPomComponentDetector is marked IDefaultOffComponenDetector, but MvnPomCliComponentDetector is not

{
public MvnPomCliComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<MvnCliComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id => "MvnPomCli";

public override IList<string> SearchPatterns => new List<string>() { "*.pom" };

public override IEnumerable<string> Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) };

public override IEnumerable<ComponentType> SupportedComponentTypes => new[] { ComponentType.Maven };

public override int Version => 2;

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs)
{
await this.ProcessFileAsync(processRequest);
}

private async Task ProcessFileAsync(ProcessRequest processRequest)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var stream = processRequest.ComponentStream;

try
{
byte[] pomBytes = null;

if ("*.pom".Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase))
AbhinavAbhinav11 marked this conversation as resolved.
Show resolved Hide resolved
{
using (var contentStream = File.Open(stream.Location, FileMode.Open))
{
pomBytes = new byte[contentStream.Length];
await contentStream.ReadAsync(pomBytes.AsMemory(0, (int)contentStream.Length));

using var pomStream = new MemoryStream(pomBytes, false);
var doc = new XmlDocument();
doc.Load(pomStream);

var nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("ns", "http://maven.apache.org/POM/4.0.0");

var dependencies = doc.SelectSingleNode("//ns:project/ns:dependencies", nsmgr);
if (dependencies == null)
{
return;
}

foreach (XmlNode node in dependencies.ChildNodes)
melotic marked this conversation as resolved.
Show resolved Hide resolved
{
this.RegisterComponent(node, nsmgr, singleFileComponentRecorder);
}
}
}
else
{
return;
AbhinavAbhinav11 marked this conversation as resolved.
Show resolved Hide resolved
}
}
catch (Exception e)
{
// If something went wrong, just ignore the component
this.Logger.LogError(e, "Error parsing pom maven component from {PomLocation}", stream.Location);
singleFileComponentRecorder.RegisterPackageParseFailure(stream.Location);
}
}

private void RegisterComponent(XmlNode node, XmlNamespaceManager nsmgr, ISingleFileComponentRecorder singleFileComponentRecorder)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method looks the same as RegisterComponent(XmlNode node, XmlNamespaceManager nsmgr, ISingleFileComponentRecorder singleFileComponentRecorder) in MavenFileParserService. We should refactor this and DRY.

{
var groupIdNode = node.SelectSingleNode("ns:groupId", nsmgr);
var artifactIdNode = node.SelectSingleNode("ns:artifactId", nsmgr);
var versionNode = node.SelectSingleNode("ns:version", nsmgr);

if (groupIdNode == null || artifactIdNode == null || versionNode == null)
{
return;
AbhinavAbhinav11 marked this conversation as resolved.
Show resolved Hide resolved
}

var groupId = groupIdNode.InnerText;
var artifactId = artifactIdNode.InnerText;
var version = versionNode.InnerText;
var dependencyScope = DependencyScope.MavenCompile;

var component = new MavenComponent(groupId, artifactId, version);

singleFileComponentRecorder.RegisterUsage(
new DetectedComponent(component),
isDevelopmentDependency: null,
dependencyScope: dependencyScope);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
namespace Microsoft.ComponentDetection.Orchestrator.Extensions;

using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Common.Telemetry;
Expand Down Expand Up @@ -97,6 +97,8 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IMavenCommandService, MavenCommandService>();
services.AddSingleton<IMavenStyleDependencyGraphParserService, MavenStyleDependencyGraphParserService>();
services.AddSingleton<IComponentDetector, MvnCliComponentDetector>();
services.AddSingleton<IMavenFileParserService, MavenFileParserService>();
services.AddSingleton<IComponentDetector, MavenPomComponentDetector>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing MvnPomCliComponentDetector?


// npm
services.AddSingleton<IComponentDetector, NpmComponentDetector>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Microsoft.ComponentDetection.Detectors.Tests;

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Maven;
using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
public class MvnPomComponentDetectorTest : BaseDetectorTest<MavenPomComponentDetector>
{
private readonly Mock<IMavenFileParserService> mavenFileParserServiceMock;

public MvnPomComponentDetectorTest()
{
this.mavenFileParserServiceMock = new Mock<IMavenFileParserService>();
this.DetectorTestUtility.AddServiceMock(this.mavenFileParserServiceMock);
}

[TestMethod]
public async Task MavenRootsAsync()
{
const string componentString = "org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT";
const string childComponentString = "org.apache.maven:maven-compat-child:jar:3.6.1-SNAPSHOT";
var content = $@"com.bcde.test:top-level:jar:1.0.0{Environment.NewLine}\- {componentString}{Environment.NewLine} \- {childComponentString}";
this.DetectorTestUtility.WithFile("pom.xml", content)
.WithFile("pom.xml", content, searchPatterns: new[] { "pom.xml" });

this.mavenFileParserServiceMock.Setup(x => x.ParseDependenciesFile(It.IsAny<ProcessRequest>()))
.Callback((ProcessRequest pr) =>
{
pr.SingleFileComponentRecorder.RegisterUsage(
new DetectedComponent(
new MavenComponent("com.bcde.test", "top-levelt", "1.0.0")),
isExplicitReferencedDependency: true);
pr.SingleFileComponentRecorder.RegisterUsage(
new DetectedComponent(
new MavenComponent("org.apache.maven", "maven-compat", "3.6.1-SNAPSHOT")),
isExplicitReferencedDependency: true);
pr.SingleFileComponentRecorder.RegisterUsage(
new DetectedComponent(
new MavenComponent("org.apache.maven", "maven-compat-child", "3.6.1-SNAPSHOT")),
isExplicitReferencedDependency: false,
parentComponentId: "org.apache.maven maven-compat 3.6.1-SNAPSHOT - Maven");
});

var (detectorResult, componentRecorder) = await this.DetectorTestUtility.ExecuteDetectorAsync();

var detectedComponents = componentRecorder.GetDetectedComponents();
Assert.AreEqual(detectedComponents.Count(), 3);
Assert.AreEqual(detectorResult.ResultCode, ProcessingResultCode.Success);

var splitComponent = componentString.Split(':');
var splitChildComponent = childComponentString.Split(':');

var mavenComponent = detectedComponents.FirstOrDefault(x => (x.Component as MavenComponent).ArtifactId == splitChildComponent[1]);
Assert.IsNotNull(mavenComponent);

componentRecorder.AssertAllExplicitlyReferencedComponents<MavenComponent>(
mavenComponent.Component.Id,
parentComponent => parentComponent.ArtifactId == splitComponent[1]);
}
}