diff --git a/sysdig/data_source_sysdig_secure_managed_policy_test.go b/sysdig/data_source_sysdig_secure_managed_policy_test.go index 17a64fc2..e5a85caa 100644 --- a/sysdig/data_source_sysdig_secure_managed_policy_test.go +++ b/sysdig/data_source_sysdig_secure_managed_policy_test.go @@ -4,6 +4,7 @@ package sysdig_test import ( "os" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -13,6 +14,18 @@ import ( ) func TestAccManagedPolicyDataSource(t *testing.T) { + steps := []resource.TestStep{ + { + Config: managedPolicyDataSource(), + }, + } + + if !strings.HasSuffix(os.Getenv("SYSDIG_SECURE_URL"), "ibm.com") { + steps = append(steps, resource.TestStep{ + Config: managedStatefulPolicyDataSource(), + }, + ) + } resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { if v := os.Getenv("SYSDIG_SECURE_API_TOKEN"); v == "" { @@ -24,11 +37,7 @@ func TestAccManagedPolicyDataSource(t *testing.T) { return sysdig.Provider(), nil }, }, - Steps: []resource.TestStep{ - { - Config: managedPolicyDataSource(), - }, - }, + Steps: steps, }) } @@ -40,3 +49,12 @@ data "sysdig_secure_managed_policy" "example" { } ` } + +func managedStatefulPolicyDataSource() string { + return ` +data "sysdig_secure_managed_policy" "stateful_example" { + name = "Sysdig AWS Behavioral Analytics Threat Detection" + type = "awscloudtrail_stateful" +} +` +} diff --git a/sysdig/internal/client/v2/model.go b/sysdig/internal/client/v2/model.go index 17047bbf..a7b6183c 100644 --- a/sysdig/internal/client/v2/model.go +++ b/sysdig/internal/client/v2/model.go @@ -520,12 +520,15 @@ type Rule struct { } const ( - RuleTypeContainer = "CONTAINER" - RuleTypeFalco = "FALCO" - RuleTypeFilesystem = "FILESYSTEM" - RuleTypeNetwork = "NETWORK" - RuleTypeProcess = "PROCESS" - RuleTypeSyscall = "SYSCALL" + RuleTypeContainer = "CONTAINER" + RuleTypeFalco = "FALCO" + RuleTypeFilesystem = "FILESYSTEM" + RuleTypeNetwork = "NETWORK" + RuleTypeProcess = "PROCESS" + RuleTypeSyscall = "SYSCALL" + RuleTypeStatefulSequence = "STATEFUL_SEQUENCE" + RuleTypeStatefulUniqPercent = "STATEFUL_UNIQ_PERCENT" + RuleTypeStatefulCount = "STATEFUL_COUNT" ) type Details struct { diff --git a/sysdig/internal/client/v2/rules.go b/sysdig/internal/client/v2/rules.go index b151fd5d..b14070de 100644 --- a/sysdig/internal/client/v2/rules.go +++ b/sysdig/internal/client/v2/rules.go @@ -8,11 +8,15 @@ import ( ) const ( - CreateRulePath = "%s/api/secure/rules?skipPolicyV2Msg=%t" - GetRuleByIDPath = "%s/api/secure/rules/%d" - UpdateRulePath = "%s/api/secure/rules/%d?skipPolicyV2Msg=%t" - DeleteURLPath = "%s/api/secure/rules/%d?skipPolicyV2Msg=%t" - GetRuleGroupPath = "%s/api/secure/rules/groups?name=%s&type=%s" + CreateRulePath = "%s/api/secure/rules?skipPolicyV2Msg=%t" + GetRuleByIDPath = "%s/api/secure/rules/%d" + UpdateRulePath = "%s/api/secure/rules/%d?skipPolicyV2Msg=%t" + DeleteURLPath = "%s/api/secure/rules/%d?skipPolicyV2Msg=%t" + GetRuleGroupPath = "%s/api/secure/rules/groups?name=%s&type=%s" + CreateStatefulRulePath = "%s/api/policies/v3/statefulRules" + UpdateStatefulRulePath = "%s/api/policies/v3/statefulRules/%d" + DeleteStatefulRulePath = "%s/api/policies/v3/statefulRules/%d" + GetStatefulRuleGroupPath = "%s/api/policies/v3/statefulRules/groups?name=%s&type=%s" ) type RuleInterface interface { @@ -22,6 +26,10 @@ type RuleInterface interface { UpdateRule(ctx context.Context, rule Rule) (Rule, error) DeleteRule(ctx context.Context, ruleID int) error GetRuleGroup(ctx context.Context, ruleName string, ruleType string) ([]Rule, error) + CreateStatefulRule(ctx context.Context, rule Rule) (Rule, error) + UpdateStatefulRule(ctx context.Context, rule Rule) (Rule, error) + DeleteStatefulRule(ctx context.Context, ruleID int) error + GetStatefulRuleGroup(ctx context.Context, ruleName string, ruleType string) ([]Rule, error) } func (client *Client) CreateRule(ctx context.Context, rule Rule) (Rule, error) { @@ -125,3 +133,85 @@ func (client *Client) DeleteRuleURL(ruleID int) string { func (client *Client) GetRuleGroupURL(ruleName string, ruleType string) string { return fmt.Sprintf(GetRuleGroupPath, client.config.url, url.QueryEscape(ruleName), url.QueryEscape(ruleType)) } + +func (client *Client) CreateStatefulRuleURL() string { + return fmt.Sprintf(CreateStatefulRulePath, client.config.url) +} + +func (client *Client) UpdateStatefulRuleURL(ruleID int) string { + return fmt.Sprintf(UpdateStatefulRulePath, client.config.url, ruleID) +} + +func (client *Client) DeleteStatefulRuleURL(ruleID int) string { + return fmt.Sprintf(DeleteStatefulRulePath, client.config.url, ruleID) +} + +func (client *Client) GetStatefulRuleGroupURL(ruleName string, ruleType string) string { + return fmt.Sprintf(GetStatefulRuleGroupPath, client.config.url, url.QueryEscape(ruleName), url.QueryEscape(ruleType)) +} + +func (client *Client) CreateStatefulRule(ctx context.Context, rule Rule) (Rule, error) { + payload, err := Marshal(rule) + if err != nil { + return Rule{}, err + } + response, err := client.requester.Request(ctx, http.MethodPost, client.CreateStatefulRuleURL(), payload) + if err != nil { + return Rule{}, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return Rule{}, client.ErrorFromResponse(response) + } + + return Unmarshal[Rule](response.Body) +} + +func (client *Client) UpdateStatefulRule(ctx context.Context, rule Rule) (Rule, error) { + payload, err := Marshal(rule) + if err != nil { + return Rule{}, err + } + + response, err := client.requester.Request(ctx, http.MethodPut, client.UpdateStatefulRuleURL(rule.ID), payload) + if err != nil { + return Rule{}, err + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return Rule{}, client.ErrorFromResponse(response) + } + + return Unmarshal[Rule](response.Body) +} + +func (client *Client) DeleteStatefulRule(ctx context.Context, ruleID int) error { + response, err := client.requester.Request(ctx, http.MethodDelete, client.DeleteStatefulRuleURL(ruleID), nil) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK { + return client.ErrorFromResponse(response) + } + + return err +} + +func (client *Client) GetStatefulRuleGroup(ctx context.Context, ruleName string, ruleType string) ([]Rule, error) { + response, err := client.requester.Request(ctx, http.MethodGet, client.GetStatefulRuleGroupURL(ruleName, ruleType), nil) + if err != nil { + return []Rule{}, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return []Rule{}, client.ErrorFromResponse(response) + } + + return Unmarshal[[]Rule](response.Body) +} diff --git a/sysdig/provider.go b/sysdig/provider.go index 3d86a76c..1562c240 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -148,6 +148,7 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_secure_rule_process": resourceSysdigSecureRuleProcess(), "sysdig_secure_rule_syscall": resourceSysdigSecureRuleSyscall(), "sysdig_secure_rule_falco": resourceSysdigSecureRuleFalco(), + "sysdig_secure_rule_stateful": resourceSysdigSecureStatefulRule(), "sysdig_secure_team": resourceSysdigSecureTeam(), "sysdig_secure_list": resourceSysdigSecureList(), "sysdig_secure_macro": resourceSysdigSecureMacro(), diff --git a/sysdig/resource_sysdig_secure_policy.go b/sysdig/resource_sysdig_secure_policy.go index fa5948f3..d7ff2874 100644 --- a/sysdig/resource_sysdig_secure_policy.go +++ b/sysdig/resource_sysdig_secure_policy.go @@ -33,6 +33,7 @@ var validatePolicyType = validation.StringInSlice([]string{ "aws_machine_learning", "machine_learning", "guardduty", + "awscloudtrail_stateful", }, false) func resourceSysdigSecurePolicy() *schema.Resource { diff --git a/sysdig/resource_sysdig_secure_rule_stateful.go b/sysdig/resource_sysdig_secure_rule_stateful.go new file mode 100644 index 00000000..522fd363 --- /dev/null +++ b/sysdig/resource_sysdig_secure_rule_stateful.go @@ -0,0 +1,288 @@ +package sysdig + +import ( + "context" + "encoding/json" + "errors" + "strconv" + "time" + + v2 "github.com/draios/terraform-provider-sysdig/sysdig/internal/client/v2" + + "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" + "github.com/spf13/cast" +) + +var validateStatefulRuleSource = validation.StringInSlice([]string{"awscloudtrail_stateful"}, false) + +var validateStatefulRuleType = validation.StringInSlice([]string{ + v2.RuleTypeStatefulSequence, + v2.RuleTypeStatefulCount, + v2.RuleTypeStatefulUniqPercent, +}, false) + +func resourceSysdigSecureStatefulRule() *schema.Resource { + timeout := 1 * time.Minute + + return &schema.Resource{ + CreateContext: resourceSysdigRuleStatefulCreate, + UpdateContext: resourceSysdigRuleStatefulUpdate, + ReadContext: resourceSysdigRuleStatefulRead, + DeleteContext: resourceSysdigRuleStatefulDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "version": { + Type: schema.TypeInt, + Computed: true, + }, + "source": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validateStatefulRuleSource), + }, + "ruletype": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validateStatefulRuleType), + }, + "append": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "exceptions": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} + +func resourceSysdigRuleStatefulCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sysdigClients := meta.(SysdigClients) + client, err := getSecureRuleClient(sysdigClients) + if err != nil { + return diag.FromErr(err) + } + + rule, err := resourceSysdigRuleStatefulFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + rule, err = client.CreateStatefulRule(ctx, rule) + if err != nil { + return diag.FromErr(err) + } + d.SetId(strconv.Itoa(rule.ID)) + _ = d.Set("version", rule.Version) + + return nil +} + +// Retrieves the information of a resource form the file and loads it in Terraform +func resourceSysdigRuleStatefulRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getSecureRuleClient(meta.(SysdigClients)) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + nameObj, ok := d.GetOk("name") + if !ok { + return diag.FromErr(errors.New("name is required")) + } + + name := nameObj.(string) + + sourceObj, ok := d.GetOk("source") + if !ok { + return diag.FromErr(errors.New("source is required")) + } + + source := sourceObj.(string) + + rules, err := client.GetStatefulRuleGroup(ctx, name, source) + if err != nil { + return diag.FromErr(err) + } + + if len(rules) == 0 { + d.SetId("") + } + + var rule v2.Rule + + for _, r := range rules { + if r.ID == id { + rule = r + break + } + } + + _ = d.Set("name", rule.Name) + _ = d.Set("source", rule.Details.Source) + + if rule.Details.Append != nil { + _ = d.Set("append", *rule.Details.Append) + } + + exceptions := make([]any, 0, len(rule.Details.Exceptions)) + for _, exception := range rule.Details.Exceptions { + if exception == nil { + return diag.Errorf("exception is nil") + } + valuesData, err := json.Marshal(exception.Values) + if err != nil { + return diag.Errorf("error marshalling exception values '%+v': %s", exception.Values, err) + } + + exceptions = append(exceptions, map[string]any{ + "name": exception.Name, + "values": string(valuesData), + }) + } + + if err := d.Set("exceptions", exceptions); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigRuleStatefulUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sysdigClients := meta.(SysdigClients) + client, err := getSecureRuleClient(sysdigClients) + if err != nil { + return diag.FromErr(err) + } + + rule, err := resourceSysdigRuleStatefulFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + + version, ok := d.Get("version").(int) + if !ok { + return diag.FromErr(errors.New("version is required")) + } + rule.Version = version + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + rule.ID = id + + _, err = client.UpdateStatefulRule(ctx, rule) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigRuleStatefulDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sysdigClients := meta.(SysdigClients) + client, err := getSecureRuleClient(sysdigClients) + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = client.DeleteStatefulRule(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceSysdigRuleStatefulFromResourceData(d *schema.ResourceData) (v2.Rule, error) { + rule := v2.Rule{ + Name: d.Get("name").(string), + } + + ruleType := d.Get("ruletype").(string) + rule.Details.RuleType = ruleType + + appendMode, appendModeIsSet := d.GetOk("append") + if appendModeIsSet { + ptr := appendMode.(bool) + rule.Details.Append = &ptr + } + + if source, ok := d.GetOk("source"); ok && source.(string) != "" { + rule.Details.Source = source.(string) + } else if !appendModeIsSet || !(appendMode.(bool)) { + return v2.Rule{}, errors.New("source must be set when append = false") + } + + if exceptionsField, ok := d.GetOk("exceptions"); ok { + StatefulExceptions := []*v2.Exception{} + for _, exception := range exceptionsField.([]interface{}) { + exceptionMap := exception.(map[string]interface{}) + newStatefulException := &v2.Exception{ + Name: exceptionMap["name"].(string), + } + + fields := cast.ToStringSlice(exceptionMap["fields"]) + if len(fields) >= 1 { + newStatefulException.Fields = fields + } + + comps := cast.ToStringSlice(exceptionMap["comps"]) + if len(comps) >= 1 { + newStatefulException.Comps = comps + } + + values := cast.ToString(exceptionMap["values"]) + err := json.Unmarshal([]byte(values), &newStatefulException.Values) + if err != nil { + return v2.Rule{}, err + } + + StatefulExceptions = append(StatefulExceptions, newStatefulException) + } + rule.Details.Exceptions = StatefulExceptions + } + + return rule, nil +} diff --git a/sysdig/resource_sysdig_secure_rule_stateful_test.go b/sysdig/resource_sysdig_secure_rule_stateful_test.go new file mode 100644 index 00000000..c8ba776a --- /dev/null +++ b/sysdig/resource_sysdig_secure_rule_stateful_test.go @@ -0,0 +1,57 @@ +//go:build tf_acc_sysdig_secure || tf_acc_policies || tf_acc_onprem_secure + +package sysdig_test + +import ( + "os" + "strings" + "testing" + + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestRuleStatefulAppends(t *testing.T) { + if strings.HasSuffix(os.Getenv("SYSDIG_SECURE_URL"), "ibm.com") { + t.Skip("Skipping stateful tests for IBM Cloud") + return + } + steps := []resource.TestStep{ + { + Config: ruleStatefulAppend(), + }, + } + runStatefulTest(steps, t) +} + +func ruleStatefulAppend() string { + return ` + resource "sysdig_secure_rule_stateful" "stateful_rule_append" { + name = "API Gateway Enumeration Detected" + source = "awscloudtrail_stateful" + ruletype = "STATEFUL_SEQUENCE" + append = true + exceptions { + values = jsonencode([["user_abc", ["12345"]]]) + name = "user_accountid" + } + }` +} + +func runStatefulTest(steps []resource.TestStep, t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + if v := os.Getenv("SYSDIG_SECURE_API_TOKEN"); v == "" { + t.Fatal("SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: steps, + }) + +} diff --git a/website/docs/d/secure_rule_stateful.md b/website/docs/d/secure_rule_stateful.md new file mode 100644 index 00000000..bb74ed85 --- /dev/null +++ b/website/docs/d/secure_rule_stateful.md @@ -0,0 +1,47 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_rule_stateful" +description: |- + Retrieves a Sysdig Secure Stateful Rule. +--- + +# Data Source: sysdig_secure_rule_stateful + +Retrieves the information of an existing Sysdig Secure Stateful Rule. + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +```terraform +data "sysdig_secure_rule_stateful" "example" { + name = "Access Key Enumeration Detected" + source = "awscloudtrail_stateful" + ruletype = "STATEFUL_SEQUENCE" +} +``` + +## Argument Reference + +* `name` - (Required) The name of the Secure rule to retrieve. +* `source` - (Required) The source of the Secure rule to retrieve. +* `ruletype` - (Required) The type of the Secure rule to retrieve. + +## Attributes Reference + +In addition to the argument above, the following attributes are exported: + +* `exceptions` - The exceptions key is a list of identifier plus list of tuples of filtercheck fields. See below for details. +* `append` - This indicates that the rule being created appends the condition to an existing Sysdig-provided rule + +### Exceptions + +Stateful rules support an optional exceptions property to rules. The exceptions key is a list of identifier plus list of tuples of filtercheck fields. + +Supported fields for exceptions: + +* `name` - The name of the existing exception definition. +* `values` - Contains tuples of values. Each item in the tuple should align 1-1 with the corresponding field + and comparison operator. + diff --git a/website/docs/r/secure_rule_stateful.md b/website/docs/r/secure_rule_stateful.md new file mode 100644 index 00000000..8373afe2 --- /dev/null +++ b/website/docs/r/secure_rule_stateful.md @@ -0,0 +1,53 @@ +--- +subcategory: "Sysdig Secure" +layout: "sysdig" +page_title: "Sysdig: sysdig_secure_rule_stateful" +description: |- + Creates a Sysdig Secure Stateful Rule Append. +--- + +# Resource: sysdig_secure_rule_stateful + +Creates a Sysdig Secure Stateful Rule Append. + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +```terraform +resource "sysdig_secure_rule_stateful" "stateful_rule" { + name = "API Gateway Enumeration Detected" + source = "awscloudtrail_stateful" + ruletype = "STATEFUL_SEQUENCE" + exceptions { + values = jsonencode([["user_abc", ["12345"]]]) + name = "user_accountid" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Stateful rule that the exception is being appended to. +* `source` - (Required) The source of the event. We currently support the "awscloudtrail_stateful" source. +* `exceptions` - (Required) The exceptions key is a list of identifier plus list of tuples of filtercheck fields. See below for details. +* `append` - (Optional) This indicates that the rule being created appends the condition to an existing Sysdig-provided. For stateful rules, the default value is true. +* `ruletype` - (Required) The type of Stateful rule being appended to. We currently support "STATEFUL_SEQUENCE", "STATEFUL_COUNT", and "STATEFUL_UNIQ_PERCENT". + +### Exceptions +Supported fields for exceptions: + +* `name` - (Required) The name of the exception. +* `values` - (Required) Contains tuples of values. Each item in the tuple should align 1-1 with the corresponding field + and comparison operator. Since the value can be a string, a list of strings or a list of a list of strings, the value + of this field must be supplied in JSON format. You can use the default `jsonencode` function to provide this value. + See the usage example on the top. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `version` - Current version of the resource in Sysdig Secure. +