Skip to content

Commit

Permalink
[datadog_sensitive_data_scanner_rule] Proof of concept for test patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandre-pocquet committed Jun 10, 2024
1 parent c2b7677 commit d8d2019
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 0 deletions.
99 changes: 99 additions & 0 deletions datadog/resource_datadog_sensitive_data_scanner_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
}
70 changes: 70 additions & 0 deletions datadog/tests/resource_datadog_sensitive_data_scanner_rule_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package test

import (
"bytes"
"context"
"fmt"
"regexp"
"testing"
"text/template"

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"

Expand Down Expand Up @@ -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" {
Expand Down

0 comments on commit d8d2019

Please sign in to comment.