From a106f7e1ce23c9bb3f22afc28223f297046bbdcf Mon Sep 17 00:00:00 2001 From: dobarx <111326505+dobarx@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:30:06 +0200 Subject: [PATCH] feat: Add iris plugin (#256) --- .goreleaser-dev.yaml | 9 + .goreleaser.yaml | 25 ++ .mockery.yaml | 4 + docs/plugins/iris/_index.md | 34 +++ docs/plugins/iris/data-sources/iris_alerts.md | 151 +++++++++++ docs/plugins/iris/data-sources/iris_cases.md | 133 ++++++++++ docs/plugins/plugins.json | 52 ++++ examples/templates/iris/example.fabric | 25 ++ internal/iris/client/client.go | 142 +++++++++++ internal/iris/client/client_test.go | 201 +++++++++++++++ internal/iris/client/dto.go | 108 ++++++++ internal/iris/cmd/main.go | 14 + internal/iris/data_iris_alerts.go | 240 ++++++++++++++++++ internal/iris/data_iris_alerts_test.go | 194 ++++++++++++++ internal/iris/data_iris_cases.go | 204 +++++++++++++++ internal/iris/data_iris_cases_test.go | 182 +++++++++++++ internal/iris/plugin.go | 57 +++++ internal/iris/plugin_test.go | 15 ++ internal/plugin_validity_test.go | 2 + mocks/internalpkg/iris/client/client.go | 156 ++++++++++++ tools/docgen/main.go | 2 + 21 files changed, 1950 insertions(+) create mode 100644 docs/plugins/iris/_index.md create mode 100644 docs/plugins/iris/data-sources/iris_alerts.md create mode 100644 docs/plugins/iris/data-sources/iris_cases.md create mode 100644 examples/templates/iris/example.fabric create mode 100644 internal/iris/client/client.go create mode 100644 internal/iris/client/client_test.go create mode 100644 internal/iris/client/dto.go create mode 100644 internal/iris/cmd/main.go create mode 100644 internal/iris/data_iris_alerts.go create mode 100644 internal/iris/data_iris_alerts_test.go create mode 100644 internal/iris/data_iris_cases.go create mode 100644 internal/iris/data_iris_cases_test.go create mode 100644 internal/iris/plugin.go create mode 100644 internal/iris/plugin_test.go create mode 100644 mocks/internalpkg/iris/client/client.go diff --git a/.goreleaser-dev.yaml b/.goreleaser-dev.yaml index cc4fc88e..3d5a8ee9 100644 --- a/.goreleaser-dev.yaml +++ b/.goreleaser-dev.yaml @@ -157,3 +157,12 @@ builds: # no_unique_dist_dir: true # tags: # - fabricplugin + + # - id: iris + # main: ./internal/iris/cmd + # binary: "plugins/blackstork/iris@{{ .Version }}" + # ldflags: "-X main.version={{.Version}}" + # gcflags: all=-N -l + # no_unique_dist_dir: true + # tags: + # - fabricplugin diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1a23aebf..e71e68f3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -247,6 +247,20 @@ builds: tags: - fabricplugin + - id: plugin_iris + main: ./internal/iris/cmd + binary: "iris@{{ .Version }}" + flags: "-trimpath" + hooks: + post: + - go run ./tools/pluginmeta --namespace blackstork --version {{.Version}} patch --plugin {{.Path}} --os {{.Os}} --arch {{.Arch}} + goos: + - linux + - windows + - darwin + tags: + - fabricplugin + archives: - id: fabric format: tar.gz @@ -443,6 +457,17 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} + - id: plugin_iris + format: tar.gz + builds: + - plugin_iris + name_template: >- + plugin_iris_ + {{- .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} dockers: - use: buildx diff --git a/.mockery.yaml b/.mockery.yaml index f135379e..a22df80a 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -35,6 +35,10 @@ packages: config: interfaces: Client: + github.com/blackstork-io/fabric/internal/iris/client: + config: + interfaces: + Client: github.com/blackstork-io/fabric/internal/elastic/kbclient: config: interfaces: diff --git a/docs/plugins/iris/_index.md b/docs/plugins/iris/_index.md new file mode 100644 index 00000000..7be193f9 --- /dev/null +++ b/docs/plugins/iris/_index.md @@ -0,0 +1,34 @@ +--- +title: blackstork/iris +weight: 20 +plugin: + name: blackstork/iris + description: "The `iris` plugin for Iris Incident Response platform." + tags: [] + version: "v0.4.2" + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/iris/" +type: docs +hideInMenu: true +--- + +{{< plugin-header "blackstork/iris" "iris" "v0.4.2" >}} + +## Description +The `iris` plugin for Iris Incident Response platform. + +## Installation + +To install the plugin, add it to `plugin_versions` map in the Fabric global configuration block (see [Global configuration]({{< ref "configs.md#global-configuration" >}}) for more details), with a version constraint restricting which available versions of the plugin the codebase is compatible with: + +```hcl +fabric { + plugin_versions = { + "blackstork/iris" = ">= v0.4.2" + } +} +``` + + +## Data sources + +{{< plugin-resources "iris" "data-source" >}} diff --git a/docs/plugins/iris/data-sources/iris_alerts.md b/docs/plugins/iris/data-sources/iris_alerts.md new file mode 100644 index 00000000..197e4464 --- /dev/null +++ b/docs/plugins/iris/data-sources/iris_alerts.md @@ -0,0 +1,151 @@ +--- +title: "`iris_alerts` data source" +plugin: + name: blackstork/iris + description: "Retrieve alerts from Iris API" + tags: [] + version: "v0.4.2" + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/iris/" +resource: + type: data-source +type: docs +--- + +{{< breadcrumbs 2 >}} + +{{< plugin-resource-header "blackstork/iris" "iris" "v0.4.2" "iris_alerts" "data source" >}} + +## Description +Retrieve alerts from Iris API + +## Installation + +To use `iris_alerts` data source, you must install the plugin `blackstork/iris`. + +To install the plugin, add the full plugin name to the `plugin_versions` map in the Fabric global configuration block (see [Global configuration]({{< ref "configs.md#global-configuration" >}}) for more details), as shown below: + +```hcl +fabric { + plugin_versions = { + "blackstork/iris" = ">= v0.4.2" + } +} +``` + +Note the version constraint set for the plugin. + +## Configuration + +The data source supports the following configuration arguments: + +```hcl +config data iris_alerts { + # Iris API url + # + # Required string. + # Must be non-empty + # For example: + api_url = "some string" + + # Iris API Key + # + # Required string. + # Must be non-empty + # For example: + api_key = "some string" + + # Enable/disable insecure TLS + # + # Optional bool. + # Default value: + insecure = false +} +``` + +## Usage + +The data source supports the following execution arguments: + +```hcl +data iris_alerts { + # List of Alert IDs + # + # Optional list of number. + # Default value: + alert_ids = null + + # Alert Source + # + # Optional string. + # Default value: + alert_source = null + + # List of tags + # + # Optional list of string. + # Default value: + tags = null + + # Case ID + # + # Optional number. + # Default value: + case_id = null + + # Alert Customer ID + # + # Optional number. + # Default value: + customer_id = null + + # Alert Owner ID + # + # Optional number. + # Default value: + owner_id = null + + # Alert Severity ID + # + # Optional number. + # Default value: + severity_id = null + + # Alert Classification ID + # + # Optional number. + # Default value: + classification_id = null + + # Alert State ID + # + # Optional number. + # Default value: + status_id = null + + # Alert Date - lower boundary + # + # Optional string. + # Default value: + alert_start_date = null + + # Alert Date - higher boundary + # + # Optional string. + # Default value: + alert_end_date = null + + # Sort order + # + # Optional string. + # Must be one of: "desc", "asc" + # Default value: + sort = "desc" + + # Size limit to retrieve + # + # Optional number. + # Must be >= 0 + # Default value: + size = 0 +} +``` \ No newline at end of file diff --git a/docs/plugins/iris/data-sources/iris_cases.md b/docs/plugins/iris/data-sources/iris_cases.md new file mode 100644 index 00000000..59fb1a57 --- /dev/null +++ b/docs/plugins/iris/data-sources/iris_cases.md @@ -0,0 +1,133 @@ +--- +title: "`iris_cases` data source" +plugin: + name: blackstork/iris + description: "Retrieve cases from Iris API" + tags: [] + version: "v0.4.2" + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/iris/" +resource: + type: data-source +type: docs +--- + +{{< breadcrumbs 2 >}} + +{{< plugin-resource-header "blackstork/iris" "iris" "v0.4.2" "iris_cases" "data source" >}} + +## Description +Retrieve cases from Iris API + +## Installation + +To use `iris_cases` data source, you must install the plugin `blackstork/iris`. + +To install the plugin, add the full plugin name to the `plugin_versions` map in the Fabric global configuration block (see [Global configuration]({{< ref "configs.md#global-configuration" >}}) for more details), as shown below: + +```hcl +fabric { + plugin_versions = { + "blackstork/iris" = ">= v0.4.2" + } +} +``` + +Note the version constraint set for the plugin. + +## Configuration + +The data source supports the following configuration arguments: + +```hcl +config data iris_cases { + # Iris API url + # + # Required string. + # Must be non-empty + # For example: + api_url = "some string" + + # Iris API Key + # + # Required string. + # Must be non-empty + # For example: + api_key = "some string" + + # Enable/disable insecure TLS + # + # Optional bool. + # Default value: + insecure = false +} +``` + +## Usage + +The data source supports the following execution arguments: + +```hcl +data iris_cases { + # List of Case IDs + # + # Optional list of number. + # Default value: + case_ids = null + + # Case Customer ID + # + # Optional number. + # Default value: + customer_id = null + + # Case Owner ID + # + # Optional number. + # Default value: + owner_id = null + + # Case Severity ID + # + # Optional number. + # Default value: + severity_id = null + + # Case State ID + # + # Optional number. + # Default value: + state_id = null + + # Case SOC ID + # + # Optional string. + # Default value: + soc_id = null + + # Case opening date - lower boundary + # + # Optional string. + # Default value: + start_open_date = null + + # Case opening date - higher boundary + # + # Optional string. + # Default value: + end_open_date = null + + # Sort order + # + # Optional string. + # Must be one of: "desc", "asc" + # Default value: + sort = "desc" + + # Size limit to retrieve + # + # Optional number. + # Must be >= 0 + # Default value: + size = 0 +} +``` \ No newline at end of file diff --git a/docs/plugins/plugins.json b/docs/plugins/plugins.json index c5c9f4e5..09d65a46 100644 --- a/docs/plugins/plugins.json +++ b/docs/plugins/plugins.json @@ -389,6 +389,58 @@ } ] }, + { + "name": "blackstork/iris", + "version": "v0.4.2", + "shortname": "iris", + "resources": [ + { + "name": "iris_alerts", + "type": "data-source", + "config_params": [ + "api_key", + "api_url", + "insecure" + ], + "arguments": [ + "alert_end_date", + "alert_ids", + "alert_source", + "alert_start_date", + "case_id", + "classification_id", + "customer_id", + "owner_id", + "severity_id", + "size", + "sort", + "status_id", + "tags" + ] + }, + { + "name": "iris_cases", + "type": "data-source", + "config_params": [ + "api_key", + "api_url", + "insecure" + ], + "arguments": [ + "case_ids", + "customer_id", + "end_open_date", + "owner_id", + "severity_id", + "size", + "soc_id", + "sort", + "start_open_date", + "state_id" + ] + } + ] + }, { "name": "blackstork/microsoft", "version": "v0.4.2", diff --git a/examples/templates/iris/example.fabric b/examples/templates/iris/example.fabric new file mode 100644 index 00000000..ea6649b1 --- /dev/null +++ b/examples/templates/iris/example.fabric @@ -0,0 +1,25 @@ +fabric { + plugin_versions = { + "blackstork/iris" = ">= 0.5 < 1.0 || 0.5.0-rev0" + } +} + +config data iris_cases { + api_url = env.IRIS_API_URL + api_key = env.IRIS_API_KEY + # insecure = true +} + +document "example" { + title = "Using iris plugin" + data iris_cases "my_cases" { + size = 2 + } + content title { + value = "My Iris Cases" + } + content list { + item_template = "{{.name}}" + items = query_jq(".data.iris_cases.my_cases") + } +} \ No newline at end of file diff --git a/internal/iris/client/client.go b/internal/iris/client/client.go new file mode 100644 index 00000000..ba6e7b39 --- /dev/null +++ b/internal/iris/client/client.go @@ -0,0 +1,142 @@ +package client + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/google/go-querystring/query" +) + +type Client interface { + ListCases(ctx context.Context, req *ListCasesReq) (*ListCasesRes, error) + ListAlerts(ctx context.Context, req *ListAlertsReq) (*ListAlertsRes, error) +} + +type client struct { + apiURL string + apiKey string + insecure bool +} + +func (c *client) auth(r *http.Request) { + r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) +} + +func (c *client) makeHTTPClient() *http.Client { + httpClient := &http.Client{ + Timeout: 15 * time.Second, + } + + if c.insecure { + httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec,G402 + }, + } + } + + return httpClient +} + +func New(url, apiKey string, insecure bool) Client { + return &client{ + apiURL: url, + apiKey: apiKey, + insecure: insecure, + } +} + +func (c *client) handleError(res *http.Response) error { + var clientErr Error + + if err := json.NewDecoder(res.Body).Decode(&clientErr); err != nil { + return err + } + + return &clientErr +} + +func (c *client) ListCases(ctx context.Context, req *ListCasesReq) (*ListCasesRes, error) { + u, err := url.Parse(c.apiURL + "/manage/cases/filter") + if err != nil { + return nil, err + } + + q, err := query.Values(req) + if err != nil { + return nil, err + } + u.RawQuery = q.Encode() + + r, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + r.Header.Set("Accept", "application/json") + c.auth(r) + + client := c.makeHTTPClient() + res, err := client.Do(r) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, c.handleError(res) + } + + var data ListCasesRes + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} + +func (c *client) ListAlerts(ctx context.Context, req *ListAlertsReq) (*ListAlertsRes, error) { + u, err := url.Parse(c.apiURL + "/alerts/filter") + if err != nil { + return nil, err + } + + q, err := query.Values(req) + if err != nil { + return nil, err + } + u.RawQuery = q.Encode() + + r, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + r.Header.Set("Accept", "application/json") + c.auth(r) + + client := c.makeHTTPClient() + res, err := client.Do(r) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, c.handleError(res) + } + + var data ListAlertsRes + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/internal/iris/client/client_test.go b/internal/iris/client/client_test.go new file mode 100644 index 00000000..40f71828 --- /dev/null +++ b/internal/iris/client/client_test.go @@ -0,0 +1,201 @@ +package client + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/suite" +) + +type ClientTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc +} + +func (s *ClientTestSuite) SetupTest() { + s.ctx, s.cancel = context.WithCancel(context.Background()) +} + +func (s *ClientTestSuite) TearDownTest() { + s.cancel() +} + +func TestClientTestSuite(t *testing.T) { + suite.Run(t, new(ClientTestSuite)) +} + +func (s *ClientTestSuite) mock(fn http.HandlerFunc, apiKey string, insecure bool) (Client, *httptest.Server) { + srv := httptest.NewServer(fn) + cli := &client{ + apiURL: srv.URL, + apiKey: apiKey, + insecure: insecure, + } + return cli, srv +} + +func (s *ClientTestSuite) TestAuth() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + s.Equal("api_key", strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")) + }, "api_key", false) + defer srv.Close() + client.ListCases(s.ctx, &ListCasesReq{}) +} + +func (s *ClientTestSuite) TestListCases() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + s.Equal("/manage/cases/filter", r.URL.Path) + s.Equal(http.MethodGet, r.Method) + // page + q := r.URL.Query() + s.Equal("1", q.Get("page")) + s.Equal("1", q.Get("per_page")) + s.Equal("1,2", q.Get("case_ids")) + s.Equal("1", q.Get("case_customer_id")) + s.Equal("1", q.Get("case_owner_id")) + s.Equal("1", q.Get("case_severity_id")) + s.Equal("1", q.Get("case_state_id")) + s.Equal("test_soc_id", q.Get("case_soc_id")) + s.Equal("asc", q.Get("sort")) + s.Equal("test_start_date", q.Get("start_open_date")) + s.Equal("test_end_date", q.Get("end_open_date")) + w.Write([]byte(`{ + "status": "success", + "data": { + "last_page": 1, + "current_page": 1, + "total": 10, + "next_page": 2, + "cases": [ + { + "any": "data" + } + ] + } + }`)) + }, "test_api_key", false) + defer srv.Close() + req := ListCasesReq{ + Page: 1, + PerPage: Int(1), + CaseIDs: IntList{1, 2}, + CaseCustomerID: Int(1), + CaseOwnerID: Int(1), + CaseSeverityID: Int(1), + CaseStateID: Int(1), + CaseSocID: String("test_soc_id"), + Sort: String("asc"), + StartOpenDate: String("test_start_date"), + EndOpenDate: String("test_end_date"), + } + result, err := client.ListCases(s.ctx, &req) + s.NoError(err) + s.Equal(&ListCasesRes{ + Status: "success", + Data: &CasesData{ + CurrentPage: 1, + LastPage: 1, + NextPage: Int(2), + Total: 10, + Cases: []any{ + map[string]any{ + "any": "data", + }, + }, + }, + }, result) +} + +func (s *ClientTestSuite) TestListCasesError() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, "test_api_key", false) + defer srv.Close() + req := ListCasesReq{} + _, err := client.ListCases(s.ctx, &req) + s.Error(err) +} + +func (s *ClientTestSuite) TestListAlerts() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + s.Equal("/alerts/filter", r.URL.Path) + s.Equal(http.MethodGet, r.Method) + // page + q := r.URL.Query() + s.Equal("1", q.Get("page")) + s.Equal("1", q.Get("per_page")) + s.Equal("1,2", q.Get("alert_ids")) + s.Equal("1", q.Get("case_id")) + s.Equal("1", q.Get("alert_customer_id")) + s.Equal("1", q.Get("alert_owner_id")) + s.Equal("1", q.Get("alert_status_id")) + s.Equal("1", q.Get("alert_classification_id")) + s.Equal("test_tag_1,test_tag_2", q.Get("alert_tags")) + s.Equal("1", q.Get("alert_severity_id")) + s.Equal("test_alert_source", q.Get("alert_source")) + s.Equal("asc", q.Get("sort")) + s.Equal("test_start_date", q.Get("alert_start_date")) + s.Equal("test_end_date", q.Get("alert_end_date")) + w.Write([]byte(`{ + "status": "success", + "data": { + "last_page": 1, + "current_page": 1, + "total": 10, + "next_page": 2, + "alerts": [ + { + "any": "data" + } + ] + } + }`)) + }, "test_api_key", false) + defer srv.Close() + req := ListAlertsReq{ + Page: 1, + PerPage: Int(1), + AlertIDs: IntList{1, 2}, + AlertTags: StringList{"test_tag_1", "test_tag_2"}, + AlertCustomerID: Int(1), + AlertOwnerID: Int(1), + CaseID: Int(1), + AlertClassificationID: Int(1), + AlertSeverityID: Int(1), + AlertSource: String("test_alert_source"), + AlertStatusID: Int(1), + Sort: String("asc"), + AlertStartDate: String("test_start_date"), + AlertEndDate: String("test_end_date"), + } + result, err := client.ListAlerts(s.ctx, &req) + s.NoError(err) + s.Equal(&ListAlertsRes{ + Status: "success", + Data: &AlertsData{ + CurrentPage: 1, + LastPage: 1, + NextPage: Int(2), + Total: 10, + Alerts: []any{ + map[string]any{ + "any": "data", + }, + }, + }, + }, result) +} + +func (s *ClientTestSuite) TestListAlertsError() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, "test_api_key", false) + defer srv.Close() + req := ListAlertsReq{} + _, err := client.ListAlerts(s.ctx, &req) + s.Error(err) +} diff --git a/internal/iris/client/dto.go b/internal/iris/client/dto.go new file mode 100644 index 00000000..ac30a92e --- /dev/null +++ b/internal/iris/client/dto.go @@ -0,0 +1,108 @@ +package client + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +func String(s string) *string { + return &s +} + +func Int(i int) *int { + return &i +} + +type IntList []int + +func (list IntList) EncodeValues(key string, v *url.Values) error { + if len(list) == 0 { + return nil + } + dst := make([]string, len(list)) + for i, id := range list { + dst[i] = strconv.Itoa(id) + } + v.Add(key, strings.Join(dst, ",")) + return nil +} + +type StringList []string + +func (list StringList) EncodeValues(key string, v *url.Values) error { + if len(list) == 0 { + return nil + } + v.Add(key, strings.Join(list, ",")) + return nil +} + +type Error struct { + Status string `json:"status"` + Message string `json:"message"` +} + +func (err *Error) Error() string { + return fmt.Sprintf("status: %s message: %s", err.Status, err.Message) +} + +type ListCasesReq struct { + Page int `url:"page"` + PerPage *int `url:"per_page,omitempty"` + CaseIDs IntList `url:"case_ids,omitempty"` + CaseCustomerID *int `url:"case_customer_id,omitempty"` + CaseOwnerID *int `url:"case_owner_id,omitempty"` + CaseSeverityID *int `url:"case_severity_id,omitempty"` + CaseStateID *int `url:"case_state_id,omitempty"` + CaseSocID *string `url:"case_soc_id,omitempty"` + Sort *string `url:"sort,omitempty"` + StartOpenDate *string `url:"start_open_date,omitempty"` + EndOpenDate *string `url:"end_open_date,omitempty"` +} + +type ListCasesRes struct { + Status string `json:"status"` + Message string `json:"message"` + Data *CasesData `json:"data"` +} + +type CasesData struct { + CurrentPage int `json:"current_page"` + LastPage int `json:"last_page"` + NextPage *int `json:"next_page"` + Total int `json:"total"` + Cases []any `json:"cases"` +} + +type ListAlertsReq struct { + Page int `url:"page"` + PerPage *int `url:"per_page,omitempty"` + Sort *string `url:"sort,omitempty"` + AlertIDs IntList `url:"alert_ids,omitempty"` + AlertTags StringList `url:"alert_tags,omitempty"` + AlertSource *string `url:"alert_source,omitempty"` + CaseID *int `url:"case_id,omitempty"` + AlertOwnerID *int `url:"alert_owner_id,omitempty"` + AlertStatusID *int `url:"alert_status_id,omitempty"` + AlertSeverityID *int `url:"alert_severity_id,omitempty"` + AlertClassificationID *int `url:"alert_classification_id,omitempty"` + AlertCustomerID *int `url:"alert_customer_id,omitempty"` + AlertStartDate *string `url:"alert_start_date,omitempty"` + AlertEndDate *string `url:"alert_end_date,omitempty"` +} + +type ListAlertsRes struct { + Status string `json:"status"` + Message string `json:"message"` + Data *AlertsData `json:"data"` +} + +type AlertsData struct { + CurrentPage int `json:"current_page"` + LastPage int `json:"last_page"` + NextPage *int `json:"next_page"` + Total int `json:"total"` + Alerts []any `json:"alerts"` +} diff --git a/internal/iris/cmd/main.go b/internal/iris/cmd/main.go new file mode 100644 index 00000000..c9d6134b --- /dev/null +++ b/internal/iris/cmd/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/blackstork-io/fabric/internal/iris" + pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" +) + +var version string + +func main() { + pluginapiv1.Serve( + iris.Plugin(version, iris.DefaultClientLoader), + ) +} diff --git a/internal/iris/data_iris_alerts.go b/internal/iris/data_iris_alerts.go new file mode 100644 index 00000000..f634458b --- /dev/null +++ b/internal/iris/data_iris_alerts.go @@ -0,0 +1,240 @@ +package iris + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/iris/client" + "github.com/blackstork-io/fabric/pkg/diagnostics" + "github.com/blackstork-io/fabric/plugin" + "github.com/blackstork-io/fabric/plugin/dataspec" + "github.com/blackstork-io/fabric/plugin/dataspec/constraint" + "github.com/blackstork-io/fabric/plugin/plugindata" +) + +func makeIrisAlertsDataSource(loader ClientLoadFn) *plugin.DataSource { + return &plugin.DataSource{ + DataFunc: fetchIrisAlertsData(loader), + Doc: "Retrieve alerts from Iris API", + Config: &dataspec.RootSpec{ + Attrs: []*dataspec.AttrSpec{ + { + Name: "api_url", + Type: cty.String, + Constraints: constraint.RequiredMeaningful, + Doc: "Iris API url", + }, + { + Name: "api_key", + Type: cty.String, + Secret: true, + Constraints: constraint.RequiredMeaningful, + Doc: "Iris API Key", + }, + { + Name: "insecure", + Type: cty.Bool, + DefaultVal: cty.BoolVal(false), + Doc: "Enable/disable insecure TLS", + }, + }, + }, + Args: &dataspec.RootSpec{ + Attrs: []*dataspec.AttrSpec{ + { + Name: "alert_ids", + Type: cty.List(cty.Number), + Doc: "List of Alert IDs", + }, + { + Name: "alert_source", + Type: cty.String, + Doc: "Alert Source", + }, + { + Name: "tags", + Type: cty.List(cty.String), + Doc: "List of tags", + }, + { + Name: "case_id", + Type: cty.Number, + Doc: "Case ID", + }, + { + Name: "customer_id", + Type: cty.Number, + Doc: "Alert Customer ID", + }, + { + Name: "owner_id", + Type: cty.Number, + Doc: "Alert Owner ID", + }, + { + Name: "severity_id", + Type: cty.Number, + Doc: "Alert Severity ID", + }, + { + Name: "classification_id", + Type: cty.Number, + Doc: "Alert Classification ID", + }, + { + Name: "status_id", + Type: cty.Number, + Doc: "Alert State ID", + }, + { + Name: "alert_start_date", + Type: cty.String, + Doc: "Alert Date - lower boundary", + }, + { + Name: "alert_end_date", + Type: cty.String, + Doc: "Alert Date - higher boundary", + }, + { + Name: "sort", + Type: cty.String, + Doc: "Sort order", + DefaultVal: cty.StringVal("desc"), + OneOf: []cty.Value{ + cty.StringVal("desc"), + cty.StringVal("asc"), + }, + }, + { + Name: "size", + Type: cty.Number, + Doc: "Size limit to retrieve", + MinInclusive: cty.NumberIntVal(0), + Constraints: constraint.NonNull, + DefaultVal: cty.NumberIntVal(0), + }, + }, + }, + } +} + +func fetchIrisAlertsData(loader ClientLoadFn) plugin.RetrieveDataFunc { + return func(ctx context.Context, params *plugin.RetrieveDataParams) (plugindata.Data, diagnostics.Diag) { + cli, err := parseConfig(params.Config, loader) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse configuration", + }} + } + req, err := parseListAlertsReq(params.Args) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse arguments", + }} + } + num, _ := params.Args.GetAttrVal("size").AsBigFloat().Int64() + size := int(num) + req.Page = 1 + var cases plugindata.List + for { + res, err := cli.ListAlerts(ctx, req) + if err != nil { + return nil, handleClientError(err) + } + if res.Data == nil { + break + } + for _, v := range res.Data.Alerts { + data, err := plugindata.ParseAny(v) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse data", + }} + } + cases = append(cases, data) + if size > 0 && len(cases) == size { + break + } + } + if (size > 0 && len(cases) == size) || res.Data.NextPage == nil { + break + } + req.Page = *res.Data.NextPage + } + return cases, nil + } +} + +func parseListAlertsReq(args *dataspec.Block) (*client.ListAlertsReq, error) { + if args == nil { + return nil, fmt.Errorf("arguments are required") + } + req := &client.ListAlertsReq{} + if attr := args.GetAttrVal("alert_ids"); !attr.IsNull() { + ids := attr.AsValueSlice() + for _, id := range ids { + if id.IsNull() { + continue + } + num, _ := id.AsBigFloat().Int64() + req.AlertIDs = append(req.AlertIDs, int(num)) + } + } + if attr := args.GetAttrVal("tags"); !attr.IsNull() { + tags := attr.AsValueSlice() + for _, tag := range tags { + if tag.IsNull() { + continue + } + req.AlertTags = append(req.AlertTags, tag.AsString()) + } + } + if attr := args.GetAttrVal("case_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.CaseID = client.Int(int(num)) + } + if attr := args.GetAttrVal("classification_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertClassificationID = client.Int(int(num)) + } + if attr := args.GetAttrVal("customer_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertCustomerID = client.Int(int(num)) + } + if attr := args.GetAttrVal("owner_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertOwnerID = client.Int(int(num)) + } + if attr := args.GetAttrVal("severity_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertSeverityID = client.Int(int(num)) + } + if attr := args.GetAttrVal("classification_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertClassificationID = client.Int(int(num)) + } + if attr := args.GetAttrVal("status_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.AlertStatusID = client.Int(int(num)) + } + if attr := args.GetAttrVal("alert_source"); !attr.IsNull() { + req.AlertSource = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("alert_start_date"); !attr.IsNull() { + req.AlertStartDate = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("alert_end_date"); !attr.IsNull() { + req.AlertEndDate = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("sort"); !attr.IsNull() { + req.Sort = client.String(attr.AsString()) + } + return req, nil +} diff --git a/internal/iris/data_iris_alerts_test.go b/internal/iris/data_iris_alerts_test.go new file mode 100644 index 00000000..82cdf514 --- /dev/null +++ b/internal/iris/data_iris_alerts_test.go @@ -0,0 +1,194 @@ +package iris + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/iris/client" + client_mocks "github.com/blackstork-io/fabric/mocks/internalpkg/iris/client" + "github.com/blackstork-io/fabric/plugin" + "github.com/blackstork-io/fabric/plugin/plugindata" + "github.com/blackstork-io/fabric/plugin/plugintest" +) + +type AlertsDataSourceTestSuite struct { + suite.Suite + + plugin *plugin.Schema + ctx context.Context + cli *client_mocks.Client + storedApiURL string + storedApiKey string + storedInsecure bool +} + +func TestAlertsDataSourceTestSuite(t *testing.T) { + suite.Run(t, new(AlertsDataSourceTestSuite)) +} + +func (s *AlertsDataSourceTestSuite) SetupSuite() { + s.plugin = Plugin("v0.0.0", func(apiURL, apiKey string, insecure bool) client.Client { + s.storedApiKey = apiKey + s.storedApiURL = apiURL + s.storedInsecure = insecure + return s.cli + }) + s.ctx = context.Background() +} + +func (s *AlertsDataSourceTestSuite) SetupTest() { + s.cli = &client_mocks.Client{} +} + +func (s *AlertsDataSourceTestSuite) TearDownTest() { + s.cli.AssertExpectations(s.T()) +} + +func (s *AlertsDataSourceTestSuite) TestSchema() { + s.Require().NotNil(s.plugin.DataSources["iris_alerts"]) + s.NotNil(s.plugin.DataSources["iris_alerts"].Config) + s.NotNil(s.plugin.DataSources["iris_alerts"].Args) + s.NotNil(s.plugin.DataSources["iris_alerts"].DataFunc) +} + +func (s *AlertsDataSourceTestSuite) TestLimit() { + s.cli.On("ListAlerts", mock.Anything, &client.ListAlertsReq{ + Page: 1, + Sort: client.String("desc"), + }).Return(&client.ListAlertsRes{ + Status: "success", + Data: &client.AlertsData{ + CurrentPage: 1, + LastPage: 1, + Total: 1, + Alerts: []any{ + map[string]any{ + "id": "1", + }, + }, + }, + }, nil) + res, diags := s.plugin.RetrieveData(s.ctx, "iris_alerts", &plugin.RetrieveDataParams{ + Config: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_alerts"].Config). + SetAttr("api_url", cty.StringVal("test-url")). + SetAttr("api_key", cty.StringVal("test-key")). + SetAttr("insecure", cty.BoolVal(true)). + Decode(), + Args: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_alerts"].Args). + SetAttr("size", cty.NumberIntVal(10)). + Decode(), + }) + s.Equal("test-url", s.storedApiURL) + s.Equal("test-key", s.storedApiKey) + s.Equal(true, s.storedInsecure) + s.Len(diags, 0) + s.Equal(plugindata.List{ + plugindata.Map{ + "id": plugindata.String("1"), + }, + }, res) +} + +func (s *AlertsDataSourceTestSuite) TestFull() { + s.cli.On("ListAlerts", mock.Anything, &client.ListAlertsReq{ + Page: 1, + AlertIDs: client.IntList{1, 2}, + AlertCustomerID: client.Int(1), + AlertOwnerID: client.Int(2), + AlertSeverityID: client.Int(5), + CaseID: client.Int(3), + AlertTags: client.StringList{"test-tag-1", "test-tag-2"}, + AlertSource: client.String("test-source"), + AlertStatusID: client.Int(4), + AlertClassificationID: client.Int(5), + AlertStartDate: client.String("test-alert-start-date"), + AlertEndDate: client.String("test-alert-end-date"), + Sort: client.String("asc"), + }).Return(&client.ListAlertsRes{ + Status: "success", + Data: &client.AlertsData{ + CurrentPage: 1, + LastPage: 2, + Total: 3, + NextPage: client.Int(2), + Alerts: []any{ + map[string]any{ + "id": "1", + }, + }, + }, + }, nil) + s.cli.On("ListAlerts", mock.Anything, &client.ListAlertsReq{ + Page: 2, + AlertIDs: client.IntList{1, 2}, + AlertCustomerID: client.Int(1), + AlertOwnerID: client.Int(2), + AlertSeverityID: client.Int(5), + CaseID: client.Int(3), + AlertTags: client.StringList{"test-tag-1", "test-tag-2"}, + AlertSource: client.String("test-source"), + AlertStatusID: client.Int(4), + AlertClassificationID: client.Int(5), + AlertStartDate: client.String("test-alert-start-date"), + AlertEndDate: client.String("test-alert-end-date"), + Sort: client.String("asc"), + }).Return(&client.ListAlertsRes{ + Status: "success", + Data: &client.AlertsData{ + CurrentPage: 2, + LastPage: 2, + Total: 3, + Alerts: []any{ + map[string]any{ + "id": "2", + }, + map[string]any{ + "id": "3", + }, + }, + }, + }, nil) + res, diags := s.plugin.RetrieveData(s.ctx, "iris_alerts", &plugin.RetrieveDataParams{ + Config: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_alerts"].Config). + SetAttr("api_url", cty.StringVal("test-url")). + SetAttr("api_key", cty.StringVal("test-key")). + Decode(), + Args: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_alerts"].Args). + SetAttr("alert_ids", cty.ListVal([]cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(2), + })). + SetAttr("tags", cty.ListVal([]cty.Value{ + cty.StringVal("test-tag-1"), + cty.StringVal("test-tag-2"), + })). + SetAttr("case_id", cty.NumberIntVal(3)). + SetAttr("customer_id", cty.NumberIntVal(1)). + SetAttr("owner_id", cty.NumberIntVal(2)). + SetAttr("severity_id", cty.NumberIntVal(5)). + SetAttr("status_id", cty.NumberIntVal(4)). + SetAttr("classification_id", cty.NumberIntVal(5)). + SetAttr("alert_source", cty.StringVal("test-source")). + SetAttr("alert_start_date", cty.StringVal("test-alert-start-date")). + SetAttr("alert_end_date", cty.StringVal("test-alert-end-date")). + SetAttr("sort", cty.StringVal("asc")). + SetAttr("size", cty.NumberIntVal(2)). + Decode(), + }) + s.Equal("test-url", s.storedApiURL) + s.Equal("test-key", s.storedApiKey) + s.Equal(false, s.storedInsecure) + s.Len(diags, 0) + s.Equal(plugindata.List{ + plugindata.Map{ + "id": plugindata.String("1"), + }, + plugindata.Map{ + "id": plugindata.String("2"), + }, + }, res) +} diff --git a/internal/iris/data_iris_cases.go b/internal/iris/data_iris_cases.go new file mode 100644 index 00000000..a8efa82e --- /dev/null +++ b/internal/iris/data_iris_cases.go @@ -0,0 +1,204 @@ +package iris + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/iris/client" + "github.com/blackstork-io/fabric/pkg/diagnostics" + "github.com/blackstork-io/fabric/plugin" + "github.com/blackstork-io/fabric/plugin/dataspec" + "github.com/blackstork-io/fabric/plugin/dataspec/constraint" + "github.com/blackstork-io/fabric/plugin/plugindata" +) + +func makeIrisCasesDataSource(loader ClientLoadFn) *plugin.DataSource { + return &plugin.DataSource{ + Doc: "Retrieve cases from Iris API", + DataFunc: fetchIrisCasesData(loader), + Config: &dataspec.RootSpec{ + Attrs: []*dataspec.AttrSpec{ + { + Name: "api_url", + Type: cty.String, + Constraints: constraint.RequiredMeaningful, + Doc: "Iris API url", + }, + { + Name: "api_key", + Type: cty.String, + Secret: true, + Constraints: constraint.RequiredMeaningful, + Doc: "Iris API Key", + }, + { + Name: "insecure", + Type: cty.Bool, + DefaultVal: cty.BoolVal(false), + Doc: "Enable/disable insecure TLS", + }, + }, + }, + Args: &dataspec.RootSpec{ + Attrs: []*dataspec.AttrSpec{ + { + Name: "case_ids", + Type: cty.List(cty.Number), + Doc: "List of Case IDs", + }, + { + Name: "customer_id", + Type: cty.Number, + Doc: "Case Customer ID", + }, + { + Name: "owner_id", + Type: cty.Number, + Doc: "Case Owner ID", + }, + { + Name: "severity_id", + Type: cty.Number, + Doc: "Case Severity ID", + }, + { + Name: "state_id", + Type: cty.Number, + Doc: "Case State ID", + }, + { + Name: "soc_id", + Type: cty.String, + Doc: "Case SOC ID", + }, + { + Name: "start_open_date", + Type: cty.String, + Doc: "Case opening date - lower boundary", + }, + { + Name: "end_open_date", + Type: cty.String, + Doc: "Case opening date - higher boundary", + }, + { + Name: "sort", + Type: cty.String, + Doc: "Sort order", + DefaultVal: cty.StringVal("desc"), + OneOf: []cty.Value{ + cty.StringVal("desc"), + cty.StringVal("asc"), + }, + }, + { + Name: "size", + Type: cty.Number, + Doc: "Size limit to retrieve", + MinInclusive: cty.NumberIntVal(0), + Constraints: constraint.NonNull, + DefaultVal: cty.NumberIntVal(0), + }, + }, + }, + } +} + +func fetchIrisCasesData(loader ClientLoadFn) plugin.RetrieveDataFunc { + return func(ctx context.Context, params *plugin.RetrieveDataParams) (plugindata.Data, diagnostics.Diag) { + cli, err := parseConfig(params.Config, loader) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse configuration", + }} + } + req, err := parseListCasesReq(params.Args) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse arguments", + }} + } + num, _ := params.Args.GetAttrVal("size").AsBigFloat().Int64() + size := int(num) + req.Page = 1 + var cases plugindata.List + for { + res, err := cli.ListCases(ctx, req) + if err != nil { + return nil, handleClientError(err) + } + if res.Data == nil { + break + } + for _, v := range res.Data.Cases { + data, err := plugindata.ParseAny(v) + if err != nil { + return nil, diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to parse data", + }} + } + cases = append(cases, data) + if size > 0 && len(cases) == size { + break + } + } + if (size > 0 && len(cases) == size) || res.Data.NextPage == nil { + break + } + req.Page = *res.Data.NextPage + } + return cases, nil + } +} + +func parseListCasesReq(args *dataspec.Block) (*client.ListCasesReq, error) { + if args == nil { + return nil, fmt.Errorf("arguments are required") + } + req := &client.ListCasesReq{} + if attr := args.GetAttrVal("case_ids"); !attr.IsNull() { + ids := attr.AsValueSlice() + for _, id := range ids { + if id.IsNull() { + continue + } + num, _ := id.AsBigFloat().Int64() + req.CaseIDs = append(req.CaseIDs, int(num)) + } + } + if attr := args.GetAttrVal("customer_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.CaseCustomerID = client.Int(int(num)) + } + if attr := args.GetAttrVal("owner_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.CaseOwnerID = client.Int(int(num)) + } + if attr := args.GetAttrVal("severity_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.CaseSeverityID = client.Int(int(num)) + } + if attr := args.GetAttrVal("state_id"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + req.CaseStateID = client.Int(int(num)) + } + if attr := args.GetAttrVal("soc_id"); !attr.IsNull() { + req.CaseSocID = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("start_open_date"); !attr.IsNull() { + req.StartOpenDate = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("end_open_date"); !attr.IsNull() { + req.EndOpenDate = client.String(attr.AsString()) + } + if attr := args.GetAttrVal("sort"); !attr.IsNull() { + req.Sort = client.String(attr.AsString()) + } + return req, nil +} diff --git a/internal/iris/data_iris_cases_test.go b/internal/iris/data_iris_cases_test.go new file mode 100644 index 00000000..44a53c7e --- /dev/null +++ b/internal/iris/data_iris_cases_test.go @@ -0,0 +1,182 @@ +package iris + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/iris/client" + client_mocks "github.com/blackstork-io/fabric/mocks/internalpkg/iris/client" + "github.com/blackstork-io/fabric/plugin" + "github.com/blackstork-io/fabric/plugin/plugindata" + "github.com/blackstork-io/fabric/plugin/plugintest" +) + +type CasesDataSourceTestSuite struct { + suite.Suite + + plugin *plugin.Schema + ctx context.Context + cli *client_mocks.Client + storedApiURL string + storedApiKey string + storedInsecure bool +} + +func TestCasesDataSourceTestSuite(t *testing.T) { + suite.Run(t, new(CasesDataSourceTestSuite)) +} + +func (s *CasesDataSourceTestSuite) SetupSuite() { + s.plugin = Plugin("v0.0.0", func(apiURL, apiKey string, insecure bool) client.Client { + s.storedApiKey = apiKey + s.storedApiURL = apiURL + s.storedInsecure = insecure + return s.cli + }) + s.ctx = context.Background() +} + +func (s *CasesDataSourceTestSuite) SetupTest() { + s.cli = &client_mocks.Client{} +} + +func (s *CasesDataSourceTestSuite) TearDownTest() { + s.cli.AssertExpectations(s.T()) +} + +func (s *CasesDataSourceTestSuite) TestSchema() { + s.Require().NotNil(s.plugin.DataSources["iris_cases"]) + s.NotNil(s.plugin.DataSources["iris_cases"].Config) + s.NotNil(s.plugin.DataSources["iris_cases"].Args) + s.NotNil(s.plugin.DataSources["iris_cases"].DataFunc) +} + +func (s *CasesDataSourceTestSuite) TestLimit() { + s.cli.On("ListCases", mock.Anything, &client.ListCasesReq{ + Page: 1, + Sort: client.String("desc"), + }).Return(&client.ListCasesRes{ + Status: "success", + Data: &client.CasesData{ + CurrentPage: 1, + LastPage: 1, + Total: 1, + Cases: []any{ + map[string]any{ + "id": "1", + }, + }, + }, + }, nil) + res, diags := s.plugin.RetrieveData(s.ctx, "iris_cases", &plugin.RetrieveDataParams{ + Config: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_cases"].Config). + SetAttr("api_url", cty.StringVal("test-url")). + SetAttr("api_key", cty.StringVal("test-key")). + SetAttr("insecure", cty.BoolVal(true)). + Decode(), + Args: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_cases"].Args). + SetAttr("size", cty.NumberIntVal(10)). + Decode(), + }) + s.Equal("test-url", s.storedApiURL) + s.Equal("test-key", s.storedApiKey) + s.Equal(true, s.storedInsecure) + s.Len(diags, 0) + s.Equal(plugindata.List{ + plugindata.Map{ + "id": plugindata.String("1"), + }, + }, res) +} + +func (s *CasesDataSourceTestSuite) TestFull() { + s.cli.On("ListCases", mock.Anything, &client.ListCasesReq{ + Page: 1, + CaseIDs: client.IntList{1, 2}, + CaseCustomerID: client.Int(1), + CaseOwnerID: client.Int(2), + CaseSeverityID: client.Int(4), + CaseStateID: client.Int(3), + CaseSocID: client.String("test-soc"), + Sort: client.String("asc"), + StartOpenDate: client.String("test-start-open-date"), + EndOpenDate: client.String("test-end-open-date"), + }).Return(&client.ListCasesRes{ + Status: "success", + Data: &client.CasesData{ + CurrentPage: 1, + LastPage: 2, + Total: 3, + NextPage: client.Int(2), + Cases: []any{ + map[string]any{ + "id": "1", + }, + }, + }, + }, nil) + s.cli.On("ListCases", mock.Anything, &client.ListCasesReq{ + Page: 2, + CaseIDs: client.IntList{1, 2}, + CaseCustomerID: client.Int(1), + CaseOwnerID: client.Int(2), + CaseSeverityID: client.Int(4), + CaseStateID: client.Int(3), + CaseSocID: client.String("test-soc"), + Sort: client.String("asc"), + StartOpenDate: client.String("test-start-open-date"), + EndOpenDate: client.String("test-end-open-date"), + }).Return(&client.ListCasesRes{ + Status: "success", + Data: &client.CasesData{ + CurrentPage: 2, + LastPage: 2, + Total: 3, + Cases: []any{ + map[string]any{ + "id": "2", + }, + map[string]any{ + "id": "3", + }, + }, + }, + }, nil) + res, diags := s.plugin.RetrieveData(s.ctx, "iris_cases", &plugin.RetrieveDataParams{ + Config: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_cases"].Config). + SetAttr("api_url", cty.StringVal("test-url")). + SetAttr("api_key", cty.StringVal("test-key")). + Decode(), + Args: plugintest.NewTestDecoder(s.T(), s.plugin.DataSources["iris_cases"].Args). + SetAttr("case_ids", cty.ListVal([]cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(2), + })). + SetAttr("customer_id", cty.NumberIntVal(1)). + SetAttr("owner_id", cty.NumberIntVal(2)). + SetAttr("severity_id", cty.NumberIntVal(4)). + SetAttr("state_id", cty.NumberIntVal(3)). + SetAttr("soc_id", cty.StringVal("test-soc")). + SetAttr("start_open_date", cty.StringVal("test-start-open-date")). + SetAttr("end_open_date", cty.StringVal("test-end-open-date")). + SetAttr("sort", cty.StringVal("asc")). + SetAttr("size", cty.NumberIntVal(2)). + Decode(), + }) + s.Equal("test-url", s.storedApiURL) + s.Equal("test-key", s.storedApiKey) + s.Equal(false, s.storedInsecure) + s.Len(diags, 0) + s.Equal(plugindata.List{ + plugindata.Map{ + "id": plugindata.String("1"), + }, + plugindata.Map{ + "id": plugindata.String("2"), + }, + }, res) +} diff --git a/internal/iris/plugin.go b/internal/iris/plugin.go new file mode 100644 index 00000000..e829c522 --- /dev/null +++ b/internal/iris/plugin.go @@ -0,0 +1,57 @@ +package iris + +import ( + "errors" + "fmt" + + "github.com/hashicorp/hcl/v2" + + "github.com/blackstork-io/fabric/internal/iris/client" + "github.com/blackstork-io/fabric/pkg/diagnostics" + "github.com/blackstork-io/fabric/plugin" + "github.com/blackstork-io/fabric/plugin/dataspec" +) + +type ClientLoadFn func(url, apiKey string, insecure bool) client.Client + +var DefaultClientLoader ClientLoadFn = client.New + +func Plugin(version string, loader ClientLoadFn) *plugin.Schema { + if loader == nil { + loader = DefaultClientLoader + } + return &plugin.Schema{ + Name: "blackstork/iris", + Doc: "The `iris` plugin for Iris Incident Response platform.", + Version: version, + DataSources: plugin.DataSources{ + "iris_cases": makeIrisCasesDataSource(loader), + "iris_alerts": makeIrisAlertsDataSource(loader), + }, + } +} + +func parseConfig(cfg *dataspec.Block, loader ClientLoadFn) (client.Client, error) { + if cfg == nil { + return nil, fmt.Errorf("configuration is required") + } + apiURL := cfg.GetAttrVal("api_url").AsString() + apiKey := cfg.GetAttrVal("api_key").AsString() + insecure := cfg.GetAttrVal("insecure").True() + return loader(apiURL, apiKey, insecure), nil +} + +func handleClientError(err error) diagnostics.Diag { + var clientErr *client.Error + if errors.As(err, &clientErr) { + return diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Failed to call Iris API", + Detail: clientErr.Message, + }} + } + return diagnostics.Diag{{ + Severity: hcl.DiagError, + Summary: "Unknown error while calling Iris API", + }} +} diff --git a/internal/iris/plugin_test.go b/internal/iris/plugin_test.go new file mode 100644 index 00000000..fd92aa47 --- /dev/null +++ b/internal/iris/plugin_test.go @@ -0,0 +1,15 @@ +package iris + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPlugin_Schema(t *testing.T) { + schema := Plugin("1.2.3", nil) + assert.Equal(t, "blackstork/iris", schema.Name) + assert.Equal(t, "1.2.3", schema.Version) + assert.NotNil(t, schema.DataSources["iris_cases"]) + assert.NotNil(t, schema.DataSources["iris_alerts"]) +} diff --git a/internal/plugin_validity_test.go b/internal/plugin_validity_test.go index 6366ce65..90545e32 100644 --- a/internal/plugin_validity_test.go +++ b/internal/plugin_validity_test.go @@ -11,6 +11,7 @@ import ( "github.com/blackstork-io/fabric/internal/github" "github.com/blackstork-io/fabric/internal/graphql" "github.com/blackstork-io/fabric/internal/hackerone" + "github.com/blackstork-io/fabric/internal/iris" "github.com/blackstork-io/fabric/internal/microsoft" "github.com/blackstork-io/fabric/internal/nistnvd" "github.com/blackstork-io/fabric/internal/openai" @@ -46,6 +47,7 @@ func TestAllPluginSchemaValidity(t *testing.T) { nistnvd.Plugin(ver, nil), snyk.Plugin(ver, nil), microsoft.Plugin(ver, nil, nil, nil), + iris.Plugin(ver, nil), } for _, p := range plugins { p := p diff --git a/mocks/internalpkg/iris/client/client.go b/mocks/internalpkg/iris/client/client.go new file mode 100644 index 00000000..68e40bc9 --- /dev/null +++ b/mocks/internalpkg/iris/client/client.go @@ -0,0 +1,156 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package client_mocks + +import ( + context "context" + + client "github.com/blackstork-io/fabric/internal/iris/client" + + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +type Client_Expecter struct { + mock *mock.Mock +} + +func (_m *Client) EXPECT() *Client_Expecter { + return &Client_Expecter{mock: &_m.Mock} +} + +// ListAlerts provides a mock function with given fields: ctx, req +func (_m *Client) ListAlerts(ctx context.Context, req *client.ListAlertsReq) (*client.ListAlertsRes, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for ListAlerts") + } + + var r0 *client.ListAlertsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *client.ListAlertsReq) (*client.ListAlertsRes, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *client.ListAlertsReq) *client.ListAlertsRes); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.ListAlertsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *client.ListAlertsReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListAlerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAlerts' +type Client_ListAlerts_Call struct { + *mock.Call +} + +// ListAlerts is a helper method to define mock.On call +// - ctx context.Context +// - req *client.ListAlertsReq +func (_e *Client_Expecter) ListAlerts(ctx interface{}, req interface{}) *Client_ListAlerts_Call { + return &Client_ListAlerts_Call{Call: _e.mock.On("ListAlerts", ctx, req)} +} + +func (_c *Client_ListAlerts_Call) Run(run func(ctx context.Context, req *client.ListAlertsReq)) *Client_ListAlerts_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*client.ListAlertsReq)) + }) + return _c +} + +func (_c *Client_ListAlerts_Call) Return(_a0 *client.ListAlertsRes, _a1 error) *Client_ListAlerts_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListAlerts_Call) RunAndReturn(run func(context.Context, *client.ListAlertsReq) (*client.ListAlertsRes, error)) *Client_ListAlerts_Call { + _c.Call.Return(run) + return _c +} + +// ListCases provides a mock function with given fields: ctx, req +func (_m *Client) ListCases(ctx context.Context, req *client.ListCasesReq) (*client.ListCasesRes, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for ListCases") + } + + var r0 *client.ListCasesRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *client.ListCasesReq) (*client.ListCasesRes, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *client.ListCasesReq) *client.ListCasesRes); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.ListCasesRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *client.ListCasesReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListCases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListCases' +type Client_ListCases_Call struct { + *mock.Call +} + +// ListCases is a helper method to define mock.On call +// - ctx context.Context +// - req *client.ListCasesReq +func (_e *Client_Expecter) ListCases(ctx interface{}, req interface{}) *Client_ListCases_Call { + return &Client_ListCases_Call{Call: _e.mock.On("ListCases", ctx, req)} +} + +func (_c *Client_ListCases_Call) Run(run func(ctx context.Context, req *client.ListCasesReq)) *Client_ListCases_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*client.ListCasesReq)) + }) + return _c +} + +func (_c *Client_ListCases_Call) Return(_a0 *client.ListCasesRes, _a1 error) *Client_ListCases_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListCases_Call) RunAndReturn(run func(context.Context, *client.ListCasesReq) (*client.ListCasesRes, error)) *Client_ListCases_Call { + _c.Call.Return(run) + return _c +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tools/docgen/main.go b/tools/docgen/main.go index 9538df2b..5a8b7a57 100644 --- a/tools/docgen/main.go +++ b/tools/docgen/main.go @@ -21,6 +21,7 @@ import ( "github.com/blackstork-io/fabric/internal/github" "github.com/blackstork-io/fabric/internal/graphql" "github.com/blackstork-io/fabric/internal/hackerone" + "github.com/blackstork-io/fabric/internal/iris" "github.com/blackstork-io/fabric/internal/microsoft" "github.com/blackstork-io/fabric/internal/nistnvd" "github.com/blackstork-io/fabric/internal/openai" @@ -280,6 +281,7 @@ func main() { snyk.Plugin(version, nil), microsoft.Plugin(version, nil, nil, nil), crowdstrike.Plugin(version, nil), + iris.Plugin(version, nil), } // generate markdown for each plugin for _, p := range plugins {