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); }