From c6efe6eb0f2aeca1614f35547c2eb67bd8c69aef Mon Sep 17 00:00:00 2001 From: batemansogq Date: Tue, 31 Oct 2023 16:07:57 +1100 Subject: [PATCH] AML - initial set of AML rules and severity update (#2500) * initial ML updates * initial AML changes * corrected ML test * corrected ML JSON, backing out test data * finalised md doco for ML * inital ML disable local rule * ML disable admin rule * ML compute vnet added, md update on hyperlinks * links updated for ML rules * update ML compute test ref for extend to workspaces * ML Workspace rule added, ML titles added to mds * text upds * ML Wks pub access rule added. Refs updated * ML Wkspace md updated * ML wrkspc rule for use mg id * updates to rule conditions * minor text update * updated ML - WrkspUserMgId rule logic * correct ML.WrkspUserMgId error * Updates from previous PR * Final updates * Bump change log * Fix dup --------- Co-authored-by: Bernie White --- CONTRIBUTING.md | 2 +- docs/CHANGELOG-v1.md | 12 ++ docs/en/rules/Azure.ML.ComputeIdleShutdown.md | 90 ++++++++++++++ docs/en/rules/Azure.ML.ComputeVnet.md | 97 +++++++++++++++ docs/en/rules/Azure.ML.DisableLocalAuth.md | 95 ++++++++++++++ docs/en/rules/Azure.ML.PublicAccess.md | 114 +++++++++++++++++ docs/en/rules/Azure.ML.UserManagedIdentity.md | 108 ++++++++++++++++ docs/examples-ML.bicep | 81 ++++++++++++ docs/examples-ML.json | 87 +++++++++++++ .../rules/Azure.ML.Rule.yaml | 107 ++++++++++++++++ .../Azure.ML.Tests.ps1 | 116 +++++++++++++++++- .../Resources.ML.json | 114 +++++++++++++++++ 12 files changed, 1021 insertions(+), 2 deletions(-) create mode 100644 docs/en/rules/Azure.ML.ComputeIdleShutdown.md create mode 100644 docs/en/rules/Azure.ML.ComputeVnet.md create mode 100644 docs/en/rules/Azure.ML.DisableLocalAuth.md create mode 100644 docs/en/rules/Azure.ML.PublicAccess.md create mode 100644 docs/en/rules/Azure.ML.UserManagedIdentity.md create mode 100644 docs/examples-ML.bicep create mode 100644 docs/examples-ML.json create mode 100644 tests/PSRule.Rules.Azure.Tests/Resources.ML.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c0c87b2c9b..5f995cddd28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ When writing documentation in Markdown, please follow these formatting guideline Before improving rule recommendations familiarize yourself with writing [rule markdown documentation][4]. Rule documentation requires the following annotations for use with PSRule for Azure: -- `severity` - A subjective rating of the impact of a rule the solution or platform. +- `severity` - A subjective rating of the impact of a rule on the solution or platform. *NB* - the severity ratings reflect a productionised implementation, consideration should be applied for pre-production environments. Available severities are: diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index f15e092bd5c..9965f8a2760 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -30,6 +30,18 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since v1.30.3: +- New rules: + - Machine Learning: + - Check compute instances are configured for an idle shutdown by @batemansogq. + [#2484](https://github.com/Azure/PSRule.Rules.Azure/issues/2484) + - Check workspace compute has local authentication disabled by @batemansogq. + [#2484](https://github.com/Azure/PSRule.Rules.Azure/issues/2484) + - Check workspace compute is connected to a VNET by @batemansogq. + [#2484](https://github.com/Azure/PSRule.Rules.Azure/issues/2484) + - Check public access to a workspace is disabled by @batemansogq. + [#2484](https://github.com/Azure/PSRule.Rules.Azure/issues/2484) + - Check workspaces use a user-assigned identity by @batemansogq. + [#2484](https://github.com/Azure/PSRule.Rules.Azure/issues/2484) - Engineering: - Bump development tools to .NET 7.0 SDK by @BernieWhite. [#1870](https://github.com/Azure/PSRule.Rules.Azure/issues/1870) diff --git a/docs/en/rules/Azure.ML.ComputeIdleShutdown.md b/docs/en/rules/Azure.ML.ComputeIdleShutdown.md new file mode 100644 index 00000000000..d4d3741d6c0 --- /dev/null +++ b/docs/en/rules/Azure.ML.ComputeIdleShutdown.md @@ -0,0 +1,90 @@ +--- +reviewed: 2023-10-06 +severity: Critical +pillar: Cost Optimization +category: Provision +resource: Machine Learning +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ML.ComputeIdleShutdown/ +--- + +# Configure idle shutdown for compute instances + +## SYNOPSIS + +Configure an idle shutdown timeout for Machine Learning compute instances. + +## DESCRIPTION + +Machine Learning uses compute instances as a training or inference compute for development and testing. +It's similar to a virtual machine on the cloud. + +To avoid getting charged for a compute instance that is switched on but not being actively used, +you can configure when to automatically shutdown compute instances due to inactivity. + +## RECOMMENDATION + +Consider configuring ML - Compute Instances to automatically shutdown after a period of inactivity to optimize compute costs. + +## EXAMPLES + +### Configure with Azure template + +To deploy compute instances that passes this rule: + +- Set the `properties.properties.idleTimeBeforeShutdown` property with a ISO 8601 formatted string. + i.e. For an idle shutdown time of 15 minutes use `PT15M`. + +For example: + +```json +{ + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "computeType": "ComputeInstance", + "disableLocalAuth": true, + "properties": { + "vmSize": "[parameters('vmSize')]", + "idleTimeBeforeShutdown": "PT15M" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('name'))]" + ] +} +``` + +### Configure with Bicep + +To deploy compute instances that passes this rule: + +- Set the `properties.properties.idleTimeBeforeShutdown` property with a ISO 8601 formatted string. + i.e. For an idle shutdown time of 15 minutes use `PT15M`. + +For example: + +```bicep +resource compute_instance 'Microsoft.MachineLearningServices/workspaces/computes@2023-06-01-preview' = { + parent: workspace + name: name + location: location + properties: { + computeType: 'ComputeInstance' + disableLocalAuth: true + properties: { + vmSize: vmSize + idleTimeBeforeShutdown: 'PT15M' + } + } +} +``` + +## LINKS + +- [AI + Machine Learning cost estimates](https://learn.microsoft.com/azure/well-architected/cost/provision-ai-ml) +- [Configure idle shutdown](https://learn.microsoft.com/azure/machine-learning/how-to-create-compute-instance#configure-idle-shutdown) +- [ML Compute](https://learn.microsoft.com/azure/machine-learning/azure-machine-learning-glossary#compute) +- [Azure deployment reference - Compute objects](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces/computes#compute-objects) +- [Azure deployment reference - Workspaces](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces) diff --git a/docs/en/rules/Azure.ML.ComputeVnet.md b/docs/en/rules/Azure.ML.ComputeVnet.md new file mode 100644 index 00000000000..57abf9db682 --- /dev/null +++ b/docs/en/rules/Azure.ML.ComputeVnet.md @@ -0,0 +1,97 @@ +--- +reviewed: 2023-10-10 +severity: Critical +pillar: Security +category: Connectivity +resource: Machine Learning +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ML.ComputeVnet/ +--- + +# Host ML Compute in VNet + +## SYNOPSIS + +Azure Machine Learning Computes should be hosted in a virtual network (VNet). + +## DESCRIPTION + +When using Azure Machine Learning (ML), you can configure compute instances to be private or accessible from the public Internet. +By default, the ML compute is configured to be accessible from the public Internet. + +ML compute can be deployed into an virtual network (VNet) to provide private connectivity, enhanaced security, and isolation. +Using a VNet reduces the attack surface for your solution, and the chances of data exfiltration. +Additionally, network controls such as Network Security Groups (NSGs) can be used to further restrict access. + +## RECOMMENDATION + +Consider using ML - compute hosted in a VNet to provide private connectivity, enhanaced security, and isolation. + +## EXAMPLES + +### Configure with Azure template + +To deploy an ML - compute that passes this rule: + +- Set the `properties.properties.subnet.id` property with a resource Id of a specific VNET subnet. + +For example: + +```json +{ + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "computeType": "ComputeInstance", + "disableLocalAuth": true, + "properties": { + "vmSize": "[parameters('vmSize')]", + "idleTimeBeforeShutdown": "PT15M", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split('vnet/subnet', '/')[0], split('vnet/subnet', '/')[1])]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('name'))]" + ] +} +``` + +### Configure with Bicep + +To deploy an ML - compute that passes this rule: + +- Set the `properties.properties.subnet.id` property with a resource Id of a specific VNET subnet. + +For example: + +```bicep +resource compute_instance 'Microsoft.MachineLearningServices/workspaces/computes@2023-06-01-preview' = { + parent: workspace + name: name + location: location + properties: { + computeType: 'ComputeInstance' + disableLocalAuth: true + properties: { + vmSize: vmSize + idleTimeBeforeShutdown: 'PT15M' + subnet: { + id: subnet.id + } + } + } +} +``` + +## LINKS + +- [WAF - Azure services for securing network connectivity](https://learn.microsoft.com/azure/well-architected/security/design-network-connectivity) +- [Managed compute in a managed virtual network](https://learn.microsoft.com/azure/machine-learning/how-to-managed-network-compute) +- [ML - Network security and isolation](https://learn.microsoft.com/azure/machine-learning/concept-enterprise-security#network-security-and-isolation) +- [ML Compute](https://learn.microsoft.com/azure/machine-learning/azure-machine-learning-glossary#compute) +- [NS-1: Establish network segmentation boundaries](https://learn.microsoft.com/security/benchmark/azure/baselines/machine-learning-service-security-baseline#ns-1-establish-network-segmentation-boundaries) +- [Azure deployment reference - Compute objects](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces/computes#compute-objects) +- [Azure deployment reference - Workspaces](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces) diff --git a/docs/en/rules/Azure.ML.DisableLocalAuth.md b/docs/en/rules/Azure.ML.DisableLocalAuth.md new file mode 100644 index 00000000000..bf7dfcc6dfe --- /dev/null +++ b/docs/en/rules/Azure.ML.DisableLocalAuth.md @@ -0,0 +1,95 @@ +--- +reviewed: 2023-10-10 +severity: Critical +pillar: Security +category: Authentication +resource: Machine Learning +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ML.DisableLocalAuth/ +--- + +# Disable local authentication on ML Compute + +## SYNOPSIS + +Azure Machine Learning compute resources should have local authentication methods disabled. + +## DESCRIPTION + +Azure Machine Learning (ML) compute can have local authenication enabled or disabled. +When enabled local authentication methods must be managed and audited separately. + +Disabling local authentication ensures that Entra ID (previously Azure Active Directory) is used exclusively for authentication. +Using Entra ID, provides consistency as a single authoritative source which: + +- Increases clarity and reduces security risks from human errors and configuration complexity. +- Provides support for advanced identity security and governance features. + +## RECOMMENDATION + +Consider disabling local authentication on ML - Compute as part of a broader security strategy. + +## EXAMPLES + +### Configure with Azure template + +To deploy ML - compute that passes this rule: + +- Set the `properties.disableLocalAuth` property to `true`. + +For example: + +```json +{ + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "computeType": "ComputeInstance", + "disableLocalAuth": true, + "properties": { + "vmSize": "[parameters('vmSize')]", + "idleTimeBeforeShutdown": "PT15M" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('name'))]" + ] +} +``` + +### Configure with Bicep + +To deploy ML - compute that passes this rule: + +- Set the `properties.disableLocalAuth` property to `true`. + +For example: + +```bicep +resource compute_instance 'Microsoft.MachineLearningServices/workspaces/computes@2023-06-01-preview' = { + parent: workspace + name: name + location: location + properties: { + computeType: 'ComputeInstance' + disableLocalAuth: true + properties: { + vmSize: vmSize + idleTimeBeforeShutdown: 'PT15M' + subnet: { + id: subnet.id + } + } + } +} +``` + +## LINKS + +- [WAF - Authentication with Azure AD](https://learn.microsoft.com/azure/well-architected/security/design-identity-authentication) +- [Disable local authentication](https://learn.microsoft.com/azure/machine-learning/how-to-integrate-azure-policy#disable-local-authentication) +- [ML Compute](https://learn.microsoft.com/azure/machine-learning/azure-machine-learning-glossary#compute) +- [Azure Policy Regulatory Compliance controls for Azure Machine Learning](https://learn.microsoft.com/azure/machine-learning/security-controls-policy) +- [Azure deployment reference - Compute objects](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces/computes#compute-objects) +- [Azure deployment reference - Workspaces](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces) diff --git a/docs/en/rules/Azure.ML.PublicAccess.md b/docs/en/rules/Azure.ML.PublicAccess.md new file mode 100644 index 00000000000..c311fc03625 --- /dev/null +++ b/docs/en/rules/Azure.ML.PublicAccess.md @@ -0,0 +1,114 @@ +--- +reviewed: 2023-10-12 +severity: Critical +pillar: Security +category: Connectivity +resource: Machine Learning +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ML.PublicAccess/ +--- + +# ML Workspace has public access disabled + +## SYNOPSIS + +Disable public network access from a Azure Machine Learning workspace. + +## DESCRIPTION + +Disabling public network access improves security by ensuring that the Machine Learning Workspaces aren't exposed on the public internet. +You can control exposure of your workspaces by creating private endpoints instead. +By default, a public endpoint is enabled for Machine Learning workspaces. +The public endpoint is used for all access except for requests that use a Private Endpoint. +Access through the public endpoint can be disabled or restricted to authorized virtual networks. + +Data exfiltration is an attack where an malicious actor does an unauthorized data transfer. +Private Endpoints help control exposure of a workspace to data exfiltration by an internal or external malicious actor. +They do this by providing clear separation between public and private endpoints. +As a result, broad access to public endpoints which could be operated by a malicious actor are not required. + +## RECOMMENDATION + +Consider disabling access from public endpoints by setting the `publicNetworkAccess` property to `Disabled` as part of a broader security strategy. + +## EXAMPLES + +### Configure with Azure template + +To deploy an ML - Workspace that passes this rule: + +- Set the `properties.publicNetworkAccess` property to `Disabled`. +- If the `properties.allowPublicAccessWhenBehindVnet` property is defined remove the property. + Switch to using the `properties.publicNetworkAccess` property instead. + Configuring both properties is not required. + +For example: + +```json +{ + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "sku": { + "name": "basic", + "tier": "basic" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[parameters('name')]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', parameters('KeyVaultName'))]", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', parameters('StorageAccountName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', parameters('AppInsightsName'))]", + "containerRegistry": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('ContainerRegistryName'))]", + "publicNetworkAccess": "Disabled" + } +} +``` + +### Configure with Bicep + +To deploy an ML - Workspace that passes this rule: + +- Set the `properties.publicNetworkAccess` property to `Disabled`. +- If the `properties.allowPublicAccessWhenBehindVnet` property is defined remove the property. + Switch to using the `properties.publicNetworkAccess` property instead. + Configuring both properties is not required. + +For example: + +```bicep +resource workspace 'Microsoft.MachineLearningServices/workspaces@2023-04-01' = { + name: name + location: location + sku: { + name: 'basic' + tier: 'basic' + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } + properties: { + friendlyName: friendlyName + keyVault: keyVault.id + storageAccount: storageAccount.id + applicationInsights: appInsights.id + containerRegistry: containerRegistry.id + publicNetworkAccess: 'Disabled' + primaryUserAssignedIdentity: identity.id + } +} +``` + +## LINKS + +- [WAF - Azure services for securing network connectivity](https://learn.microsoft.com/azure/well-architected/security/design-network-connectivity) +- [Configure a private endpoint for an Azure Machine Learning workspace](https://learn.microsoft.com/azure/machine-learning/how-to-configure-private-link?view=azureml-api-2&tabs=cli) +- [ML - Public access to Workspaces](https://learn.microsoft.com/azure/machine-learning/how-to-secure-workspace-vnet?view=azureml-api-2&tabs=required%2Cpe%2Ccli#public-access-to-workspace) +- [NS-2: Secure cloud services with network controls](https://learn.microsoft.com/security/benchmark/azure/baselines/machine-learning-service-security-baseline#ns-2-secure-cloud-services-with-network-controls) +- [Security and governance for ML](https://learn.microsoft.com/azure/machine-learning/concept-enterprise-security?view=azureml-api-2) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces?pivots=deployment-language-bicep#workspaceproperties) diff --git a/docs/en/rules/Azure.ML.UserManagedIdentity.md b/docs/en/rules/Azure.ML.UserManagedIdentity.md new file mode 100644 index 00000000000..19b1c562f5c --- /dev/null +++ b/docs/en/rules/Azure.ML.UserManagedIdentity.md @@ -0,0 +1,108 @@ +--- +reviewed: 2023-10-13 +severity: Important +pillar: Security +category: Identity and Access Management +resource: Machine Learning +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ML.UserManagedIdentity/ +--- + +# Azure Machine Learning workspaces should use user-assigned managed identity + +## SYNOPSIS + +ML workspaces should use user-assigned managed identity, rather than the default system-assigned managed identity. + +## DESCRIPTION + +Manange access to Azure ML workspace and associated resources, Azure Container Registry, KeyVault, Storage, and App Insights using user-assigned managed identity. +By default, system-assigned managed identity is used by Azure ML workspace to access the associated resources. +User-assigned managed identity allows you to create the identity as an Azure resource and maintain the life cycle of that identity. + +## RECOMMENDATION + +Consider using a User-Assigned Managed Identity, as part of a broader security and lifecycle management strategy. + +## EXAMPLES + +### Configure with Azure template + +To deploy an ML - Workspace that passes this rule: + +- Set the `identity.type` property to `UserAssigned`. +- Reference the identity with `identity.userAssignedIdentities`. +- Set the `properties.primaryUserAssignedIdentity` property value to the User-Assigned Managed Identity. + +For example: + +```json +{ + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "sku": { + "name": "basic", + "tier": "basic" + }, + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example'))]": {} + } + }, + "properties": { + "friendlyName": "[parameters('friendlyName')]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', 'example')]", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', 'example')]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', 'example')]", + "containerRegistry": "[resourceId('Microsoft.ContainerRegistry/registries', 'example')]", + "publicNetworkAccess": "Disabled", + "primaryUserAssignedIdentity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example')]" + } +} +``` + +### Configure with Bicep + +To deploy an ML - Workspace that passes this rule: + +- Set the `identity.type` property to `UserAssigned`. +- Reference the identity with `identity.userAssignedIdentities`. +- Set the `properties.primaryUserAssignedIdentity` property value to the User-Assigned Managed Identity. + +For example: + +```bicep +resource workspace 'Microsoft.MachineLearningServices/workspaces@2023-04-01' = { + name: name + location: location + sku: { + name: 'basic' + tier: 'basic' + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } + properties: { + friendlyName: friendlyName + keyVault: keyVault.id + storageAccount: storageAccount.id + applicationInsights: appInsights.id + containerRegistry: containerRegistry.id + publicNetworkAccess: 'Disabled' + primaryUserAssignedIdentity: identity.id + } +} +``` + +## LINKS + +- [WAF - Authentication with Azure AD](https://learn.microsoft.com/azure/well-architected/security/design-identity-authentication) +- [Set up authentication between Azure Machine Learning and other services](https://learn.microsoft.com/azure/machine-learning/how-to-identity-based-service-authentication) +- [IM-3: Manage application identities securely and automatically](https://learn.microsoft.com/security/benchmark/azure/baselines/machine-learning-service-security-baseline#im-3-manage-application-identities-securely-and-automatically) +- [Azure Policy Regulatory Compliance controls for Azure Machine Learning](https://learn.microsoft.com/azure/machine-learning/security-controls-policy) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.machinelearningservices/workspaces#workspaceproperties) diff --git a/docs/examples-ML.bicep b/docs/examples-ML.bicep new file mode 100644 index 00000000000..aa8557d250e --- /dev/null +++ b/docs/examples-ML.bicep @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Bicep documentation examples + +@description('The name of the ML - Compute Instance.') +param name string + +@description('A friendly name for the workspace.') +param friendlyName string = name + +@description('The location resources will be deployed.') +param location string = resourceGroup().location + +@description('The VM SKU to be deployed.') +param vmSize string = 'STANDARD_D2_V2' + +resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { + name: 'example' +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: 'example' +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: 'example' +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { + name: 'example' +} + +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: 'example' +} + +resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' existing = { + name: 'vnet/subnet' +} + +resource workspace 'Microsoft.MachineLearningServices/workspaces@2023-04-01' = { + name: name + location: location + sku: { + name: 'basic' + tier: 'basic' + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } + properties: { + friendlyName: friendlyName + keyVault: keyVault.id + storageAccount: storageAccount.id + applicationInsights: appInsights.id + containerRegistry: containerRegistry.id + publicNetworkAccess: 'Disabled' + primaryUserAssignedIdentity: identity.id + } +} + +resource compute_instance 'Microsoft.MachineLearningServices/workspaces/computes@2023-06-01-preview' = { + parent: workspace + name: name + location: location + properties: { + computeType: 'ComputeInstance' + disableLocalAuth: true + properties: { + vmSize: vmSize + idleTimeBeforeShutdown: 'PT15M' + subnet: { + id: subnet.id + } + } + } +} diff --git a/docs/examples-ML.json b/docs/examples-ML.json new file mode 100644 index 00000000000..fe35dbb5df7 --- /dev/null +++ b/docs/examples-ML.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.22.6.54827", + "templateHash": "6184102579990317886" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the ML - Compute Instance." + } + }, + "friendlyName": { + "type": "string", + "defaultValue": "[parameters('name')]", + "metadata": { + "description": "A friendly name for the workspace." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location resources will be deployed." + } + }, + "vmSize": { + "type": "string", + "defaultValue": "STANDARD_D2_V2", + "metadata": { + "description": "The VM SKU to be deployed." + } + } + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "sku": { + "name": "basic", + "tier": "basic" + }, + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example'))]": {} + } + }, + "properties": { + "friendlyName": "[parameters('friendlyName')]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', 'example')]", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', 'example')]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', 'example')]", + "containerRegistry": "[resourceId('Microsoft.ContainerRegistry/registries', 'example')]", + "publicNetworkAccess": "Disabled", + "primaryUserAssignedIdentity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'example')]" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "computeType": "ComputeInstance", + "disableLocalAuth": true, + "properties": { + "vmSize": "[parameters('vmSize')]", + "idleTimeBeforeShutdown": "PT15M", + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split('vnet/subnet', '/')[0], split('vnet/subnet', '/')[1])]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('name'))]" + ] + } + ] +} \ No newline at end of file diff --git a/src/PSRule.Rules.Azure/rules/Azure.ML.Rule.yaml b/src/PSRule.Rules.Azure/rules/Azure.ML.Rule.yaml index 530a03f7ec7..452cd06768b 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ML.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Azure.ML.Rule.yaml @@ -4,3 +4,110 @@ # # Validation rules for Azure Machine Learning # + +--- +# Synopsis: ML Compute Instances have idle shutdown enabled. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ML.ComputeIdleShutdown + ref: AZR-000403 + tags: + release: GA + ruleSet: 2023_12 + Azure.WAF/pillar: Cost Optimization +spec: + type: + - Microsoft.MachineLearningServices/workspaces/computes + where: + field: properties.computeType + equals: ComputeInstance + condition: + field: properties.properties.idleTimeBeforeShutdown + hasValue: true + +--- +# Synopsis: ML Compute has local authentication disabled. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ML.DisableLocalAuth + ref: AZR-000404 + tags: + release: GA + ruleSet: 2023_12 + Azure.WAF/pillar: Security +spec: + type: + - Microsoft.MachineLearningServices/workspaces/computes + condition: + field: properties.disableLocalAuth + equals: true + +--- +# Synopsis: ML Compute should be in a virtual network. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ML.ComputeVnet + ref: AZR-000405 + tags: + release: GA + ruleSet: 2023_12 + Azure.WAF/pillar: Security + labels: + Azure.MCSB.v1/control: [ 'NS-1' ] +spec: + type: + - Microsoft.MachineLearningServices/workspaces/computes + condition: + field: properties.properties.subnet.id + hasValue: true + +--- +# Synopsis: Disable public network access from a Azure Machine Learning workspace. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ML.PublicAccess + ref: AZR-000406 + tags: + release: GA + ruleSet: 2023_12 + Azure.WAF/pillar: Security + labels: + Azure.MCSB.v1/control: [ 'NS-2' ] +spec: + type: + - Microsoft.MachineLearningServices/workspaces + condition: + allOf: + - field: properties.allowPublicAccessWhenBehindVnet + hasDefault: false + - field: properties.publicNetworkAccess + equals: Disabled + +--- +# Synopsis: ML Workspaces should use user-assigned managed identity. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ML.UserManagedIdentity + ref: AZR-000407 + tags: + release: GA + ruleSet: 2023_12 + Azure.WAF/pillar: Security + labels: + Azure.MCSB.v1/control: [ 'IM-3' ] +spec: + type: + - Microsoft.MachineLearningServices/workspaces + condition: + allOf: + - field: identity.type + equals: UserAssigned + - field: identity.userAssignedIdentities + exists: true + - field: properties.primaryUserAssignedIdentity + hasValue: true diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.ML.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.ML.Tests.ps1 index 4ee95f2b342..5eb762f16d0 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.ML.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.ML.Tests.ps1 @@ -2,5 +2,119 @@ # Licensed under the MIT License. # -# Unit tests for Azure Machine Learning rules +# Unit tests for Azure ML rules # + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + if ($Env:SYSTEM_DEBUG -eq 'true') { + $VerbosePreference = 'Continue'; + } + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSRule.Rules.Azure) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; +} + +Describe 'Azure.ML' -Tag 'ML' { + Context 'Conditions' { + BeforeAll { + $invokeParams = @{ + Baseline = 'Azure.All' + Module = 'PSRule.Rules.Azure' + WarningAction = 'Ignore' + ErrorAction = 'Stop' + } + $dataPath = Join-Path -Path $here -ChildPath 'Resources.ML.json'; + $result = Invoke-PSRule @invokeParams -InputPath $dataPath; + } + + It 'Azure.ML.ComputeIdleShutdown' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ML.ComputeIdleShutdown' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-b'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-a'; + } + + It 'Azure.ML.DisableLocalAuth' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ML.DisableLocalAuth' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-b'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-a'; + } + + It 'Azure.ML.ComputeVnet' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ML.ComputeVnet' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-b'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'mlcomp-a'; + } + + It 'Azure.ML.PublicAccess' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ML.PublicAccess' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'ml-wks-b', 'ml-wks-c'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'ml-wks-a'; + } + + It 'Azure.ML.UserManagedIdentity' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ML.UserManagedIdentity' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -Be 'ml-wks-b'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'ml-wks-a', 'ml-wks-c'; + } + + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.ML.json b/tests/PSRule.Rules.Azure.Tests/Resources.ML.json new file mode 100644 index 00000000000..d73cc32f2c2 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Resources.ML.json @@ -0,0 +1,114 @@ +[ + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-04-01", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg/providers/Microsoft.MachineLearningServices/workspaces/test-ws/computes/mlcomp-a", + "name": "mlcomp-a", + "location": "westus", + "tags": { + "application": "ML" + }, + "properties": { + "managedResourceGroupId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg", + "computeType": "ComputeInstance", + "disableLocalAuth": true, + "properties": { + "vmSize": "STANDARD_D2_V2", + "idleTimeBeforeShutdown": "PT15M", + "subnet": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-sub" + } + } + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-04-01", + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg/providers/Microsoft.MachineLearningServices/workspaces/test-ws/computes/mlcomp-b", + "name": "mlcomp-b", + "location": "westus", + "tags": { + "application": "ML" + }, + "properties": { + "managedResourceGroupId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg", + "computeType": "ComputeInstance", + "properties": { + "vmSize": "STANDARD_D2_V2" + } + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "ml-wks-a", + "location": "westus", + "tags": { + "application": "ML" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "kind": "Default", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UserAssignedManagedIdentity": {} + } + }, + "properties": { + "friendlyName": "WorkspaceFriendlyName", + "allowPublicAccessWhenBehindVnet": false, + "primaryUserAssignedIdentity": "UserAssignedManagedIdentity", + "publicNetworkAccess": "Disabled" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "ml-wks-b", + "location": "westus", + "tags": { + "application": "ML" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "kind": "Default", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "WorkspaceFriendlyName", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "ml-wks-c", + "location": "westus", + "tags": { + "application": "ML" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "kind": "Default", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UserAssignedManagedIdentity": {} + } + }, + "properties": { + "friendlyName": "WorkspaceFriendlyName", + "allowPublicAccessWhenBehindVnet": true, + "primaryUserAssignedIdentity": "UserAssignedManagedIdentity", + "publicNetworkAccess": "Disabled" + } + } +]