Skip to content

Commit d10f07f

Browse files
tehbooomtobio
andauthored
Install prebuilt rules (#1296)
* feat: Add prebuilt_rules resource * fix: ensure minimum versions is satisfied * fix diags * fix versions * fix verions * skip versions not 8.x+ * fix docs with new generation * update changelog * fix: Only install prebuilt rules * fix resource name in test * Don't update during a read operation * make lint * Fix tests --------- Co-authored-by: Toby Brain <toby.brain@elastic.co>
1 parent 525d7c2 commit d10f07f

File tree

16 files changed

+561
-0
lines changed

16 files changed

+561
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ alias = [
4040

4141
### Changes
4242

43+
- Create `elasticstack_kibana_prebuilt_rule` resource ([#1296](https://github.com/elastic/terraform-provider-elasticstack/pull/1296))
4344
- Add `required_versions` to `elasticstack_fleet_agent_policy` ([#1436](https://github.com/elastic/terraform-provider-elasticstack/pull/1436))
4445
- Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330))
4546
- Fix an issue where the `elasticstack_fleet_output` resource would error due to inconsistent state after an ouptut was edited in the Kibana UI ([#1506](https://github.com/elastic/terraform-provider-elasticstack/pull/1506))
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "elasticstack_kibana_install_prebuilt_rules Resource - terraform-provider-elasticstack"
4+
subcategory: "Kibana"
5+
description: |-
6+
Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html
7+
---
8+
9+
# elasticstack_kibana_install_prebuilt_rules (Resource)
10+
11+
Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html
12+
13+
## Example Usage
14+
15+
```terraform
16+
provider "elasticstack" {
17+
kibana {}
18+
}
19+
20+
21+
resource "elasticstack_kibana_install_prebuilt_rules" "example" {
22+
space_id = "default"
23+
}
24+
```
25+
26+
<!-- schema generated by tfplugindocs -->
27+
## Schema
28+
29+
### Optional
30+
31+
- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used.
32+
33+
### Read-Only
34+
35+
- `id` (String) The ID of this resource.
36+
- `rules_installed` (Number) Number of prebuilt rules that are installed.
37+
- `rules_not_installed` (Number) Number of prebuilt rules that are not installed.
38+
- `rules_not_updated` (Number) Number of prebuilt rules that have updates available.
39+
- `timelines_installed` (Number) Number of prebuilt timelines that are installed.
40+
- `timelines_not_installed` (Number) Number of prebuilt timelines that are not installed.
41+
- `timelines_not_updated` (Number) Number of prebuilt timelines that have updates available.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
provider "elasticstack" {
2+
kibana {}
3+
}
4+
5+
6+
resource "elasticstack_kibana_install_prebuilt_rules" "example" {
7+
space_id = "default"
8+
}

internal/clients/kibana_oapi/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kibana_oapi
22

33
import (
4+
"context"
45
"crypto/tls"
56
"crypto/x509"
67
"fmt"
@@ -103,3 +104,20 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
103104

104105
return t.next.RoundTrip(req)
105106
}
107+
108+
// BuildSpaceAwarePath constructs an API path with space awareness.
109+
// If spaceID is empty or "default", returns the basePath unchanged.
110+
// Otherwise, prepends "/s/{spaceID}" to the basePath.
111+
func BuildSpaceAwarePath(spaceID, basePath string) string {
112+
if spaceID != "" && spaceID != "default" {
113+
return fmt.Sprintf("/s/%s%s", spaceID, basePath)
114+
}
115+
return basePath
116+
}
117+
118+
func SpaceAwarePathRequestEditor(spaceID string) func(ctx context.Context, req *http.Request) error {
119+
return func(ctx context.Context, req *http.Request) error {
120+
req.URL.Path = BuildSpaceAwarePath(spaceID, req.URL.Path)
121+
return nil
122+
}
123+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package kibana_oapi
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
)
11+
12+
// GetPrebuiltRulesStatus retrieves the status of prebuilt rules and timelines for a given space.
13+
func GetPrebuiltRulesStatus(ctx context.Context, client *Client, spaceID string) (*kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse, diag.Diagnostics) {
14+
resp, err := client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx, SpaceAwarePathRequestEditor(spaceID))
15+
16+
if err != nil {
17+
return nil, diagutil.FrameworkDiagFromError(err)
18+
}
19+
20+
if resp.StatusCode() != 200 {
21+
return nil, diagutil.FrameworkDiagFromError(fmt.Errorf("failed to get prebuilt rules status: %s", resp.Status()))
22+
}
23+
24+
return resp, nil
25+
}
26+
27+
// InstallPrebuiltRules installs or updates prebuilt rules and timelines for a given space.
28+
func InstallPrebuiltRules(ctx context.Context, client *Client, spaceID string) diag.Diagnostics {
29+
resp, err := client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx, SpaceAwarePathRequestEditor(spaceID))
30+
31+
if err != nil {
32+
return diagutil.FrameworkDiagFromError(err)
33+
}
34+
35+
if resp.StatusCode() != 200 {
36+
return diagutil.CheckHttpErrorFromFW(resp.HTTPResponse, "failed to install prebuilt rules")
37+
}
38+
39+
return nil
40+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package prebuilt_rules_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
9+
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
10+
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
11+
"github.com/elastic/terraform-provider-elasticstack/internal/versionutils"
12+
"github.com/google/uuid"
13+
"github.com/hashicorp/go-version"
14+
"github.com/hashicorp/terraform-plugin-testing/config"
15+
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
16+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
17+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
18+
"github.com/hashicorp/terraform-plugin-testing/plancheck"
19+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
20+
"github.com/stretchr/testify/require"
21+
)
22+
23+
var minVersionPrebuiltRules = version.Must(version.NewVersion("8.0.0"))
24+
25+
func TestAccResourcePrebuiltRules(t *testing.T) {
26+
testAccResourcePrebuiltRules(t, "default")
27+
}
28+
29+
func TestAccResourcePrebuiltRulesInSpace(t *testing.T) {
30+
spaceID := "security_rules" + sdkacctest.RandStringFromCharSet(4, sdkacctest.CharSetAlphaNum)
31+
testAccResourcePrebuiltRules(t, spaceID)
32+
}
33+
34+
func testAccResourcePrebuiltRules(t *testing.T, spaceID string) {
35+
resource.Test(t, resource.TestCase{
36+
PreCheck: func() { acctest.PreCheck(t) },
37+
ProtoV6ProviderFactories: acctest.Providers,
38+
Steps: []resource.TestStep{
39+
{
40+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules),
41+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
42+
ConfigVariables: config.Variables{
43+
"space_id": config.StringVariable(spaceID),
44+
},
45+
Check: resource.ComposeTestCheckFunc(
46+
resource.TestCheckResourceAttr("elasticstack_kibana_install_prebuilt_rules.test", "space_id", spaceID),
47+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_installed"),
48+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_installed"),
49+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_updated"),
50+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_installed"),
51+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_installed"),
52+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_updated"),
53+
),
54+
},
55+
{
56+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules),
57+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
58+
ConfigVariables: config.Variables{
59+
"space_id": config.StringVariable(spaceID),
60+
},
61+
PlanOnly: true,
62+
ExpectNonEmptyPlan: true,
63+
PreConfig: func() {
64+
deleteSingleDetectionRule(t, spaceID)
65+
},
66+
},
67+
{
68+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules),
69+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
70+
ConfigVariables: config.Variables{
71+
"space_id": config.StringVariable(spaceID),
72+
},
73+
ConfigPlanChecks: resource.ConfigPlanChecks{
74+
PreApply: []plancheck.PlanCheck{
75+
plancheck.ExpectNonEmptyPlan(),
76+
plancheck.ExpectKnownValue("elasticstack_kibana_install_prebuilt_rules.test", tfjsonpath.New("rules_not_installed"), knownvalue.Int64Exact(0)),
77+
},
78+
},
79+
Check: resource.ComposeTestCheckFunc(
80+
resource.TestCheckResourceAttr("elasticstack_kibana_install_prebuilt_rules.test", "space_id", spaceID),
81+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_installed"),
82+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_installed"),
83+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_updated"),
84+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_installed"),
85+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_installed"),
86+
resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_updated"),
87+
),
88+
},
89+
},
90+
})
91+
}
92+
93+
func deleteSingleDetectionRule(t *testing.T, spaceID string) {
94+
unsupported, err := versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules)()
95+
require.NoError(t, err)
96+
97+
if unsupported {
98+
return
99+
}
100+
101+
client, err := clients.NewAcceptanceTestingClient()
102+
require.NoError(t, err)
103+
104+
oapiClient, err := client.GetKibanaOapiClient()
105+
require.NoError(t, err)
106+
107+
resp, err := oapiClient.API.FindRulesWithResponse(t.Context(), &kbapi.FindRulesParams{}, kibana_oapi.SpaceAwarePathRequestEditor(spaceID))
108+
require.NoError(t, err)
109+
require.Equal(t, 200, resp.StatusCode())
110+
111+
ruleBytes, err := resp.JSON200.Data[0].MarshalJSON()
112+
require.NoError(t, err)
113+
114+
var ruleMap map[string]interface{}
115+
err = json.Unmarshal(ruleBytes, &ruleMap)
116+
require.NoError(t, err)
117+
118+
id, ok := ruleMap["id"].(string)
119+
require.True(t, ok, "rule ID not found or not a string")
120+
121+
idUUID, err := uuid.Parse(id)
122+
require.NoError(t, err)
123+
124+
deleteResp, err := oapiClient.API.DeleteRuleWithResponse(t.Context(), spaceID, &kbapi.DeleteRuleParams{Id: &idUUID})
125+
require.NoError(t, err)
126+
require.Equal(t, 200, deleteResp.StatusCode())
127+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package prebuilt_rules
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/resource"
7+
)
8+
9+
func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
10+
resp.Diagnostics.Append(r.upsert(ctx, req.Plan, &resp.State)...)
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package prebuilt_rules
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/resource"
7+
"github.com/hashicorp/terraform-plugin-log/tflog"
8+
)
9+
10+
func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
11+
tflog.Info(ctx, "Delete isn't supported for elasticstack_kibana_install_prebuilt_rules")
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package prebuilt_rules
2+
3+
import (
4+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
5+
"github.com/hashicorp/terraform-plugin-framework/types"
6+
)
7+
8+
type prebuiltRuleModel struct {
9+
ID types.String `tfsdk:"id"`
10+
SpaceID types.String `tfsdk:"space_id"`
11+
RulesInstalled types.Int64 `tfsdk:"rules_installed"`
12+
RulesNotInstalled types.Int64 `tfsdk:"rules_not_installed"`
13+
RulesNotUpdated types.Int64 `tfsdk:"rules_not_updated"`
14+
TimelinesInstalled types.Int64 `tfsdk:"timelines_installed"`
15+
TimelinesNotInstalled types.Int64 `tfsdk:"timelines_not_installed"`
16+
TimelinesNotUpdated types.Int64 `tfsdk:"timelines_not_updated"`
17+
}
18+
19+
func (model *prebuiltRuleModel) populateFromStatus(status *kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse) {
20+
model.RulesInstalled = types.Int64Value(int64(status.JSON200.RulesInstalled))
21+
model.RulesNotInstalled = types.Int64Value(int64(status.JSON200.RulesNotInstalled))
22+
model.RulesNotUpdated = types.Int64Value(int64(status.JSON200.RulesNotUpdated))
23+
model.TimelinesInstalled = types.Int64Value(int64(status.JSON200.TimelinesInstalled))
24+
model.TimelinesNotInstalled = types.Int64Value(int64(status.JSON200.TimelinesNotInstalled))
25+
model.TimelinesNotUpdated = types.Int64Value(int64(status.JSON200.TimelinesNotUpdated))
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package prebuilt_rules
2+
3+
import (
4+
"context"
5+
6+
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
7+
"github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
8+
"github.com/hashicorp/go-version"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
)
11+
12+
func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
13+
var model prebuiltRuleModel
14+
15+
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
16+
if resp.Diagnostics.HasError() {
17+
return
18+
}
19+
20+
serverVersion, sdkDiags := r.client.ServerVersion(ctx)
21+
resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...)
22+
if resp.Diagnostics.HasError() {
23+
return
24+
}
25+
26+
minVersion := version.Must(version.NewVersion("8.0.0"))
27+
if serverVersion.LessThan(minVersion) {
28+
resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource")
29+
return
30+
}
31+
32+
client, err := r.client.GetKibanaOapiClient()
33+
if err != nil {
34+
resp.Diagnostics.AddError(err.Error(), "")
35+
return
36+
}
37+
38+
spaceID := model.ID.ValueString()
39+
40+
// Get current status
41+
status, statusDiags := kibana_oapi.GetPrebuiltRulesStatus(ctx, client, spaceID)
42+
resp.Diagnostics.Append(statusDiags...)
43+
if resp.Diagnostics.HasError() {
44+
return
45+
}
46+
47+
model.populateFromStatus(status)
48+
49+
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
50+
}

0 commit comments

Comments
 (0)