From ff021f897e78467b120137845d8f328acb2312e6 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 22 Oct 2024 04:07:19 +1000 Subject: [PATCH] Updated Azure.Deployment.SecureValue #2650 #2651 (#3139) --- data/secret-property.json | 204 ++++++++++++++++ docs/CHANGELOG-v1.md | 8 + docs/en/rules/Azure.Deployment.SecureValue.md | 15 +- .../ClientBuilder.cs | 37 ++- .../ProviderResource.cs | 227 +++++++++--------- .../Data/SecretPropertyData.cs | 54 +++++ .../Data/Template/ResourceProviderHelper.cs | 35 ++- src/PSRule.Rules.Azure/Runtime/Helper.cs | 21 +- .../Runtime/RuntimeService.cs | 6 + .../rules/Azure.Deployment.Rule.ps1 | 215 ++--------------- .../Azure.Baseline.Tests.ps1 | 16 +- .../Azure.Deployment.Tests.ps1 | 4 +- .../Data/SecretPropertyDataTests.cs | 24 ++ .../Tests.Bicep.9.badContainerApps.bicep | 21 ++ .../Tests.Bicep.9.bicep | 7 + .../Tests.Bicep.9.json | 74 +++++- .../TokenStreamValidatorTests.cs | 95 ++++---- 17 files changed, 635 insertions(+), 428 deletions(-) create mode 100644 data/secret-property.json create mode 100644 src/PSRule.Rules.Azure/Data/SecretPropertyData.cs create mode 100644 tests/PSRule.Rules.Azure.Tests/Data/SecretPropertyDataTests.cs create mode 100644 tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.badContainerApps.bicep diff --git a/data/secret-property.json b/data/secret-property.json new file mode 100644 index 00000000000..a8a00a74c06 --- /dev/null +++ b/data/secret-property.json @@ -0,0 +1,204 @@ +{ + "Microsoft.AAD/DomainServices": [ + "properties.ldapsSettings.pfxCertificatePassword" + ], + "Microsoft.ApiManagement/Service": [ + "properties.hostnameConfigurations.certificatePassword", + "properties.certificates.certificatePassword" + ], + "Microsoft.ApiManagement/Service/AuthorizationServers": [ + "properties.clientSecret", + "properties.resourceOwnerPassword" + ], + "Microsoft.ApiManagement/Service/Backends": [ + "properties.proxy.password" + ], + "Microsoft.ApiManagement/Service/Certificates": [ + "properties.password" + ], + "Microsoft.ApiManagement/Service/IdentityProviders": [ + "properties.clientSecret" + ], + "Microsoft.ApiManagement/Service/OpenidConnectProviders": [ + "properties.clientSecret" + ], + "Microsoft.ApiManagement/Service/Users": [ + "properties.password" + ], + "Microsoft.App/containerApps": [ + "properties.configuration.secrets[*].value" + ], + "Microsoft.App/jobs": [ + "properties.configuration.secrets[*].value" + ], + "Microsoft.App/managedEnvironments": [ + "properties.customDomainConfiguration.certificatePassword", + "properties.customDomainConfiguration.certificateValue" + ], + "Microsoft.App/managedEnvironments/certificates": [ + "properties.password", + "properties.value" + ], + "Microsoft.Automation/AutomationAccounts/Credentials": [ + "properties.password" + ], + "Microsoft.Batch/BatchAccounts/Pools": [ + "properties.userAccounts.linuxUserConfiguration.sshPrivateKey" + ], + "Microsoft.Blockchain/BlockchainMembers": [ + "properties.password", + "properties.consortiumManagementAccountPassword" + ], + "Microsoft.Blockchain/BlockchainMembers/TransactionNodes": [ + "properties.password" + ], + "Microsoft.BotService/BotServices/Connections": [ + "properties.clientSecret" + ], + "Microsoft.Compute/virtualMachineScaleSets": [ + "properties.virtualMachineProfile.osProfile.adminPassword" + ], + "Microsoft.Compute/VirtualMachineScaleSets/Virtualmachines": [ + "properties.osProfile.adminPassword" + ], + "Microsoft.Compute/VirtualMachines": [ + "properties.osProfile.adminPassword" + ], + "Microsoft.ContainerInstance/ContainerGroups": [ + "properties.imageRegistryCredentials.password" + ], + "Microsoft.ContainerService/ContainerServices": [ + "properties.servicePrincipalProfile.secret", + "properties.windowsProfile.adminPassword" + ], + "Microsoft.ContainerService/ManagedClusters": [ + "properties.windowsProfile.adminPassword", + "properties.servicePrincipalProfile.secret", + "properties.aadProfile.serverAppSecret" + ], + "Microsoft.ContainerService/OpenShiftManagedClusters": [ + "properties.authProfile.identityProviders.provider.secret" + ], + "Microsoft.Resources/deploymentScripts": [ + "properties.storageAccountSettings.storageAccountKey", + "properties.environmentVariables[*].secureValue" + ], + "Microsoft.DBforMariaDB/Servers": [ + "properties.administratorLoginPassword" + ], + "Microsoft.DBforMySQL/Servers": [ + "properties.administratorLoginPassword" + ], + "Microsoft.DBforPostgreSQL/Servers": [ + "properties.administratorLoginPassword" + ], + "Microsoft.DataMigration/Services/Projects": [ + "properties.sourceConnectionInfo.password", + "properties.targetConnectionInfo.password" + ], + "Microsoft.DevTestLab/Labs/Formulas": [ + "properties.formulaContent.properties.password" + ], + "Microsoft.DevTestLab/Labs/Users/Secrets": [ + "properties.value" + ], + "Microsoft.DevTestLab/Labs/Virtualmachines": [ + "properties.password" + ], + "Microsoft.HDInsight/Clusters": [ + "properties.securityProfile.domainUserPassword", + "properties.computeProfile.roles.osProfile.linuxOperatingSystemProfile.password" + ], + "Microsoft.HDInsight/Clusters/Applications": [ + "properties.computeProfile.roles.osProfile.linuxOperatingSystemProfile.password" + ], + "Microsoft.KeyVault/Vaults/Secrets": [ + "properties.value" + ], + "Microsoft.Logic/IntegrationAccounts/Agreements": [ + "properties.content.x12.receiveAgreement.protocolSettings.securitySettings.passwordValue", + "properties.content.x12.sendAgreement.protocolSettings.securitySettings.passwordValue", + "properties.content.edifact.receiveAgreement.protocolSettings.envelopeSettings.recipientReferencePasswordValue", + "properties.content.edifact.sendAgreement.protocolSettings.envelopeSettings.recipientReferencePasswordValue", + "properties.content.edifact.receiveAgreement.protocolSettings.envelopeSettings.groupApplicationPassword", + "properties.content.edifact.sendAgreement.protocolSettings.envelopeSettings.groupApplicationPassword", + "properties.content.edifact.receiveAgreement.protocolSettings.envelopeOverrides.applicationPassword", + "properties.content.edifact.sendAgreement.protocolSettings.envelopeOverrides.applicationPassword" + ], + "Microsoft.NetApp/NetAppAccounts": [ + "properties.activeDirectories.password" + ], + "Microsoft.Network/ApplicationGateways": [ + "properties.sslCertificates.properties.password" + ], + "Microsoft.Network/Connections": [ + "properties.virtualNetworkGateway1.properties.vpnClientConfiguration.radiusServerSecret", + "properties.virtualNetworkGateway2.properties.vpnClientConfiguration.radiusServerSecret", + "properties.sharedKey" + ], + "Microsoft.Network/VirtualNetworkGateways": [ + "properties.vpnClientConfiguration.radiusServerSecret" + ], + "Microsoft.Network/VirtualWans/P2sVpnServerConfigurations": [ + "properties.radiusServerSecret" + ], + "Microsoft.Network/VpnServerConfigurations": [ + "properties.radiusServerSecret" + ], + "Microsoft.NotificationHubs/Namespaces/NotificationHubs": [ + "properties.wnsCredential.properties.secretKey", + "properties.admCredential.properties.clientSecret", + "properties.baiduCredential.properties.baiduSecretKey" + ], + "Microsoft.ServiceFabricMesh/Applications": [ + "properties.services.properties.codePackages.imageRegistryCredential.password" + ], + "Microsoft.ServiceFabricMesh/Secrets/Values": [ + "properties.value" + ], + "Microsoft.Sql/ManagedInstances": [ + "properties.administratorLoginPassword" + ], + "Microsoft.Sql/Servers": [ + "properties.administratorLoginPassword" + ], + "Microsoft.Sql/Servers/Databases/Extensions": [ + "properties.administratorLoginPassword" + ], + "Microsoft.Sql/Servers/Databases/SyncGroups": [ + "properties.hubDatabasePassword" + ], + "Microsoft.Sql/Servers/Databases/SyncGroups/SyncMembers": [ + "properties.password" + ], + "Microsoft.Sql/Servers/JobAgents/Credentials": [ + "properties.password" + ], + "Microsoft.SqlVirtualMachine/SqlVirtualMachines": [ + "properties.wsfcDomainCredentials.clusterBootstrapAccountPassword", + "properties.wsfcDomainCredentials.clusterOperatorAccountPassword", + "properties.wsfcDomainCredentials.sqlServiceAccountPassword", + "properties.autoBackupSettings.password", + "properties.keyVaultCredentialSettings.servicePrincipalSecret", + "properties.serverConfigurationsManagementSettings.sqlConnectivityUpdateSettings.sqlAuthUpdatePassword" + ], + "Microsoft.StorSimple/Managers/Devices/VolumeContainers": [ + "properties.encryptionKey.value" + ], + "Microsoft.StorSimple/Managers/StorageAccountCredentials": [ + "properties.accessKey.value" + ], + "Microsoft.StreamAnalytics/Streamingjobs": [ + "properties.inputs[*].properties.datasource.properties.password", + "properties.outputs[*].properties.datasource.properties.password" + ], + "Microsoft.StreamAnalytics/Streamingjobs/Outputs": [ + "properties.datasource.properties.password" + ], + "Microsoft.Web/Certificates": [ + "properties.password" + ], + "Microsoft.Web/Sourcecontrols": [ + "properties.tokenSecret" + ] +} diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 345dcc11271..864aac3b35f 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -31,6 +31,14 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since v1.39.3: +- Updated rules: + - Deployment: + - Updated `Azure.Deployment.SecureValue` to check additional resource types by @BernieWhite. + [#2650](https://github.com/Azure/PSRule.Rules.Azure/issues/2650) + [#2651](https://github.com/Azure/PSRule.Rules.Azure/issues/2651) + - Added support for container apps secret properties. + - Added support for deployment script secret properties. + - Bumped rule set to `2024_12`. - Engineering: - Migrated Azure samples into PSRule for Azure by @BernieWhite. [#3085](https://github.com/Azure/PSRule.Rules.Azure/issues/3085) diff --git a/docs/en/rules/Azure.Deployment.SecureValue.md b/docs/en/rules/Azure.Deployment.SecureValue.md index 24daa07e487..d4e43839589 100644 --- a/docs/en/rules/Azure.Deployment.SecureValue.md +++ b/docs/en/rules/Azure.Deployment.SecureValue.md @@ -2,16 +2,16 @@ reviewed: 2022-10-10 severity: Critical pillar: Security -category: Infrastructure provisioning +category: SE:02 Secured development lifecycle resource: Deployment online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Deployment.SecureValue/ --- -# Use secure resource values +# Deployment sets a secret property with a non-secure value ## SYNOPSIS -Use secure parameters for setting properties of resources that contain sensitive information. +A secret property set from a non-secure value may leak the secret into deployment history or logs. ## DESCRIPTION @@ -80,15 +80,12 @@ resource goodSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { ## NOTES -This rule checks the following resource type properties: +For a list of resource types and properties that are checked by this rule see: -- `Microsoft.KeyVault/vaults/secrets`: - - `properties.value` -- `Microsoft.Compute/virtualMachineScaleSets`: - - `properties.virtualMachineProfile.osProfile.adminPassword` +- https://github.com/Azure/PSRule.Rules.Azure/blob/main/data/secret-property.json ## LINKS -- [Infrastructure provisioning considerations in Azure](https://learn.microsoft.com/azure/architecture/framework/security/deploy-infrastructure) +- [SE:02 Secured development lifecycle](https://learn.microsoft.com/azure/well-architected/security/secure-development-lifecycle) - [Use Azure Key Vault to pass secure parameter value during Bicep deployment](https://learn.microsoft.com/azure/azure-resource-manager/bicep/key-vault-parameter) - [Integrate Azure Key Vault in your ARM template deployment](https://learn.microsoft.com/azure/azure-resource-manager/templates/template-tutorial-use-key-vault#edit-the-parameters-file) diff --git a/src/PSRule.Rules.Azure.BuildTool/ClientBuilder.cs b/src/PSRule.Rules.Azure.BuildTool/ClientBuilder.cs index 277b8cd30f8..062a0703ee9 100644 --- a/src/PSRule.Rules.Azure.BuildTool/ClientBuilder.cs +++ b/src/PSRule.Rules.Azure.BuildTool/ClientBuilder.cs @@ -6,27 +6,26 @@ using System.CommandLine.Invocation; using PSRule.Rules.Azure.BuildTool.Resources; -namespace PSRule.Rules.Azure.BuildTool +namespace PSRule.Rules.Azure.BuildTool; + +internal sealed class ClientBuilder : CommandBuilder { - internal sealed class ClientBuilder : CommandBuilder - { - private ClientBuilder(RootCommand cmd) : base(cmd) { } + private ClientBuilder(RootCommand cmd) : base(cmd) { } - public static ClientBuilder New() - { - var cmd = new RootCommand(); - return new ClientBuilder(cmd); - } + public static ClientBuilder New() + { + var cmd = new RootCommand(); + return new ClientBuilder(cmd); + } - public ClientBuilder AddProviderResource() - { - var cmd = new Command("provider", CmdStrings.Provider_Description); - cmd.AddOption(new Option( - new string[] { "--output-path" } - )); - cmd.Handler = CommandHandler.Create(ProviderResource.Build); - Command.AddCommand(cmd); - return this; - } + public ClientBuilder AddProviderResource() + { + var cmd = new Command("provider", CmdStrings.Provider_Description); + cmd.AddOption(new Option( + ["--output-path"] + )); + cmd.Handler = CommandHandler.Create(ProviderResource.Build); + Command.AddCommand(cmd); + return this; } } diff --git a/src/PSRule.Rules.Azure.BuildTool/ProviderResource.cs b/src/PSRule.Rules.Azure.BuildTool/ProviderResource.cs index e7999d596c8..06ff00ad23e 100644 --- a/src/PSRule.Rules.Azure.BuildTool/ProviderResource.cs +++ b/src/PSRule.Rules.Azure.BuildTool/ProviderResource.cs @@ -11,146 +11,153 @@ using Newtonsoft.Json.Linq; using PSRule.Rules.Azure.BuildTool.Models; -namespace PSRule.Rules.Azure.BuildTool +namespace PSRule.Rules.Azure.BuildTool; + +internal sealed class ProviderResourceOption +{ + public string OutputPath { get; set; } +} + +/// +/// Build a Azure resource type index for each resource provider. +/// +internal sealed class ProviderResource { - internal sealed class ProviderResourceOption + private sealed class TypeSet { - public string OutputPath { get; set; } + public string Provider { get; set; } + + public string Type { get; set; } } - /// - /// Build a Azure resource type index for each resource provider. - /// - internal sealed class ProviderResource + public static void Build(ProviderResourceOption options, InvocationContext invocation) { - private sealed class TypeSet - { - public string Provider { get; set; } - - public string Type { get; set; } - } + BuildIndex(options); + MinifyTypes(options); + MinifyEnvironments(options); + MinifyPolicyIgnore(options); + MinifySecretTemplate(options); + MinifySecretProperty(options); + } - public static void Build(ProviderResourceOption options, InvocationContext invocation) - { - BuildIndex(options); - MinifyTypes(options); - MinifyEnvironments(options); - MinifyPolicyIgnore(options); - MinifySecretTemplate(options); - } + private static void MinifyEnvironments(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Minify environments"); + var environments = ReadFile(GetSourcePath("./data/environments.json")); + WriteFile(GetSourcePath("./data/environments.min.json"), environments); + } - private static void MinifyEnvironments(ProviderResourceOption options) - { - Console.WriteLine("BuildTool -- Minify environments"); - var environments = ReadFile(GetSourcePath("./data/environments.json")); - WriteFile(GetSourcePath("./data/environments.min.json"), environments); - } + private static void MinifyPolicyIgnore(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Minify policy-ignore"); + var entries = ReadFile(GetSourcePath("./data/policy-ignore.json")); + WriteFile(GetSourcePath("./data/policy-ignore.min.json"), entries.Select(e => e.Min)); + } - private static void MinifyPolicyIgnore(ProviderResourceOption options) - { - Console.WriteLine("BuildTool -- Minify policy-ignore"); - var entries = ReadFile(GetSourcePath("./data/policy-ignore.json")); - WriteFile(GetSourcePath("./data/policy-ignore.min.json"), entries.Select(e => e.Min)); - } + private static void MinifySecretTemplate(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Minify secret-template"); + var secretTemplate = ReadFile(GetSourcePath("./data/secret-template.json")); + WriteFile(GetSourcePath("./data/secret-template.min.json"), secretTemplate); + } - private static void MinifySecretTemplate(ProviderResourceOption options) - { - Console.WriteLine("BuildTool -- Minify secret-template"); - var secretTemplate = ReadFile(GetSourcePath("./data/secret-template.json")); - WriteFile(GetSourcePath("./data/secret-template.min.json"), secretTemplate); - } + private static void MinifySecretProperty(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Minify secret-property"); + var secretProperty = ReadFile(GetSourcePath("./data/secret-property.json")); + WriteFile(GetSourcePath("./data/secret-property.min.json"), secretProperty); + } - private static void MinifyTypes(ProviderResourceOption options) + private static void MinifyTypes(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Minify types"); + var count = 0; + foreach (var provider in GetProviders(GetSourcePath("./data/providers"))) { - Console.WriteLine("BuildTool -- Minify types"); - var count = 0; - foreach (var provider in GetProviders(GetSourcePath("./data/providers"))) - { - var entries = ReadFile(provider); - WriteMinifiedResourceType(provider, entries.Select(e => e.Min)); - count++; - } - Console.WriteLine($"BuildTool -- {count} providers processed"); + var entries = ReadFile(provider); + WriteMinifiedResourceType(provider, entries.Select(e => e.Min)); + count++; } + Console.WriteLine($"BuildTool -- {count} providers processed"); + } - private static void BuildIndex(ProviderResourceOption options) + private static void BuildIndex(ProviderResourceOption options) + { + Console.WriteLine("BuildTool -- Building type index"); + var index = new Dictionary(); + var count = 0; + foreach (var provider in GetProviders(GetSourcePath("./data/providers"))) { - Console.WriteLine("BuildTool -- Building type index"); - var index = new Dictionary(); - var count = 0; - foreach (var provider in GetProviders(GetSourcePath("./data/providers"))) + var source = ReadFile(provider); + var set = GetTypeSet(provider, source); + foreach (var type in set) { - var source = ReadFile(provider); - var set = GetTypeSet(provider, source); - foreach (var type in set) + index[type.Type] = new IndexEntry { - index[type.Type] = new IndexEntry - { - RelativePath = type.Provider, - Index = Array.IndexOf(set, type) - }; - } - count++; + RelativePath = type.Provider, + Index = Array.IndexOf(set, type) + }; } - var json = JsonConvert.SerializeObject(index); - File.WriteAllText(GetSourcePath(options.OutputPath ?? "./data/types_index.min.json"), json); - Console.WriteLine($"BuildTool -- {count} providers processed"); + count++; } + var json = JsonConvert.SerializeObject(index); + File.WriteAllText(GetSourcePath(options.OutputPath ?? "./data/types_index.min.json"), json); + Console.WriteLine($"BuildTool -- {count} providers processed"); + } - private static void WriteMinifiedResourceType(string provider, IEnumerable entries) - { - var file = provider.Replace("types.json", "types.min.json"); - WriteFile(file, entries); - } + private static void WriteMinifiedResourceType(string provider, IEnumerable entries) + { + var file = provider.Replace("types.json", "types.min.json"); + WriteFile(file, entries); + } - private static IEnumerable GetProviders(string path) - { - return Directory.EnumerateFiles(path, "types.json", SearchOption.AllDirectories); - } + private static IEnumerable GetProviders(string path) + { + return Directory.EnumerateFiles(path, "types.json", SearchOption.AllDirectories); + } - private static TypeSet[] GetTypeSet(string path, JArray o) + private static TypeSet[] GetTypeSet(string path, JArray o) + { + var d = Path.GetFileName(Path.GetDirectoryName(path)); + var result = new TypeSet[o.Count]; + for (var i = 0; i < result.Length; i++) { - var d = Path.GetFileName(Path.GetDirectoryName(path)); - var result = new TypeSet[o.Count]; - for (var i = 0; i < result.Length; i++) + result[i] = new TypeSet { - result[i] = new TypeSet - { - Provider = d, - Type = string.Concat(d, "/", o[i]["resourceType"].Value().ToLowerInvariant()) - }; - } - return result; + Provider = d, + Type = string.Concat(d, "/", o[i]["resourceType"].Value().ToLowerInvariant()) + }; } + return result; + } - private static T ReadFile(string path) + private static T ReadFile(string path) + { + using var stream = new StreamReader(path); + using var reader = new JsonTextReader(stream); + try { - using var stream = new StreamReader(path); - using var reader = new JsonTextReader(stream); - try - { - - var d = new JsonSerializer(); - return d.Deserialize(reader); - } - catch (Exception) - { - Console.WriteLine($"ERROR - Failed to read file: {path}"); - throw; - } - } - private static void WriteFile(string path, T o) - { - using var stream = new StreamWriter(path, false, Encoding.UTF8); - using var writer = new JsonTextWriter(stream); var d = new JsonSerializer(); - d.Serialize(writer, o); + return d.Deserialize(reader); } - - private static string GetSourcePath(string name) + catch (Exception) { - return Path.Combine(Directory.GetCurrentDirectory(), name); + Console.WriteLine($"ERROR - Failed to read file: {path}"); + throw; } } + + private static void WriteFile(string path, T o) + { + using var stream = new StreamWriter(path, false, Encoding.UTF8); + using var writer = new JsonTextWriter(stream); + var d = new JsonSerializer(); + d.Serialize(writer, o); + } + + private static string GetSourcePath(string name) + { + return Path.Combine(Directory.GetCurrentDirectory(), name); + } } diff --git a/src/PSRule.Rules.Azure/Data/SecretPropertyData.cs b/src/PSRule.Rules.Azure/Data/SecretPropertyData.cs new file mode 100644 index 00000000000..6b18d478f86 --- /dev/null +++ b/src/PSRule.Rules.Azure/Data/SecretPropertyData.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using PSRule.Rules.Azure.Resources; + +namespace PSRule.Rules.Azure.Data; + +/// +/// Defines a datastore of secret properties for Azure resources. +/// +internal sealed class SecretPropertyData : ResourceLoader +{ + private const string RESOURCE_PATH = "secret-property.min.json.deflated"; + + /// + /// Settings for JSON deserialization. + /// + private static readonly JsonSerializerSettings _Settings = new() + { + Converters = + [ + new ReadOnlyDictionaryConverter(StringComparer.OrdinalIgnoreCase), + ] + }; + + private IReadOnlyDictionary _SecretProperty; + +#nullable enable + + /// + /// Get a template for a secret. + /// + /// The resource type. + /// A list of secret properties for the resource type.. + /// Return true when the resource type was found. Otherwise, false is returned. + public bool TryGetValue(string resourceType, out string[]? properties) + { + _SecretProperty ??= ReadSecretProperty(GetContent(RESOURCE_PATH)); + return _SecretProperty.TryGetValue(resourceType, out properties); + } + +#nullable restore + + /// + /// Deserialize a secret property structure from JSON. + /// + private static IReadOnlyDictionary ReadSecretProperty(string content) + { + return JsonConvert.DeserializeObject>(content, _Settings) ?? throw new JsonException(PSRuleResources.CloudEnvironmentInvalid); + } +} diff --git a/src/PSRule.Rules.Azure/Data/Template/ResourceProviderHelper.cs b/src/PSRule.Rules.Azure/Data/Template/ResourceProviderHelper.cs index 41f0b8faa34..22b0756a730 100644 --- a/src/PSRule.Rules.Azure/Data/Template/ResourceProviderHelper.cs +++ b/src/PSRule.Rules.Azure/Data/Template/ResourceProviderHelper.cs @@ -1,31 +1,28 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; +namespace PSRule.Rules.Azure.Data.Template; -namespace PSRule.Rules.Azure.Data.Template +internal sealed class ResourceProviderHelper { - internal sealed class ResourceProviderHelper - { - private readonly ProviderData _Providers; + private readonly ProviderData _Providers; - public ResourceProviderHelper() - { - _Providers = new ProviderData(); - } + public ResourceProviderHelper() + { + _Providers = new ProviderData(); + } - internal ResourceProviderType[] GetResourceType(string providerNamespace, string resourceType) - { - if (_Providers == null) - return Array.Empty(); + internal ResourceProviderType[] GetResourceType(string providerNamespace, string resourceType) + { + if (_Providers == null) + return []; - if (resourceType == null) - return _Providers.GetProviderTypes(providerNamespace); + if (resourceType == null) + return _Providers.GetProviderTypes(providerNamespace); - if (_Providers.TryResourceType(providerNamespace, resourceType, out var type)) - return new ResourceProviderType[] { type }; + if (_Providers.TryResourceType(providerNamespace, resourceType, out var type)) + return [type]; - return Array.Empty(); - } + return []; } } diff --git a/src/PSRule.Rules.Azure/Runtime/Helper.cs b/src/PSRule.Rules.Azure/Runtime/Helper.cs index 860f7e43e79..988390f3f89 100644 --- a/src/PSRule.Rules.Azure/Runtime/Helper.cs +++ b/src/PSRule.Rules.Azure/Runtime/Helper.cs @@ -65,7 +65,7 @@ public static bool HasLiteralValue(string expression) internal static string[] GetParameterTokenValue(string expression) { return !IsTemplateExpression(expression) - ? Array.Empty() + ? [] : TokenStreamValidator.GetParameterTokenValue(ExpressionParser.Parse(expression)); } @@ -171,6 +171,25 @@ public static ResourceProviderType[] GetResourceType(string providerNamespace, s return resourceProviderHelper.GetResourceType(providerNamespace, resourceType); } +#nullable enable + + /// + /// Get a list of secret properties for a resource type. + /// + /// A reference to the runtime service. + /// An Azure resource type to lookup. + /// A list of properties that store secret values. + public static string[]? GetSecretProperty(IService service, string resourceType) + { + if (service is not RuntimeService runtimeService) + return null; + + runtimeService.SecretProperty ??= new SecretPropertyData(); + return runtimeService.SecretProperty.TryGetValue(resourceType, out var properties) ? properties : null; + } + +#nullable restore + /// /// Get the last element in the sub-resource name by splitting the name by '/' separator. /// diff --git a/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs b/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs index 5d909d52865..c8a191e8a51 100644 --- a/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs +++ b/src/PSRule.Rules.Azure/Runtime/RuntimeService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Management.Automation; using PSRule.Rules.Azure.Configuration; +using PSRule.Rules.Azure.Data; using PSRule.Rules.Azure.Data.Bicep; #nullable enable @@ -49,6 +50,11 @@ public RuntimeService(string minimum, int timeout) /// public BicepHelper.BicepInfo? Bicep { get; internal set; } + /// + /// Lookup secret properties for Azure resource types. + /// + public SecretPropertyData? SecretProperty { get; internal set; } + /// public void WithAllowedLocations(string[] locations) { diff --git a/src/PSRule.Rules.Azure/rules/Azure.Deployment.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.Deployment.Rule.ps1 index c2830185c9d..9a8cc9a54d1 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.Deployment.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.Deployment.Rule.ps1 @@ -23,7 +23,7 @@ Rule 'Azure.Deployment.SecureParameter' -Ref 'AZR-000408' -Type 'Microsoft.Resou } # Synopsis: Use secure parameters for setting properties of resources that contain sensitive information. -Rule 'Azure.Deployment.SecureValue' -Ref 'AZR-000316' -Type 'Microsoft.Resources/deployments' -Tag @{ release = 'GA'; ruleSet = '2022_12'; 'Azure.WAF/pillar' = 'Security'; } { +Rule 'Azure.Deployment.SecureValue' -Ref 'AZR-000316' -Type 'Microsoft.Resources/deployments' -Tag @{ release = 'GA'; ruleSet = '2024_12'; 'Azure.WAF/pillar' = 'Security'; } { RecurseSecureValue -Deployment $TargetObject } @@ -186,19 +186,15 @@ function global:CheckPropertyUsesSecureParameter { [Bool]$ShouldUseSecret = $True ) process { - $propertiesInPath = $PropertyPath.Split(".") # properties.example.name - $propertyValue = $Resource - foreach ($aPropertyInThePath in $propertiesInPath) { - $propertyValue = $propertyValue."$aPropertyInThePath" + $propertyValues = $PSRule.GetPath($Resource, $PropertyPath); + if ($propertyValues.Length -eq 0) { + return $Assert.Pass(); } - if ($propertyValue) { + foreach ($propertyValue in $propertyValues) { $hasSecureParam = [PSRule.Rules.Azure.Runtime.Helper]::HasSecureValue($propertyValue, $SecureParameters); $Assert.Create($hasSecureParam -eq $ShouldUseSecret, $LocalizedData.SecureParameterRequired, $PropertyPath); } - else { - $Assert.Pass(); - } } } @@ -222,200 +218,19 @@ function global:RecurseSecureValue { Write-Debug -Message "Secure parameters are: $($secureParameters -join ', ')"; foreach ($resource in $resources) { - switch ($resource.type) { - 'Microsoft.Resources/Deployments' { - RecurseSecureValue -Deployment $resource; - } - 'Microsoft.AAD/DomainServices' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.ldapsSettings.pfxCertificatePassword' - } - 'Microsoft.ApiManagement/Service' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.hostnameConfigurations.certificatePassword' - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.certificates.certificatePassword' - } - 'Microsoft.ApiManagement/Service/AuthorizationServers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.clientSecret' - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.resourceOwnerPassword' - } - 'Microsoft.ApiManagement/Service/Backends' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.proxy.password" - } - 'Microsoft.ApiManagement/Service/Certificates' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.ApiManagement/Service/IdentityProviders' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.clientSecret" - } - 'Microsoft.ApiManagement/Service/OpenidConnectProviders' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.clientSecret" - } - 'Microsoft.ApiManagement/Service/Users' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.Automation/AutomationAccounts/Credentials' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.Batch/BatchAccounts/Pools' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.userAccounts.linuxUserConfiguration.sshPrivateKey" - } - 'Microsoft.Blockchain/BlockchainMembers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.consortiumManagementAccountPassword" - } - 'Microsoft.Blockchain/BlockchainMembers/TransactionNodes' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.BotService/BotServices/Connections' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.clientSecret" - } - 'Microsoft.Compute/VirtualMachineScaleSets' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.virtualMachineProfile.osProfile.adminPassword' - } - 'Microsoft.Compute/VirtualMachineScaleSets/Virtualmachines' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.osProfile.adminPassword" - } - 'Microsoft.Compute/VirtualMachines' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.osProfile.adminPassword' - } - 'Microsoft.ContainerInstance/ContainerGroups' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.imageRegistryCredentials.password" - } - 'Microsoft.ContainerService/ContainerServices' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.servicePrincipalProfile.secret" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.windowsProfile.adminPassword" - } - 'Microsoft.ContainerService/ManagedClusters' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.windowsProfile.adminPassword' - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.servicePrincipalProfile.secret' - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.aadProfile.serverAppSecret' - } - 'Microsoft.ContainerService/OpenShiftManagedClusters' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.authProfile.identityProviders.provider.secret' - } - 'Microsoft.DBforMariaDB/Servers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.DBforMySQL/Servers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.DBforPostgreSQL/Servers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.DataMigration/Services/Projects' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.sourceConnectionInfo.password" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.targetConnectionInfo.password" - } - 'Microsoft.DevTestLab/Labs/Formulas' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.formulaContent.properties.password" - } - 'Microsoft.DevTestLab/Labs/Users/Secrets' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.value" - } - 'Microsoft.DevTestLab/Labs/Virtualmachines' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.HDInsight/Clusters' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.securityProfile.domainUserPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.computeProfile.roles.osProfile.linuxOperatingSystemProfile.password" - } - 'Microsoft.HDInsight/Clusters/Applications' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.computeProfile.roles.osProfile.linuxOperatingSystemProfile.password" - } - 'Microsoft.KeyVault/Vaults/Secrets' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath 'properties.value' - } - 'Microsoft.Logic/IntegrationAccounts/Agreements' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.x12.receiveAgreement.protocolSettings.securitySettings.passwordValue" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.x12.sendAgreement.protocolSettings.securitySettings.passwordValue" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.receiveAgreement.protocolSettings.envelopeSettings.recipientReferencePasswordValue" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.sendAgreement.protocolSettings.envelopeSettings.recipientReferencePasswordValue" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.receiveAgreement.protocolSettings.envelopeSettings.groupApplicationPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.sendAgreement.protocolSettings.envelopeSettings.groupApplicationPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.receiveAgreement.protocolSettings.envelopeOverrides.applicationPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.content.edifact.sendAgreement.protocolSettings.envelopeOverrides.applicationPassword" - } - 'Microsoft.NetApp/NetAppAccounts' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.activeDirectories.password" - } - 'Microsoft.Network/ApplicationGateways' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.sslCertificates.properties.password" - } - 'Microsoft.Network/Connections' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.virtualNetworkGateway1.properties.vpnClientConfiguration.radiusServerSecret" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.virtualNetworkGateway2.properties.vpnClientConfiguration.radiusServerSecret" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.sharedKey" - } - 'Microsoft.Network/VirtualNetworkGateways' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.vpnClientConfiguration.radiusServerSecret" - } - 'Microsoft.Network/VirtualWans/P2sVpnServerConfigurations' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.radiusServerSecret" - } - 'Microsoft.Network/VpnServerConfigurations' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.radiusServerSecret" - } - 'Microsoft.NotificationHubs/Namespaces/NotificationHubs' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.wnsCredential.properties.secretKey" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.admCredential.properties.clientSecret" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.baiduCredential.properties.baiduSecretKey" - } - 'Microsoft.ServiceFabricMesh/Applications' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.services.properties.codePackages.imageRegistryCredential.password" - } - 'Microsoft.ServiceFabricMesh/Secrets/Values' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.value" - } - 'Microsoft.Sql/ManagedInstances' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.Sql/Servers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.Sql/Servers/Databases/Extensions' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.administratorLoginPassword" - } - 'Microsoft.Sql/Servers/Databases/SyncGroups' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.hubDatabasePassword" - } - 'Microsoft.Sql/Servers/Databases/SyncGroups/SyncMembers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.Sql/Servers/JobAgents/Credentials' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.wsfcDomainCredentials.clusterBootstrapAccountPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.wsfcDomainCredentials.clusterOperatorAccountPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.wsfcDomainCredentials.sqlServiceAccountPassword" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.autoBackupSettings.password" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.keyVaultCredentialSettings.servicePrincipalSecret" - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.serverConfigurationsManagementSettings.sqlConnectivityUpdateSettings.sqlAuthUpdatePassword" - } - 'Microsoft.StorSimple/Managers/Devices/VolumeContainers' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.encryptionKey.value" - } - 'Microsoft.StorSimple/Managers/StorageAccountCredentials' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.accessKey.value" + if ($resource.type -eq 'Microsoft.Resources/Deployments') { + RecurseSecureValue -Deployment $resource; + } + else { + $properties = [PSRule.Rules.Azure.Runtime.Helper]::GetSecretProperty($PSRule.GetService('Azure.Context'), $resource.type); + if ($Null -eq $properties -or $properties.Length -eq 0) { + $Assert.Pass(); } - 'Microsoft.StreamAnalytics/Streamingjobs' { - $objectsWithPasswords = $resource.properties.inputs + $resource.properties.outputs - - foreach ($objectWithPassword in $objectsWithPasswords) { - CheckPropertyUsesSecureParameter -Resource $objectWithPassword -SecureParameters $secureParameters -PropertyPath "properties.datasource.properties.password" + else { + foreach ($property in $properties) { + CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath $property } } - 'Microsoft.StreamAnalytics/Streamingjobs/Outputs' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.datasource.properties.password" - } - 'Microsoft.Web/Certificates' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.password" - } - 'Microsoft.Web/Sourcecontrols' { - CheckPropertyUsesSecureParameter -Resource $resource -SecureParameters $secureParameters -PropertyPath "properties.tokenSecret" - } - Default { - $Assert.Pass(); - } } } } diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.Baseline.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.Baseline.Tests.ps1 index 5ea894d39f5..d86a72847a1 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.Baseline.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.Baseline.Tests.ps1 @@ -164,7 +164,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2022_12' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 331; + $filteredResult.Length | Should -Be 330; } It 'With Azure.Preview_2022_12' { @@ -178,7 +178,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2023_03' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 351; + $filteredResult.Length | Should -Be 350; } It 'With Azure.Preview_2023_03' { @@ -192,7 +192,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2023_06' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 366; + $filteredResult.Length | Should -Be 365; } It 'With Azure.Preview_2023_06' { @@ -206,7 +206,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2023_09' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 377; + $filteredResult.Length | Should -Be 376; } It 'With Azure.Preview_2023_09' { @@ -220,7 +220,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2023_12' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 386; + $filteredResult.Length | Should -Be 385; } It 'With Azure.Preview_2023_12' { @@ -234,7 +234,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2024_03' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 396; + $filteredResult.Length | Should -Be 395; } It 'With Azure.Preview_2024_03' { @@ -248,7 +248,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2024_06' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 417; + $filteredResult.Length | Should -Be 416; } It 'With Azure.Preview_2024_06' { @@ -262,7 +262,7 @@ Describe 'Baselines' -Tag Baseline { $result = @(Get-PSRule -Module PSRule.Rules.Azure -Baseline 'Azure.GA_2024_09' -WarningAction Ignore); $filteredResult = @($result | Where-Object { $_.Tag.release -in 'GA'}); $filteredResult | Should -Not -BeNullOrEmpty; - $filteredResult.Length | Should -Be 434; + $filteredResult.Length | Should -Be 433; } It 'With Azure.Preview_2024_09' { diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.Deployment.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.Deployment.Tests.ps1 index d7b1ac74f2e..f45b005b4ad 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.Deployment.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.Deployment.Tests.ps1 @@ -64,9 +64,9 @@ Describe 'Azure.Deployment' -Tag 'Deployment' { # Fail $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; + $ruleResult.Length | Should -Be 4; $targetNames = $ruleResult | ForEach-Object { $_.TargetObject.name }; - $targetNames | Should -BeIn 'secret_bad', 'ps-rule-test-deployment', 'streaming_jobs_bad'; + $targetNames | Should -BeIn 'secret_bad', 'ps-rule-test-deployment', 'streaming_jobs_bad', 'container_apps_bad'; # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); diff --git a/tests/PSRule.Rules.Azure.Tests/Data/SecretPropertyDataTests.cs b/tests/PSRule.Rules.Azure.Tests/Data/SecretPropertyDataTests.cs new file mode 100644 index 00000000000..8ee92f14a55 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Data/SecretPropertyDataTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Rules.Azure.Data; + +public sealed class SecretPropertyDataTests +{ + private readonly SecretPropertyData _Data; + + public SecretPropertyDataTests() + { + _Data = new SecretPropertyData(); + } + + [Theory] + [InlineData("Microsoft.KeyVault/vaults/secrets", new string[] { "properties.value" })] + [InlineData("Microsoft.Resources/deploymentScripts", new string[] { "properties.storageAccountSettings.storageAccountKey", "properties.environmentVariables[*].secureValue" })] + [InlineData("Microsoft.App/containerApps", new string[] { "properties.configuration.secrets[*].value" })] + public void GetSecretProperty(string resourceType, string[] path) + { + Assert.True(_Data.TryGetValue(resourceType, out var properties)); + Assert.Equal(path, properties); + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.badContainerApps.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.badContainerApps.bicep new file mode 100644 index 00000000000..0656ac49ad2 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.badContainerApps.bicep @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Disable linter rule +#disable-next-line secure-secrets-in-params +param notSecret string + +resource app 'Microsoft.App/containerApps@2024-03-01' = { + name: 'badContainerApp' + location: resourceGroup().location + properties: { + configuration: { + secrets: [ + { + name: 'badSecret' + value: notSecret + } + ] + } + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.bicep index 2f9f1dc6bb4..bad8ae3987c 100644 --- a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.bicep +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.bicep @@ -34,6 +34,13 @@ module streaming_jobs_bad 'Tests.Bicep.9.badStreamingJobs.bicep' = { } } +module container_apps_bad 'Tests.Bicep.9.badContainerApps.bicep' = { + name: 'container_apps_bad' + params: { + notSecret: '' + } +} + module secret_goodreference 'Tests.Bicep.9.goodReference.bicep' = { name: 'reference_good' } diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.json index f626db2367c..2da49d23a3a 100644 --- a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.json +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.9.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "18275092912549928193" + "version": "0.30.23.60470", + "templateHash": "14187359232601139223" } }, "resources": [ @@ -29,8 +29,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1518984315103849805" + "version": "0.30.23.60470", + "templateHash": "17919476539610503167" } }, "parameters": { @@ -71,8 +71,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "6215663120730168687" + "version": "0.30.23.60470", + "templateHash": "15649027828701608410" } }, "parameters": { @@ -121,8 +121,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "3797411740136360330" + "version": "0.30.23.60470", + "templateHash": "11413388873748694759" } }, "parameters": { @@ -229,8 +229,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4134825371520485157" + "version": "0.30.23.60470", + "templateHash": "6931469984343125497" } }, "parameters": { @@ -277,6 +277,56 @@ } } }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "container_apps_bad", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "notSecret": { + "value": "" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "17669244616149029030" + } + }, + "parameters": { + "notSecret": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "badContainerApp", + "location": "[resourceGroup().location]", + "properties": { + "configuration": { + "secrets": [ + { + "name": "badSecret", + "value": "[parameters('notSecret')]" + } + ] + } + } + } + ] + } + } + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", @@ -292,8 +342,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "6922313715624563061" + "version": "0.30.23.60470", + "templateHash": "9581683583109687527" } }, "parameters": { diff --git a/tests/PSRule.Rules.Azure.Tests/TokenStreamValidatorTests.cs b/tests/PSRule.Rules.Azure.Tests/TokenStreamValidatorTests.cs index 98e68a5de0d..94f2dc180eb 100644 --- a/tests/PSRule.Rules.Azure.Tests/TokenStreamValidatorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TokenStreamValidatorTests.cs @@ -3,59 +3,58 @@ using PSRule.Rules.Azure.Runtime; -namespace PSRule.Rules.Azure +namespace PSRule.Rules.Azure; + +public sealed class TokenStreamValidatorTests { - public sealed class TokenStreamValidatorTests + [Fact] + public void HasLiteralValue() { - [Fact] - public void HasLiteralValue() - { - Assert.True(Helper.HasLiteralValue("password")); - Assert.True(Helper.HasLiteralValue("123")); - Assert.False(Helper.HasLiteralValue("[parameters('adminPassword')]")); - Assert.True(Helper.HasLiteralValue("[variables('password')]")); - Assert.True(Helper.HasLiteralValue("[if(true(), variables('password'), parameters('password'))]")); - Assert.True(Helper.HasLiteralValue("[if(true(), 'password', parameters('password'))]")); - Assert.False(Helper.HasLiteralValue("[if(and(empty(parameters('sqlLogin')),parameters('useAADOnlyAuthentication')),null(),parameters('sqlLogin'))]")); - } + Assert.True(Helper.HasLiteralValue("password")); + Assert.True(Helper.HasLiteralValue("123")); + Assert.False(Helper.HasLiteralValue("[parameters('adminPassword')]")); + Assert.True(Helper.HasLiteralValue("[variables('password')]")); + Assert.True(Helper.HasLiteralValue("[if(true(), variables('password'), parameters('password'))]")); + Assert.True(Helper.HasLiteralValue("[if(true(), 'password', parameters('password'))]")); + Assert.False(Helper.HasLiteralValue("[if(and(empty(parameters('sqlLogin')),parameters('useAADOnlyAuthentication')),null(),parameters('sqlLogin'))]")); + } - [Fact] - public void GetParameterTokenValue() - { - Assert.Equal(new string[] { "adminPassword" }, Helper.GetParameterTokenValue("[parameters('adminPassword')]")); - Assert.Empty(Helper.GetParameterTokenValue("[variables('adminPassword')]")); - Assert.Empty(Helper.GetParameterTokenValue("password")); - Assert.Equal(new string[] { "adminPassword" }, Helper.GetParameterTokenValue("[if(true(), null(), parameters('adminPassword'))]")); - Assert.Equal(new string[] { "adminPassword2", "adminPassword1" }, Helper.GetParameterTokenValue("[if(true(), parameters('adminPassword2'), parameters('adminPassword1'))]")); - } + [Fact] + public void GetParameterTokenValue() + { + Assert.Equal(new string[] { "adminPassword" }, Helper.GetParameterTokenValue("[parameters('adminPassword')]")); + Assert.Empty(Helper.GetParameterTokenValue("[variables('adminPassword')]")); + Assert.Empty(Helper.GetParameterTokenValue("password")); + Assert.Equal(new string[] { "adminPassword" }, Helper.GetParameterTokenValue("[if(true(), null(), parameters('adminPassword'))]")); + Assert.Equal(new string[] { "adminPassword2", "adminPassword1" }, Helper.GetParameterTokenValue("[if(true(), parameters('adminPassword2'), parameters('adminPassword1'))]")); + } - [Fact] - public void UsesListKeysFunction() - { - Assert.True(Helper.UsesListKeysFunction("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'storage1'), '2021-09-01').keys[0].value]")); - Assert.True(Helper.UsesListKeysFunction("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'storage1'), '2021-09-01')]")); - Assert.True(Helper.UsesListKeysFunction("[listAdminKeys(resourceId('Microsoft.Search/searchServices', 'search1'), '2022-09-01').primaryKey]")); - Assert.True(Helper.UsesListKeysFunction("[listQueryKeys(resourceId('Microsoft.Search/searchServices', 'search1'), '2021-09-01').value[0].key]")); - Assert.False(Helper.UsesListKeysFunction("[list(resourceId('Microsoft.OperationalInsights/workspaces', 'workspace1'), '2023-09-01').value[0].properties.name]")); - Assert.False(Helper.UsesListKeysFunction("[resourceId('Microsoft.Storage/storageAccounts', 'storage1')]")); - } + [Fact] + public void UsesListKeysFunction() + { + Assert.True(Helper.UsesListKeysFunction("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'storage1'), '2021-09-01').keys[0].value]")); + Assert.True(Helper.UsesListKeysFunction("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'storage1'), '2021-09-01')]")); + Assert.True(Helper.UsesListKeysFunction("[listAdminKeys(resourceId('Microsoft.Search/searchServices', 'search1'), '2022-09-01').primaryKey]")); + Assert.True(Helper.UsesListKeysFunction("[listQueryKeys(resourceId('Microsoft.Search/searchServices', 'search1'), '2021-09-01').value[0].key]")); + Assert.False(Helper.UsesListKeysFunction("[list(resourceId('Microsoft.OperationalInsights/workspaces', 'workspace1'), '2023-09-01').value[0].properties.name]")); + Assert.False(Helper.UsesListKeysFunction("[resourceId('Microsoft.Storage/storageAccounts', 'storage1')]")); + } - [Fact] - public void HasSecureValue() - { - var secureParameters = new string[] { "adminPassword" }; + [Fact] + public void HasSecureValue() + { + var secureParameters = new string[] { "adminPassword" }; - Assert.True(Helper.HasSecureValue("[parameters('adminPassword')]", secureParameters)); - Assert.True(Helper.HasSecureValue("[parameters('adminPassword')]", new string[] { "AdminPassword" })); - Assert.False(Helper.HasSecureValue("[variables('adminPassword')]", secureParameters)); - Assert.False(Helper.HasSecureValue("password", secureParameters)); - Assert.False(Helper.HasSecureValue("[parameters('notSecure')]", secureParameters)); - Assert.False(Helper.HasSecureValue("[parameters('notSecure')]", System.Array.Empty())); - Assert.True(Helper.HasSecureValue("[if(true(), parameters('adminPassword2'), parameters('adminPassword1'))]", new string[] { "adminPassword1", "adminPassword2" })); - Assert.False(Helper.HasSecureValue("[if(true(), parameters('notSecure'), parameters('adminPassword'))]", secureParameters)); - Assert.True(Helper.HasSecureValue("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'aStorageAccount'), '2021-09-01').keys[0].value]", secureParameters)); - Assert.True(Helper.HasSecureValue("{{SecretReference aName}}", secureParameters)); - Assert.True(Helper.HasSecureValue("[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').InstrumentationKey]", secureParameters)); - } + Assert.True(Helper.HasSecureValue("[parameters('adminPassword')]", secureParameters)); + Assert.True(Helper.HasSecureValue("[parameters('adminPassword')]", ["AdminPassword"])); + Assert.False(Helper.HasSecureValue("[variables('adminPassword')]", secureParameters)); + Assert.False(Helper.HasSecureValue("password", secureParameters)); + Assert.False(Helper.HasSecureValue("[parameters('notSecure')]", secureParameters)); + Assert.False(Helper.HasSecureValue("[parameters('notSecure')]", [])); + Assert.True(Helper.HasSecureValue("[if(true(), parameters('adminPassword2'), parameters('adminPassword1'))]", ["adminPassword1", "adminPassword2"])); + Assert.False(Helper.HasSecureValue("[if(true(), parameters('notSecure'), parameters('adminPassword'))]", secureParameters)); + Assert.True(Helper.HasSecureValue("[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'aStorageAccount'), '2021-09-01').keys[0].value]", secureParameters)); + Assert.True(Helper.HasSecureValue("{{SecretReference aName}}", secureParameters)); + Assert.True(Helper.HasSecureValue("[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').InstrumentationKey]", secureParameters)); } }