Skip to content

Commit

Permalink
Added tag and annotation to policy rules #1652 (#1699)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Sep 23, 2022
1 parent 2783342 commit 1b60c70
Show file tree
Hide file tree
Showing 9 changed files with 17,587 additions and 56 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ What's changed since pre-release v1.20.0-B0028:
[#1672](https://github.com/Azure/PSRule.Rules.Azure/issues/1672)
- Updated KeyVault and FrontDoor documentation with code snippets by @lluppesms.
[#1667](https://github.com/Azure/PSRule.Rules.Azure/issues/1667)
- Added tag and annotation metadata from policy for rules generation by @BernieWhite.
[#1652](https://github.com/Azure/PSRule.Rules.Azure/issues/1652)
- Bug fixes:
- Fixed continue processing policy assignments on error by @BernieWhite.
[#1651](https://github.com/Azure/PSRule.Rules.Azure/issues/1651)
Expand Down
55 changes: 46 additions & 9 deletions src/PSRule.Rules.Azure/Data/Policy/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,62 @@ public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
}
}

/// <summary>
/// Defines an Azure Policy Definition represented as a PSRule rule.
/// </summary>
internal sealed class PolicyDefinition
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Effect { get; set; }
public JObject Value { get; set; }
public JObject Condition { get; set; }
private readonly IDictionary<string, IParameterValue> _Parameters;

public PolicyDefinition(string id, string name, string description, JObject value)
public PolicyDefinition(string definitionId, string name, string description, JObject value)
{
Id = id;
DefinitionId = definitionId;
Name = name;
Description = description;
Value = value;
_Parameters = new Dictionary<string, IParameterValue>(StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// The policy definition id.
/// </summary>
public string DefinitionId { get; set; }

/// <summary>
/// The name of the rule.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The synopsis of the rule.
/// </summary>
public string Description { get; set; }

/// <summary>
/// The resulting effect of the policy.
/// </summary>
public string Effect { get; set; }

/// <summary>
/// The raw original policy definition.
/// </summary>
public JObject Value { get; set; }

/// <summary>
/// The spec condition for the rule.
/// </summary>
public JObject Condition { get; set; }

/// <summary>
/// An optional metadata category of the policy.
/// </summary>
public string Category { get; internal set; }

/// <summary>
/// An optional metadata version of the policy.
/// </summary>
public string Version { get; internal set; }

internal void AddParameter(string name, ParameterType type, object value)
{
_Parameters.Add(name, new SimpleParameterValue(name, type, value));
Expand Down Expand Up @@ -125,4 +162,4 @@ public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
return _Value;
}
}
}
}
76 changes: 47 additions & 29 deletions src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace PSRule.Rules.Azure.Data.Policy
{
/// <summary>
/// This visitor processes each assignment to convert the assignment in to one or mny rules.
/// </summary>
internal abstract class PolicyAssignmentVisitor
{
private const string PROPERTY_PARAMETERS = "parameters";
Expand Down Expand Up @@ -42,6 +45,9 @@ internal abstract class PolicyAssignmentVisitor
private const string FIELD_EXISTS = "exists";
private const string PROPERTY_DISPLAYNAME = "displayName";
private const string PROPERTY_DESCRIPTION = "description";
private const string PROPERTY_METADATA = "metadata";
private const string PROPERTY_VERSION = "version";
private const string PROPERTY_CATEGORY = "category";
private const string PROPERTY_DEPLOYMENT = "deployment";
private const string PROPERTY_VALUE = "value";
private const string PROPERTY_COUNT = "count";
Expand Down Expand Up @@ -132,40 +138,48 @@ internal void AddParameterAssignment(string name, JToken value)

public void AddDefinition(JObject definition, string definitionId)
{
// A definition must have properties, policyRule, and a non-disabled effect.
if (!definition.TryObjectProperty(PROPERTY_PROPERTIES, out var properties) ||
!properties.TryObjectProperty(PROPERTY_POLICYRULE, out var policyRule) ||
!TryPolicyRuleEffect(policyRule, out var effect) ||
effect.Equals(DISABLED_EFFECT, StringComparison.OrdinalIgnoreCase))
return;

if (definition.TryObjectProperty(PROPERTY_PROPERTIES, out var properties))
properties.TryStringProperty(PROPERTY_DISPLAYNAME, out var displayName);
properties.TryStringProperty(PROPERTY_DESCRIPTION, out var description);
var policyDefinition = new PolicyDefinition(definitionId, displayName, description, definition)
{
properties.TryStringProperty(PROPERTY_DISPLAYNAME, out var displayName);
properties.TryStringProperty(PROPERTY_DESCRIPTION, out var description);
Effect = effect
};

var policyDefinition = new PolicyDefinition(definitionId, displayName, description, definition);
// Set annotations
if (properties.TryObjectProperty(PROPERTY_METADATA, out var metadata))
{
if (metadata.TryStringProperty(PROPERTY_CATEGORY, out var category))
policyDefinition.Category = category;

// Set parameters
if (properties.TryObjectProperty(PROPERTY_PARAMETERS, out var parameters))
{
foreach (var parameter in parameters.Properties())
SetDefinitionParameterAssignment(policyDefinition, parameter);
if (metadata.TryStringProperty(PROPERTY_VERSION, out var version))
policyDefinition.Version = version;

_DefinitionIds.Add(definitionId, policyDefinition);
}
}

// Modify policy rule
if (properties.TryObjectProperty(PROPERTY_POLICYRULE, out var policyRule))
{
RemovePolicyRuleDeployment(policyRule);
ExpandPolicyRule(policyRule);
MergePolicyRuleConditions(policyRule);
if (policyRule.TryObjectProperty(PROPERTY_CONDITION, out var condition))
policyDefinition.Condition = condition;

if (TryPolicyRuleEffect(policyRule, out var effect))
policyDefinition.Effect = effect;
}
// Set parameters
if (properties.TryObjectProperty(PROPERTY_PARAMETERS, out var parameters))
{
foreach (var parameter in parameters.Properties())
SetDefinitionParameterAssignment(policyDefinition, parameter);

// Skip adding definitions with disabled effect
if (!policyDefinition.Effect.Equals(DISABLED_EFFECT, StringComparison.OrdinalIgnoreCase))
_Definitions.Add(policyDefinition);
_DefinitionIds.Add(definitionId, policyDefinition);
}

// Modify policy rule
RemovePolicyRuleDeployment(policyRule);
ExpandPolicyRule(policyRule);
MergePolicyRuleConditions(policyRule);
if (policyRule.TryObjectProperty(PROPERTY_CONDITION, out var condition))
policyDefinition.Condition = condition;

_Definitions.Add(policyDefinition);
}

private void SetDefinitionParameterAssignment(PolicyDefinition definition, JProperty parameter)
Expand Down Expand Up @@ -674,6 +688,9 @@ protected virtual void VisitAssignmentParameters(PolicyAssignmentContext context
context.AddParameterAssignment(parameter.Name, parameter.Value);
}

/// <summary>
/// Process each policy definition of the assignment.
/// </summary>
protected virtual void VisitDefinitions(PolicyAssignmentContext context, IEnumerable<JObject> definitions)
{
if (definitions == null || !definitions.Any())
Expand All @@ -689,10 +706,11 @@ protected virtual void VisitDefinitions(PolicyAssignmentContext context, IEnumer
}
}

/// <summary>
/// Process each policy assignment and linked definitions.
/// </summary>
protected virtual void Assignment(PolicyAssignmentContext context, JObject assignment)
{
// Process assignment sections

// Assignment Properties
if (assignment.TryObjectProperty(PROPERTY_PROPERTIES, out var properties))
{
Expand All @@ -711,4 +729,4 @@ internal sealed class PolicyAssignmentDataExportVisitor : PolicyAssignmentVisito
{

}
}
}
45 changes: 44 additions & 1 deletion src/PSRule.Rules.Azure/Data/Policy/PolicyJsonRuleMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

namespace PSRule.Rules.Azure.Data.Policy
{
/// <summary>
/// Serializes a policy definition to a rule.
/// </summary>
internal static class PolicyJsonRuleMapper
{
private const string SYNOPSIS_COMMENT = "Synopsis: ";
Expand All @@ -16,6 +19,11 @@ internal static class PolicyJsonRuleMapper
private const string PROPERTY_NAME = "name";
private const string PROPERTY_SPEC = "spec";
private const string PROPERTY_CONDITION = "condition";
private const string PROPERTY_TAGS = "tags";
private const string PROPERTY_ANNOTATIONS = "annotations";
private const string PROPERTY_CATEGORY = "Azure.Policy/category";
private const string PROPERTY_VERSION = "Azure.Policy/version";
private const string PROPERTY_ID = "Azure.Policy/id";

internal static void MapRule(JsonWriter writer, JsonSerializer serializer, PolicyDefinition definition)
{
Expand All @@ -37,6 +45,8 @@ internal static void MapRule(JsonWriter writer, JsonSerializer serializer, Polic
writer.WriteStartObject();
writer.WritePropertyName(PROPERTY_NAME);
writer.WriteValue(definition.Name);
WriteTags(writer, definition);
WriteAnnotations(writer, definition);
writer.WriteEndObject();

// Spec
Expand All @@ -48,5 +58,38 @@ internal static void MapRule(JsonWriter writer, JsonSerializer serializer, Polic

writer.WriteEndObject();
}

private static void WriteTags(JsonWriter writer, PolicyDefinition definition)
{
if (string.IsNullOrEmpty(definition.Category))
return;

writer.WritePropertyName(PROPERTY_TAGS);
writer.WriteStartObject();
writer.WritePropertyName(PROPERTY_CATEGORY);
writer.WriteValue(definition.Category);
writer.WriteEndObject();
}

private static void WriteAnnotations(JsonWriter writer, PolicyDefinition definition)
{
if (string.IsNullOrEmpty(definition.Version) &&
string.IsNullOrEmpty(definition.DefinitionId))
return;

writer.WritePropertyName(PROPERTY_ANNOTATIONS);
writer.WriteStartObject();
if (!string.IsNullOrEmpty(definition.DefinitionId))
{
writer.WritePropertyName(PROPERTY_ID);
writer.WriteValue(definition.DefinitionId);
}
if (!string.IsNullOrEmpty(definition.Version))
{
writer.WritePropertyName(PROPERTY_VERSION);
writer.WriteValue(definition.Version);
}
writer.WriteEndObject();
}
}
}
}
12 changes: 6 additions & 6 deletions tests/PSRule.Rules.Azure.Tests/Cmdlet.Common.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ Describe 'Get-AzRuleTemplateLink' -Tag 'Cmdlet', 'Get-AzRuleTemplateLink' {

#region Export-AzPolicyAssignmentData

Describe 'Export-AzPolicyAssignmentData' -Tag 'Cmdlet', 'Export-AzPolicyAssignmentData' {
Describe 'Export-AzPolicyAssignmentData' -Tag 'Cmdlet', 'Export-AzPolicyAssignmentData', 'assignment' {
Context 'With Defaults' {
BeforeAll {
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith ${function:MockSingleSubscription};
Expand Down Expand Up @@ -654,7 +654,7 @@ Describe 'Export-AzPolicyAssignmentData' -Tag 'Cmdlet', 'Export-AzPolicyAssignme

#region Export-AzPolicyAssignmentRuleData

Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssignmentRuleData' {
Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssignmentRuleData', 'assignment' {
BeforeAll {
$emittedJsonRulesDataFile = Join-Path -Path $here -ChildPath 'emittedJsonRulesData.jsonc';
$jsonRulesData = ((Get-Content -Path $emittedJsonRulesDataFile) -replace '^\s*//.*') | ConvertFrom-Json;
Expand Down Expand Up @@ -733,14 +733,14 @@ Describe 'Export-AzPolicyAssignmentRuleData' -Tag 'Cmdlet', 'Export-AzPolicyAssi

#region Get-AzPolicyAssignmentDataSource

Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignmentDataSource' {
Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignmentDataSource', 'assignment' {
BeforeAll {
$emittedJsonRulesDataFile = Join-Path -Path $here -ChildPath 'emittedJsonRulesData.jsonc';
$jsonRulesData = ((Get-Content -Path $emittedJsonRulesDataFile) -replace '^\s*//.*') | ConvertFrom-Json;
}

It 'Get assignment sources from current working directory' {
$sources = Get-AzPolicyAssignmentDataSource | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') }
$sources = Get-AzPolicyAssignmentDataSource | Where-Object { $_.AssignmentFile -notlike "*Policy.assignment.json" } | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') }
$sources.Length | Should -Be 10;
$sources[0].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test.assignment.json');
$sources[1].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test2.assignment.json');
Expand All @@ -755,7 +755,7 @@ Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignme
}

It 'Get assignment sources from tests folder' {
$sources = Get-AzPolicyAssignmentDataSource -Path $here | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') }
$sources = Get-AzPolicyAssignmentDataSource -Path $here | Where-Object { $_.AssignmentFile -notlike "*Policy.assignment.json" } | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') }
$sources.Length | Should -Be 10;
$sources[0].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test.assignment.json');
$sources[1].AssignmentFile | Should -BeExactly (Join-Path -Path $here -ChildPath 'test2.assignment.json');
Expand All @@ -770,7 +770,7 @@ Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignme
}

It 'Pipe to Export-AzPolicyAssignmentRuleData and generate JSON rules' {
$result = @(Get-AzPolicyAssignmentDataSource | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') } | Export-AzPolicyAssignmentRuleData -Name 'tests' -OutputPath $outputPath);
$result = @(Get-AzPolicyAssignmentDataSource | Where-Object { $_.AssignmentFile -notlike "*Policy.assignment.json" } | Sort-Object { [int](Split-Path -Path $_.AssignmentFile -Leaf).Split('.')[0].TrimStart('test') } | Export-AzPolicyAssignmentRuleData -Name 'tests' -OutputPath $outputPath);
$result.Length | Should -Be 1;
$result | Should -BeOfType System.IO.FileInfo;
$filename = Split-Path -Path $result.FullName -Leaf;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
<None Update="environments.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Policy.assignment.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ps-rule-options.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading

0 comments on commit 1b60c70

Please sign in to comment.