diff --git a/docs/resources/tag.md b/docs/resources/tag.md new file mode 100644 index 00000000..808f826e --- /dev/null +++ b/docs/resources/tag.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "prismacloudcompute_tag Resource - terraform-provider-prismacloudcompute" +subcategory: "" +description: |- + +--- + +# prismacloudcompute_tag (Resource) + + + + + + +## Schema + +### Required + +- `name` (String) A unique tag name. + +### Optional + +- `assignment` (Block List) Specify how vulnerabilities are tagged, based on CVE ID, package, and resources. (see [below for nested schema](#nestedblock--assignment)) +- `color` (String) A hex color code for the tag to display in the Console. +- `description` (String) A free-form text description of the tag. + +### Read-Only + +- `id` (String) The ID of the tag. + + +### Nested Schema for `assignment` + +Optional: + +- `check_base_layer` (Boolean) Whether or not to check the base layer. +- `comment` (String) Free-form text field. +- `id` (String) Common Vulnerability and Exposures (CVE) ID. +- `package_name` (String) Source or binary package name where the vulnerability is found. +- `resource_type` (String) Specifies the resource type for tagging where the vulnerability is found. +- `resources` (List of String) Resource names separated by a comma or use the wildcard * to apply the tag to all the resources where the vulnerability is found. diff --git a/internal/api/tag/tag.go b/internal/api/tag/tag.go new file mode 100755 index 00000000..2c32ff00 --- /dev/null +++ b/internal/api/tag/tag.go @@ -0,0 +1,87 @@ +package tag + +import ( + "fmt" + "net/http" + + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api" +) + +const TagsEndpoint = "api/v1/tags" + +type Vuln struct { + CheckBaseLayer bool `json:"checkBaseLayer,omitempty"` + Comment string `json:"comment,omitempty"` + Id string `json:"id,omitempty"` + PackageName string `json:"packageName,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + Resources []string `json:"resources,omitempty"` +} + +type Tag struct { + Color string `json:"color,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Vulns []Vuln `json:"vulns,omitempty"` +} + +// Get all tags. +func ListTags(c api.Client) ([]Tag, error) { + var ans []Tag + if err := c.Request(http.MethodGet, TagsEndpoint, nil, nil, &ans); err != nil { + return nil, fmt.Errorf("error listing tags: %s", err) + } + return ans, nil +} + +// Get a specific tag. +func GetTag(c api.Client, name string) (*Tag, error) { + tags, err := ListTags(c) + if err != nil { + return nil, err + } + for _, val := range tags { + if val.Name == name { + if val.Vulns == nil { + val.Vulns = []Vuln{} + } + return &val, nil + } + } + return nil, fmt.Errorf("tag '%s' not found", name) +} + +// Create a new tag. +func CreateTag(c api.Client, tag Tag) error { + // there's a bug in Prisma that causes tags to not work correctly if created + // with vulns, so we create it without vulns and then add them individually + tagWithoutVulns := tag + tagWithoutVulns.Vulns = []Vuln{} + + err := c.Request(http.MethodPost, TagsEndpoint, nil, tagWithoutVulns, nil) + if err != nil { + return err + } + + return createVuln(c, tag, tag.Vulns) +} + +func createVuln(c api.Client, tag Tag, vuln []Vuln) error { + for _, val := range vuln { + err := c.Request(http.MethodPost, fmt.Sprintf("%s/%s/vuln", TagsEndpoint, tag.Name), nil, val, nil) + if err != nil { + return err + } + } + return nil +} + +// Update an existing tag. +func UpdateTag(c api.Client, tag Tag) error { + return c.Request(http.MethodPut, fmt.Sprintf("%s/%s", TagsEndpoint, tag.Name), nil, tag, nil) +} + +// Delete an existing tag. +func DeleteTag(c api.Client, name string) error { + return c.Request(http.MethodDelete, fmt.Sprintf("%s/%s", TagsEndpoint, name), nil, nil, nil) +} diff --git a/internal/convert/tag.go b/internal/convert/tag.go new file mode 100644 index 00000000..79ba1683 --- /dev/null +++ b/internal/convert/tag.go @@ -0,0 +1,49 @@ +package convert + +import ( + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api/tag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Converts a tag schema to a tag object for SDK compatibility. +func SchemaToTag(d *schema.ResourceData) tag.Tag { + ans := tag.Tag{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Color: d.Get("color").(string), + Vulns: []tag.Vuln{}, + } + if assignments, ok := d.GetOk("assignment"); ok { + presentAssignments := assignments.([]interface{}) + for _, val := range presentAssignments { + presentAssignment := val.(map[string]interface{}) + parsedAssignment := tag.Vuln{ + CheckBaseLayer: presentAssignment["check_base_layer"].(bool), + Comment: presentAssignment["comment"].(string), + Id: presentAssignment["id"].(string), + PackageName: presentAssignment["package_name"].(string), + ResourceType: presentAssignment["resource_type"].(string), + Resources: SchemaToStringSlice(presentAssignment["resources"].([]interface{})), + } + if len(parsedAssignment.Resources) == 0 { + parsedAssignment.Resources = []string{"*"} + } + ans.Vulns = append(ans.Vulns, parsedAssignment) + } + } + return ans +} + +func TagVulnsToSchema(in []tag.Vuln) []interface{} { + ans := make([]interface{}, 0, len(in)) + for _, val := range in { + m := make(map[string]interface{}) + m["resources"] = val.Resources + m["comment"] = val.Comment + m["id"] = val.Id + m["resource_type"] = val.ResourceType + m["package_name"] = val.PackageName + ans = append(ans, m) + } + return ans +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 06064333..b9c1085c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -77,6 +77,7 @@ func Provider() *schema.Provider { "prismacloudcompute_credential": resourceCredentials(), "prismacloudcompute_custom_compliance": resourceCustomCompliance(), "prismacloudcompute_cloud_account": resourceCloudAccount(), + "prismacloudcompute_tag": resourceTag(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/internal/provider/resource_tag.go b/internal/provider/resource_tag.go new file mode 100644 index 00000000..19644d98 --- /dev/null +++ b/internal/provider/resource_tag.go @@ -0,0 +1,169 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api" + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api/tag" + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/convert" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceTag() *schema.Resource { + return &schema.Resource{ + CreateContext: createTag, + ReadContext: readTag, + UpdateContext: updateTag, + DeleteContext: deleteTag, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of the tag.", + Type: schema.TypeString, + Computed: true, + }, + "color": { + Type: schema.TypeString, + Optional: true, + Description: "A hex color code for the tag to display in the Console.", + Default: "#A020F0", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "A free-form text description of the tag.", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "A unique tag name.", + }, + "assignment": { + Type: schema.TypeList, + Optional: true, + Description: "Specify how vulnerabilities are tagged, based on CVE ID, package, and resources.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "check_base_layer": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether or not to check the base layer.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Free-form text field.", + }, + "id": { + Type: schema.TypeString, + Required: true, + Description: "Common Vulnerability and Exposures (CVE) ID.", + }, + "package_name": { + Type: schema.TypeString, + Optional: true, + Description: "Source or binary package name where the vulnerability is found.", + Default: "*", + }, + "resource_type": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: "Specifies the resource type for tagging where the vulnerability is found.", + ValidateDiagFunc: func(v interface{}, p cty.Path) diag.Diagnostics { + validValues := []string{"image", "host", "function", "codeRepo"} + value := v.(string) + + for _, item := range validValues { + if item == value { + return diag.Diagnostics{} + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid resource_type", + Detail: fmt.Sprintf("Valid values are %v", validValues), + }, + } + }, + }, + "resources": { + Type: schema.TypeList, + Optional: true, + Description: "Resource names separated by a comma or use the wildcard * to apply the tag to all the resources where the vulnerability is found.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + } +} + +func createTag(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.Client) + parsedTag := convert.SchemaToTag(d) + if err := tag.CreateTag(*client, parsedTag); err != nil { + return diag.Errorf("error creating tag'%+v': %s", parsedTag, err) + } + + d.SetId(parsedTag.Name) + + return readTag(ctx, d, meta) +} + +func readTag(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.Client) + + var diags diag.Diagnostics + + retrievedTag, err := tag.GetTag(*client, d.Id()) + if err != nil { + return diag.Errorf("error reading tag: %s", err) + } + + if err := d.Set("assignment", convert.TagVulnsToSchema(retrievedTag.Vulns)); err != nil { + return diag.Errorf("error reading tag: %s, %v", err, retrievedTag.Vulns) + } + d.Set("color", retrievedTag.Color) + d.Set("description", retrievedTag.Description) + d.Set("name", retrievedTag.Name) + + return diags +} + +func updateTag(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.Client) + + parsedTag := convert.SchemaToTag(d) + + if err := tag.UpdateTag(*client, parsedTag); err != nil { + return diag.Errorf("error updating tag: %s", err) + } + + return readTag(ctx, d, meta) +} + +func deleteTag(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.Client) + + var diags diag.Diagnostics + + if err := tag.DeleteTag(*client, d.Id()); err != nil { + return diag.Errorf("error updating tag '%s': %s", d.Id(), err) + } + + d.SetId("") + + return diags +} diff --git a/internal/provider/resource_tag_test.go b/internal/provider/resource_tag_test.go new file mode 100755 index 00000000..6bd45f13 --- /dev/null +++ b/internal/provider/resource_tag_test.go @@ -0,0 +1,146 @@ +package provider + +import ( + "bytes" + "fmt" + "reflect" + "testing" + + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api" + "github.com/PaloAltoNetworks/terraform-provider-prismacloudcompute/internal/api/tag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccTagConfig(t *testing.T) { + fmt.Printf("\n\nStart TestAccTagConfig") + var o tag.Tag + name := fmt.Sprintf("tf%s", acctest.RandString(6)) + description := fmt.Sprintf("tf%s", acctest.RandString(6)) + vuln := []tag.Vuln{{ + Id: "CVE-2024-0001", + PackageName: "*", + ResourceType: "*", + }} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTagConfig(name, description, nil), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagExists("prismacloudcompute_tag.test", &o), + testAccCheckTagAttributes(&o, name, description, "#A020F0", []tag.Vuln{}), + ), + }, + { + Config: testAccTagConfig(name, description, &vuln), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagExists("prismacloudcompute_tag.test", &o), + testAccCheckTagAttributes(&o, name, description, "#A020F0", vuln), + ), + }, + }, + }) +} + +func testAccCheckTagExists(n string, o *tag.Tag) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Object label Name is not set") + } + + client := testAccProvider.Meta().(*api.Client) + name := rs.Primary.ID + lo, err := tag.GetTag(*client, name) + if err != nil { + return fmt.Errorf("Error in get: %s", err) + } + + *o = *lo + + return nil + } +} + +func testAccCheckTagAttributes(o *tag.Tag, name string, description string, color string, vulns []tag.Vuln) resource.TestCheckFunc { + return func(s *terraform.State) error { + if o.Name != name { + return fmt.Errorf("\n\nName is %s, expected %s", o.Name, name) + } + + if o.Description != description { + return fmt.Errorf("Description is %s, expected %s", o.Description, description) + } + + if o.Color != color { + return fmt.Errorf("Color type is %q, expected %q", o.Color, color) + } + + if !reflect.DeepEqual(o.Vulns, vulns) { + return fmt.Errorf("Vulns is %#v, expected %#v", o.Vulns, vulns) + } + + return nil + } +} + +func testAccTagDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + + if rs.Type != "prismacloudcompute_tag" { + continue + } + + if rs.Primary.ID != "" { + name := rs.Primary.ID + if err := tag.DeleteTag(*client, name); err == nil { + return fmt.Errorf("Object %q still exists", name) + } + } + return nil + } + + return nil +} + +func testAccTagConfig(name string, description string, vulns *[]tag.Vuln) string { + var buf bytes.Buffer + buf.Grow(500) + + if vulns == nil { + buf.WriteString(fmt.Sprintf(` +resource "prismacloudcompute_tag" "test" { +name = %q +description = %q +}`, name, description)) + } else { + buf.WriteString(fmt.Sprintf(` +resource "prismacloudcompute_tag" "test" { +name = %q +description = %q +`, name, description)) + + for _, vuln := range *vulns { + buf.WriteString(fmt.Sprintf(` +assignment { +id = %q +} +`, vuln.Id)) + } + + buf.WriteString("}") + } + + return buf.String() +}