diff --git a/src/Microsoft.ComponentDetection.Common/ComponentDetectionConfigFileService.cs b/src/Microsoft.ComponentDetection.Common/ComponentDetectionConfigFileService.cs
new file mode 100644
index 000000000..ed9632dde
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Common/ComponentDetectionConfigFileService.cs
@@ -0,0 +1,129 @@
+namespace Microsoft.ComponentDetection.Common;
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.Extensions.Logging;
+using YamlDotNet.Serialization;
+
+///
+public class ComponentDetectionConfigFileService : IComponentDetectionConfigFileService
+{
+ private const string ComponentDetectionConfigFileEnvVar = "ComponentDetection.ComponentDetectionConfigFile";
+ private readonly IFileUtilityService fileUtilityService;
+ private readonly IEnvironmentVariableService environmentVariableService;
+ private readonly IPathUtilityService pathUtilityService;
+ private readonly ComponentDetectionConfigFile componentDetectionConfig;
+ private readonly ILogger logger;
+ private bool serviceInitComplete;
+
+ public ComponentDetectionConfigFileService(
+ IFileUtilityService fileUtilityService,
+ IEnvironmentVariableService environmentVariableService,
+ IPathUtilityService pathUtilityService,
+ ILogger logger)
+ {
+ this.fileUtilityService = fileUtilityService;
+ this.pathUtilityService = pathUtilityService;
+ this.environmentVariableService = environmentVariableService;
+ this.logger = logger;
+ this.componentDetectionConfig = new ComponentDetectionConfigFile();
+ this.serviceInitComplete = false;
+ }
+
+ public ComponentDetectionConfigFile GetComponentDetectionConfig()
+ {
+ this.EnsureInit();
+ return this.componentDetectionConfig;
+ }
+
+ public async Task InitAsync(string explicitConfigPath, string rootDirectoryPath = null)
+ {
+ await this.LoadFromEnvironmentVariableAsync();
+ if (!string.IsNullOrEmpty(explicitConfigPath))
+ {
+ await this.LoadComponentDetectionConfigAsync(explicitConfigPath);
+ }
+
+ if (!string.IsNullOrEmpty(rootDirectoryPath))
+ {
+ await this.LoadComponentDetectionConfigFilesFromRootDirectoryAsync(rootDirectoryPath);
+ }
+
+ this.serviceInitComplete = true;
+ }
+
+ private async Task LoadComponentDetectionConfigFilesFromRootDirectoryAsync(string rootDirectoryPath)
+ {
+ var workingDir = this.pathUtilityService.NormalizePath(rootDirectoryPath);
+
+ var reportFile = new FileInfo(Path.Combine(workingDir, "ComponentDetection.yml"));
+ if (this.fileUtilityService.Exists(reportFile.FullName))
+ {
+ await this.LoadComponentDetectionConfigAsync(reportFile.FullName);
+ }
+ }
+
+ private async Task LoadFromEnvironmentVariableAsync()
+ {
+ if (this.environmentVariableService.DoesEnvironmentVariableExist(ComponentDetectionConfigFileEnvVar))
+ {
+ var possibleConfigFilePath = this.environmentVariableService.GetEnvironmentVariable(ComponentDetectionConfigFileEnvVar);
+ if (this.fileUtilityService.Exists(possibleConfigFilePath))
+ {
+ await this.LoadComponentDetectionConfigAsync(possibleConfigFilePath);
+ }
+ }
+ }
+
+ private async Task LoadComponentDetectionConfigAsync(string configFile)
+ {
+ if (!this.fileUtilityService.Exists(configFile))
+ {
+ throw new InvalidOperationException($"Attempted to load non-existant ComponentDetectionConfig file: {configFile}");
+ }
+
+ var configFileInfo = new FileInfo(configFile);
+ var fileContents = await this.fileUtilityService.ReadAllTextAsync(configFileInfo);
+ var newConfig = this.ParseComponentDetectionConfig(fileContents);
+ this.MergeComponentDetectionConfig(newConfig);
+ this.logger.LogInformation("Loaded component detection config file from {ConfigFile}", configFile);
+ }
+
+ ///
+ /// Merges two component detection configs, giving precedence to values already set in the first file.
+ ///
+ /// The new config file to be merged into the existing config set.
+ private void MergeComponentDetectionConfig(ComponentDetectionConfigFile newConfig)
+ {
+ foreach ((var name, var value) in newConfig.Variables)
+ {
+ if (!this.componentDetectionConfig.Variables.ContainsKey(name))
+ {
+ this.componentDetectionConfig.Variables[name] = value;
+ }
+ }
+ }
+
+ ///
+ /// Reads the component detection config from a file path.
+ ///
+ /// The string contents of the config yaml file.
+ /// The ComponentDetection config file as an object.
+ private ComponentDetectionConfigFile ParseComponentDetectionConfig(string configFileContent)
+ {
+ var deserializer = new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .Build();
+ return deserializer.Deserialize(new StringReader(configFileContent));
+ }
+
+ private void EnsureInit()
+ {
+ if (!this.serviceInitComplete)
+ {
+ throw new InvalidOperationException("ComponentDetection config files have not been loaded yet!");
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Common/IComponentDetectionConfigFileService.cs b/src/Microsoft.ComponentDetection.Common/IComponentDetectionConfigFileService.cs
new file mode 100644
index 000000000..09309fe25
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Common/IComponentDetectionConfigFileService.cs
@@ -0,0 +1,25 @@
+namespace Microsoft.ComponentDetection.Common;
+
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+
+///
+/// Provides methods for writing files.
+///
+public interface IComponentDetectionConfigFileService
+{
+ ///
+ /// Initializes the Component detection config service.
+ /// Checks the following for the presence of a config file:
+ /// 1. The environment variable "ComponentDetection.ConfigFilePath" and the path exists
+ /// 2. If there is a file present at the root directory named "ComponentDetection.yml".
+ ///
+ /// A task that represents the asynchronous operation.
+ Task InitAsync(string explicitConfigPath, string rootDirectoryPath = null);
+
+ ///
+ /// Retrieves the merged config files.
+ ///
+ /// The ComponentDetection config file as an object.
+ ComponentDetectionConfigFile GetComponentDetectionConfig();
+}
diff --git a/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj b/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj
index cf206abea..424af33d6 100644
--- a/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj
+++ b/src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj
@@ -5,6 +5,7 @@
+
diff --git a/src/Microsoft.ComponentDetection.Contracts/ComponentDetectionConfigFile.cs b/src/Microsoft.ComponentDetection.Contracts/ComponentDetectionConfigFile.cs
new file mode 100644
index 000000000..fa62cdfc7
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Contracts/ComponentDetectionConfigFile.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.ComponentDetection.Contracts;
+
+using System.Collections.Generic;
+using YamlDotNet.Serialization;
+
+///
+/// Represents the ComponentDetection.yml config file.
+///
+public class ComponentDetectionConfigFile
+{
+ ///
+ /// Gets or sets a value indicating whether the detection should be stopped.
+ ///
+ [YamlMember(Alias = "variables")]
+ public Dictionary Variables { get; set; } = [];
+}
diff --git a/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj b/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj
index d7aab1c0a..a573a7279 100644
--- a/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj
+++ b/src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Commands/BaseSettings.cs b/src/Microsoft.ComponentDetection.Orchestrator/Commands/BaseSettings.cs
index eec348e92..3b22fc207 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Commands/BaseSettings.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Commands/BaseSettings.cs
@@ -37,6 +37,10 @@ public abstract class BaseSettings : CommandSettings
[CommandOption("--Output")]
public string Output { get; set; }
+ [Description("File path for a ComponentDetection.yml config file with more instructions for detection")]
+ [CommandOption("--ComponentDetectionConfigFile")]
+ public string ComponentDetectionConfigFile { get; set; }
+
///
public override ValidationResult Validate()
{
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs b/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs
index e2d7d39e0..6b42c03bc 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Commands/ScanCommand.cs
@@ -18,6 +18,7 @@ public sealed class ScanCommand : AsyncCommand
private const string ManifestRelativePath = "ScanManifest_{timestamp}.json";
private readonly IFileWritingService fileWritingService;
private readonly IScanExecutionService scanExecutionService;
+ private readonly IComponentDetectionConfigFileService componentDetectionConfigFileService;
private readonly ILogger logger;
///
@@ -25,14 +26,17 @@ public sealed class ScanCommand : AsyncCommand
///
/// The file writing service.
/// The scan execution service.
+ /// The component detection config file service.
/// The logger.
public ScanCommand(
IFileWritingService fileWritingService,
IScanExecutionService scanExecutionService,
+ IComponentDetectionConfigFileService componentDetectionConfigFileService,
ILogger logger)
{
this.fileWritingService = fileWritingService;
this.scanExecutionService = scanExecutionService;
+ this.componentDetectionConfigFileService = componentDetectionConfigFileService;
this.logger = logger;
}
@@ -40,6 +44,7 @@ public ScanCommand(
public override async Task ExecuteAsync(CommandContext context, ScanSettings settings)
{
this.fileWritingService.Init(settings.Output);
+ await this.componentDetectionConfigFileService.InitAsync(settings.ComponentDetectionConfigFile, settings.SourceDirectory.FullName);
var result = await this.scanExecutionService.ExecuteScanAsync(settings);
this.WriteComponentManifest(settings, result);
return 0;
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
index 029ce096d..23e208de0 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
@@ -46,6 +46,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Commands/ScanCommandTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Commands/ScanCommandTests.cs
index c2c8f7154..47bb64a6c 100644
--- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Commands/ScanCommandTests.cs
+++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Commands/ScanCommandTests.cs
@@ -19,6 +19,7 @@ public class ScanCommandTests
{
private Mock fileWritingServiceMock;
private Mock scanExecutionServiceMock;
+ private Mock componentDetectionConfigFileServiceMock;
private Mock> loggerMock;
private ScanCommand command;
@@ -27,11 +28,13 @@ public void TestInitialize()
{
this.fileWritingServiceMock = new Mock();
this.scanExecutionServiceMock = new Mock();
+ this.componentDetectionConfigFileServiceMock = new Mock();
this.loggerMock = new Mock>();
this.command = new ScanCommand(
this.fileWritingServiceMock.Object,
this.scanExecutionServiceMock.Object,
+ this.componentDetectionConfigFileServiceMock.Object,
this.loggerMock.Object);
}