From 2036f3442438d018050522de8814bf26d350d24f Mon Sep 17 00:00:00 2001 From: Subhajit Kumar Mondal Date: Fri, 29 Jan 2021 16:12:01 +0530 Subject: [PATCH] Add gcp firewall table. Closes #42 --- .../gcp_compute_firewall/dependencies.txt | 0 .../test-get-expected.json | 30 +++ .../gcp_compute_firewall/test-get-query.sql | 3 + .../test-hydrate-expected.json | 10 + .../test-hydrate-query.sql | 3 + .../test-invalid-name-expected.json | 1 + .../test-invalid-name-query.sql | 3 + .../test-list-expected.json | 6 + .../gcp_compute_firewall/test-list-query.sql | 3 + .../test-not-found-expected.json | 1 + .../test-not-found-query.sql | 3 + .../test-turbot-expected.json | 8 + .../test-turbot-query.sql | 3 + .../tests/gcp_compute_firewall/variables.json | 1 + .../tests/gcp_compute_firewall/variables.tf | 76 ++++++ gcp/plugin.go | 1 + gcp/table_gcp_compute_firewall.go | 229 ++++++++++++++++++ 17 files changed, 381 insertions(+) create mode 100644 gcp-test/tests/gcp_compute_firewall/dependencies.txt create mode 100644 gcp-test/tests/gcp_compute_firewall/test-get-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-get-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/test-hydrate-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-hydrate-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/test-invalid-name-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-invalid-name-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/test-list-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-list-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/test-not-found-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-not-found-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/test-turbot-expected.json create mode 100644 gcp-test/tests/gcp_compute_firewall/test-turbot-query.sql create mode 100644 gcp-test/tests/gcp_compute_firewall/variables.json create mode 100644 gcp-test/tests/gcp_compute_firewall/variables.tf create mode 100644 gcp/table_gcp_compute_firewall.go diff --git a/gcp-test/tests/gcp_compute_firewall/dependencies.txt b/gcp-test/tests/gcp_compute_firewall/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/gcp-test/tests/gcp_compute_firewall/test-get-expected.json b/gcp-test/tests/gcp_compute_firewall/test-get-expected.json new file mode 100644 index 00000000..489c3a9b --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-get-expected.json @@ -0,0 +1,30 @@ +[ + { + "action": "Allow", + "allowed": [ + { + "IPProtocol": "icmp" + }, + { + "IPProtocol":"tcp", + "ports":[ + "80", + "8080", + "1000-2000" + ] + } + ], + "description": "Test firewall rule to verify the table.", + "direction": "INGRESS", + "disabled": false, + "kind": "compute#firewall", + "log_config_enable": false, + "name": "{{ resourceName }}", + "network": "{{ output.network.value }}", + "project": "{{ output.project_id.value }}", + "self_link": "{{ output.self_link.value }}", + "source_tags": [ + "web" + ] + } +] \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-get-query.sql b/gcp-test/tests/gcp_compute_firewall/test-get-query.sql new file mode 100644 index 00000000..ed883bda --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-get-query.sql @@ -0,0 +1,3 @@ +select name, direction, description, kind, disabled, self_link, action, project, network, log_config_enable, allowed, source_tags +from gcp.gcp_compute_firewall +where name = '{{ resourceName }}' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-hydrate-expected.json b/gcp-test/tests/gcp_compute_firewall/test-hydrate-expected.json new file mode 100644 index 00000000..f9366816 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-hydrate-expected.json @@ -0,0 +1,10 @@ +[ + { + "action": "Allow", + "description": "Test firewall rule to verify the table.", + "direction": "INGRESS", + "kind": "compute#firewall", + "name": "{{ resourceName }}", + "self_link": "{{ output.self_link.value }}" + } +] \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-hydrate-query.sql b/gcp-test/tests/gcp_compute_firewall/test-hydrate-query.sql new file mode 100644 index 00000000..b47bf491 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-hydrate-query.sql @@ -0,0 +1,3 @@ +select name, direction, description, kind, action, self_link +from gcp.gcp_compute_firewall +where name = '{{ resourceName }}' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-invalid-name-expected.json b/gcp-test/tests/gcp_compute_firewall/test-invalid-name-expected.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-invalid-name-expected.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-invalid-name-query.sql b/gcp-test/tests/gcp_compute_firewall/test-invalid-name-query.sql new file mode 100644 index 00000000..ad0999b8 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-invalid-name-query.sql @@ -0,0 +1,3 @@ +select name, id, description +from gcp.gcp_compute_firewall +where name = '' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-list-expected.json b/gcp-test/tests/gcp_compute_firewall/test-list-expected.json new file mode 100644 index 00000000..a701eb44 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-list-expected.json @@ -0,0 +1,6 @@ +[ + { + "description": "Test firewall rule to verify the table.", + "name": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-list-query.sql b/gcp-test/tests/gcp_compute_firewall/test-list-query.sql new file mode 100644 index 00000000..f15d3792 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-list-query.sql @@ -0,0 +1,3 @@ +select name, description +from gcp.gcp_compute_firewall +where title = '{{ resourceName }}' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-not-found-expected.json b/gcp-test/tests/gcp_compute_firewall/test-not-found-expected.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-not-found-expected.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-not-found-query.sql b/gcp-test/tests/gcp_compute_firewall/test-not-found-query.sql new file mode 100644 index 00000000..7f4104c1 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, id, direction, kind +from gcp.gcp_compute_firewall +where name = 'dummy-{{ resourceName }}' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-turbot-expected.json b/gcp-test/tests/gcp_compute_firewall/test-turbot-expected.json new file mode 100644 index 00000000..3ac169de --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-turbot-expected.json @@ -0,0 +1,8 @@ +[ + { + "akas": [ + "{{ output.resource_aka.value }}" + ], + "title": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/test-turbot-query.sql b/gcp-test/tests/gcp_compute_firewall/test-turbot-query.sql new file mode 100644 index 00000000..91ed249d --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/test-turbot-query.sql @@ -0,0 +1,3 @@ +select title, akas +from gcp.gcp_compute_firewall +where name = '{{ resourceName }}' \ No newline at end of file diff --git a/gcp-test/tests/gcp_compute_firewall/variables.json b/gcp-test/tests/gcp_compute_firewall/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/variables.json @@ -0,0 +1 @@ +{} diff --git a/gcp-test/tests/gcp_compute_firewall/variables.tf b/gcp-test/tests/gcp_compute_firewall/variables.tf new file mode 100644 index 00000000..34022ec8 --- /dev/null +++ b/gcp-test/tests/gcp_compute_firewall/variables.tf @@ -0,0 +1,76 @@ + +variable "resource_name" { + type = string + default = "turbot-test-20200125-create-update" + description = "Name of the resource used throughout the test." +} + +variable "gcp_project" { + type = string + default = "niteowl-aaa" + description = "GCP project used for the test." +} + +variable "gcp_region" { + type = string + default = "us-east1" + description = "GCP region used for the test." +} + +provider "google" { + project = var.gcp_project + region = var.gcp_region +} + +data "google_client_config" "current" {} + +data "null_data_source" "resource" { + inputs = { + scope = "gcp://cloudresourcemanager.googleapis.com/projects/${data.google_client_config.current.project}" + } +} + +resource "google_compute_network" "named_test_resource" { + name = var.resource_name +} + +resource "google_compute_firewall" "named_test_resource" { + name = var.resource_name + network = google_compute_network.named_test_resource.name + description = "Test firewall rule to verify the table." + + allow { + protocol = "icmp" + } + + allow { + protocol = "tcp" + ports = ["80", "8080", "1000-2000"] + } + + source_tags = ["web"] +} + +output "resource_aka" { + value = "gcp://compute.googleapis.com/${google_compute_firewall.named_test_resource.id}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = google_compute_firewall.named_test_resource.id +} + +output "self_link" { + value = google_compute_firewall.named_test_resource.self_link +} + +output "network" { + value = google_compute_network.named_test_resource.self_link +} + +output "project_id" { + value = var.gcp_project +} diff --git a/gcp/plugin.go b/gcp/plugin.go index 0b1f6a54..195d5ed4 100644 --- a/gcp/plugin.go +++ b/gcp/plugin.go @@ -27,6 +27,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "gcp_audit_policy": tableGcpAuditPolicy(ctx), "gcp_cloudfunctions_function": tableGcpCloudfunctionFunction(ctx), "gcp_compute_address": tableGcpComputeAddress(ctx), + "gcp_compute_firewall": tableGcpComputeFirewall(ctx), "gcp_compute_global_address": tableGcpComputeGlobalAddress(ctx), "gcp_compute_global_forwarding_rule": tableGcpComputeGlobalForwardingRule(ctx), "gcp_compute_instance": tableGcpComputeInstance(ctx), diff --git a/gcp/table_gcp_compute_firewall.go b/gcp/table_gcp_compute_firewall.go new file mode 100644 index 00000000..f9c45845 --- /dev/null +++ b/gcp/table_gcp_compute_firewall.go @@ -0,0 +1,229 @@ +package gcp + +import ( + "context" + + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" + + "google.golang.org/api/compute/v1" +) + +//// TABLE DEFINITION + +func tableGcpComputeFirewall(ctx context.Context) *plugin.Table { + return &plugin.Table{ + Name: "gcp_compute_firewall", + Description: "GCP Compute Firewall", + Get: &plugin.GetConfig{ + KeyColumns: plugin.SingleColumn("name"), + Hydrate: getComputeFirewall, + }, + List: &plugin.ListConfig{ + Hydrate: listComputeFirewalles, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "A friendly name that identifies the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The unique identifier for the resource.", + Type: proto.ColumnType_INT, + }, + { + Name: "direction", + Description: "Direction of traffic to which this firewall applies.", + Type: proto.ColumnType_STRING, + }, + { + Name: "kind", + Description: "Specifies the type of the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "disabled", + Description: "Indicates whether the firewall rule is disabled, or not.", + Type: proto.ColumnType_BOOL, + }, + { + Name: "description", + Description: "A user-specified, human-readable description of the firewall.", + Type: proto.ColumnType_STRING, + }, + { + Name: "creation_timestamp", + Description: "The creation timestamp of the resource.", + Type: proto.ColumnType_TIMESTAMP, + }, + { + Name: "action", + Description: "Describes the type action specified by the rule.", + Type: proto.ColumnType_STRING, + Transform: transform.FromP(gcpComputeFirewallTurbotData, "Action"), + }, + { + Name: "log_config_enable", + Description: "Specifies whether to enable logging for a particular firewall rule, or not.", + Type: proto.ColumnType_BOOL, + Transform: transform.FromField("LogConfig.Enable"), + }, + { + Name: "log_config_metadata", + Description: "Specifies whether to include or exclude metadata for firewall logs.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("LogConfig.Metadata"), + }, + { + Name: "network", + Description: "The URL of the network resource for this firewall rule.", + Type: proto.ColumnType_STRING, + }, + { + Name: "priority", + Description: "Specifies the priority for this rule. Relative priorities determine which rule takes effect if multiple rules apply. Lower values indicate higher priority.", + Type: proto.ColumnType_INT, + }, + { + Name: "self_link", + Description: "The server-defined URL for the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "allowed", + Description: "The list of ALLOW rules specified by this firewall.", + Type: proto.ColumnType_JSON, + }, + { + Name: "denied", + Description: "The list of DENY rules specified by this firewall.", + Type: proto.ColumnType_JSON, + }, + { + Name: "destination_ranges", + Description: "A list of CIDR ranges. The firewall rule applies only to traffic that has destination IP address in these ranges.", + Type: proto.ColumnType_JSON, + }, + { + Name: "source_ranges", + Description: "A list of CIDR ranges. The firewall rule applies only to traffic originating from an instance with a service account in this list.", + Type: proto.ColumnType_JSON, + }, + { + Name: "source_service_accounts", + Description: "A list of service account. The firewall rule applies only to traffic that has a source IP address in these ranges.", + Type: proto.ColumnType_JSON, + }, + { + Name: "source_tags", + Description: "A list of tags. The firewall rule applies only to traffic with source IPs that match the primary network interfaces of VM instances that have the tag and are in the same VPC network.", + Type: proto.ColumnType_JSON, + }, + { + Name: "target_service_accounts", + Description: "A list of service accounts indicating sets of instances located in the network that may make network connections as specified in Allowed", + Type: proto.ColumnType_JSON, + }, + { + Name: "target_tags", + Description: "A list of tags that controls which instances the firewall rule applies to.", + Type: proto.ColumnType_JSON, + }, + + // standard steampipe columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromP(gcpComputeFirewallTurbotData, "Akas"), + }, + + // standard gcp columns + { + Name: "project", + Description: ColumnDescriptionProject, + Type: proto.ColumnType_STRING, + Transform: transform.FromConstant(activeProject()), + }, + }, + } +} + +//// LIST FUNCTION + +func listComputeFirewalles(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listComputeFirewalles") + service, err := compute.NewService(ctx) + if err != nil { + return nil, err + } + + project := activeProject() + resp := service.Firewalls.List(project) + if err := resp.Pages(ctx, func(page *compute.FirewallList) error { + for _, firewall := range page.Items { + d.StreamListItem(ctx, firewall) + } + return nil + }); err != nil { + return nil, err + } + + return nil, nil +} + +//// HYDRATE FUNCTIONS + +func getComputeFirewall(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + service, err := compute.NewService(ctx) + if err != nil { + return nil, err + } + + name := d.KeyColumnQuals["name"].GetStringValue() + project := activeProject() + + // Error: pq: rpc error: code = Unknown desc = json: invalid use of ,string struct tag, + // trying to unmarshal "projects/project/global/firewalls/" into uint64 + if len(name) < 1 { + return nil, nil + } + + req, err := service.Firewalls.Get(project, name).Do() + if err != nil { + return nil, err + } + + return req, nil +} + +//// TRANSFORM FUNCTIONS + +func gcpComputeFirewallTurbotData(_ context.Context, d *transform.TransformData) (interface{}, error) { + firewall := d.HydrateItem.(*compute.Firewall) + param := d.Param.(string) + + var action string + if firewall.Allowed != nil { + action = "Allow" + } + if firewall.Denied != nil { + action = "Deny" + } + + turbotData := map[string]interface{}{ + "Action": action, + "Akas": []string{"gcp://compute.googleapis.com/projects/" + activeProject() + "/global/firewalls/" + firewall.Name}, + } + + return turbotData[param], nil +}