Skip to content

Commit

Permalink
update PSRule, part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
nonik0 committed Feb 22, 2024
1 parent 57d832c commit 165cb3d
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyzer.Core.UnitTests/TemplateAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@
</Content>
</ItemGroup>

<ItemGroup>
<None Remove="templates\templateWithDefaultLocation.json" />
<None Remove="templates\templateWithHardcodedLocation.json" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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");
Expand All @@ -115,19 +132,18 @@ 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");
var newBaselineLocation = baselineLocation + ".moved";
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
Expand All @@ -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<PowerShellRuleEvaluation>();

foreach (PowerShellRuleEvaluation evaluation in evaluations)
{
if (!evaluation.Passed)
{
failedEvaluations.Add(evaluation);
}
}
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void AnalyzeTemplate_NullTemplateContext_ThrowsException()
Expand Down
Original file line number Diff line number Diff line change
@@ -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')]"
}
]
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.3.11" />
<PackageReference Include="Microsoft.PSRule.Rules.Azure" Version="1.32.1" />
<PackageReference Include="Microsoft.PSRule.Rules.Azure" Version="1.33.2" />
<PackageReference Include="Microsoft.PSRule.SDK" Version="2.9.0" />
</ItemGroup>

Expand Down
7 changes: 0 additions & 7 deletions src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ public PowerShellRuleEngine(bool includeNonSecurityRules, ILogger logger = null)
/// <returns>The <see cref="IEvaluation"/>s of the PowerShell rules against the template.</returns>
public IEnumerable<IEvaluation> AnalyzeTemplate(TemplateContext templateContext)
{
string templString = templateContext.ExpandedTemplate.ToString();

if (templateContext?.TemplateIdentifier == null)
{
throw new ArgumentException($"{nameof(TemplateContext.TemplateIdentifier)} must not be null.", nameof(templateContext));
Expand All @@ -96,11 +94,6 @@ public IEnumerable<IEvaluation> 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
Expand Down

0 comments on commit 165cb3d

Please sign in to comment.