diff --git a/docs/tables/gcp_compute_firewall.md b/docs/tables/gcp_compute_firewall.md new file mode 100644 index 00000000..46c023b8 --- /dev/null +++ b/docs/tables/gcp_compute_firewall.md @@ -0,0 +1,62 @@ +# Table: gcp_compute_firewall + +VPC firewall rules allows or denies connections to or from your virtual machine (VM) instances based on a specified configuration. Enabled VPC firewall rules are always enforced, protecting instances regardless of their configuration and operating system, even if they have not started up. + +### Firewall rules basic info + +```sql +select + name, + id, + description, + direction +from + gcp_compute_firewall; +``` + + +### List of rules which are applied to TCP protocol + +```sql +select + name, + id, + p ->> 'IPProtocol' as ip_protocol, + p ->> 'ports' as ports +from + gcp_compute_firewall, + jsonb_array_elements(allowed) as p +where + p ->> 'IPProtocol' = 'tcp'; +``` + + +### List of disabled rules + +```sql +select + name, + id, + description, + disabled +from + gcp_compute_firewall +where + disabled +``` + + +### List of Egress rules + +```sql +select + name, + id, + direction, + allowed, + denied +from + gcp_compute_firewall +where + direction = 'EGRESS'; +``` 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..837c1c55 --- /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: listComputeFirewalls, + }, + 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 listComputeFirewalls(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listComputeFirewalls") + 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 +}