diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index bc0f3af..c9964f5 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -93,6 +93,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Check for CODEOWNERS file uses: andstor/file-existence-action@v2 id: check_codeowners_1 diff --git a/docs/resources/report_graph_query.md b/docs/resources/report_graph_query.md new file mode 100644 index 0000000..6583fa3 --- /dev/null +++ b/docs/resources/report_graph_query.md @@ -0,0 +1,78 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_report_graph_query Resource - terraform-provider-wiz" +subcategory: "" +description: |- + A GraphQL Query Report is an automated query that can be scheduled to run at hourly intervals. +--- + +# wiz_report_graph_query (Resource) + +A GraphQL Query Report is an automated query that can be scheduled to run at hourly intervals. + +## Example Usage + +```terraform +# A simple example +resource "wiz_report_graph_query" "foo" { + name = "foo" + project_id = "2c38b8fa-c315-57ea-9de4-e3a19592d796" + query = < +## Schema + +### Required + +- `name` (String) Name of the Report. +- `query` (String) The query that the report will run. Required by the GRAPH_QUERY report type. + +### Optional + +- `project_id` (String) The ID of the project that this report belongs to (changing this requires re-creatting the report). Defaults to all projects. + - Defaults to `*`. +- `run_interval_hours` (Number) Run interval for scheduled reports (in hours). +- `run_starts_at` (String) String representing the time and date when the scheduling should start (required when run_interval_hours is set). Must be in the following format: 2006-01-02 15:04:05 +0000 UTC. Also, Wiz will always round this down by the hour. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/wiz_report_graph_query/resource.tf b/examples/resources/wiz_report_graph_query/resource.tf new file mode 100644 index 0000000..06daf11 --- /dev/null +++ b/examples/resources/wiz_report_graph_query/resource.tf @@ -0,0 +1,43 @@ +# A simple example +resource "wiz_report_graph_query" "foo" { + name = "foo" + project_id = "2c38b8fa-c315-57ea-9de4-e3a19592d796" + query = < 0 { + return diags + } + + return diags +} diff --git a/internal/provider/resource_report_graph_query.go b/internal/provider/resource_report_graph_query.go new file mode 100644 index 0000000..d670a26 --- /dev/null +++ b/internal/provider/resource_report_graph_query.go @@ -0,0 +1,328 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "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" + + "wiz.io/hashicorp/terraform-provider-wiz/internal" + "wiz.io/hashicorp/terraform-provider-wiz/internal/client" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" +) + +const reportRunStartsAtLayout = "2006-01-02 15:04:05 +0000 UTC" + +func resourceWizReportGraphQuery() *schema.Resource { + return &schema.Resource{ + Description: "A GraphQL Query Report is an automated query that can be scheduled to run at hourly intervals.", + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Report.", + }, + "project_id": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Default: "*", + Description: "The ID of the project that this report belongs to (changing this requires re-creatting the report). Defaults to all projects.", + }, + "query": { + Type: schema.TypeString, + Required: true, + Description: "The query that the report will run. Required by the GRAPH_QUERY report type.", + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsJSON, + ), + }, + "run_interval_hours": { + Type: schema.TypeInt, + Optional: true, + Description: "Run interval for scheduled reports (in hours).", + }, + "run_starts_at": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf( + "String representing the time and date when the scheduling should start (required when run_interval_hours is set). Must be in the following format: %s. Also, Wiz will always round this down by the hour.", + reportRunStartsAtLayout, + ), + }, + }, + CreateContext: resourceWizReportGraphQueryCreate, + ReadContext: resourceWizReportGraphQueryRead, + UpdateContext: resourceWizReportGraphQueryUpdate, + DeleteContext: resourceWizReportDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func setScheduling(diags diag.Diagnostics, d *schema.ResourceData, vars interface{}) diag.Diagnostics { + runIntervalHours, hasOk := d.GetOk("run_interval_hours") + if !hasOk { + return nil + } + + runIntervalHoursVal, _ := runIntervalHours.(int) + runStartsAt, hasOk := d.GetOk("run_starts_at") + if !hasOk { + return append(diags, diag.FromErr(fmt.Errorf("both run_interval_hours ad run_starts_at must be set to enable scheduling"))...) + } + + runStartsAtVal, _ := runStartsAt.(string) + dt, err := time.Parse(reportRunStartsAtLayout, runStartsAtVal) + if err != nil { + return append(diags, diag.FromErr(fmt.Errorf("run_starts_at %s does not match layout %s", runStartsAtVal, reportRunStartsAtLayout))...) + } + + switch vars := vars.(type) { + case *wiz.CreateReportInput: + vars.RunIntervalHours = &runIntervalHoursVal + vars.RunStartsAt = &dt + case *wiz.UpdateReportInput: + vars.Override.RunIntervalHours = &runIntervalHoursVal + vars.Override.RunStartsAt = &dt + default: + return append(diags, diag.FromErr(fmt.Errorf("vars is an invalid ReportInput type"))...) + } + + return nil +} + +func resourceWizReportGraphQueryCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizReportGraphQueryCreate called...") + + query := `mutation createReport( + $input: CreateReportInput! + ) { + createReport( + input: $input + ) { + report { + id + name + params { + ... on ReportParamsGraphQuery { + query + entityOptions { + entityType + propertyOptions { + key + } + } + } + } + type { + id + name + description + } + project { + id + name + } + runIntervalHours + runStartsAt + } + } + }` + + vars := &wiz.CreateReportInput{} + vars.Name = d.Get("name").(string) + projectID, _ := d.Get("project_id").(string) + vars.ProjectID = &projectID + vars.Type = wiz.ReportTypeNameGraphQuery + reportQuery := json.RawMessage(d.Get("query").(string)) + vars.GraphQueryParams = &wiz.CreateReportGraphQueryParamsInput{ + Query: reportQuery, + } + + if diags := setScheduling(diags, d, vars); diags != nil { + return diags + } + + data := &CreateReport{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "report", "create") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + d.SetId(data.CreateReport.Report.ID) + + return resourceWizReportGraphQueryRead(ctx, d, m) +} + +func resourceWizReportGraphQueryRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizReportGraphQueryRead called...") + + if d.Id() == "" { + return nil + } + + query := `query Report ( + $id: ID! + ){ + report( + id: $id + ) { + id + name + params { + ... on ReportParamsGraphQuery { + query + entityOptions { + entityType + propertyOptions { + key + } + } + } + } + type { + id + name + description + } + project { + id + name + } + runIntervalHours + runStartsAt + } + }` + + vars := &internal.QueryVariables{} + vars.ID = d.Id() + + tflog.Info(ctx, fmt.Sprintf("report ID during read: %s", vars.ID)) + + data := &ReadReportPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "report", "read") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + tflog.Info(ctx, "Error from API call, checking if resource was deleted outside Terraform.") + if data.Report.ID == "" { + tflog.Debug(ctx, fmt.Sprintf("Response: (%T) %s", data, utils.PrettyPrint(data))) + tflog.Info(ctx, "Resource not found, marking as new.") + d.SetId("") + d.MarkNewResource() + return nil + } + return diags + } + + err := d.Set("name", data.Report.Name) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + projectID := "*" + if data.Report.Project != nil { + projectID = data.Report.Project.ID + } + + err = d.Set("project_id", projectID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + if data.Report.RunIntervalHours != nil { + err = d.Set("run_interval_hours", data.Report.RunIntervalHours) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + } + + if data.Report.RunStartsAt != nil { + runStartsAt := data.Report.RunStartsAt.Format(reportRunStartsAtLayout) + err = d.Set("run_starts_at", runStartsAt) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + } + + switch params := data.Report.Params.(type) { + case wiz.ReportParamsGraphQuery: + err = d.Set("query", params.Query) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + } + + return diags +} + +func resourceWizReportGraphQueryUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizReportGraphQueryUpdate called...") + + if d.Id() == "" { + return nil + } + + query := `mutation UpdateReport( + $input: UpdateReportInput! + ) { + updateReport( + input: $input + ) { + report { + id + name + params { + ... on ReportParamsGraphQuery { + query + entityOptions { + entityType + propertyOptions { + key + } + } + } + } + type { + id + name + description + } + project { + id + name + } + runIntervalHours + runStartsAt + } + } + }` + + vars := &wiz.UpdateReportInput{} + vars.ID = d.Id() + vars.Override = &wiz.UpdateReportChange{} + vars.Override.GraphQueryParams = &wiz.UpdateReportGraphQueryParamsInput{} + reportQuery, _ := d.Get("query").(string) + vars.Override.GraphQueryParams.Query = json.RawMessage(reportQuery) + vars.Override.Name = d.Get("name").(string) + + if diags := setScheduling(diags, d, vars); diags != nil { + return diags + } + + data := &UpdateReport{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "report", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return resourceWizReportGraphQueryRead(ctx, d, m) +} diff --git a/internal/wiz/enums.go b/internal/wiz/enums.go index aadd232..3f10337 100644 --- a/internal/wiz/enums.go +++ b/internal/wiz/enums.go @@ -192,6 +192,9 @@ var IACScanSeverity = []string{ "CRITICAL", } +// ReportTypeNameGraphQuery alias +const ReportTypeNameGraphQuery = "GRAPH_QUERY" + // Severity enum var Severity = []string{ "INFORMATIONAL", diff --git a/internal/wiz/structs.go b/internal/wiz/structs.go index 7745bb6..be9b8bc 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -2,6 +2,7 @@ package wiz import ( "encoding/json" + "time" "wiz.io/hashicorp/terraform-provider-wiz/internal" ) @@ -2394,3 +2395,184 @@ type UpdateConnectorPatch struct { AuthParams json.RawMessage `json:"authParams,omitempty"` ExtraConfig json.RawMessage `json:"extraConfig,omitempty"` } + +// CreateReportPayload struct +type CreateReportPayload struct { + Report Report `json:"report,omitempty"` +} + +// CreateReportInput struct +type CreateReportInput struct { + Name string `json:"name"` + Type string `json:"type"` + ProjectID *string `json:"projectId,omitempty"` + RunIntervalHours *int `json:"runIntervalHours,omitempty"` + RunStartsAt *time.Time `json:"runStartsAt,omitempty"` + EmailTargetParams *EmailTargetParams `json:"emailTargetParams,omitempty"` + GraphQueryParams *CreateReportGraphQueryParamsInput `json:"graphQueryParams,omitempty"` + ColumnSelection []string `json:"columnSelection,omitempty"` + CSVDelimiter *CSVDelimiter `json:"csvDelimiter,omitempty"` + ExportDestinations []CreateReportExportDestinationInput `json:"exportDestinations,omitempty"` +} + +// CSVDelimiter alias +type CSVDelimiter = string + +// EmailTargetParams struct +type EmailTargetParams struct { + To []string `json:"to"` +} + +// CreateReportExportDestinationInput struct +// NOTE: this is incomplete, and there is no CreateReportExportDestinationCloudStorageInput yet +type CreateReportExportDestinationInput struct { + Snowflake *CreateReportExportDestinationSnowflakeInput `json:"snowflake,omitempty"` +} + +// CreateReportExportDestinationSnowflakeInput struct +type CreateReportExportDestinationSnowflakeInput struct { + IntegrationID string `json:"integrationId"` + Database string `json:"database"` + Schema string `json:"schema"` + Table string `json:"table"` +} + +// CreateReportGraphQueryParamsInput struct +type CreateReportGraphQueryParamsInput struct { + Query GraphEntityQueryValue `json:"query"` + EntityOptions []CreateReportGraphQueryEntityOptions `json:"entityOptions,omitempty"` +} + +// GraphEntityQueryValue alias +type GraphEntityQueryValue = json.RawMessage + +// GraphEntityTypeValue alias +type GraphEntityTypeValue = string + +// CreateReportGraphQueryEntityOptions struct +type CreateReportGraphQueryEntityOptions struct { + EntityType GraphEntityTypeValue `json:"entityType"` + PropertyOptions []CreateReportGraphQueryPropertyOptions `json:"propertyOptions"` +} + +// CreateReportGraphQueryPropertyOptions struct +type CreateReportGraphQueryPropertyOptions struct { + Key string `json:"key"` +} + +// Report struct +type Report struct { + ID string `json:"id"` + Name string `json:"name"` + Type ReportType `json:"type"` + Project *Project `json:"project"` + Params ReportParams `json:"params"` + LastRun *ReportRun `json:"lastRun"` + LastSuccessfulRun *ReportRun `json:"lastSuccessfulRun"` + RunStartsAt *time.Time `json:"runStartsAt"` + EmailTarget *EmailTarget `json:"emailTarget"` + NextRunAt *time.Time `json:"nextRunAt"` + RunIntervalHours *int `json:"runIntervalHours"` + CreatedBy User `json:"createdBy"` + ColumnSelection []string `json:"columnSelection"` + CSVDelimiter *CSVDelimiter `json:"csvDelimiter"` + ExportDestinations []ReportExportDestination `json:"exportDestinations"` +} + +// ReportParams interface +type ReportParams interface{} + +// ReportParamsGraphQuery struct +type ReportParamsGraphQuery struct { + Query GraphEntityQueryValue `json:"query"` + EntityOptions []ReportGraphQueryEntityOptions `json:"entityOptions,omitempty"` +} + +// ReportGraphQueryEntityOptions interface +type ReportGraphQueryEntityOptions interface{} + +// ReportExportDestination interface +type ReportExportDestination interface{} + +// ReportRun struct +type ReportRun struct{} + +// ReportType struct +type ReportType struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` +} + +// EmailTarget struct +type EmailTarget struct{} + +// ReportExportDestinationSnowflake struct +type ReportExportDestinationSnowflake struct { + Integration Integration `json:"integration"` + Database string `json:"database"` + Schema string `json:"schema"` + Table string `json:"table"` +} + +// UpdateReportInput struct +type UpdateReportInput struct { + ID string `json:"id"` + Override *UpdateReportChange `json:"override,omitempty"` +} + +// UpdateReportChange struct +type UpdateReportChange struct { + Name string `json:"name"` + RunIntervalHours *int `json:"runIntervalHours,omitempty"` + RunStartsAt *time.Time `json:"runStartsAt,omitempty"` + EmailTargetParams *EmailTargetParams `json:"emailTargetParams,omitempty"` + GraphQueryParams *UpdateReportGraphQueryParamsInput `json:"graphQueryParams,omitempty"` + ColumnSelection []string `json:"columnSelection,omitempty"` + CSVDelimiter *CSVDelimiter `json:"csvDelimiter,omitempty"` + ExportDestinations []UpdateReportExportDestinationInput `json:"exportDestinations,omitempty"` +} + +// UpdateReportGraphQueryParamsInput struct +type UpdateReportGraphQueryParamsInput struct { + Query GraphEntityQueryValue `json:"query"` + EntityOptions []UpdateReportGraphQueryEntityOptions `json:"entityOptions"` + Type *GraphSearchExportType `json:"type,omitempty"` +} + +// UpdateReportExportDestinationInput struct +type UpdateReportExportDestinationInput struct { + Snowflake *UpdateReportExportDestinationSnowflakeInput `json:"snowflake,omitempty"` +} + +// UpdateReportGraphQueryEntityOptions struct +type UpdateReportGraphQueryEntityOptions struct { + EntityType GraphEntityTypeValue `json:"entityType"` + PropertyOptions []UpdateReportGraphQueryPropertyOptions `json:"propertyOptions"` +} + +// UpdateReportGraphQueryPropertyOptions struct +type UpdateReportGraphQueryPropertyOptions struct { + Key string `json:"key"` +} + +// GraphSearchExportType alias +type GraphSearchExportType = string + +// UpdateReportExportDestinationSnowflakeInput struct +type UpdateReportExportDestinationSnowflakeInput struct { + IntegrationID string `json:"integrationId"` + Database string `json:"database"` + Schema string `json:"schema"` + Table string `json:"table"` +} + +// DeleteReportPayload struct +type DeleteReportPayload struct { + Stub string `json:"_stub"` +} + +// DeleteReportInput struct +type DeleteReportInput struct { + ID string `json:"id"` +}