From f14b46c9e5a3954c4a89700f0976bad137fa4061 Mon Sep 17 00:00:00 2001 From: Matthias Theuermann <73223147+mati007thm@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:14:23 +0100 Subject: [PATCH] feat: added MsDefender integration (#171) --- .github/actions/spelling/expect.txt | 1 + docs/resources/integration_msdefender.md | 301 +++++++++++++++++ .../mondoo_integration_msdefender/main.tf | 20 ++ .../mondoo_integration_msdefender/outputs.tf | 22 ++ .../mondoo_integration_msdefender/resource.tf | 258 +++++++++++++++ internal/provider/gql.go | 34 +- .../integration_msdefender_resource.go | 313 ++++++++++++++++++ .../integration_msdefender_resource_test.go | 93 ++++++ internal/provider/provider.go | 1 + 9 files changed, 1031 insertions(+), 12 deletions(-) create mode 100644 docs/resources/integration_msdefender.md create mode 100644 examples/resources/mondoo_integration_msdefender/main.tf create mode 100644 examples/resources/mondoo_integration_msdefender/outputs.tf create mode 100644 examples/resources/mondoo_integration_msdefender/resource.tf create mode 100644 internal/provider/integration_msdefender_resource.go create mode 100644 internal/provider/integration_msdefender_resource_test.go diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 7c88240..7d0383a 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -20,6 +20,7 @@ KBp ljq LQV mrns +msdefender NCIs nestedatt NHar diff --git a/docs/resources/integration_msdefender.md b/docs/resources/integration_msdefender.md new file mode 100644 index 0000000..5ffb320 --- /dev/null +++ b/docs/resources/integration_msdefender.md @@ -0,0 +1,301 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "mondoo_integration_msdefender Resource - terraform-provider-mondoo" +subcategory: "" +description: |- + Microsoft Defender for Cloud integration. +--- + +# mondoo_integration_msdefender (Resource) + +Microsoft Defender for Cloud integration. + +## Example Usage + +```terraform +# Variables +# ---------------------------------------------- + +variable "tenant_id" { + description = "The Azure Active Directory Tenant ID" + type = string + default = "ffffffff-ffff-ffff-ffff-ffffffffffff" +} + +variable "primary_subscription" { + description = "The primary Azure Subscription ID" + type = string + default = "ffffffff-ffff-ffff-ffff-ffffffffffff" +} + +locals { + mondoo_security_integration_name = "Mondoo Security Integration" +} + +# Azure AD with Application and Certificate +# ---------------------------------------------- + +provider "azuread" { + tenant_id = var.tenant_id +} + +data "azuread_client_config" "current" {} + +# Add the required permissions to the application +# User still need to be grant the permissions to the application via the Azure Portal +resource "azuread_application" "mondoo_security" { + display_name = local.mondoo_security_integration_name + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "246dd0d5-5bd0-4def-940b-0421030a5b68" + type = "Role" + } + + resource_access { + id = "e321f0bb-e7f7-481e-bb28-e3b0b32d4bd0" + type = "Role" + } + + resource_access { + id = "5e0edab9-c148-49d0-b423-ac253e121825" + type = "Role" + } + + resource_access { + id = "bf394140-e372-4bf9-a898-299cfc7564e5" + type = "Role" + } + + resource_access { + id = "6e472fd1-ad78-48da-a0f0-97ab2c6b769e" + type = "Role" + } + + resource_access { + id = "dc5007c0-2d7d-4c42-879c-2dab87571379" + type = "Role" + } + + resource_access { + id = "b0afded3-3588-46d8-8b3d-9842eff778da" + type = "Role" + } + + resource_access { + id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61" + type = "Role" + } + + resource_access { + id = "197ee4e9-b993-4066-898f-d6aecc55125b" + type = "Role" + } + + resource_access { + id = "9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30" + type = "Role" + } + + resource_access { + id = "f8f035bb-2cce-47fb-8bf5-7baf3ecbee48" + type = "Role" + } + + resource_access { + id = "dbb9058a-0e50-45d7-ae91-66909b5d4664" + type = "Role" + } + + resource_access { + id = "9e640839-a198-48fb-8b9a-013fd6f6cbcd" + type = "Role" + } + + resource_access { + id = "37730810-e9ba-4e46-b07e-8ca78d182097" + type = "Role" + } + + resource_access { + id = "c7fbd983-d9aa-4fa7-84b8-17382c103bc4" + type = "Role" + } + } +} + +resource "tls_private_key" "credential" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "tls_self_signed_cert" "credential" { + private_key_pem = tls_private_key.credential.private_key_pem + + # Certificate expires after 3 months. + validity_period_hours = 1680 + + # Generate a new certificate if Terraform is run within three + # hours of the certificate's expiration time. + early_renewal_hours = 3 + + # Reasonable set of uses for a server SSL certificate. + allowed_uses = [ + "key_encipherment", + "digital_signature", + "data_encipherment", + "cert_signing", + ] + + subject { + common_name = "mondoo" + } +} + +# Attach the certificate to the application +resource "azuread_application_certificate" "mondoo_security_integration" { + # see https://github.com/hashicorp/terraform-provider-azuread/issues/1227 + application_id = azuread_application.mondoo_security.id + type = "AsymmetricX509Cert" + value = tls_self_signed_cert.credential.cert_pem +} + +# Create a service principal for the application +resource "azuread_service_principal" "mondoo_security" { + client_id = azuread_application.mondoo_security.client_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] +} + +# Azure Permissions to Azure AD Application +# ---------------------------------------------- + +provider "azurerm" { + tenant_id = var.tenant_id + features {} +} + +data "azurerm_subscription" "primary" { + subscription_id = var.primary_subscription +} + +data "azurerm_subscriptions" "available" {} + +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition +resource "azurerm_role_definition" "mondoo_security_role" { + name = "tf-mondoo-security-role" + description = "This role includes all permissions for Mondoo Security to assess the security." + scope = data.azurerm_subscription.primary.id + + permissions { + actions = [ + "Microsoft.Authorization/*/read", + "Microsoft.ResourceHealth/availabilityStatuses/read", + "Microsoft.Insights/alertRules/*", + "Microsoft.Resources/deployments/*", + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Support/*", + "Microsoft.Web/listSitesAssignedToHostName/read", + "Microsoft.Web/serverFarms/read", + "Microsoft.Web/sites/config/read", + "Microsoft.Web/sites/config/web/appsettings/read", + "Microsoft.Web/sites/config/web/connectionstrings/read", + "Microsoft.Web/sites/config/appsettings/read", + "Microsoft.web/sites/config/snapshots/read", + "Microsoft.Web/sites/config/list/action", + "Microsoft.Web/sites/read", + "Microsoft.KeyVault/checkNameAvailability/read", + "Microsoft.KeyVault/deletedVaults/read", + "Microsoft.KeyVault/locations/*/read", + "Microsoft.KeyVault/vaults/*/read", + "Microsoft.KeyVault/operations/read", + "Microsoft.Compute/virtualMachines/runCommands/read", + "Microsoft.Compute/virtualMachines/runCommands/write", + "Microsoft.Compute/virtualMachines/runCommands/delete" + ] + not_actions = [] + data_actions = [ + "Microsoft.KeyVault/vaults/*/read", + "Microsoft.KeyVault/vaults/secrets/readMetadata/action" + ] + not_data_actions = [] + } + + assignable_scopes = data.azurerm_subscriptions.available.subscriptions[*].id +} + +# add custom role to all subscriptions +resource "azurerm_role_assignment" "mondoo_security" { + count = length(data.azurerm_subscriptions.available.subscriptions) + scope = data.azurerm_subscriptions.available.subscriptions[count.index].id + role_definition_id = azurerm_role_definition.mondoo_security_role.role_definition_resource_id + principal_id = azuread_service_principal.mondoo_security.object_id + depends_on = [ + azurerm_role_definition.mondoo_security_role, + ] +} + +# add reader role to all subscriptions +resource "azurerm_role_assignment" "reader" { + count = length(data.azurerm_subscriptions.available.subscriptions) + scope = data.azurerm_subscriptions.available.subscriptions[count.index].id + role_definition_name = "Reader" + principal_id = azuread_service_principal.mondoo_security.object_id +} + +# Configure the Mondoo +# ---------------------------------------------- + +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the MsDefender integration +resource "mondoo_integration_msdefender" "msdefender_integration" { + name = "Azure ${local.mondoo_security_integration_name}" + tenant_id = var.tenant_id + client_id = azuread_application.mondoo_security.client_id + + # subscription_allow_list= ["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"] + # subscription_deny_list = ["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"] + credentials = { + pem_file = join("\n", [tls_self_signed_cert.credential.cert_pem, tls_private_key.credential.private_key_pem]) + } + # wait for the permissions to provisioned + depends_on = [ + azuread_application.mondoo_security, + azuread_service_principal.mondoo_security, + azurerm_role_assignment.mondoo_security, + azurerm_role_assignment.reader, + ] +} +``` + + +## Schema + +### Required + +- `client_id` (String) Azure Client ID. +- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials)) +- `name` (String) Name of the integration. +- `tenant_id` (String) Azure Tenant ID. + +### Optional + +- `space_id` (String) Mondoo Space Identifier. If it is not provided, the provider space is used. +- `subscription_allow_list` (List of String) List of Azure subscriptions to scan. +- `subscription_deny_list` (List of String) List of Azure subscriptions to exclude from scanning. + +### Read-Only + +- `mrn` (String) Integration identifier + + +### Nested Schema for `credentials` + +Required: + +- `pem_file` (String, Sensitive) PEM file for Azure integration. diff --git a/examples/resources/mondoo_integration_msdefender/main.tf b/examples/resources/mondoo_integration_msdefender/main.tf new file mode 100644 index 0000000..70e7508 --- /dev/null +++ b/examples/resources/mondoo_integration_msdefender/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + azuread = { + source = "hashicorp/azuread" + version = ">= 2.48.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.0.0" + } + mondoo = { + source = "mondoohq/mondoo" + version = ">= 0.19" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.5" + } + } +} diff --git a/examples/resources/mondoo_integration_msdefender/outputs.tf b/examples/resources/mondoo_integration_msdefender/outputs.tf new file mode 100644 index 0000000..82debb7 --- /dev/null +++ b/examples/resources/mondoo_integration_msdefender/outputs.tf @@ -0,0 +1,22 @@ +output "cert_pem" { + description = "The self-signed certificate in PEM format" + value = tls_self_signed_cert.credential.cert_pem + sensitive = true +} + +output "private_key_pem" { + description = "The private key in PEM format" + value = join("\n", [tls_self_signed_cert.credential.cert_pem, tls_private_key.credential.private_key_pem]) + sensitive = true +} + +output "available_subscriptions" { + description = "Azure Subscriptions" + value = data.azurerm_subscriptions.available.subscriptions +} + +output "cnspec" { + description = "cnspec cli command" + value = "terraform output -raw private_key_pem > key.pem\ncnspec scan azure --tenant-id ${var.tenant_id} --client-id ${azuread_application.mondoo_security.client_id} --certificate-path key.pem" +} + diff --git a/examples/resources/mondoo_integration_msdefender/resource.tf b/examples/resources/mondoo_integration_msdefender/resource.tf new file mode 100644 index 0000000..3f12c31 --- /dev/null +++ b/examples/resources/mondoo_integration_msdefender/resource.tf @@ -0,0 +1,258 @@ +# Variables +# ---------------------------------------------- + +variable "tenant_id" { + description = "The Azure Active Directory Tenant ID" + type = string + default = "ffffffff-ffff-ffff-ffff-ffffffffffff" +} + +variable "primary_subscription" { + description = "The primary Azure Subscription ID" + type = string + default = "ffffffff-ffff-ffff-ffff-ffffffffffff" +} + +locals { + mondoo_security_integration_name = "Mondoo Security Integration" +} + +# Azure AD with Application and Certificate +# ---------------------------------------------- + +provider "azuread" { + tenant_id = var.tenant_id +} + +data "azuread_client_config" "current" {} + +# Add the required permissions to the application +# User still need to be grant the permissions to the application via the Azure Portal +resource "azuread_application" "mondoo_security" { + display_name = local.mondoo_security_integration_name + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "246dd0d5-5bd0-4def-940b-0421030a5b68" + type = "Role" + } + + resource_access { + id = "e321f0bb-e7f7-481e-bb28-e3b0b32d4bd0" + type = "Role" + } + + resource_access { + id = "5e0edab9-c148-49d0-b423-ac253e121825" + type = "Role" + } + + resource_access { + id = "bf394140-e372-4bf9-a898-299cfc7564e5" + type = "Role" + } + + resource_access { + id = "6e472fd1-ad78-48da-a0f0-97ab2c6b769e" + type = "Role" + } + + resource_access { + id = "dc5007c0-2d7d-4c42-879c-2dab87571379" + type = "Role" + } + + resource_access { + id = "b0afded3-3588-46d8-8b3d-9842eff778da" + type = "Role" + } + + resource_access { + id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61" + type = "Role" + } + + resource_access { + id = "197ee4e9-b993-4066-898f-d6aecc55125b" + type = "Role" + } + + resource_access { + id = "9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30" + type = "Role" + } + + resource_access { + id = "f8f035bb-2cce-47fb-8bf5-7baf3ecbee48" + type = "Role" + } + + resource_access { + id = "dbb9058a-0e50-45d7-ae91-66909b5d4664" + type = "Role" + } + + resource_access { + id = "9e640839-a198-48fb-8b9a-013fd6f6cbcd" + type = "Role" + } + + resource_access { + id = "37730810-e9ba-4e46-b07e-8ca78d182097" + type = "Role" + } + + resource_access { + id = "c7fbd983-d9aa-4fa7-84b8-17382c103bc4" + type = "Role" + } + } +} + +resource "tls_private_key" "credential" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "tls_self_signed_cert" "credential" { + private_key_pem = tls_private_key.credential.private_key_pem + + # Certificate expires after 3 months. + validity_period_hours = 1680 + + # Generate a new certificate if Terraform is run within three + # hours of the certificate's expiration time. + early_renewal_hours = 3 + + # Reasonable set of uses for a server SSL certificate. + allowed_uses = [ + "key_encipherment", + "digital_signature", + "data_encipherment", + "cert_signing", + ] + + subject { + common_name = "mondoo" + } +} + +# Attach the certificate to the application +resource "azuread_application_certificate" "mondoo_security_integration" { + # see https://github.com/hashicorp/terraform-provider-azuread/issues/1227 + application_id = azuread_application.mondoo_security.id + type = "AsymmetricX509Cert" + value = tls_self_signed_cert.credential.cert_pem +} + +# Create a service principal for the application +resource "azuread_service_principal" "mondoo_security" { + client_id = azuread_application.mondoo_security.client_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] +} + +# Azure Permissions to Azure AD Application +# ---------------------------------------------- + +provider "azurerm" { + tenant_id = var.tenant_id + features {} +} + +data "azurerm_subscription" "primary" { + subscription_id = var.primary_subscription +} + +data "azurerm_subscriptions" "available" {} + +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition +resource "azurerm_role_definition" "mondoo_security_role" { + name = "tf-mondoo-security-role" + description = "This role includes all permissions for Mondoo Security to assess the security." + scope = data.azurerm_subscription.primary.id + + permissions { + actions = [ + "Microsoft.Authorization/*/read", + "Microsoft.ResourceHealth/availabilityStatuses/read", + "Microsoft.Insights/alertRules/*", + "Microsoft.Resources/deployments/*", + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Support/*", + "Microsoft.Web/listSitesAssignedToHostName/read", + "Microsoft.Web/serverFarms/read", + "Microsoft.Web/sites/config/read", + "Microsoft.Web/sites/config/web/appsettings/read", + "Microsoft.Web/sites/config/web/connectionstrings/read", + "Microsoft.Web/sites/config/appsettings/read", + "Microsoft.web/sites/config/snapshots/read", + "Microsoft.Web/sites/config/list/action", + "Microsoft.Web/sites/read", + "Microsoft.KeyVault/checkNameAvailability/read", + "Microsoft.KeyVault/deletedVaults/read", + "Microsoft.KeyVault/locations/*/read", + "Microsoft.KeyVault/vaults/*/read", + "Microsoft.KeyVault/operations/read", + "Microsoft.Compute/virtualMachines/runCommands/read", + "Microsoft.Compute/virtualMachines/runCommands/write", + "Microsoft.Compute/virtualMachines/runCommands/delete" + ] + not_actions = [] + data_actions = [ + "Microsoft.KeyVault/vaults/*/read", + "Microsoft.KeyVault/vaults/secrets/readMetadata/action" + ] + not_data_actions = [] + } + + assignable_scopes = data.azurerm_subscriptions.available.subscriptions[*].id +} + +# add custom role to all subscriptions +resource "azurerm_role_assignment" "mondoo_security" { + count = length(data.azurerm_subscriptions.available.subscriptions) + scope = data.azurerm_subscriptions.available.subscriptions[count.index].id + role_definition_id = azurerm_role_definition.mondoo_security_role.role_definition_resource_id + principal_id = azuread_service_principal.mondoo_security.object_id + depends_on = [ + azurerm_role_definition.mondoo_security_role, + ] +} + +# add reader role to all subscriptions +resource "azurerm_role_assignment" "reader" { + count = length(data.azurerm_subscriptions.available.subscriptions) + scope = data.azurerm_subscriptions.available.subscriptions[count.index].id + role_definition_name = "Reader" + principal_id = azuread_service_principal.mondoo_security.object_id +} + +# Configure the Mondoo +# ---------------------------------------------- + +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the MsDefender integration +resource "mondoo_integration_msdefender" "msdefender_integration" { + name = "Azure ${local.mondoo_security_integration_name}" + tenant_id = var.tenant_id + client_id = azuread_application.mondoo_security.client_id + + # subscription_allow_list= ["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"] + # subscription_deny_list = ["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"] + credentials = { + pem_file = join("\n", [tls_self_signed_cert.credential.cert_pem, tls_private_key.credential.private_key_pem]) + } + # wait for the permissions to provisioned + depends_on = [ + azuread_application.mondoo_security, + azuread_service_principal.mondoo_security, + azurerm_role_assignment.mondoo_security, + azurerm_role_assignment.reader, + ] +} diff --git a/internal/provider/gql.go b/internal/provider/gql.go index 6ea0c55..48eaafc 100644 --- a/internal/provider/gql.go +++ b/internal/provider/gql.go @@ -651,19 +651,29 @@ type EmailRecipient struct { ReferenceURL string } +type MicrosoftDefenderConfigurationOptionsInput struct { + TenantId string + ClientId string + SubscriptionsAllowlist []string + SubscriptionsDenylist []string + Certificate string + ClientSecret string +} + type ClientIntegrationConfigurationOptions struct { - AzureConfigurationOptions AzureConfigurationOptions `graphql:"... on AzureConfigurationOptions"` - HostConfigurationOptions HostConfigurationOptions `graphql:"... on HostConfigurationOptions"` - Ms365ConfigurationOptions Ms365ConfigurationOptions `graphql:"... on Ms365ConfigurationOptions"` - GcpConfigurationOptions GcpConfigurationOptions `graphql:"... on GcpConfigurationOptions"` - SlackConfigurationOptions SlackConfigurationOptions `graphql:"... on SlackConfigurationOptions"` - GithubConfigurationOptions GithubConfigurationOptions `graphql:"... on GithubConfigurationOptions"` - HostedAwsConfigurationOptions HostedAwsConfigurationOptions `graphql:"... on HostedAwsConfigurationOptions"` - ShodanConfigurationOptions ShodanConfigurationOptions `graphql:"... on ShodanConfigurationOptions"` - ZendeskConfigurationOptions ZendeskConfigurationOptions `graphql:"... on ZendeskConfigurationOptions"` - JiraConfigurationOptions JiraConfigurationOptions `graphql:"... on JiraConfigurationOptions"` - EmailConfigurationOptions EmailConfigurationOptions `graphql:"... on EmailConfigurationOptions"` - GitlabConfigurationOptions GitlabConfigurationOptions `graphql:"... on GitlabConfigurationOptions"` + AzureConfigurationOptions AzureConfigurationOptions `graphql:"... on AzureConfigurationOptions"` + HostConfigurationOptions HostConfigurationOptions `graphql:"... on HostConfigurationOptions"` + Ms365ConfigurationOptions Ms365ConfigurationOptions `graphql:"... on Ms365ConfigurationOptions"` + GcpConfigurationOptions GcpConfigurationOptions `graphql:"... on GcpConfigurationOptions"` + SlackConfigurationOptions SlackConfigurationOptions `graphql:"... on SlackConfigurationOptions"` + GithubConfigurationOptions GithubConfigurationOptions `graphql:"... on GithubConfigurationOptions"` + HostedAwsConfigurationOptions HostedAwsConfigurationOptions `graphql:"... on HostedAwsConfigurationOptions"` + ShodanConfigurationOptions ShodanConfigurationOptions `graphql:"... on ShodanConfigurationOptions"` + ZendeskConfigurationOptions ZendeskConfigurationOptions `graphql:"... on ZendeskConfigurationOptions"` + JiraConfigurationOptions JiraConfigurationOptions `graphql:"... on JiraConfigurationOptions"` + EmailConfigurationOptions EmailConfigurationOptions `graphql:"... on EmailConfigurationOptions"` + GitlabConfigurationOptions GitlabConfigurationOptions `graphql:"... on GitlabConfigurationOptions"` + MicrosoftDefenderConfigurationOptionsInput MicrosoftDefenderConfigurationOptionsInput `graphql:"... on MicrosoftDefenderConfigurationOptionsInput"` // Add other configuration options here } diff --git a/internal/provider/integration_msdefender_resource.go b/internal/provider/integration_msdefender_resource.go new file mode 100644 index 0000000..f64bbad --- /dev/null +++ b/internal/provider/integration_msdefender_resource.go @@ -0,0 +1,313 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + mondoov1 "go.mondoo.com/mondoo-go" +) + +var _ resource.Resource = (*integrationMsDefenderResource)(nil) + +func NewIntegrationMsDefenderResource() resource.Resource { + return &integrationMsDefenderResource{} +} + +type integrationMsDefenderResource struct { + client *ExtendedGqlClient +} + +type integrationMsDefenderResourceModel struct { + // scope + SpaceID types.String `tfsdk:"space_id"` + + // integration details + Mrn types.String `tfsdk:"mrn"` + Name types.String `tfsdk:"name"` + ClientId types.String `tfsdk:"client_id"` + TenantId types.String `tfsdk:"tenant_id"` + SubscriptionAllowList types.List `tfsdk:"subscription_allow_list"` + SubscriptionDenyList types.List `tfsdk:"subscription_deny_list"` + + // credentials + Credential integrationMsDefenderCredentialModel `tfsdk:"credentials"` +} + +type integrationMsDefenderCredentialModel struct { + PEMFile types.String `tfsdk:"pem_file"` +} + +func (m integrationMsDefenderResourceModel) GetConfigurationOptions() *mondoov1.MicrosoftDefenderConfigurationOptionsInput { + opts := &mondoov1.MicrosoftDefenderConfigurationOptionsInput{ + TenantID: mondoov1.String(m.TenantId.ValueString()), + ClientID: mondoov1.String(m.ClientId.ValueString()), + Certificate: mondoov1.NewStringPtr(mondoov1.String(m.Credential.PEMFile.ValueString())), + } + + ctx := context.Background() + var listAllow []mondoov1.String + allowlist, _ := m.SubscriptionAllowList.ToListValue(ctx) + allowlist.ElementsAs(ctx, &listAllow, true) + + var listDeny []mondoov1.String + denylist, _ := m.SubscriptionDenyList.ToListValue(ctx) + denylist.ElementsAs(ctx, &listDeny, true) + + opts.SubscriptionsAllowlist = &listAllow + opts.SubscriptionsDenylist = &listDeny + + return opts +} + +func (r *integrationMsDefenderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_integration_msdefender" +} + +func (r *integrationMsDefenderResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Microsoft Defender for Cloud integration.", + Attributes: map[string]schema.Attribute{ + "space_id": schema.StringAttribute{ + MarkdownDescription: "Mondoo Space Identifier. If it is not provided, the provider space is used.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "mrn": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Integration identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the integration.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(250), + }, + }, + "client_id": schema.StringAttribute{ + MarkdownDescription: "Azure Client ID.", + Required: true, + }, + "tenant_id": schema.StringAttribute{ + MarkdownDescription: "Azure Tenant ID.", + Required: true, + }, + "subscription_allow_list": schema.ListAttribute{ + MarkdownDescription: "List of Azure subscriptions to scan.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + // Validate only this attribute or other_attr is configured. + listvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("subscription_deny_list"), + }...), + }, + }, + "subscription_deny_list": schema.ListAttribute{ + MarkdownDescription: "List of Azure subscriptions to exclude from scanning.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + // Validate only this attribute or other_attr is configured. + listvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("subscription_allow_list"), + }...), + }, + }, + "credentials": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "pem_file": schema.StringAttribute{ + MarkdownDescription: "PEM file for Azure integration.", + Required: true, + Sensitive: true, + }, + }, + }, + }, + } +} + +func (r *integrationMsDefenderResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*ExtendedGqlClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *integrationMsDefenderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data integrationMsDefenderResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + space, err := r.client.ComputeSpace(data.SpaceID) + if err != nil { + resp.Diagnostics.AddError("Invalid Configuration", err.Error()) + return + } + ctx = tflog.SetField(ctx, "space_mrn", space.MRN()) + + // Do GraphQL request to API to create the resource. + tflog.Debug(ctx, "Creating integration") + integration, err := r.client.CreateIntegration(ctx, + space.MRN(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeMicrosoftDefender, + mondoov1.ClientIntegrationConfigurationInput{ + MicrosoftDefenderConfigurationOptions: data.GetConfigurationOptions(), + }) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to create MsDefender integration, got error: %s", err), + ) + return + } + + // trigger integration to gather results quickly after the first setup + // NOTE: we ignore the error since the integration state does not depend on it + _, err = r.client.TriggerAction(ctx, string(integration.Mrn), mondoov1.ActionTypeRunScan) + if err != nil { + resp.Diagnostics. + AddWarning("Client Error", + fmt.Sprintf("Unable to trigger integration, got error: %s", err), + ) + return + } + + // Save space mrn into the Terraform state. + data.Mrn = types.StringValue(string(integration.Mrn)) + data.Name = types.StringValue(string(integration.Name)) + data.SpaceID = types.StringValue(space.ID()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationMsDefenderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data integrationMsDefenderResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationMsDefenderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data integrationMsDefenderResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + opts := mondoov1.ClientIntegrationConfigurationInput{ + MicrosoftDefenderConfigurationOptions: data.GetConfigurationOptions(), + } + + _, err := r.client.UpdateIntegration(ctx, + data.Mrn.ValueString(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeMicrosoftDefender, + opts, + ) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to update MsDefender integration, got error: %s", err), + ) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationMsDefenderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data integrationMsDefenderResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + _, err := r.client.DeleteIntegration(ctx, data.Mrn.ValueString()) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to delete MsDefender integration, got error: %s", err), + ) + return + } +} + +func (r *integrationMsDefenderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + integration, ok := r.client.ImportIntegration(ctx, req, resp) + if !ok { + return + } + + allowList := ConvertListValue(integration.ConfigurationOptions.MicrosoftDefenderConfigurationOptionsInput.SubscriptionsAllowlist) + denyList := ConvertListValue(integration.ConfigurationOptions.MicrosoftDefenderConfigurationOptionsInput.SubscriptionsDenylist) + + model := integrationMsDefenderResourceModel{ + Mrn: types.StringValue(integration.Mrn), + Name: types.StringValue(integration.Name), + SpaceID: types.StringValue(integration.SpaceID()), + ClientId: types.StringValue(integration.ConfigurationOptions.MicrosoftDefenderConfigurationOptionsInput.ClientId), + TenantId: types.StringValue(integration.ConfigurationOptions.MicrosoftDefenderConfigurationOptionsInput.TenantId), + SubscriptionAllowList: allowList, + SubscriptionDenyList: denyList, + Credential: integrationMsDefenderCredentialModel{ + PEMFile: types.StringValue(integration.ConfigurationOptions.MicrosoftDefenderConfigurationOptionsInput.Certificate), + }, + } + + resp.State.Set(ctx, &model) +} diff --git a/internal/provider/integration_msdefender_resource_test.go b/internal/provider/integration_msdefender_resource_test.go new file mode 100644 index 0000000..e9a1b24 --- /dev/null +++ b/internal/provider/integration_msdefender_resource_test.go @@ -0,0 +1,93 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccMsDefenderIntegrationResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccMsDefenderIntegrationResourceConfig(accSpace.ID(), "one", "ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff", `["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "tenant_id", "ffffffff-ffff-ffff-ffff-ffffffffffff"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "client_id", "ffffffff-ffff-ffff-ffff-ffffffffffff"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "subscription_allow_list", `["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"]`), + ), + }, + { + Config: testAccMsDefenderIntegrationResourceWithSpaceInProviderConfig(accSpace.ID(), "two", "abcd1234567890", `["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "subscription_deny_list", `["ffffffff-ffff-ffff-ffff-ffffffffffff", "ffffffff-ffff-ffff-ffff-ffffffffffff"]`), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "credentials.pem_file", "abcd1234567890"), + ), + }, + // Update and Read testing + { + Config: testAccMsDefenderIntegrationResourceConfig(accSpace.ID(), "three", "ffffffff-ffff-ffff-ffff-ffffffffff", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", `["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "name", "three"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "tenant_id", "ffffffff-ffff-ffff-ffff-ffffffffff"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "client_id", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "subscription_allow_list", `["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]`), + ), + }, + { + Config: testAccMsDefenderIntegrationResourceWithSpaceInProviderConfig(accSpace.ID(), "four", "abcd1234567890", `["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "name", "four"), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "subscription_deny_list", `["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]`), + resource.TestCheckResourceAttr("mondoo_integration_msdefender.msdefender_integration", "credentials.pem_file", "abcd1234567890"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccMsDefenderIntegrationResourceConfig(spaceID, intName, tenantID, clientID, allowList string) string { + return fmt.Sprintf(` +resource "mondoo_integration_msdefender" "msdefender_integration" { + space_id = %[1]q + name = %[2]q + tenant_id = %[3]q + client_id = %[4]q + subscription_allow_list= %[5]s + credentials = { + pem_file = "abcd1234567890" + } +} +`, spaceID, intName, tenantID, clientID, allowList) +} + +func testAccMsDefenderIntegrationResourceWithSpaceInProviderConfig(spaceID, intName, pemFile, denyList string) string { + return fmt.Sprintf(` +provider "mondoo" { + space = %[1]q +} +resource "mondoo_integration_msdefender" "msdefender_integration" { + name = %[2]q + tenant_id = "ffffffff-ffff-ffff-ffff-ffffffffffff" + client_id = "ffffffff-ffff-ffff-ffff-ffffffffffff" + subscription_deny_list = %[3]s + credentials = { + pem_file = %[4]q + } +} +`, spaceID, intName, denyList, pemFile) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fec7263..1624c46 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -208,6 +208,7 @@ func (p *MondooProvider) Resources(ctx context.Context) []func() resource.Resour NewIntegrationJiraResource, NewIntegrationEmailResource, NewIntegrationGitlabResource, + NewIntegrationMsDefenderResource, } }