Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/exception.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ resource "mondoo_exception" "exception" {

- `action` (String) The action to perform. Default is `RISK_ACCEPTED`. Other valid values are `WORKAROUND`, `FALSE_POSITIVE`, `ENABLE`, `DISABLE`, `OUT_OF_SCOPE` and `SNOOZE`.
- `check_mrns` (List of String) List of check MRNs to set exceptions for. If set, `vulnerability_mrns` must not be set.
- `exception_id` (String) The ID of the exception
- `justification` (String) Description why the exception is required.
- `scope_mrn` (String) The MRN of the scope (either asset mrn or space mrn).
- `valid_until` (String) The date when the exception is no longer valid.
Expand Down
151 changes: 128 additions & 23 deletions internal/provider/exception_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package provider
import (
"context"
"fmt"
"maps"
"regexp"
"slices"
"sort"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
Expand Down Expand Up @@ -47,6 +50,7 @@ type exceptionResourceModel struct {
Action types.String `tfsdk:"action"`
CheckMrns types.List `tfsdk:"check_mrns"`
VulnerabilityMrns types.List `tfsdk:"vulnerability_mrns"`
ExceptionId types.String `tfsdk:"exception_id"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens to existing state files where this doesnt exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. good question. ill check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, i tested and that then would fail to delete the exception on update.
im implementing a change that will searchf or the exception if we dont have the exception id. if we search for it and find it, great.
if not, then my thought was we return an error to the user and then allow them to import the exception resource

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. i tested this all out.

  • if a user has an exception in their state file and there are no changes between the rest of the data in the exception and the state file, the tf wont complain because the exception id is optional
  • if a user has an exception in their state file and there are changes between what exists and the state file, then the tf will trigger an update, which will now search for the exception, if found delete and recreate, if not found return error and instruct to import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

working on import resource now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import resource should be working now as well

}

func (r *exceptionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
Expand Down Expand Up @@ -221,6 +225,14 @@ func (r *exceptionResource) Schema(ctx context.Context, req resource.SchemaReque
listvalidator.ExactlyOneOf(path.MatchRoot("check_mrns"), path.MatchRoot("vulnerability_mrns")),
},
},
"exception_id": schema.StringAttribute{
MarkdownDescription: "The ID of the exception",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Expand Down Expand Up @@ -268,22 +280,14 @@ func (r *exceptionResource) Create(ctx context.Context, req resource.CreateReque
return
}

// disable existing exceptions
tflog.Debug(ctx, fmt.Sprintf("Creating exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.ApplyException(ctx, scopeMrn, mondoov1.ExceptionMutationActionEnable, checks, []string{}, []string{}, vulnerabilities, (*string)(mondoov1.NewStringPtr("")), (*string)(mondoov1.NewStringPtr("")), (*bool)(mondoov1.NewBooleanPtr(false)))
if err != nil {
resp.Diagnostics.AddError("Failed to disable existing exception", err.Error())
return
}

// Create API call logic
tflog.Debug(ctx, fmt.Sprintf("Creating exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.ApplyException(ctx, scopeMrn, mondoov1.ExceptionMutationAction(data.Action.ValueString()), checks, []string{}, []string{}, vulnerabilities, data.Justification.ValueStringPointer(), &validUntilStr, (*bool)(mondoov1.NewBooleanPtr(false)))
id, err := r.client.CreateException(ctx, scopeMrn, mondoov1.ExceptionMutationAction(data.Action.ValueString()), checks, []string{}, []string{}, vulnerabilities, data.Justification.ValueStringPointer(), &validUntilStr, (*bool)(mondoov1.NewBooleanPtr(false)))
if err != nil {
resp.Diagnostics.AddError("Failed to create exception", err.Error())
return
}

data.ExceptionId = types.StringValue(id)
data.ScopeMrn = types.StringValue(scopeMrn)

// Save data into Terraform state
Expand Down Expand Up @@ -321,12 +325,30 @@ func (r *exceptionResource) Update(ctx context.Context, req resource.UpdateReque
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
}
exceptionId := data.ExceptionId.ValueString()
if data.ExceptionId.IsNull() || data.ExceptionId.ValueString() == "" {
tflog.Debug(ctx, "No exception ID found in state, searching for existing exception")
// list the exceptions using data from the state
finding, findingType := getFindingType(data)
res, err := r.client.FindException(ctx, data.ScopeMrn.ValueString(), finding, findingType)
if err != nil {
// warn the user that the exception was not found. instruct them to import the exception
resp.Diagnostics.AddError("Failed to find existing exception. Please import the exception.", err.Error())
return
}
fmt.Printf("Found exception ID: %s\n", res.ExceptionID)
// if we find an exception, set the exception id on the data
data.ExceptionId = types.StringValue(res.ExceptionID)
exceptionId = res.ExceptionID
}

tflog.Debug(ctx, fmt.Sprintf("Deleting exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.ApplyException(ctx, data.ScopeMrn.ValueString(), mondoov1.ExceptionMutationActionEnable, checks, []string{}, []string{}, vulnerabilities, (*string)(mondoov1.NewStringPtr("")), (*string)(mondoov1.NewStringPtr("")), (*bool)(mondoov1.NewBooleanPtr(false)))
if err != nil {
resp.Diagnostics.AddError("Failed to disable existing exception", err.Error())
return
if exceptionId != "" {
tflog.Debug(ctx, fmt.Sprintf("Deleting exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.DeleteExceptions(ctx, []string{exceptionId}, data.ScopeMrn.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to disable existing exception", err.Error())
return
}
}

if data.Action.ValueString() == "SNOOZE" {
Expand All @@ -338,7 +360,7 @@ func (r *exceptionResource) Update(ctx context.Context, req resource.UpdateReque

// Update API call logic
tflog.Debug(ctx, fmt.Sprintf("Creating exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.ApplyException(ctx, data.ScopeMrn.ValueString(), mondoov1.ExceptionMutationAction(data.Action.ValueString()), checks, []string{}, []string{}, vulnerabilities, data.Justification.ValueStringPointer(), &validUntilStr, (*bool)(mondoov1.NewBooleanPtr(false)))
_, err = r.client.CreateException(ctx, data.ScopeMrn.ValueString(), mondoov1.ExceptionMutationAction(data.Action.ValueString()), checks, []string{}, []string{}, vulnerabilities, data.Justification.ValueStringPointer(), &validUntilStr, (*bool)(mondoov1.NewBooleanPtr(false)))
if err != nil {
resp.Diagnostics.AddError("Failed to update exception", err.Error())
return
Expand All @@ -357,18 +379,101 @@ func (r *exceptionResource) Delete(ctx context.Context, req resource.DeleteReque
if resp.Diagnostics.HasError() {
return
}

_, checks, vulnerabilities, _, err := r.GetConfigurationOptions(ctx, &data)
if err != nil {
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
exceptionId := data.ExceptionId.ValueString()
if data.ExceptionId.IsNull() || data.ExceptionId.ValueString() == "" {
tflog.Debug(ctx, "No exception ID found in state, searching for existing exception")
// list the exceptions using data from the state
finding, findingType := getFindingType(data)
res, err := r.client.FindException(ctx, data.ScopeMrn.ValueString(), finding, findingType)
if err != nil {
resp.Diagnostics.AddError("Failed to find existing exception. Please import the exception.", err.Error())
return
}
tflog.Debug(ctx, fmt.Sprintf("Found exception ID: %s", res.ExceptionID))
// if we find an exception, set the exception id on the data
data.ExceptionId = types.StringValue(res.ExceptionID)
exceptionId = res.ExceptionID
}

// Delete API call logic
tflog.Debug(ctx, fmt.Sprintf("Deleting exception for scope %s", data.ScopeMrn.ValueString()))
err = r.client.ApplyException(ctx, data.ScopeMrn.ValueString(), mondoov1.ExceptionMutationActionEnable, checks, []string{}, []string{}, vulnerabilities, (*string)(mondoov1.NewStringPtr("")), (*string)(mondoov1.NewStringPtr("")), (*bool)(mondoov1.NewBooleanPtr(false)))
tflog.Debug(ctx, fmt.Sprintf("Deleting exception %s for scope %s", exceptionId, data.ScopeMrn.ValueString()))
err := r.client.DeleteExceptions(ctx, []string{exceptionId}, data.ScopeMrn.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to delete exception", err.Error())
return
}
}

func getFindingType(data exceptionResourceModel) (string, mondoov1.ExceptionType) {
if len(data.CheckMrns.Elements()) > 0 {
var checks []string
data.CheckMrns.ElementsAs(context.Background(), &checks, false)
if len(checks) > 0 {
return checks[0], mondoov1.ExceptionTypeSecurity
}
}
if len(data.VulnerabilityMrns.Elements()) > 0 {
var vulnerabilities []string
data.VulnerabilityMrns.ElementsAs(context.Background(), &vulnerabilities, false)
if len(vulnerabilities) > 0 {
return vulnerabilities[0], mondoov1.ExceptionTypeCve
}
}
return "", ""
}
func (r *exceptionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
var data exceptionResourceModel

// Read the import ID into the model
data.ExceptionId = types.StringValue(req.ID)

exception, ok := r.client.ImportException(ctx, req, resp, r.client.space.MRN())
if !ok {
resp.Diagnostics.AddError("Failed to import exception", "Please check the import ID and try again.")
return
}
// set the state with the imported exception data
data.ScopeMrn = types.StringValue(exception.ScopeMrn)
if exception.ValidUntil != nil {
t, _ := time.Parse(time.RFC3339, *exception.ValidUntil)
st := t.UTC().Format(time.DateOnly) // Ensure the date is parsed correctly
data.ValidUntil = types.StringValue(st)
}
if exception.Justification != nil {
data.Justification = types.StringValue(*exception.Justification)
}
data.Action = types.StringValue(exception.Action)
checkMrns := make(map[string]bool)
vulnMrns := make(map[string]bool)
advisoryMrns := make(map[string]bool)
if len(exception.Exceptions) > 0 {
for _, mrn := range exception.Exceptions {
// @vj: i dont understand why the items are being marshalled into both structs,
// but they seem to be, so im filtering by the mrn prefix to ensure we dont double up
if strings.HasPrefix(mrn.CheckMrns.Mrn, "//policy.api.mondoo.app/queries") {
checkMrns[mrn.CheckMrns.Mrn] = true
} else if strings.HasPrefix(mrn.VulnerabilityMrns.Mrn, "//vadvisor.api.mondoo.app/cves") {
vulnMrns[mrn.VulnerabilityMrns.Mrn] = true
} else if strings.HasPrefix(mrn.AdvisoryMrns.Mrn, "//vadvisor.api.mondoo.app/advisories") {
advisoryMrns[mrn.AdvisoryMrns.Mrn] = true
}
}
}
if len(checkMrns) > 0 {
l := slices.Collect(maps.Keys((checkMrns)))
sort.Strings(l)
data.CheckMrns = ConvertListValue(l)
} else {
data.CheckMrns = ConvertListValue([]string{})
}
if len(vulnMrns) > 0 {
l := slices.Collect(maps.Keys((vulnMrns)))
sort.Strings(l)
data.VulnerabilityMrns = ConvertListValue(l)
} else {
data.VulnerabilityMrns = ConvertListValue([]string{})
}

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
98 changes: 98 additions & 0 deletions internal/provider/exception_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package provider

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestExceptionResource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testCreateException(accSpace.ID(), accSpace.MRN(), "RISK_ACCEPTED"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mondoo_exception.windows_defender_exception", "action", "RISK_ACCEPTED"),
),
},
// Update testing
{
Config: testCreateException(accSpace.ID(), accSpace.MRN(), "FALSE_POSITIVE"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mondoo_exception.windows_defender_exception", "action", "FALSE_POSITIVE"),
),
},
// // import testing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is, but i want to try to get it working in the future. we should have the imports tested. this one is being real tricky on me so i was hoping to address the test itself in a followup

// {
// Config: importException(accSpace.ID()),
// ResourceName: "mondoo_exception.windows_defender_exception",
// ImportStateIdFunc: func(s *terraform.State) (string, error) {
// return s.RootModule().Resources["mondoo_exception.windows_defender_exception"].Primary.Attributes["exception_id"], nil
// },

// ImportStateVerifyIdentifierAttribute: "exception_id",
// ImportState: true,
// ImportStateVerify: true,
// },
},
})
}

// func importException(spaceId string) string {
// return fmt.Sprintf(`
// provider "mondoo" {
// space = "%s"
// }
// resource "mondoo_exception" "windows_defender_exception" {
// }
// `, spaceId)
// }

func testCreateException(spaceId string, spaceMrn string, action string) string {
return fmt.Sprintf(`
resource "mondoo_policy_assignment" "cis_policy_assignment_enabled" {
space_id = "%s"
policies = [
"//policy.api.mondoo.app/policies/cis-microsoft-windows-10-l1-ce",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-10-l1-bl",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-11-l1-ce",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-11-l1-bl",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2016-dc-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2016-ms-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2019-dc-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2019-ms-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2022-dc-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-windows-server-2022-ms-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-azure-windows-server-2019-dc-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-azure-windows-server-2019-ms-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-azure-windows-server-2022-dc-level-1",
"//policy.api.mondoo.app/policies/cis-microsoft-azure-windows-server-2022-ms-level-1",
"//policy.api.mondoo.app/policies/mondoo-edr-policy",
]
state = "enabled"
}
# Set exceptions for Windows policies in the space
resource "mondoo_exception" "windows_defender_exception" {
justification = "Windows Defender is disabled. Other EDR is used/configured instead."
scope_mrn = "%s"
action = "%s"
valid_until = "2025-09-09"
check_mrns = [
"//policy.api.mondoo.app/queries/cis-microsoft-windows-10--18.10.42.5.1",
"//policy.api.mondoo.app/queries/cis-microsoft-windows-11--18.10.42.5.1",
"//policy.api.mondoo.app/queries/cis-microsoft-windows-server-2016--18.10.42.5.1",
"//policy.api.mondoo.app/queries/cis-microsoft-windows-server-2019--18.10.42.5.1",
"//policy.api.mondoo.app/queries/cis-microsoft-windows-server-2022--18.10.42.5.1",
]
depends_on = [
mondoo_policy_assignment.cis_policy_assignment_enabled
]
}
`, spaceId, spaceMrn, action)
}
Loading