Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[datadog_sensitive_data_scanner_rule] Proof of concept for test patterns #2429

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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
}
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
Loading