diff --git a/src/Analyzer.Cli.FunctionalTests/Tests/TriggersOnlyNonSecurityRules.json b/src/Analyzer.Cli.FunctionalTests/Tests/TriggersOnlyNonSecurityRules.json index 73b80652..32e698d8 100644 --- a/src/Analyzer.Cli.FunctionalTests/Tests/TriggersOnlyNonSecurityRules.json +++ b/src/Analyzer.Cli.FunctionalTests/Tests/TriggersOnlyNonSecurityRules.json @@ -10,7 +10,16 @@ "name": "aWorkspace", "apiVersion": "2016-12-01", "location": "", - "properties": {} + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example'))]": {} + } + }, + "properties": { + "primaryUserAssignedIdentity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example')]", + "publicNetworkAccess": "Disabled" + } } ], "outputs": {} diff --git a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs index 4804538c..ae1a10e8 100644 --- a/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs +++ b/src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs @@ -28,7 +28,7 @@ public static void AssemblyInitialize(TestContext context) [DataTestMethod] [DataRow(@"{ ""azureActiveDirectory"": { ""tenantId"": ""tenantId"" } }", "Microsoft.ServiceFabric/clusters", 1, 1, DisplayName = "1 matching resource with 1 passing evaluation")] [DataRow(@"{ ""azureActiveDirectory"": { ""someProperty"": ""propertyValue"" } }", "Microsoft.ServiceFabric/clusters", 1, 0, DisplayName = "1 matching resource with 1 failing evaluation")] - [DataRow(@"{ ""property1"": { ""someProperty"": ""propertyValue"" } }", "Microsoft.MachineLearningServices/workspaces", 0, 0, DisplayName = "0 matching resources with no results")] + [DataRow(@"{ ""property1"": { ""someProperty"": ""propertyValue"" } }", "Microsoft.MachineLearningServices/workspaces", 2, 0, DisplayName = "0 matching resources with no results")] [DataRow(@"{ ""azureActiveDirectory"": { ""tenantId"": ""tenantId"" } }", "Microsoft.ServiceFabric/clusters", 2, 1, @"{ ""azureActiveDirectory"": { ""someProperty"": ""propertyValue"" } }", DisplayName = "2 matching resources with 1 passing evaluation")] public void AnalyzeTemplate_ValidInputValues_ReturnCorrectEvaluations(string resource1Properties, string resourceType, int expectedEvaluationCount, int expectedEvaluationPassCount, string resource2Properties = null) { diff --git a/src/Analyzer.PowerShellRuleEngine.UnitTests/Analyzer.PowerShellRuleEngine.UnitTests.csproj b/src/Analyzer.PowerShellRuleEngine.UnitTests/Analyzer.PowerShellRuleEngine.UnitTests.csproj index 6aae6da5..bf27b42f 100644 --- a/src/Analyzer.PowerShellRuleEngine.UnitTests/Analyzer.PowerShellRuleEngine.UnitTests.csproj +++ b/src/Analyzer.PowerShellRuleEngine.UnitTests/Analyzer.PowerShellRuleEngine.UnitTests.csproj @@ -24,4 +24,9 @@ + + + + + \ No newline at end of file diff --git a/src/Analyzer.PowerShellRuleEngine.UnitTests/PowerShellRuleEngineTests.cs b/src/Analyzer.PowerShellRuleEngine.UnitTests/PowerShellRuleEngineTests.cs index c5482bfa..67f7bd76 100644 --- a/src/Analyzer.PowerShellRuleEngine.UnitTests/PowerShellRuleEngineTests.cs +++ b/src/Analyzer.PowerShellRuleEngine.UnitTests/PowerShellRuleEngineTests.cs @@ -16,6 +16,23 @@ namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.PowerShellEngine.UnitTe [TestClass] public class PowerShellRuleEngineTests { + private const string EmptyBaseline = @" + [ + { + ""kind"": ""Baseline"", + ""metadata"": { + ""name"": ""RepeatedRulesBaseline"" + }, + ""apiVersion"": ""github.com/microsoft/PSRule/v1"", + ""spec"": { + ""rule"": { + ""exclude"": [ + ] + } + } + } + ]"; + private readonly string templatesFolder = @"templates"; private static PowerShellRuleEngine powerShellRuleEngineAllRules; private static PowerShellRuleEngine powerShellRuleEngineSecurityRules; @@ -29,8 +46,8 @@ public static void AssemblyInitialize(TestContext context) [DataTestMethod] // PSRule detects errors in two analysis stages: when looking at the whole file (through the file path), and when looking at each resource (pipeline.Process(resource)): - [DataRow("template_and_resource_level_results.json", true, 13, new int[] { 1, 1, 1, 1, 8, 14, 17, 1, 17, 17, 1, 17, 1 }, DisplayName = "Running all the rules against a template with errors reported in both analysis stages")] - [DataRow("template_and_resource_level_results.json", false, 4, new int[] { 17, 17, 17, 17 }, DisplayName = "Running only the security rules against a template with errors reported in both analysis stages")] + [DataRow("template_and_resource_level_results.json", true, 14, new int[] { 1, 1, 1, 1, 8, 14, 17, 1, 17, 17, 1, 17, 1, 11 }, DisplayName = "Running all the rules against a template with errors reported in both analysis stages")] + [DataRow("template_and_resource_level_results.json", false, 5, new int[] { 17, 17, 17, 17, 11 }, DisplayName = "Running only the security rules against a template with errors reported in both analysis stages")] // TODO add test case for error, warning (rule with severity level of warning?) and informational (also rule with that severity level?) public void AnalyzeTemplate_ValidTemplate_ReturnsExpectedEvaluations(string templateFileName, bool runsAllRules, int expectedErrorCount, dynamic expectedLineNumbers) { @@ -80,7 +97,7 @@ public void AnalyzeTemplate_ValidTemplate_ReturnsExpectedEvaluations(string temp Assert.AreEqual(expectedErrorCount, failedEvaluations.Count); // PSRule evaluations can change order depending on the OS: - foreach(var expectedLineNumber in expectedLineNumbers) + foreach (var expectedLineNumber in expectedLineNumbers) { var matchingEvaluation = failedEvaluations.Find(evaluation => evaluation.Result.SourceLocation.LineNumber == expectedLineNumber); failedEvaluations.Remove(matchingEvaluation); @@ -90,7 +107,7 @@ public void AnalyzeTemplate_ValidTemplate_ReturnsExpectedEvaluations(string temp [DataTestMethod] [DataRow(true, DisplayName = "Repeated rules are excluded when running all the rules")] - [DataRow(true, DisplayName = "Repeated rules are excluded when running only the security rules")] + [DataRow(false, DisplayName = "Repeated rules are excluded when running only the security rules")] public void AnalyzeTemplate_ValidTemplate_ExcludesRepeatedRules(bool runsAllRules) { var templateFilePath = Path.Combine(templatesFolder, "triggers_excluded_rules.json"); @@ -115,7 +132,7 @@ public void AnalyzeTemplate_ValidTemplate_ExcludesRepeatedRules(bool runsAllRule // The RepeatedRulesBaseline will only be used when all rules are run // Otherwise SecurityBaseline is used, those rules are not in the "include" array of the baseline so they won't be executed either - // Next we validate that when RepeatedRulesBaseline is an empty file then the test file does indeed trigger the excluded rule: + // Next we validate that when RepeatedRulesBaseline has no exclusions then the test file does indeed trigger the excluded rule: if (runsAllRules) { var baselineLocation = Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory), "baselines", "RepeatedRulesBaseline.Rule.json"); @@ -123,11 +140,10 @@ public void AnalyzeTemplate_ValidTemplate_ExcludesRepeatedRules(bool runsAllRule try { File.Move(baselineLocation, newBaselineLocation); - var emptyBaseline = File.Create(baselineLocation); - emptyBaseline.Close(); - + File.WriteAllText(baselineLocation, EmptyBaseline); + evaluations = powerShellRuleEngineAllRules.AnalyzeTemplate(templateContext); - + Assert.IsTrue(evaluations.Any(evaluation => evaluation.RuleId == "AZR-000081")); } finally @@ -138,6 +154,39 @@ public void AnalyzeTemplate_ValidTemplate_ExcludesRepeatedRules(bool runsAllRule } } + // Sanity checks for using hardcoded AZURE_RESOURCE_ALLOWED_LOCATIONS in the rules + [TestMethod] + [DataRow("templateWithDefaultLocation.json", DisplayName = "Template with default location")] + [DataRow("templateWithHardcodedLocation.json", DisplayName = "Template with hardcoded location")] + public void AnalyzeTemplate_ValidTemplate_SpecifiedLocations(string templateFileName) + { + var templateFilePath = Path.Combine(templatesFolder, templateFileName); + + var template = File.ReadAllText(templateFilePath); + var armTemplateProcessor = new ArmTemplateProcessor(template); + var templatejObject = armTemplateProcessor.ProcessTemplate(); + + var templateContext = new TemplateContext + { + OriginalTemplate = JObject.Parse(template), + ExpandedTemplate = templatejObject, + ResourceMappings = armTemplateProcessor.ResourceMappings, + TemplateIdentifier = templateFilePath + }; + + var evaluations = powerShellRuleEngineSecurityRules.AnalyzeTemplate(templateContext); + + var failedEvaluations = new List(); + + foreach (PowerShellRuleEvaluation evaluation in evaluations) + { + if (!evaluation.Passed) + { + failedEvaluations.Add(evaluation); + } + } + } + [TestMethod] [ExpectedException(typeof(ArgumentException))] public void AnalyzeTemplate_NullTemplateContext_ThrowsException() diff --git a/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithDefaultLocation.json b/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithDefaultLocation.json new file mode 100644 index 00000000..96111e9c --- /dev/null +++ b/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithDefaultLocation.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "type": "string", + "defaultValue": "southcentralus", + "metadata": { + "description": "Location for all resources." + } + }, + "managedIdentityName": { + "type": "string", + "metadata": { + "description": "Specifies managed identity name" + } + } + }, + "resources": [ + { + "apiVersion": "2019-08-01", + "type": "Microsoft.Web/sites", + "kind": "functionapp", + "name": "functionAppKind", + "location": "[parameters('location')]", + "properties": { + "httpsOnly": true, + "siteConfig": { + "detailedErrorLoggingEnabled": false, + "ftpsState": "Disabled", + "httpLoggingEnabled": false, + "minTlsVersion": "1.2", + "requestTracingEnabled": false + } + }, + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]": {} + } + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('managedIdentityName')]", + "location": "[parameters('location')]" + } + ] +} \ No newline at end of file diff --git a/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithHardcodedLocation.json b/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithHardcodedLocation.json new file mode 100644 index 00000000..3c47374f --- /dev/null +++ b/src/Analyzer.PowerShellRuleEngine.UnitTests/templates/templateWithHardcodedLocation.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "managedIdentityName": { + "type": "string", + "metadata": { + "description": "Specifies managed identity name" + } + } + }, + "resources": [ + { + "apiVersion": "2019-08-01", + "type": "Microsoft.Web/sites", + "kind": "functionapp", + "name": "functionAppKind", + "location": "southcentralus", + "properties": { + "httpsOnly": true, + "siteConfig": { + "detailedErrorLoggingEnabled": false, + "ftpsState": "Disabled", + "httpLoggingEnabled": false, + "minTlsVersion": "1.2", + "requestTracingEnabled": false + } + }, + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]": {} + } + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('managedIdentityName')]", + "location": "southcentralus" + } + ] +} \ No newline at end of file diff --git a/src/Analyzer.PowerShellRuleEngine/Analyzer.PowerShellRuleEngine.csproj b/src/Analyzer.PowerShellRuleEngine/Analyzer.PowerShellRuleEngine.csproj index 93c09331..824372dd 100644 --- a/src/Analyzer.PowerShellRuleEngine/Analyzer.PowerShellRuleEngine.csproj +++ b/src/Analyzer.PowerShellRuleEngine/Analyzer.PowerShellRuleEngine.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs b/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs index 1d18f1fa..cc62887f 100644 --- a/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs +++ b/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs @@ -68,8 +68,6 @@ public PowerShellRuleEngine(bool includeNonSecurityRules, ILogger logger = null) /// The s of the PowerShell rules against the template. public IEnumerable AnalyzeTemplate(TemplateContext templateContext) { - string templString = templateContext.ExpandedTemplate.ToString(); - if (templateContext?.TemplateIdentifier == null) { throw new ArgumentException($"{nameof(TemplateContext.TemplateIdentifier)} must not be null.", nameof(templateContext)); @@ -96,11 +94,6 @@ public IEnumerable AnalyzeTemplate(TemplateContext templateContext) { var fileOptions = PSRuleOption.FromFileOrEmpty(); - if (fileOptions.Configuration.ContainsKey("AZURE_RESOURCE_ALLOWED_LOCATIONS")) - { - // TODO: support external config for PSRule - } - hostContext = new PSRuleHostContext(templateContext, logger); var modules = new string[] { PSRuleModuleName }; var optionsForFileAnalysis = new PSRuleOption