зеркало из
1
0
Форкнуть 0

Fixed handling of multi-line descriptions #2973 (#2975)

This commit is contained in:
Bernie White 2024-07-06 16:46:27 +10:00 коммит произвёл GitHub
Родитель 6091771d93
Коммит 7788c3e295
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 430 добавлений и 211 удалений

Просмотреть файл

@ -36,6 +36,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- Bug fixes:
- Rollback Az.Resources to v6.7.0.
[#2970](https://github.com/Azure/PSRule.Rules.Azure/issues/2970)
- Fixed handling of multi-line descriptions for policy definition and assignment exports by @BernieWhite.
[#2973](https://github.com/Azure/PSRule.Rules.Azure/issues/2973)
## v1.38.0-B0068 (pre-release)

Просмотреть файл

@ -8,6 +8,9 @@ namespace PSRule.Rules.Azure
{
internal static class StringExtensions
{
private static readonly string[] LINE_SEPARATORS = new string[] { "\r\n", "\n", "\r" };
private static readonly char[] RAW_LINE_SEPARATORS = new char[] { '\r', '\n' };
/// <summary>
/// Convert the first character of the string to lower-case.
/// </summary>
@ -83,5 +86,38 @@ namespace PSRule.Rules.Azure
str[1] != '[' &&
str[str.Length - 1] == ']';
}
/// <summary>
/// Get the first line of a string.
/// If the string contains new line characters, only the first line is returned.
/// </summary>
/// <param name="str">The string to use.</param>
/// <returns>A formatted string.</returns>
internal static string ToFirstLine(this string str)
{
if (string.IsNullOrEmpty(str))
return string.Empty;
var firstLineEnd = str.IndexOfAny(RAW_LINE_SEPARATORS);
return firstLineEnd == -1
? str
: str.Substring(0, firstLineEnd);
}
/// <summary>
/// Replace new line separators with the system default.
/// </summary>
/// <param name="str">The string to replace.</param>
/// <param name="replacement">Replace the new line with the supplied sequence. By default this will be the new line separator for the current operating system.</param>
/// <returns>A formatted string with new line separators replaced.</returns>
internal static string ReplaceNewLineSeparator(this string str, string replacement)
{
if (str == null || str.Length == 0) return str;
replacement ??= Environment.NewLine;
var s = str.Split(LINE_SEPARATORS, StringSplitOptions.None);
return string.Join(replacement, s);
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Policy
{
internal interface ILazyValue
{
object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context);
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Policy
{
internal interface IParameterValue
{
string Name { get; }
ParameterType Type { get; }
object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context);
}
}

Просмотреть файл

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Data.Template;
namespace PSRule.Rules.Azure.Data.Policy
{
internal sealed class LazyParameter<T> : ILazyValue, IParameterValue
{
private readonly JToken _LazyValue;
private T _Value;
private bool _Resolved;
public LazyParameter(string name, ParameterType type, JToken defaultValue)
{
Name = name;
Type = type;
_LazyValue = defaultValue;
}
public string Name { get; }
public ParameterType Type { get; }
public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
{
if (!_Resolved)
{
_Value = TemplateVisitor.ExpandToken<T>(context, _LazyValue);
_Resolved = true;
}
return _Value;
}
}
}

Просмотреть файл

@ -1,202 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Data.Template;
namespace PSRule.Rules.Azure.Data.Policy
{
[JsonConverter(typeof(StringEnumConverter))]
internal enum ParameterType
{
String,
Array,
Object,
Boolean,
Integer,
Float,
DateTime
}
internal interface IParameterValue
{
string Name { get; }
ParameterType Type { get; }
object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context);
}
internal sealed class SimpleParameterValue : IParameterValue
{
private readonly object _Value;
public SimpleParameterValue(string name, ParameterType type, object value)
{
Name = name;
Type = type;
_Value = value;
}
public string Name { get; }
public ParameterType Type { get; }
public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
{
return _Value;
}
}
/// <summary>
/// Defines an Azure Policy Definition represented as a PSRule rule.
/// </summary>
internal sealed class PolicyDefinition
{
public PolicyDefinition(string definitionId, string description, JObject value, string displayName)
{
DefinitionId = definitionId;
Description = description;
Value = value;
DisplayName = displayName;
Parameters = new Dictionary<string, IParameterValue>(StringComparer.OrdinalIgnoreCase);
Types = new List<string>();
}
/// <summary>
/// The policy definition parameters
/// </summary>
public readonly IDictionary<string, IParameterValue> Parameters;
/// <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 display name of the rule.
/// </summary>
public string DisplayName { 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>
/// The spec where pre-condition for the rule.
/// </summary>
public JObject Where { get; set; }
/// <summary>
/// The spec with pre-condition for the rule.
/// </summary>
public string[] With { get; set; }
/// <summary>
/// The spec type pre-condition for the rule.
/// </summary>
public List<string> Types { get; }
/// <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));
}
internal void AddParameter(IParameterValue value)
{
Parameters.Add(value.Name, value);
}
}
internal interface ILazyValue
{
object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context);
}
internal sealed class LazyParameter<T> : ILazyValue, IParameterValue
{
private readonly JToken _LazyValue;
private T _Value;
private bool _Resolved;
public LazyParameter(string name, ParameterType type, JToken defaultValue)
{
Name = name;
Type = type;
_LazyValue = defaultValue;
}
public string Name { get; }
public ParameterType Type { get; }
public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
{
if (!_Resolved)
{
_Value = TemplateVisitor.ExpandToken<T>(context, _LazyValue);
_Resolved = true;
}
return _Value;
}
}
[JsonConverter(typeof(PolicyBaselineConverter))]
internal sealed class PolicyBaseline
{
public PolicyBaseline(string name, string description, IEnumerable<string> definitionRuleNames, IEnumerable<string> replacedRuleNames)
{
Name = name;
Description = description;
Include = Union(definitionRuleNames ?? Array.Empty<string>(), replacedRuleNames ?? Array.Empty<string>());
}
private static string[] Union(IEnumerable<string> definitionRuleNames, IEnumerable<string> replacedRuleNames)
{
return definitionRuleNames.Union(replacedRuleNames).ToArray();
}
public string Name { get; }
public string Description { get; }
public string[] Include { get; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace PSRule.Rules.Azure.Data.Policy
{
[JsonConverter(typeof(StringEnumConverter))]
internal enum ParameterType
{
String,
Array,
Object,
Boolean,
Integer,
Float,
DateTime
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Policy
{
internal sealed class PolicyAssignmentDataExportVisitor : PolicyAssignmentVisitor
{
}
}

Просмотреть файл

@ -1986,9 +1986,4 @@ namespace PSRule.Rules.Azure.Data.Policy
return new PolicyDefinitionEmptyConditionException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.EmptyConditionExpandResult, policyDefinitionId, context.AssignmentId), context.AssignmentFile, context.AssignmentId, policyDefinitionId);
}
}
internal sealed class PolicyAssignmentDataExportVisitor : PolicyAssignmentVisitor
{
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace PSRule.Rules.Azure.Data.Policy
{
[JsonConverter(typeof(PolicyBaselineConverter))]
internal sealed class PolicyBaseline
{
public PolicyBaseline(string name, string description, IEnumerable<string> definitionRuleNames, IEnumerable<string> replacedRuleNames)
{
Name = name;
Description = description;
Include = Union(definitionRuleNames ?? Array.Empty<string>(), replacedRuleNames ?? Array.Empty<string>());
}
private static string[] Union(IEnumerable<string> definitionRuleNames, IEnumerable<string> replacedRuleNames)
{
return definitionRuleNames.Union(replacedRuleNames).ToArray();
}
public string Name { get; }
public string Description { get; }
public string[] Include { get; }
}
}

Просмотреть файл

@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace PSRule.Rules.Azure.Data.Policy
{
/// <summary>
/// Defines an Azure Policy Definition represented as a PSRule rule.
/// </summary>
internal sealed class PolicyDefinition
{
public PolicyDefinition(string definitionId, string description, JObject value, string displayName)
{
DefinitionId = definitionId;
Recommendation = description.ReplaceNewLineSeparator(replacement: "\n");
Synopsis = description.ToFirstLine();
Value = value;
DisplayName = displayName;
Parameters = new Dictionary<string, IParameterValue>(StringComparer.OrdinalIgnoreCase);
Types = new List<string>();
}
/// <summary>
/// The policy definition parameters
/// </summary>
public readonly IDictionary<string, IParameterValue> Parameters;
/// <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 Synopsis { get; set; }
/// <summary>
/// The recommendation of the rule.
/// </summary>
public string Recommendation { get; set; }
/// <summary>
/// The display name of the rule.
/// </summary>
public string DisplayName { 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>
/// The spec where pre-condition for the rule.
/// </summary>
public JObject Where { get; set; }
/// <summary>
/// The spec with pre-condition for the rule.
/// </summary>
public string[] With { get; set; }
/// <summary>
/// The spec type pre-condition for the rule.
/// </summary>
public List<string> Types { get; }
/// <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));
}
internal void AddParameter(IParameterValue value)
{
Parameters.Add(value.Name, value);
}
}
}

Просмотреть файл

@ -37,7 +37,7 @@ namespace PSRule.Rules.Azure.Data.Policy
writer.WriteStartObject();
// Synopsis
writer.WriteComment(string.Concat(SYNOPSIS_COMMENT, definition.Description));
writer.WriteComment(string.Concat(SYNOPSIS_COMMENT, definition.Synopsis));
// Api Version
writer.WritePropertyName(PROPERTY_APIVERSION);
@ -65,7 +65,7 @@ namespace PSRule.Rules.Azure.Data.Policy
writer.WritePropertyName(PROPERTY_SPEC);
writer.WriteStartObject();
writer.WritePropertyName(PROPERTY_RECOMMEND);
writer.WriteValue(definition.Description);
writer.WriteValue(definition.Recommendation);
WriteType(writer, serializer, definition);
WriteWith(writer, serializer, definition);
WriteWhere(writer, serializer, definition);

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Policy
{
internal sealed class SimpleParameterValue : IParameterValue
{
private readonly object _Value;
public SimpleParameterValue(string name, ParameterType type, object value)
{
Name = name;
Type = type;
_Value = value;
}
public string Name { get; }
public ParameterType Type { get; }
public object GetValue(PolicyAssignmentVisitor.PolicyAssignmentContext context)
{
return _Value;
}
}
}

Просмотреть файл

@ -53,6 +53,9 @@
<None Update="Policy.assignment.8.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Policy.assignment.9.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Policy.assignment.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Просмотреть файл

@ -0,0 +1,112 @@
[
{
"Identity": null,
"Location": null,
"Name": "assignment.9",
"ResourceId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/assignment.9",
"ResourceName": "assignment.9",
"ResourceGroupName": null,
"ResourceType": "Microsoft.Authorization/policyAssignments",
"SubscriptionId": null,
"Sku": null,
"PolicyAssignmentId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/assignment.9",
"Properties": {
"Scope": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000",
"NotScopes": null,
"DisplayName": "Org Deny Policies",
"Description": "https://docs.contoso.com/azure/policies",
"Metadata": {
"Version": "20240706"
},
"EnforcementMode": 0,
"PolicyDefinitionId": "/providers/Microsoft.Management/managementGroups/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policySetDefinitions/assignment.9",
"Parameters": null,
"NonComplianceMessages": null
},
"PolicyDefinitions": [
{
"Name": "policyDefinition.1",
"ResourceId": "/providers/Microsoft.Authorization/policyDefinitions/policyDefinition.1",
"ResourceName": "policyDefinition.1",
"ResourceType": "Microsoft.Authorization/policyDefinitions",
"SubscriptionId": null,
"Properties": {
"Description": "Microsoft Defender for Azure Cosmos DB is an Azure-native layer of security that detects attempts to exploit databases in your Azure Cosmos DB accounts.\r\nDefender for Azure Cosmos DB detects potential SQL injections, known bad actors based on Microsoft Threat Intelligence, suspicious access patterns, and potential exploitations of your database through compromised identities or malicious insiders.",
"DisplayName": "Configure Microsoft Defender for Azure Cosmos DB to be enabled",
"Metadata": {
"version": "1.0.0",
"category": "Security Center"
},
"Mode": "All",
"Parameters": {
"effect": {
"type": "String",
"metadata": {
"displayName": "Effect",
"description": "Enable or disable the execution of the policy"
},
"allowedValues": [
"DeployIfNotExists",
"Disabled"
],
"defaultValue": "DeployIfNotExists"
}
},
"PolicyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Resources/subscriptions"
}
]
},
"then": {
"effect": "[parameters('effect')]",
"details": {
"type": "Microsoft.Security/pricings",
"name": "CosmosDbs",
"deploymentScope": "subscription",
"existenceScope": "subscription",
"roleDefinitionIds": [
"/providers/Microsoft.Authorization/roleDefinitions/fb1c8493-542b-48eb-b624-b4c8fea62acd"
],
"existenceCondition": {
"field": "Microsoft.Security/pricings/pricingTier",
"equals": "Standard"
},
"deployment": {
"location": "westeurope",
"properties": {
"mode": "incremental",
"parameters": {},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"resources": [
{
"type": "Microsoft.Security/pricings",
"apiVersion": "2018-06-01",
"name": "CosmosDbs",
"properties": {
"pricingTier": "Standard"
}
}
],
"outputs": {}
}
}
}
}
}
},
"PolicyType": 2
},
"PolicyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/policyDefinition.1"
}
],
"exemptions": []
}
]

