From c0239f8a7e57f95c8302092abe2b810b4c4c97aa Mon Sep 17 00:00:00 2001 From: borisforzun Date: Sun, 28 Jul 2024 19:19:32 +0300 Subject: [PATCH 1/4] Added the ability to specify custom rule --- src/Analyzer.Cli/CommandLineParser.cs | 21 ++++++++++------ .../TemplateAnalyzerTests.cs | 24 +++++++++++++++++++ src/Analyzer.Core/TemplateAnalyzer.cs | 15 ++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/Analyzer.Cli/CommandLineParser.cs b/src/Analyzer.Cli/CommandLineParser.cs index 17eefd4d..22db204f 100644 --- a/src/Analyzer.Cli/CommandLineParser.cs +++ b/src/Analyzer.Cli/CommandLineParser.cs @@ -143,7 +143,11 @@ private void SetupCommonOptionsForCommands(List commands) new Option( "--include-non-security-rules", - "Run all the rules against the templates, including non-security rules") + "Run all the rules against the templates, including non-security rules"), + + new Option( + "--custom-rules-json-path", + "The rules JSON file to use against the templates. If not specified, will use the default rule set that is shipped with the tool.") }; commands.ForEach(c => options.ForEach(c.AddOption)); @@ -157,7 +161,8 @@ private int AnalyzeTemplateCommandHandler( ReportFormat reportFormat, FileInfo outputFilePath, bool includeNonSecurityRules, - bool verbose) + bool verbose, + FileInfo customRulesJsonPath) { // Check that template file paths exist if (!templateFilePath.Exists) @@ -166,7 +171,7 @@ private int AnalyzeTemplateCommandHandler( return (int)ExitCode.ErrorInvalidPath; } - var setupResult = SetupAnalysis(configFilePath, directoryToAnalyze: null, reportFormat, outputFilePath, includeNonSecurityRules, verbose); + var setupResult = SetupAnalysis(configFilePath, directoryToAnalyze: null, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customRulesJsonPath); if (setupResult != ExitCode.Success) { return (int)setupResult; @@ -203,7 +208,8 @@ private int AnalyzeDirectoryCommandHandler( ReportFormat reportFormat, FileInfo outputFilePath, bool includeNonSecurityRules, - bool verbose) + bool verbose, + FileInfo customRulesJsonPath) { if (!directoryPath.Exists) { @@ -211,7 +217,7 @@ private int AnalyzeDirectoryCommandHandler( return (int)ExitCode.ErrorInvalidPath; } - var setupResult = SetupAnalysis(configFilePath, directoryPath, reportFormat, outputFilePath, includeNonSecurityRules, verbose); + var setupResult = SetupAnalysis(configFilePath, directoryPath, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customRulesJsonPath); if (setupResult != ExitCode.Success) { return (int)setupResult; @@ -278,7 +284,8 @@ private ExitCode SetupAnalysis( ReportFormat reportFormat, FileInfo outputFilePath, bool includeNonSecurityRules, - bool verbose) + bool verbose, + FileInfo customRulesJsonPath) { // Output file path must be specified if SARIF was chosen as the report format if (reportFormat == ReportFormat.Sarif && outputFilePath == null) @@ -290,7 +297,7 @@ private ExitCode SetupAnalysis( this.reportWriter = GetReportWriter(reportFormat, outputFilePath, directoryToAnalyze?.FullName); CreateLoggers(verbose); - this.templateAnalyzer = TemplateAnalyzer.Create(includeNonSecurityRules, this.logger); + this.templateAnalyzer = TemplateAnalyzer.Create(includeNonSecurityRules, this.logger, customRulesJsonPath); if (!TryReadConfigurationFile(configurationFile, out var config)) { diff --git a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs index db1d7a68..40556198 100644 --- a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs +++ b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs @@ -226,6 +226,30 @@ public void FilterRules_ValidConfiguration_NoExceptionThrown() TemplateAnalyzer.Create(false).FilterRules(new ConfigurationDefinition()); } + [TestMethod] + public void CustomRulesFileIsProvided_NoExceptionThrown() + { + var rulesDir = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "Rules"); + var rulesFile = Path.Combine(rulesDir, "BuiltInRules.json"); + var movedFile = Path.Combine(rulesDir, "MovedRules.json"); + + // Move rules file + File.Move(rulesFile, movedFile); + + var customRulesFile = new FileInfo(movedFile); + + try + { + TemplateAnalyzer.Create(false, null, customRulesFile); + } + finally + { + File.Move(movedFile, rulesFile, overwrite: true); + } + } + [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void FilterRules_ConfigurationNull_ExceptionThrown() diff --git a/src/Analyzer.Core/TemplateAnalyzer.cs b/src/Analyzer.Core/TemplateAnalyzer.cs index 7b0873ed..d4f0e6f9 100644 --- a/src/Analyzer.Core/TemplateAnalyzer.cs +++ b/src/Analyzer.Core/TemplateAnalyzer.cs @@ -51,13 +51,14 @@ private TemplateAnalyzer(JsonRuleEngine jsonRuleEngine, PowerShellRuleEngine pow /// /// Whether or not to run also non-security rules against the template. /// A logger to report errors and debug information + /// An optional custom rules json file path. /// A new instance. - public static TemplateAnalyzer Create(bool includeNonSecurityRules, ILogger logger = null) + public static TemplateAnalyzer Create(bool includeNonSecurityRules, ILogger logger = null, FileInfo customRulesJsonPath = null) { string rules; try { - rules = LoadRules(); + rules = LoadRules(customRulesJsonPath); } catch (Exception e) { @@ -224,12 +225,16 @@ private IEnumerable AnalyzeAllIncludedTemplates(string populatedTem } } - private static string LoadRules() + private static string LoadRules(FileInfo rulesFile) { - return File.ReadAllText( - Path.Combine( + rulesFile ??= new FileInfo(Path.Combine( Path.GetDirectoryName(AppContext.BaseDirectory), "Rules/BuiltInRules.json")); + + using var fileStream = rulesFile.OpenRead(); + using var streamReader = new StreamReader(fileStream); + + return streamReader.ReadToEnd(); } /// From 78f3438483a74c0e64a5e28b51e079a6f3d3ba72 Mon Sep 17 00:00:00 2001 From: borisforzun Date: Thu, 1 Aug 2024 12:34:40 +0300 Subject: [PATCH 2/4] fix from cr --- .../CommandLineParserTests.cs | 45 ++++++++++++++++--- src/Analyzer.Cli/CommandLineParser.cs | 16 +++---- src/Analyzer.Core/TemplateAnalyzer.cs | 6 +-- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs b/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs index fd1928b0..158fac53 100644 --- a/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs +++ b/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs @@ -4,8 +4,10 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Azure.Templates.Analyzer.Cli; +using Microsoft.Azure.Templates.Analyzer.Core; using Microsoft.Azure.Templates.Analyzer.Types; using Microsoft.Azure.Templates.Analyzer.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -37,7 +39,7 @@ public void TestInit() [DataRow("Invalid.bicep", ExitCode.ErrorInvalidBicepTemplate, DisplayName = "Path exists, invalid Bicep template")] public void AnalyzeTemplate_ValidInputValues_ReturnExpectedExitCode(string relativeTemplatePath, ExitCode expectedExitCode, params string[] additionalCliOptions) { - var args = new string[] { "analyze-template" , GetFilePath(relativeTemplatePath)}; + var args = new string[] { "analyze-template", GetFilePath(relativeTemplatePath) }; args = args.Concat(additionalCliOptions).ToArray(); var result = _commandLineParser.InvokeCommandLineAPIAsync(args); @@ -64,7 +66,7 @@ public void AnalyzeTemplate_UseConfigurationFileOption_ReturnExpectedExitCodeUsi { var templatePath = GetFilePath(relativeTemplatePath); var configurationPath = GetFilePath("Configuration.json"); - var args = new string[] { "analyze-template", templatePath, "--config-file-path", configurationPath}; + var args = new string[] { "analyze-template", templatePath, "--config-file-path", configurationPath }; var result = _commandLineParser.InvokeCommandLineAPIAsync(args); Assert.AreEqual((int)ExitCode.Violation, result.Result); @@ -81,7 +83,7 @@ public void AnalyzeTemplate_ReportFormatAsSarif_ReturnExpectedExitCodeUsingOptio var result = _commandLineParser.InvokeCommandLineAPIAsync(args); Assert.AreEqual((int)ExitCode.Violation, result.Result); - + File.Delete(outputFilePath); } @@ -117,6 +119,35 @@ public void AnalyzeTemplate_ValidInputValues_AnalyzesUsingAutoDetectedParameters StringAssert.Contains(outputWriter.ToString(), "Parameters File: " + Path.Combine(Directory.GetCurrentDirectory(), "Tests", "ToTestSeparateParametersFile", "TemplateWithSeparateParametersFile.parameters.json")); } + [TestMethod] + public void AnalyzeTemplate_ValidInputValues_AnalyzesUsingCustomJSONRulesPath() + { + var rulesDir = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "Rules"); + var rulesFile = Path.Combine(rulesDir, "BuiltInRules.json"); + var customJSONRulesPath = Path.Combine(rulesDir, "MovedRules.json"); + var templatePath = GetFilePath(Path.Combine("ToTestSeparateParametersFile", "TemplateWithSeparateParametersFile.bicep")); + + var args = new string[] { + "analyze-template", templatePath, + "--custom-json-rules-path", customJSONRulesPath + }; + + // Move rules file + File.Move(rulesFile, customJSONRulesPath, true); + + try + { + var result = _commandLineParser.InvokeCommandLineAPIAsync(args); + Assert.AreEqual((int)ExitCode.Success, result.Result); + } + finally + { + File.Move(customJSONRulesPath, rulesFile, overwrite: true); + } + } + [TestMethod] public void AnalyzeDirectory_ValidInputValues_AnalyzesExpectedNumberOfFiles() { @@ -161,7 +192,7 @@ public void AnalyzeDirectory_ValidInputValues_ReturnExpectedExitCode(bool useTes args = args.Concat(additionalCliOptions).ToArray(); var result = _commandLineParser.InvokeCommandLineAPIAsync(args); - + Assert.AreEqual((int)expectedExitCode, result.Result); } @@ -170,7 +201,7 @@ public void AnalyzeDirectory_DirectoryWithInvalidTemplates_LogsExpectedErrorInSa { var outputFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Output.sarif"); var directoryToAnalyze = GetFilePath("ToTestSarifNotifications"); - + var args = new string[] { "analyze-directory", directoryToAnalyze, "--report-format", "Sarif", "--output-file-path", outputFilePath }; var result = _commandLineParser.InvokeCommandLineAPIAsync(args); @@ -228,7 +259,7 @@ public void AnalyzeDirectory_ExecutionWithErrorAndWarning_PrintsExpectedMessages $"{Environment.NewLine}\t\t1 instance of: {errorMessage1}" + $"{Environment.NewLine}\t\t1 instance of: {errorMessage2}"; } - + expectedLogSummary += ($"{Environment.NewLine}{Environment.NewLine}\t1 Warning" + $"{Environment.NewLine}\t{(multipleErrors ? "2 Errors" : "1 Error")}{Environment.NewLine}"); @@ -320,7 +351,7 @@ public void FilterRules_ValidConfig_RulesFiltered(bool isBicep, string configNam } }) .ToString()); - + if (specifyInCommand) { args = args.Concat(new[] { "--config-file-path", configName }).ToArray(); diff --git a/src/Analyzer.Cli/CommandLineParser.cs b/src/Analyzer.Cli/CommandLineParser.cs index 22db204f..fd2e212e 100644 --- a/src/Analyzer.Cli/CommandLineParser.cs +++ b/src/Analyzer.Cli/CommandLineParser.cs @@ -146,8 +146,8 @@ private void SetupCommonOptionsForCommands(List commands) "Run all the rules against the templates, including non-security rules"), new Option( - "--custom-rules-json-path", - "The rules JSON file to use against the templates. If not specified, will use the default rule set that is shipped with the tool.") + "--custom-json-rules-path", + "The JSON rules file to use against the templates. If not specified, will use the default rule set that is shipped with the tool.") }; commands.ForEach(c => options.ForEach(c.AddOption)); @@ -162,7 +162,7 @@ private int AnalyzeTemplateCommandHandler( FileInfo outputFilePath, bool includeNonSecurityRules, bool verbose, - FileInfo customRulesJsonPath) + FileInfo customJsonRulesPath) { // Check that template file paths exist if (!templateFilePath.Exists) @@ -171,7 +171,7 @@ private int AnalyzeTemplateCommandHandler( return (int)ExitCode.ErrorInvalidPath; } - var setupResult = SetupAnalysis(configFilePath, directoryToAnalyze: null, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customRulesJsonPath); + var setupResult = SetupAnalysis(configFilePath, directoryToAnalyze: null, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customJsonRulesPath); if (setupResult != ExitCode.Success) { return (int)setupResult; @@ -209,7 +209,7 @@ private int AnalyzeDirectoryCommandHandler( FileInfo outputFilePath, bool includeNonSecurityRules, bool verbose, - FileInfo customRulesJsonPath) + FileInfo customJsonRulesPath) { if (!directoryPath.Exists) { @@ -217,7 +217,7 @@ private int AnalyzeDirectoryCommandHandler( return (int)ExitCode.ErrorInvalidPath; } - var setupResult = SetupAnalysis(configFilePath, directoryPath, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customRulesJsonPath); + var setupResult = SetupAnalysis(configFilePath, directoryPath, reportFormat, outputFilePath, includeNonSecurityRules, verbose, customJsonRulesPath); if (setupResult != ExitCode.Success) { return (int)setupResult; @@ -285,7 +285,7 @@ private ExitCode SetupAnalysis( FileInfo outputFilePath, bool includeNonSecurityRules, bool verbose, - FileInfo customRulesJsonPath) + FileInfo customJsonRulesPath) { // Output file path must be specified if SARIF was chosen as the report format if (reportFormat == ReportFormat.Sarif && outputFilePath == null) @@ -297,7 +297,7 @@ private ExitCode SetupAnalysis( this.reportWriter = GetReportWriter(reportFormat, outputFilePath, directoryToAnalyze?.FullName); CreateLoggers(verbose); - this.templateAnalyzer = TemplateAnalyzer.Create(includeNonSecurityRules, this.logger, customRulesJsonPath); + this.templateAnalyzer = TemplateAnalyzer.Create(includeNonSecurityRules, this.logger, customJsonRulesPath); if (!TryReadConfigurationFile(configurationFile, out var config)) { diff --git a/src/Analyzer.Core/TemplateAnalyzer.cs b/src/Analyzer.Core/TemplateAnalyzer.cs index d4f0e6f9..88ce1876 100644 --- a/src/Analyzer.Core/TemplateAnalyzer.cs +++ b/src/Analyzer.Core/TemplateAnalyzer.cs @@ -51,14 +51,14 @@ private TemplateAnalyzer(JsonRuleEngine jsonRuleEngine, PowerShellRuleEngine pow /// /// Whether or not to run also non-security rules against the template. /// A logger to report errors and debug information - /// An optional custom rules json file path. + /// An optional custom rules json file path. /// A new instance. - public static TemplateAnalyzer Create(bool includeNonSecurityRules, ILogger logger = null, FileInfo customRulesJsonPath = null) + public static TemplateAnalyzer Create(bool includeNonSecurityRules, ILogger logger = null, FileInfo customJsonRulesPath = null) { string rules; try { - rules = LoadRules(customRulesJsonPath); + rules = LoadRules(customJsonRulesPath); } catch (Exception e) { From 7b510de36d8fc9987510e57b639e41233daa2d2a Mon Sep 17 00:00:00 2001 From: borisforzun Date: Thu, 1 Aug 2024 12:35:55 +0300 Subject: [PATCH 3/4] fix --- src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs | 2 +- src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs b/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs index 158fac53..b724d216 100644 --- a/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs +++ b/src/Analyzer.Cli.FunctionalTests/CommandLineParserTests.cs @@ -135,7 +135,7 @@ public void AnalyzeTemplate_ValidInputValues_AnalyzesUsingCustomJSONRulesPath() }; // Move rules file - File.Move(rulesFile, customJSONRulesPath, true); + File.Move(rulesFile, customJSONRulesPath, overwrite: true); try { diff --git a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs index 40556198..0eb7636f 100644 --- a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs +++ b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs @@ -236,7 +236,7 @@ public void CustomRulesFileIsProvided_NoExceptionThrown() var movedFile = Path.Combine(rulesDir, "MovedRules.json"); // Move rules file - File.Move(rulesFile, movedFile); + File.Move(rulesFile, movedFile, overwrite: true); var customRulesFile = new FileInfo(movedFile); From 479664d326f4cd4b685e17759a7c8a53468ad03a Mon Sep 17 00:00:00 2001 From: Johnathon Mohr Date: Thu, 1 Aug 2024 11:25:59 -0700 Subject: [PATCH 4/4] Update README.md to document new --custom-json-rules-path parameter --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc627429..f4f3ade4 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Argument | Description `-o` or `--output-file-path` | **(Required if `--report-format` is *Sarif*)** File path to output SARIF results to. **(Optional)** `-v` or `--verbose` | Shows details about the analysis **(Optional)** `--include-non-security-rules` | Run all the rules against the templates, including non-security rules +**(Optional)** `--custom-json-rules-path` | The [JSON rules file](docs/authoring-json-rules.md) to use against the templates.
If not specified, will use the [default JSON rule set that is shipped with the tool](docs/built-in-rules.md#json-based-rules). Template Analyzer runs the [configured rules](#understanding-and-customizing-rules) against the provided template and its corresponding [template parameters](https://docs.microsoft.com/azure/azure-resource-manager/templates/parameter-files), if specified. If no template parameters are specified, then Template Analyzer will check if templates with the [general naming standards defined by Microsoft](https://learn.microsoft.com/azure/azure-resource-manager/templates/parameter-files#file-name) are present in the same folder, otherwise it generates the minimum number of placeholder parameters to properly evaluate [template functions](https://docs.microsoft.com/azure/azure-resource-manager/templates/template-functions) in the template.