From e280a591b0e87937e505768fb25dbd0df9c63b19 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 3 May 2024 10:56:11 +0200 Subject: [PATCH] feat: added ms365 integration Signed-off-by: Paul --- .../mondoo_integration_ms365/main.tf | 20 ++ .../mondoo_integration_ms365/resource.tf | 183 ++++++++++++++ .../provider/integration_ms365_resource.go | 223 ++++++++++++++++++ internal/provider/provider.go | 1 + 4 files changed, 427 insertions(+) create mode 100644 examples/resources/mondoo_integration_ms365/main.tf create mode 100644 examples/resources/mondoo_integration_ms365/resource.tf create mode 100644 internal/provider/integration_ms365_resource.go diff --git a/examples/resources/mondoo_integration_ms365/main.tf b/examples/resources/mondoo_integration_ms365/main.tf new file mode 100644 index 0000000..9f8e236 --- /dev/null +++ b/examples/resources/mondoo_integration_ms365/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.4.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.5" + } + } +} diff --git a/examples/resources/mondoo_integration_ms365/resource.tf b/examples/resources/mondoo_integration_ms365/resource.tf new file mode 100644 index 0000000..021747e --- /dev/null +++ b/examples/resources/mondoo_integration_ms365/resource.tf @@ -0,0 +1,183 @@ +# Variables +# ---------------------------------------------- + +variable "tenant_id" { + description = "The Azure Active Directory Tenant ID" + type = string + default = "ffffffff-ffff-ffff-ffff-ffffffffffff" +} + +variable "mondoo_org" { + description = "The Mondoo Organization ID" + type = string + default = "your-org-1234567" +} + +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 = "Ms365 ${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 = "dc377aa6-52d8-4e23-b271-2a7ae04cedf3" + type = "Role" + } + + resource_access { + id = "9e640839-a198-48fb-8b9a-013fd6f6cbcd" + type = "Role" + } + + resource_access { + id = "37730810-e9ba-4e46-b07e-8ca78d182097" + type = "Role" + } + } + + required_resource_access { + resource_app_id = "00000003-0000-0ff1-ce00-000000000000" + + resource_access { + id = "678536fe-1083-478a-9c59-b99265e6b0d3" + type = "Role" + } + } + + required_resource_access { + resource_app_id = "00000002-0000-0ff1-ce00-000000000000" + + resource_access { + id = "dc50a0fb-09a3-484d-be87-e023b12c6440" + 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_subscriptions" "available" {} + +# add global 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" { + region = "eu" +} + +# Create a new space +resource "mondoo_space" "ms365_space" { + name = "Ms365 Terraform Integration" + org_id = var.mondoo_org +} + +# Setup the Azure integration +resource "mondoo_integration_ms365" "ms365_integration" { + space_id = mondoo_space.ms365_space.id + name = "Ms365 ${local.mondoo_security_integration_name}" + tenant_id = var.tenant_id + client_id = azuread_application.mondoo_security.client_id + 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 = [ + mondoo_space.ms365_space, + azuread_application.mondoo_security, + azuread_service_principal.mondoo_security, + azurerm_role_assignment.reader, + ] +} diff --git a/internal/provider/integration_ms365_resource.go b/internal/provider/integration_ms365_resource.go new file mode 100644 index 0000000..ace14db --- /dev/null +++ b/internal/provider/integration_ms365_resource.go @@ -0,0 +1,223 @@ +package provider + +import ( + "context" + "fmt" + + "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/types" + mondoov1 "go.mondoo.com/mondoo-go" +) + +var _ resource.Resource = (*integrationMs365Resource)(nil) + +func NewIntegrationMs365Resource() resource.Resource { + return &integrationMs365Resource{} +} + +type integrationMs365Resource struct { + client *ExtendedGqlClient +} + +type integrationMs365ResourceModel 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"` + + // credentials + Credential integrationMs365CredentialModel `tfsdk:"credentials"` +} + +type integrationMs365CredentialModel struct { + PEMFile types.String `tfsdk:"pem_file"` +} + +func (r *integrationMs365Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_integration_ms365" +} + +func (r *integrationMs365Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Continuously monitor your Microsoft 365 resources for misconfigurations and vulnerabilities. See [Mondoo documentation](https://mondoo.com/docs/platform/infra/saas/ms365/ms365-auto/) for more details.`, + Attributes: map[string]schema.Attribute{ + "space_id": schema.StringAttribute{ + MarkdownDescription: "Mondoo Space Identifier.", + Required: true, + }, + "mrn": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Integration identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the integration.", + Required: true, + }, + "client_id": schema.StringAttribute{ + MarkdownDescription: "Azure Client ID.", + Required: true, + }, + "tenant_id": schema.StringAttribute{ + MarkdownDescription: "Azure Tenant ID.", + Required: true, + }, + "credentials": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "pem_file": schema.StringAttribute{ + MarkdownDescription: "PEM file for Ms365 integration.", + Required: true, + Sensitive: true, + }, + }, + }, + }, + } +} + +func (r *integrationMs365Resource) 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.(*mondoov1.Client) + + 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 = &ExtendedGqlClient{client} +} + +func (r *integrationMs365Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data integrationMs365ResourceModel + + // 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 create the resource. + spaceMrn := "" + if data.SpaceId.ValueString() != "" { + spaceMrn = spacePrefix + data.SpaceId.ValueString() + } + + integration, err := r.client.CreateIntegration(ctx, + spaceMrn, + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeMs365, + mondoov1.ClientIntegrationConfigurationInput{ + Ms365ConfigurationOptions: &mondoov1.Ms365ConfigurationOptionsInput{ + TenantID: mondoov1.String(data.TenantId.ValueString()), + ClientID: mondoov1.String(data.ClientId.ValueString()), + Certificate: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PEMFile.ValueString())), + }, + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create MS365 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(data.SpaceId.ValueString()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationMs365Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data integrationMs365ResourceModel + + // 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 *integrationMs365Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data integrationMs365ResourceModel + + // 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{ + Ms365ConfigurationOptions: &mondoov1.Ms365ConfigurationOptionsInput{ + TenantID: mondoov1.String(data.TenantId.ValueString()), + ClientID: mondoov1.String(data.ClientId.ValueString()), + Certificate: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PEMFile.ValueString())), + }, + } + + _, err := r.client.UpdateIntegration(ctx, data.Mrn.ValueString(), data.Name.ValueString(), mondoov1.ClientIntegrationTypeMs365, opts) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update Ms365 integration, got error: %s", err)) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationMs365Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data integrationMs365ResourceModel + + // 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 Ms365 integration, got error: %s", err)) + return + } +} + +func (r *integrationMs365Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("mrn"), req, resp) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 08ad8da..c754020 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -167,6 +167,7 @@ func (p *MondooProvider) Resources(ctx context.Context) []func() resource.Resour NewScimGroupMappingResource, NewIntegrationDomainResource, NewIntegrationSlackResource, + NewIntegrationMs365Resource, } }