Просмотреть файл

@ -153,7 +153,7 @@ namespace PSRule.Rules.Azure
Assert.NotNull(actual);
Assert.Equal("Azure.Policy.8fc87228ae18", actual.Name);
Assert.Equal("Allowed locations for resource groups", actual.DisplayName);
Assert.Equal("This policy enables you to restrict the locations your organization can create resource groups in. Use to enforce your geo-compliance requirements.", actual.Description);
Assert.Equal("This policy enables you to restrict the locations your organization can create resource groups in. Use to enforce your geo-compliance requirements.", actual.Recommendation);
Assert.Equal("General", actual.Category);
Assert.Equal("1.0.0", actual.Version);
Assert.Single(actual.Types);
@ -179,7 +179,8 @@ namespace PSRule.Rules.Azure
Assert.NotNull(actual);
Assert.Equal("Azure.Policy.6db2a8060ade", actual.Name);
Assert.Equal("Require a tag on resource groups", actual.DisplayName);
Assert.Equal("Enforces existence of a tag on resource groups.", actual.Description);
Assert.Equal("Enforces existence of a tag on resource groups.", actual.Synopsis);
Assert.Equal("Enforces existence of a tag on resource groups.", actual.Recommendation);
Assert.Equal("Tags", actual.Category);
Assert.Equal("1.0.0", actual.Version);
Assert.Single(actual.Types);
@ -312,6 +313,23 @@ namespace PSRule.Rules.Azure
Assert.Equal("{\"anyOf\":[{\"exists\":false,\"field\":\"properties.privateEndpoint.id\"},{\"notEquals\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",\"value\":{\"$\":{\"split\":{\"concat\":[{\"path\":\"properties.privateEndpoint.id\"},{\"string\":\"//\"}]},\"delimiter\":[\"/\"]}}}]}", temp);
}
[Fact]
public void Visit_ShouldReturnFormattedSynopsis_WhenMultiLineDescription()
{
var context = new PolicyAssignmentContext(GetContext());
var visitor = new PolicyAssignmentDataExportVisitor();
foreach (var assignment in GetAssignmentData("Policy.assignment.9.json").Where(a => a["Name"].Value<string>() == "assignment.9"))
visitor.Visit(context, assignment);
var definitions = context.GetDefinitions();
Assert.NotNull(definitions);
Assert.Single(definitions);
var actual = definitions.FirstOrDefault(definition => definition.DefinitionId == "/providers/Microsoft.Authorization/policyDefinitions/policyDefinition.1");
Assert.Equal("Microsoft Defender for Azure Cosmos DB is an Azure-native layer of security that detects attempts to exploit databases in your Azure Cosmos DB accounts.", actual.Synopsis);
Assert.Equal("Microsoft Defender for Azure Cosmos DB is an Azure-native layer of security that detects attempts to exploit databases in your Azure Cosmos DB accounts.\nDefender for Azure Cosmos DB detects potential SQL injections, known bad actors based on Microsoft Threat Intelligence, suspicious access patterns, and potential exploitations of your database through compromised identities or malicious insiders.", actual.Recommendation);
}
#region Helper methods
private static PipelineContext GetContext(PSRuleOption option = null)