diff --git a/datadog/resource_datadog_sensitive_data_scanner_rule.go b/datadog/resource_datadog_sensitive_data_scanner_rule.go index 740d78549..2a8c79ca2 100644 --- a/datadog/resource_datadog_sensitive_data_scanner_rule.go +++ b/datadog/resource_datadog_sensitive_data_scanner_rule.go @@ -2,8 +2,11 @@ package datadog import ( "context" + "encoding/json" + "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -22,6 +25,11 @@ func resourceDatadogSensitiveDataScannerRule() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, metadata interface{}) error { + keys := diff.UpdatedKeys() + println(keys) + return nil + }, SchemaFunc: func() map[string]*schema.Schema { return map[string]*schema.Schema{ @@ -69,6 +77,31 @@ func resourceDatadogSensitiveDataScannerRule() *schema.Resource { Optional: true, Description: "Not included if there is a relationship to a standard pattern.", }, + "pattern_test": { + Type: schema.TypeList, + Optional: true, + Description: "An test cases to validate the pattern.\n" + + "If it fails, the Terraform plan will fail as well.\n" + + "Note: this is a synthetic field and is not persisted in the remote rule configuration.", + RequiredWith: []string{"pattern"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "input": { + Type: schema.TypeString, + Required: true, + Description: "An arbitrary input string to run the pattern against.", + }, + "matches": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether the input string should match the pattern.", + }, + }, + }, + // DiffSuppressFunc: + StateFunc: func(val any) string { return "" }, // synthetic field, don't persist it + }, "tags": { Type: schema.TypeList, Optional: true, @@ -188,6 +221,10 @@ func resourceDatadogSensitiveDataScannerRuleCreate(ctx context.Context, d *schem apiInstances := providerConf.DatadogApiInstances auth := providerConf.Auth + if testDiag := runPatternTests(providerConf, d); testDiag.HasError() { + return testDiag + } + sensitiveDataScannerMutex.Lock() defer sensitiveDataScannerMutex.Unlock() @@ -331,6 +368,10 @@ func resourceDatadogSensitiveDataScannerRuleUpdate(ctx context.Context, d *schem apiInstances := providerConf.DatadogApiInstances auth := providerConf.Auth + if testDiag := runPatternTests(providerConf, d); testDiag.HasError() { + return testDiag + } + sensitiveDataScannerMutex.Lock() defer sensitiveDataScannerMutex.Unlock() @@ -449,3 +490,61 @@ func findSensitiveDataScannerRuleHelper(ruleId string, response datadogV2.Sensit return nil } + +func runPatternTests(conf *ProviderConfiguration, d *schema.ResourceData) diag.Diagnostics { + diags := diag.Diagnostics{} + pattern := d.Get("pattern").(string) + tests := d.Get("pattern_test").([]any) + for i, test := range tests { + test := test.(map[string]any) + input := test["input"].(string) + matches := test["matches"].(bool) + + errDetail := "" + if doMatch, err := checkPatternMatches(conf, input, pattern); err != nil { + errDetail = err.Error() + } else if doMatch != matches { + matchStr := "does not match" + if matches { + matchStr = "matches" + } + errDetail = fmt.Sprintf("The pattern_test input %q %s %q", input, matchStr, pattern) + } + + if errDetail != "" { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("pattern_test %d failure", i), + Detail: errDetail, + AttributePath: cty.GetAttrPath("pattern_test").IndexInt(i), + }) + } + } + return diags +} + +func checkPatternMatches(conf *ProviderConfiguration, input string, pattern string) (bool, error) { + // TODO: use stable API + payload, _, err := utils.SendRequest( + conf.Auth, + conf.DatadogApiInstances.HttpClient, + "GET", + "/api/ui/event-platform/sensitive-data-scanner/test-pattern", + map[string]string{"content": input, "regex": pattern}, + ) + if err != nil { + return false, fmt.Errorf("API error while checking pattern: %w", err) + } + result := struct { + Regex struct { + IsValid bool `json:"isValid"` + } `json:"regex"` + Content struct { + IsMatching bool `json:"isMatching"` + } `json:"content"` + }{} + if err = json.Unmarshal(payload, &result); err != nil { + return false, fmt.Errorf("parsing error while checking pattern: %w", err) + } + return result.Regex.IsValid && result.Content.IsMatching, nil +} diff --git a/datadog/tests/resource_datadog_sensitive_data_scanner_rule_test.go b/datadog/tests/resource_datadog_sensitive_data_scanner_rule_test.go index 8bfe73e62..ff08a33a2 100644 --- a/datadog/tests/resource_datadog_sensitive_data_scanner_rule_test.go +++ b/datadog/tests/resource_datadog_sensitive_data_scanner_rule_test.go @@ -1,9 +1,12 @@ package test import ( + "bytes" "context" "fmt" + "regexp" "testing" + "text/template" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" @@ -165,6 +168,73 @@ func TestAccSensitiveDataScannerRuleWithStandardPattern(t *testing.T) { }}) } +func TestAccSensitiveDataScannerRuleWithTests(t *testing.T) { + if isRecording() || isReplaying() { + t.Skip("This test doesn't support recording or replaying") + } + + ctx, accProviders := testAccProviders(context.Background(), t) + name := uniqueEntityName(ctx, t) + + cfg := func(ruleCfg string) string { + var output bytes.Buffer + _ = template.Must(template.New("config").Parse(` + resource datadog_sensitive_data_scanner_group {{ .Name }} { + name = "{{ .Name }}" + is_enabled = false + product_list = ["logs"] + filter { + query = "*" + } + } + resource datadog_sensitive_data_scanner_rule {{ .Name }} { + name = "{{ .Name }}" + group_id = datadog_sensitive_data_scanner_group.{{ .Name }}.id + {{ .RuleCfg }} + } + `)).Execute(&output, map[string]string{"Name": name, "RuleCfg": ruleCfg}) + return output.String() + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: accProviders, + Steps: []resource.TestStep{ + { + Config: cfg(` + pattern = "needle" + pattern_test { + input = "Find the needle in the haystack" + } + `), + }, + { + Config: cfg(` + pattern = "needle" + pattern_test { + input = "oops no pattern" + } + `), + ExpectError: regexp.MustCompile(`The pattern_test input "oops no pattern" does not match "needle"`), + }, + { + Config: cfg(` + pattern = "my_secret_token[=:]\w+" + pattern_test { + input = "my_secret_token=aaaaaaaaaaa" + } + pattern_test { + input = "my_secret_token:bbbbbbbbbb" + } + pattern_test { + input = "my_secret_token_hash=ccccccccc" + matches = false + } + `), + }, + }}) +} + func testAccCheckDatadogSensitiveDataScannerRule(name string) string { return fmt.Sprintf(` resource "datadog_sensitive_data_scanner_group" "sample_group" {