The azure-native:security:CustomRecommendation resource, part of the Pulumi Azure Native provider, defines custom security recommendations that evaluate cloud resources using KQL queries and generate assessments in Microsoft Defender for Cloud. This guide focuses on three capabilities: scoping recommendations to management groups, subscriptions, or security connectors; writing KQL queries for resource evaluation; and configuring severity and remediation guidance.
Custom recommendations require Microsoft Defender for Cloud and reference existing management groups, subscriptions, or security connectors. The examples are intentionally small. Combine them with your own KQL queries and organizational hierarchy.
Define a custom recommendation at management group scope
Organizations with multiple subscriptions often enforce security policies across their entire hierarchy. Management group scope allows recommendations to evaluate all subscriptions within the group.
import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
const customRecommendation = new azure_native.security.CustomRecommendation("customRecommendation", {
cloudProviders: [azure_native.security.RecommendationSupportedClouds.AWS],
customRecommendationName: "33e7cc6e-a139-4723-a0e5-76993aee0771",
description: "organization passwords policy",
displayName: "Password Policy",
query: "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediationDescription: "Change password policy to...",
scope: "providers/Microsoft.Management/managementGroups/contoso",
securityIssue: azure_native.security.SecurityIssue.Vulnerability,
severity: azure_native.security.SeverityEnum.Medium,
});
import pulumi
import pulumi_azure_native as azure_native
custom_recommendation = azure_native.security.CustomRecommendation("customRecommendation",
cloud_providers=[azure_native.security.RecommendationSupportedClouds.AWS],
custom_recommendation_name="33e7cc6e-a139-4723-a0e5-76993aee0771",
description="organization passwords policy",
display_name="Password Policy",
query="RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediation_description="Change password policy to...",
scope="providers/Microsoft.Management/managementGroups/contoso",
security_issue=azure_native.security.SecurityIssue.VULNERABILITY,
severity=azure_native.security.SeverityEnum.MEDIUM)
package main
import (
security "github.com/pulumi/pulumi-azure-native-sdk/security/v3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := security.NewCustomRecommendation(ctx, "customRecommendation", &security.CustomRecommendationArgs{
CloudProviders: pulumi.StringArray{
pulumi.String(security.RecommendationSupportedCloudsAWS),
},
CustomRecommendationName: pulumi.String("33e7cc6e-a139-4723-a0e5-76993aee0771"),
Description: pulumi.String("organization passwords policy"),
DisplayName: pulumi.String("Password Policy"),
Query: pulumi.String("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')"),
RemediationDescription: pulumi.String("Change password policy to..."),
Scope: pulumi.String("providers/Microsoft.Management/managementGroups/contoso"),
SecurityIssue: pulumi.String(security.SecurityIssueVulnerability),
Severity: pulumi.String(security.SeverityEnumMedium),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;
return await Deployment.RunAsync(() =>
{
var customRecommendation = new AzureNative.Security.CustomRecommendation("customRecommendation", new()
{
CloudProviders = new[]
{
AzureNative.Security.RecommendationSupportedClouds.AWS,
},
CustomRecommendationName = "33e7cc6e-a139-4723-a0e5-76993aee0771",
Description = "organization passwords policy",
DisplayName = "Password Policy",
Query = "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
RemediationDescription = "Change password policy to...",
Scope = "providers/Microsoft.Management/managementGroups/contoso",
SecurityIssue = AzureNative.Security.SecurityIssue.Vulnerability,
Severity = AzureNative.Security.SeverityEnum.Medium,
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.security.CustomRecommendation;
import com.pulumi.azurenative.security.CustomRecommendationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var customRecommendation = new CustomRecommendation("customRecommendation", CustomRecommendationArgs.builder()
.cloudProviders("AWS")
.customRecommendationName("33e7cc6e-a139-4723-a0e5-76993aee0771")
.description("organization passwords policy")
.displayName("Password Policy")
.query("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')")
.remediationDescription("Change password policy to...")
.scope("providers/Microsoft.Management/managementGroups/contoso")
.securityIssue("Vulnerability")
.severity("Medium")
.build());
}
}
resources:
customRecommendation:
type: azure-native:security:CustomRecommendation
properties:
cloudProviders:
- AWS
customRecommendationName: 33e7cc6e-a139-4723-a0e5-76993aee0771
description: organization passwords policy
displayName: Password Policy
query: RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')
remediationDescription: Change password policy to...
scope: providers/Microsoft.Management/managementGroups/contoso
securityIssue: Vulnerability
severity: Medium
The scope property determines where the recommendation applies. Management group scope (format: providers/Microsoft.Management/managementGroups/{name}) evaluates resources across all child subscriptions. The query property contains a KQL expression that runs against the RawEntityMetadata table, filtering resources and calculating health status. The displayName and description appear in Defender for Cloud’s recommendation list, while remediationDescription guides users toward fixes.
Scope recommendations to a specific security connector
Multi-cloud environments use security connectors to monitor AWS or GCP resources from Azure. Scoping to a connector creates checks specific to that cloud provider.
import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
const customRecommendation = new azure_native.security.CustomRecommendation("customRecommendation", {
cloudProviders: [azure_native.security.RecommendationSupportedClouds.AWS],
customRecommendationName: "33e7cc6e-a139-4723-a0e5-76993aee0771",
description: "organization passwords policy",
displayName: "Password Policy",
query: "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediationDescription: "Change password policy to...",
scope: "subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector",
securityIssue: azure_native.security.SecurityIssue.Vulnerability,
severity: azure_native.security.SeverityEnum.Medium,
});
import pulumi
import pulumi_azure_native as azure_native
custom_recommendation = azure_native.security.CustomRecommendation("customRecommendation",
cloud_providers=[azure_native.security.RecommendationSupportedClouds.AWS],
custom_recommendation_name="33e7cc6e-a139-4723-a0e5-76993aee0771",
description="organization passwords policy",
display_name="Password Policy",
query="RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediation_description="Change password policy to...",
scope="subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector",
security_issue=azure_native.security.SecurityIssue.VULNERABILITY,
severity=azure_native.security.SeverityEnum.MEDIUM)
package main
import (
security "github.com/pulumi/pulumi-azure-native-sdk/security/v3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := security.NewCustomRecommendation(ctx, "customRecommendation", &security.CustomRecommendationArgs{
CloudProviders: pulumi.StringArray{
pulumi.String(security.RecommendationSupportedCloudsAWS),
},
CustomRecommendationName: pulumi.String("33e7cc6e-a139-4723-a0e5-76993aee0771"),
Description: pulumi.String("organization passwords policy"),
DisplayName: pulumi.String("Password Policy"),
Query: pulumi.String("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')"),
RemediationDescription: pulumi.String("Change password policy to..."),
Scope: pulumi.String("subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector"),
SecurityIssue: pulumi.String(security.SecurityIssueVulnerability),
Severity: pulumi.String(security.SeverityEnumMedium),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;
return await Deployment.RunAsync(() =>
{
var customRecommendation = new AzureNative.Security.CustomRecommendation("customRecommendation", new()
{
CloudProviders = new[]
{
AzureNative.Security.RecommendationSupportedClouds.AWS,
},
CustomRecommendationName = "33e7cc6e-a139-4723-a0e5-76993aee0771",
Description = "organization passwords policy",
DisplayName = "Password Policy",
Query = "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
RemediationDescription = "Change password policy to...",
Scope = "subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector",
SecurityIssue = AzureNative.Security.SecurityIssue.Vulnerability,
Severity = AzureNative.Security.SeverityEnum.Medium,
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.security.CustomRecommendation;
import com.pulumi.azurenative.security.CustomRecommendationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var customRecommendation = new CustomRecommendation("customRecommendation", CustomRecommendationArgs.builder()
.cloudProviders("AWS")
.customRecommendationName("33e7cc6e-a139-4723-a0e5-76993aee0771")
.description("organization passwords policy")
.displayName("Password Policy")
.query("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')")
.remediationDescription("Change password policy to...")
.scope("subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector")
.securityIssue("Vulnerability")
.severity("Medium")
.build());
}
}
resources:
customRecommendation:
type: azure-native:security:CustomRecommendation
properties:
cloudProviders:
- AWS
customRecommendationName: 33e7cc6e-a139-4723-a0e5-76993aee0771
description: organization passwords policy
displayName: Password Policy
query: RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')
remediationDescription: Change password policy to...
scope: subscriptions/20ff7fc3-e762-44dd-bd96-b71116dcdc23/resourceGroups/gcpResourceGroup/providers/Microsoft.Security/securityConnectors/gcpconnector
securityIssue: Vulnerability
severity: Medium
The scope property here references a security connector resource (format: subscriptions/{id}/resourceGroups/{rg}/providers/Microsoft.Security/securityConnectors/{name}). This limits the recommendation’s evaluation to resources monitored through that specific connector. The cloudProviders property specifies which clouds the query targets, even though the example shows AWS while the query filters for GCP resources.
Apply recommendations to a single subscription
Individual subscriptions often need custom security checks tailored to their specific workloads and compliance requirements.
import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
const customRecommendation = new azure_native.security.CustomRecommendation("customRecommendation", {
cloudProviders: [azure_native.security.RecommendationSupportedClouds.AWS],
customRecommendationName: "33e7cc6e-a139-4723-a0e5-76993aee0771",
description: "organization passwords policy",
displayName: "Password Policy",
query: "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediationDescription: "Change password policy to...",
scope: "subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b",
securityIssue: azure_native.security.SecurityIssue.Vulnerability,
severity: azure_native.security.SeverityEnum.Medium,
});
import pulumi
import pulumi_azure_native as azure_native
custom_recommendation = azure_native.security.CustomRecommendation("customRecommendation",
cloud_providers=[azure_native.security.RecommendationSupportedClouds.AWS],
custom_recommendation_name="33e7cc6e-a139-4723-a0e5-76993aee0771",
description="organization passwords policy",
display_name="Password Policy",
query="RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
remediation_description="Change password policy to...",
scope="subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b",
security_issue=azure_native.security.SecurityIssue.VULNERABILITY,
severity=azure_native.security.SeverityEnum.MEDIUM)
package main
import (
security "github.com/pulumi/pulumi-azure-native-sdk/security/v3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := security.NewCustomRecommendation(ctx, "customRecommendation", &security.CustomRecommendationArgs{
CloudProviders: pulumi.StringArray{
pulumi.String(security.RecommendationSupportedCloudsAWS),
},
CustomRecommendationName: pulumi.String("33e7cc6e-a139-4723-a0e5-76993aee0771"),
Description: pulumi.String("organization passwords policy"),
DisplayName: pulumi.String("Password Policy"),
Query: pulumi.String("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')"),
RemediationDescription: pulumi.String("Change password policy to..."),
Scope: pulumi.String("subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b"),
SecurityIssue: pulumi.String(security.SecurityIssueVulnerability),
Severity: pulumi.String(security.SeverityEnumMedium),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;
return await Deployment.RunAsync(() =>
{
var customRecommendation = new AzureNative.Security.CustomRecommendation("customRecommendation", new()
{
CloudProviders = new[]
{
AzureNative.Security.RecommendationSupportedClouds.AWS,
},
CustomRecommendationName = "33e7cc6e-a139-4723-a0e5-76993aee0771",
Description = "organization passwords policy",
DisplayName = "Password Policy",
Query = "RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')",
RemediationDescription = "Change password policy to...",
Scope = "subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b",
SecurityIssue = AzureNative.Security.SecurityIssue.Vulnerability,
Severity = AzureNative.Security.SeverityEnum.Medium,
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.security.CustomRecommendation;
import com.pulumi.azurenative.security.CustomRecommendationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var customRecommendation = new CustomRecommendation("customRecommendation", CustomRecommendationArgs.builder()
.cloudProviders("AWS")
.customRecommendationName("33e7cc6e-a139-4723-a0e5-76993aee0771")
.description("organization passwords policy")
.displayName("Password Policy")
.query("RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')")
.remediationDescription("Change password policy to...")
.scope("subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b")
.securityIssue("Vulnerability")
.severity("Medium")
.build());
}
}
resources:
customRecommendation:
type: azure-native:security:CustomRecommendation
properties:
cloudProviders:
- AWS
customRecommendationName: 33e7cc6e-a139-4723-a0e5-76993aee0771
description: organization passwords policy
displayName: Password Policy
query: RawEntityMetadata | where Environment == 'GCP' and Identifiers.Type == 'compute.firewalls' | extend IslogConfigEnabled = tobool(Record.logConfig.enable) | extend HealthStatus = iff(IslogConfigEnabled, 'HEALTHY', 'UNHEALTHY')
remediationDescription: Change password policy to...
scope: subscriptions/e5d1b86c-3051-44d5-8802-aa65d45a279b
securityIssue: Vulnerability
severity: Medium
Subscription scope (format: subscriptions/{id}) limits evaluation to resources within that subscription. The severity property (Low, Medium, High, Critical) determines how the recommendation appears in Defender for Cloud’s priority lists. The securityIssue property classifies the finding type (Vulnerability, Misconfiguration, etc.).
Beyond these examples
These snippets focus on specific custom recommendation features: scope hierarchy, KQL-based custom queries, and severity and security issue classification. They’re intentionally minimal rather than full security monitoring solutions.
The examples reference pre-existing infrastructure such as management groups, subscriptions, or security connectors, and Microsoft Defender for Cloud enabled. They focus on configuring the recommendation rather than provisioning the underlying security infrastructure.
To keep things focused, common recommendation patterns are omitted, including:
- Query optimization and performance tuning
- Assessment result handling and notifications
- Integration with Azure Policy or compliance frameworks
- Recommendation lifecycle management (updates, deletions)
These omissions are intentional: the goal is to illustrate how each recommendation feature is wired, not provide drop-in security modules. See the Custom Recommendation resource reference for all available configuration options.
Let's create Azure Custom Security Recommendations
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Resource Configuration & Immutability
customRecommendationName and scope are immutable and cannot be modified after creation. Changes to these properties require replacing the resource.providers/Microsoft.Management/managementGroups/{managementGroup}), subscription (subscriptions/{subscriptionId}), or security connector (subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Security/securityConnectors/{securityConnectorName}).Query & Assessment Logic
RawEntityMetadata table. The query should return results with a HealthStatus field indicating ‘HEALTHY’ or ‘UNHEALTHY’.assessmentKey is a computed output property containing the assessment metadata key used when an assessment is generated for this recommendation.Cloud Provider Support
cloudProviders set to AWS while the query filters for GCP resources (Environment == 'GCP'). The cloudProviders property indicates which clouds are supported, while the query determines which resources are evaluated.severity property accepts values like Medium as shown in the examples. This severity is applied to assessments generated by the recommendation.