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

Add Leaked Credential Check Rules resource #4676

Merged
merged 11 commits into from
Dec 5, 2024
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
3 changes: 3 additions & 0 deletions .changelog/4676.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_leaked_credential_check_rule
```
47 changes: 47 additions & 0 deletions docs/resources/leaked_credential_check_rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "cloudflare_leaked_credential_check_rule Resource - Cloudflare"
subcategory: ""
description: |-
Provides a Cloudflare Leaked Credential Check Rule resource for managing user-defined Leaked Credential detection patterns within a specific zone.
---

# cloudflare_leaked_credential_check_rule (Resource)

Provides a Cloudflare Leaked Credential Check Rule resource for managing user-defined Leaked Credential detection patterns within a specific zone.

## Example Usage

```terraform
# Enable the Leaked Credentials Check detection before trying
# to add detections.
resource "cloudflare_leaked_credential_check" "example" {
zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b"
enabled = true
}

resource "cloudflare_leaked_credential_check_rule" "example" {
zone_id = cloudflare_leaked_credential_check.example.zone_id
username = "lookup_json_string(http.request.body.raw, \"user\")"
password = "lookup_json_string(http.request.body.raw, \"pass\")"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `password` (String) The ruleset expression to use in matching the password in a request
- `username` (String) The ruleset expression to use in matching the username in a request.
- `zone_id` (String) The zone identifier to target for the resource.

### Read-Only

- `id` (String) The identifier of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import cloudflare_leaked_credential_check_rule.example <zone_id>/<resource_id>
```
2 changes: 1 addition & 1 deletion docs/resources/notification_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Read-Only:
Optional:

- `actions` (Set of String) Targeted actions for alert.
- `affected_components` (Set of String) Affected components for alert. Available values: `API`, `API Shield`, `Access`, `Always Online`, `Analytics`, `Apps Marketplace`, `Argo Smart Routing`, `Audit Logs`, `Authoritative DNS`, `Billing`, `Bot Management`, `Bring Your Own IP (BYOIP)`, `Browser Isolation`, `CDN Cache Purge`, `CDN/Cache`, `Cache Reserve`, `Challenge Platform`, `Cloud Access Security Broker (CASB)`, `Community Site`, `DNS Root Servers`, `DNS Updates`, `Dashboard`, `Data Loss Prevention (DLP)`, `Developer's Site`, `Digital Experience Monitoring (DEX)`, `Distributed Web Gateway`, `Durable Objects`, `Email Routing`, `Ethereum Gateway`, `Firewall`, `Gateway`, `Geo-Key Manager`, `Image Resizing`, `Images`, `Infrastructure`, `Lists`, `Load Balancing and Monitoring`, `Logs`, `Magic Firewall`, `Magic Transit`, `Magic WAN`, `Magic WAN Connector`, `Marketing Site`, `Mirage`, `Network`, `Notifications`, `Observatory`, `Page Shield`, `Pages`, `R2`, `Radar`, `Randomness Beacon`, `Recursive DNS`, `Registrar`, `Registration Data Access Protocol (RDAP)`, `SSL Certificate Provisioning`, `SSL for SaaS Provisioning`, `Security Center`, `Snippets`, `Spectrum`, `Speed Optimizations`, `Stream`, `Support Site`, `Time Services`, `Trace`, `Tunnel`, `Turnstile`, `WARP`, `Waiting Room`, `Web Analytics`, `Workers`, `Workers KV`, `Workers Preview`, `Zaraz`, `Zero Trust`, `Zero Trust Dashboard`, `Zone Versioning`.
- `affected_components` (Set of String) Affected components for alert. Available values: `API`, `API Shield`, `Access`, `Always Online`, `Analytics`, `Apps Marketplace`, `Argo Smart Routing`, `Audit Logs`, `Authoritative DNS`, `Billing`, `Bot Management`, `Bring Your Own IP (BYOIP)`, `Browser Isolation`, `CDN Cache Purge`, `CDN/Cache`, `Cache Reserve`, `Challenge Platform`, `Cloud Access Security Broker (CASB)`, `Community Site`, `D1`, `DNS Root Servers`, `DNS Updates`, `Dashboard`, `Data Loss Prevention (DLP)`, `Developer's Site`, `Digital Experience Monitoring (DEX)`, `Distributed Web Gateway`, `Durable Objects`, `Email Routing`, `Ethereum Gateway`, `Firewall`, `Gateway`, `Geo-Key Manager`, `Image Resizing`, `Images`, `Infrastructure`, `Lists`, `Load Balancing and Monitoring`, `Logs`, `Magic Firewall`, `Magic Transit`, `Magic WAN`, `Magic WAN Connector`, `Marketing Site`, `Mirage`, `Network`, `Notifications`, `Observatory`, `Page Shield`, `Pages`, `R2`, `Radar`, `Randomness Beacon`, `Recursive DNS`, `Registrar`, `Registration Data Access Protocol (RDAP)`, `SSL Certificate Provisioning`, `SSL for SaaS Provisioning`, `Security Center`, `Snippets`, `Spectrum`, `Speed Optimizations`, `Stream`, `Support Site`, `Time Services`, `Trace`, `Tunnel`, `Turnstile`, `WARP`, `Waiting Room`, `Web Analytics`, `Workers`, `Workers KV`, `Workers Preview`, `Zaraz`, `Zero Trust`, `Zero Trust Dashboard`, `Zone Versioning`.
- `airport_code` (Set of String) Filter on Points of Presence.
- `alert_trigger_preferences` (Set of String) Alert trigger preferences. Example: `slo`.
- `enabled` (Set of String) State of the pool to alert on.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import cloudflare_leaked_credential_check_rule.example <zone_id>/<resource_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Enable the Leaked Credentials Check detection before trying
# to add detections.
resource "cloudflare_leaked_credential_check" "example" {
zone_id = "399c6f4950c01a5a141b99ff7fbcbd8b"
enabled = true
}

resource "cloudflare_leaked_credential_check_rule" "example" {
zone_id = cloudflare_leaked_credential_check.example.zone_id
username = "lookup_json_string(http.request.body.raw, \"user\")"
password = "lookup_json_string(http.request.body.raw, \"pass\")"
}
2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/hyperdrive_config"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/infrastructure_access_target_deprecated"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/leaked_credential_check_rule"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/list_item"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/origin_ca_certificate"
Expand Down Expand Up @@ -389,6 +390,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re
infrastructure_access_target_deprecated.NewResource,
zero_trust_infrastructure_access_target.NewResource,
leaked_credential_check.NewResource,
leaked_credential_check_rule.NewResource,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package leaked_credential_check_rule

import "github.com/hashicorp/terraform-plugin-framework/types"

type LeakedCredentialCheckRulesModel struct {
ZoneID types.String `tfsdk:"zone_id"`
ID types.String `tfsdk:"id"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
}
161 changes: 161 additions & 0 deletions internal/framework/service/leaked_credential_check_rule/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package leaked_credential_check_rule

import (
"context"
"fmt"
"strings"

"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var (
_ resource.Resource = &LeakedCredentialCheckRuleResource{}
_ resource.ResourceWithImportState = &LeakedCredentialCheckRuleResource{}
)

func NewResource() resource.Resource {
return &LeakedCredentialCheckRuleResource{}
}

type LeakedCredentialCheckRuleResource struct {
client *muxclient.Client
}

func (r *LeakedCredentialCheckRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_leaked_credential_check_rule"
}

func (r *LeakedCredentialCheckRuleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*muxclient.Client)

if !ok {
resp.Diagnostics.AddError(
"unexpected resource configure type",
fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *LeakedCredentialCheckRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data LeakedCredentialCheckRulesModel
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

detection, err := r.client.V1.LeakedCredentialCheckCreateDetection(ctx, cloudflare.ZoneIdentifier(data.ZoneID.ValueString()), cloudflare.LeakedCredentialCheckCreateDetectionParams{
Username: data.Username.ValueString(),
Password: data.Password.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Error creating a user-defined detection patter for Leaked Credential Check", err.Error())
return
}

data.ID = types.StringValue(detection.ID)

diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}

func (r *LeakedCredentialCheckRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state LeakedCredentialCheckRulesModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

zoneID := state.ZoneID.ValueString()
var foundRule cloudflare.LeakedCredentialCheckDetectionEntry
rules, err := r.client.V1.LeakedCredentialCheckListDetections(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.LeakedCredentialCheckListDetectionsParams{})
if err != nil {
resp.Diagnostics.AddError("Error listing Leaked Credential Check user-defined detection patterns", err.Error())
return
}

// leaked credentials doens't offer a single get operation so
// loop until we find the matching ID.
for _, rule := range rules {
if rule.ID == state.ID.ValueString() {
foundRule = rule
break
}
}

state.Password = types.StringValue(foundRule.Password)
state.Username = types.StringValue(foundRule.Username)
state.ID = types.StringValue(foundRule.ID)
state.ZoneID = types.StringValue(zoneID)

diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}

func (r *LeakedCredentialCheckRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data LeakedCredentialCheckRulesModel
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
zoneID := cloudflare.ZoneIdentifier(data.ZoneID.ValueString())
_, err := r.client.V1.LeakedCredentialCheckUpdateDetection(ctx, zoneID, cloudflare.LeakedCredentialCheckUpdateDetectionParams{
LeakedCredentialCheckDetectionEntry: cloudflare.LeakedCredentialCheckDetectionEntry{
ID: data.ID.ValueString(),
Username: data.Username.ValueString(),
Password: data.Password.ValueString(),
},
})
if err != nil {
resp.Diagnostics.AddError("Error fetching Leaked Credential Check user-defined detection patterns", err.Error())
return
}

diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}

func (r *LeakedCredentialCheckRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state LeakedCredentialCheckRulesModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
zoneID := cloudflare.ZoneIdentifier(state.ZoneID.ValueString())
deleteParam := cloudflare.LeakedCredentialCheckDeleteDetectionParams{DetectionID: state.ID.ValueString()}
_, err := r.client.V1.LeakedCredentialCheckDeleteDetection(ctx, zoneID, deleteParam)
if err != nil {
resp.Diagnostics.AddError("Error deleting a user-defined detection patter for Leaked Credential Check", err.Error())
return
}
}

func (r *LeakedCredentialCheckRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idparts := strings.Split(req.ID, "/")
if len(idparts) != 2 {
resp.Diagnostics.AddError("error importing leaked credential detection", "invalid ID specified. Please specify the ID as \"zone_id/resource_id\"")
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("zone_id"), idparts[0],
)...)
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("id"), idparts[1],
)...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package leaked_credential_check_rule_test

import (
"context"
"errors"
"fmt"
"os"
"testing"

cfv1 "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"

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

func init() {
resource.AddTestSweepers("cloudflare_leaked_credential_check_rule", &resource.Sweeper{
Name: "cloudflare_leaked_credential_check_rule",
F: testSweepCloudflareLCCRules,
})
}

func testSweepCloudflareLCCRules(r string) error {
ctx := context.Background()
client, clientErr := acctest.SharedV1Client()
if clientErr != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr))
}

zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
if zoneID == "" {
return errors.New("CLOUDFLARE_ZONE_ID must be set")
}
// fetch existing rules from API
rules, err := client.LeakedCredentialCheckListDetections(ctx, cfv1.ZoneIdentifier(zoneID), cfv1.LeakedCredentialCheckListDetectionsParams{})
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Error fetching Leaked Credential Check user-defined detection patterns: %s", err))
return err
}
for _, rule := range rules {
deleteParam := cfv1.LeakedCredentialCheckDeleteDetectionParams{DetectionID: rule.ID}
_, err := client.LeakedCredentialCheckDeleteDetection(ctx, cfv1.ZoneIdentifier(zoneID), deleteParam)
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Error deleting a user-defined detection patter for Leaked Credential Check: %s", err))
}
}

return nil
}

func TestAccCloudflareLeakedCredentialCheckRule_Basic(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
name := fmt.Sprintf("cloudflare_leaked_credential_check_rule.%s", rnd)
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() {
acctest.TestAccPreCheck(t)
},
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccConfigAddHeader(rnd, zoneID, testAccLCCTwoSimpleRules(rnd)),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name+"_first", "zone_id", zoneID),
resource.TestCheckResourceAttr(name+"_first", "username", "lookup_json_string(http.request.body.raw, \"user\")"),
resource.TestCheckResourceAttr(name+"_first", "password", "lookup_json_string(http.request.body.raw, \"pass\")"),

resource.TestCheckResourceAttr(name+"_second", "zone_id", zoneID),
resource.TestCheckResourceAttr(name+"_second", "username", "lookup_json_string(http.request.body.raw, \"id\")"),
resource.TestCheckResourceAttr(name+"_second", "password", "lookup_json_string(http.request.body.raw, \"secret\")"),
),
},
},
})
}

func testAccConfigAddHeader(name, zoneID, config string) string {
header := fmt.Sprintf(`
resource "cloudflare_leaked_credential_check" "%[1]s" {
zone_id = "%[2]s"
enabled = true
}`, name, zoneID)
return header + "\n" + config
}

func testAccLCCTwoSimpleRules(name string) string {
return fmt.Sprintf(`
resource "cloudflare_leaked_credential_check_rule" "%[1]s_first" {
zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id
username = "lookup_json_string(http.request.body.raw, \"user\")"
password = "lookup_json_string(http.request.body.raw, \"pass\")"
}

resource "cloudflare_leaked_credential_check_rule" "%[1]s_second" {
zone_id = cloudflare_leaked_credential_check.%[1]s.zone_id
username = "lookup_json_string(http.request.body.raw, \"id\")"
password = "lookup_json_string(http.request.body.raw, \"secret\")"
}`, name)
}
Loading
Loading