diff --git a/.kitchen.yml b/.kitchen.yml index 4df1db70a..236136d0c 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -1,4 +1,4 @@ -# Copyright 2018 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -66,6 +66,21 @@ suites: backend: local controls: - forseti-org-iam + - name: on_gke_end_to_end + lifecycle: + pre_verify: + - chmod go-rwx test/fixtures/on_gke_end_to_end/sshkey + driver: + name: terraform + root_module_directory: test/fixtures/on_gke_end_to_end/ + command_timeout: 1800 + verifier: + systems: + - name: gke + backend: local + controls: + - gcloud + - kubectl - name: shared_vpc lifecycle: pre_verify: diff --git a/examples/on_gke_end_to_end/README.md b/examples/on_gke_end_to_end/README.md index 457734bce..f9885ad9a 100644 --- a/examples/on_gke_end_to_end/README.md +++ b/examples/on_gke_end_to_end/README.md @@ -56,6 +56,7 @@ This script will also activate necessary APIs required for Terraform to deploy F |------|-------------|:----:|:-----:|:-----:| | auto\_create\_subnetworks | When set to true, the network is created in 'auto subnet mode' and it will create a subnet for each region automatically across the 10.128.0.0/9 address range. When set to false, the network is created in 'custom subnet mode' so the user can explicitly connect subnetwork resources. | bool | `"false"` | no | | bucket\_cai\_location | GCS CAI storage bucket location | string | `"us-central1"` | no | +| client\_instance\_metadata | Metadata key/value pairs to make available from within the client instance. | map(string) | `` | no | | config\_validator\_enabled | Config Validator scanner enabled. | bool | `"false"` | no | | cscc\_source\_id | Source ID for CSCC Beta API | string | `""` | no | | cscc\_violations\_enabled | Notify for CSCC violations | bool | `"false"` | no | @@ -74,6 +75,7 @@ This script will also activate necessary APIs required for Terraform to deploy F | gke\_service\_ip\_range | The IP range of the Kubernetes services. | string | `"10.3.0.0/20"` | no | | gke\_service\_ip\_range\_name | The name of the IP range of the Kubernetes services. | string | `"gke-service-ip-range"` | no | | gsuite\_admin\_email | G-Suite administrator email address to manage your Forseti installation | string | n/a | yes | +| helm\_chart\_version | The version of the Helm chart to use | string | `"2.2.0-rc1"` | no | | helm\_repository\_url | The Helm repository containing the 'forseti-security' Helm charts | string | `"https://forseti-security-charts.storage.googleapis.com/release/"` | no | | k8s\_forseti\_namespace | The Kubernetes namespace in which to deploy Forseti. | string | `"forseti"` | no | | k8s\_forseti\_orchestrator\_image\_tag | The tag for the container image for the Forseti orchestrator | string | `"v2.24.0"` | no | @@ -97,6 +99,8 @@ This script will also activate necessary APIs required for Terraform to deploy F | Name | Description | |------|-------------| +| ca\_certificate | The cluster CA certificate | +| client\_token | The bearer token for auth | | config-validator-git-public-key-openssh | The public OpenSSH key generated to allow the Forseti Server to clone the policy library repository. | | forseti-client-service-account | Forseti Client service account | | forseti-client-storage-bucket | Forseti Client storage bucket | @@ -104,9 +108,12 @@ This script will also activate necessary APIs required for Terraform to deploy F | forseti-cloudsql-connection-name | Forseti CloudSQL Connection String | | forseti-server-service-account | Forseti Server service account | | forseti-server-storage-bucket | Forseti Server storage bucket | +| gke\_cluster\_location | Cluster location | +| gke\_cluster\_name | Cluster name | | kubernetes-forseti-namespace | The Kubernetes namespace in which Forseti is deployed | | kubernetes-forseti-server-ingress | The loadbalancer ingress address of the forseti-server service in GKE | | kubernetes-forseti-tiller-sa-name | The name of the service account deploying Forseti | +| kubernetes\_endpoint | The cluster endpoint | | suffix | The random suffix appended to Forseti resources | diff --git a/examples/on_gke_end_to_end/main.tf b/examples/on_gke_end_to_end/main.tf index e151747e7..cfc1e8b03 100644 --- a/examples/on_gke_end_to_end/main.tf +++ b/examples/on_gke_end_to_end/main.tf @@ -168,8 +168,11 @@ module "forseti" { k8s_forseti_server_image_tag = var.k8s_forseti_server_image_tag k8s_forseti_orchestrator_image_tag = var.k8s_forseti_orchestrator_image_tag helm_repository_url = var.helm_repository_url + helm_chart_version = var.helm_chart_version policy_library_repository_url = var.policy_library_repository_url policy_library_repository_branch = var.policy_library_repository_branch policy_library_sync_enabled = var.policy_library_sync_enabled server_log_level = var.server_log_level + client_instance_metadata = var.client_instance_metadata + workload_identity_namespace = module.gke.identity_namespace } diff --git a/examples/on_gke_end_to_end/outputs.tf b/examples/on_gke_end_to_end/outputs.tf index a82140c18..7ee8e8424 100644 --- a/examples/on_gke_end_to_end/outputs.tf +++ b/examples/on_gke_end_to_end/outputs.tf @@ -14,6 +14,22 @@ * limitations under the License. */ +output "client_token" { + description = "The bearer token for auth" + sensitive = true + value = base64encode(data.google_client_config.default.access_token) +} + +output "ca_certificate" { + description = "The cluster CA certificate" + value = module.gke.ca_certificate +} + +output "config-validator-git-public-key-openssh" { + description = "The public OpenSSH key generated to allow the Forseti Server to clone the policy library repository." + value = module.forseti.config-validator-git-public-key-openssh +} + output "forseti-client-service-account" { description = "Forseti Client service account" value = module.forseti.forseti-client-service-account @@ -24,14 +40,19 @@ output "forseti-client-storage-bucket" { value = module.forseti.forseti-client-storage-bucket } +output "forseti-client-vm-ip" { + description = "Forseti Client VM private IP address" + value = module.forseti.forseti-client-vm-ip +} + output "forseti-cloudsql-connection-name" { description = "Forseti CloudSQL Connection String" value = module.forseti.forseti-cloudsql-connection-name } -output "forseti-client-vm-ip" { - description = "Forseti Client VM private IP address" - value = module.forseti.forseti-client-vm-ip +output "forseti-server-service-account" { + description = "Forseti Server service account" + value = module.forseti.forseti-server-service-account } output "forseti-server-storage-bucket" { @@ -39,9 +60,20 @@ output "forseti-server-storage-bucket" { value = module.forseti.forseti-server-storage-bucket } -output "forseti-server-service-account" { - description = "Forseti Server service account" - value = module.forseti.forseti-server-service-account +output "gke_cluster_location" { + description = "Cluster location" + value = module.gke.location +} + +output "gke_cluster_name" { + description = "Cluster name" + value = var.gke_cluster_name +} + +output "kubernetes_endpoint" { + description = "The cluster endpoint" + sensitive = true + value = module.gke.endpoint } output "kubernetes-forseti-namespace" { @@ -49,22 +81,17 @@ output "kubernetes-forseti-namespace" { value = module.forseti.kubernetes-forseti-namespace } -output "kubernetes-forseti-tiller-sa-name" { - description = "The name of the service account deploying Forseti" - value = module.forseti.kubernetes-forseti-tiller-sa-name -} - output "kubernetes-forseti-server-ingress" { description = "The loadbalancer ingress address of the forseti-server service in GKE" value = module.forseti.kubernetes-forseti-server-ingress } +output "kubernetes-forseti-tiller-sa-name" { + description = "The name of the service account deploying Forseti" + value = module.forseti.kubernetes-forseti-tiller-sa-name +} + output "suffix" { description = "The random suffix appended to Forseti resources" value = module.forseti.suffix } - -output "config-validator-git-public-key-openssh" { - description = "The public OpenSSH key generated to allow the Forseti Server to clone the policy library repository." - value = module.forseti.config-validator-git-public-key-openssh -} diff --git a/examples/on_gke_end_to_end/variables.tf b/examples/on_gke_end_to_end/variables.tf index 6bf50c91f..9e24c76c5 100644 --- a/examples/on_gke_end_to_end/variables.tf +++ b/examples/on_gke_end_to_end/variables.tf @@ -114,6 +114,11 @@ variable "helm_repository_url" { default = "https://forseti-security-charts.storage.googleapis.com/release/" } +variable "helm_chart_version" { + description = "The version of the Helm chart to use" + default = "2.2.0-rc1" +} + variable "k8s_forseti_namespace" { description = "The Kubernetes namespace in which to deploy Forseti." default = "forseti" @@ -196,6 +201,12 @@ variable "server_log_level" { default = "info" } +variable "client_instance_metadata" { + description = "Metadata key/value pairs to make available from within the client instance." + type = map(string) + default = {} +} + #----------------# # Forseti bucket # #----------------# diff --git a/modules/on_gke/README.md b/modules/on_gke/README.md index ceaefb1ad..5f2b4306d 100644 --- a/modules/on_gke/README.md +++ b/modules/on_gke/README.md @@ -98,7 +98,7 @@ This sub-module deploys Forseti on GKE. In short, this deploys a server contain | groups\_settings\_violations\_should\_notify | Notify for groups settings violations | bool | `"true"` | no | | groups\_violations\_should\_notify | Notify for Groups violations | bool | `"true"` | no | | gsuite\_admin\_email | G-Suite administrator email address to manage your Forseti installation | string | `""` | no | -| helm\_chart\_version | The version of the Helm chart to use | string | `"2.1.0"` | no | +| helm\_chart\_version | The version of the Helm chart to use | string | `"2.2.0-rc1"` | no | | helm\_repository\_url | The Helm repository containing the 'forseti-security' Helm charts | string | `"https://forseti-security-charts.storage.googleapis.com/release/"` | no | | iam\_disable\_polling | Whether to disable polling for IAM API | bool | `"false"` | no | | iam\_max\_calls | Maximum calls that can be made to IAM API | string | `"90"` | no | @@ -177,6 +177,7 @@ This sub-module deploys Forseti on GKE. In short, this deploys a server contain | subnetwork | The VPC subnetwork where the Forseti client and server will be created | string | `"default"` | no | | verify\_policy\_library | Verify the Policy Library is setup correctly for the Config Validator scanner | bool | `"false"` | no | | violations\_slack\_webhook | Slack webhook for any violation. Will apply to all scanner violation notifiers. | string | `""` | no | +| workload\_identity\_namespace | Workload Identity namespace | string | `"null"` | no | ## Outputs diff --git a/modules/on_gke/main.tf b/modules/on_gke/main.tf index d1239d647..1710f9f1d 100644 --- a/modules/on_gke/main.tf +++ b/modules/on_gke/main.tf @@ -27,7 +27,7 @@ resource "null_resource" "org_id_and_folder_id_are_both_empty" { count = length(var.composite_root_resources) == 0 && var.org_id == "" && var.folder_id == "" ? 1 : 0 provisioner "local-exec" { - command = "echo 'composite_root_resources=${var.composite_root_resources} org_id=${var.org_id} folder_id=${var.org_id}' >&2; false" + command = "echo 'composite_root_resources=${var.composite_root_resources} org_id=${var.org_id} folder_id=${var.folder_id}' >&2; false" interpreter = ["bash", "-c"] } } @@ -69,7 +69,7 @@ locals { "storage-api.googleapis.com", "groupssettings.googleapis.com", ] - workload_identity = "${var.project_id}.svc.id.goog" + workload_identity_namespace = var.workload_identity_namespace == null ? "${var.project_id}.svc.id.goog" : var.workload_identity_namespace workload_identity_server_suffix = "[${local.kubernetes_namespace}/forseti-server]" workload_identity_client_suffix = "[${local.kubernetes_namespace}/forseti-orchestrator]" workload_config_validator_suffix = "[${local.kubernetes_namespace}/config-validator]" @@ -121,20 +121,24 @@ data "tls_public_key" "git_sync_public_ssh_key" { //***************************************** // Obtain Forseti Server Configuration //***************************************** -data "google_storage_object_signed_url" "file_url" { - bucket = module.server_gcs.forseti-server-storage-bucket - path = "configs/forseti_conf_server.yaml" - content_md5 = module.server_config.forseti-server-config-md5 +data "google_storage_bucket_object" "server_config_contents" { + bucket = module.server_gcs.forseti-server-storage-bucket + name = "configs/forseti_conf_server.yaml" + + depends_on = [ + module.server_config.forseti-server-config-md5 + ] } +data "google_client_config" "current" {} + data "http" "server_config_contents" { - url = data.google_storage_object_signed_url.file_url.signed_url + url = format("%s?alt=media", data.google_storage_bucket_object.server_config_contents.self_link) + # Optional request headers request_headers = { - "Content-MD5" = module.server_config.forseti-server-config-md5 + "Authorization" = "Bearer ${data.google_client_config.current.access_token}" } - - depends_on = ["data.google_storage_object_signed_url.file_url"] } //***************************************** @@ -154,7 +158,7 @@ resource "google_service_account_iam_binding" "forseti_server_workload_identity" role = "roles/iam.workloadIdentityUser" members = [ - "serviceAccount:${local.workload_identity}${local.workload_identity_server_suffix}" + "serviceAccount:${local.workload_identity_namespace}${local.workload_identity_server_suffix}" ] } @@ -163,8 +167,8 @@ resource "google_service_account_iam_binding" "forseti_client_workload_identity" role = "roles/iam.workloadIdentityUser" members = [ - "serviceAccount:${local.workload_identity}${local.workload_identity_client_suffix}", - "serviceAccount:${local.workload_identity}${local.workload_config_validator_suffix}" + "serviceAccount:${local.workload_identity_namespace}${local.workload_identity_client_suffix}", + "serviceAccount:${local.workload_identity_namespace}${local.workload_config_validator_suffix}" ] } @@ -225,10 +229,12 @@ resource "helm_release" "forseti-security" { version = var.helm_chart_version chart = "forseti-security" recreate_pods = var.recreate_pods - depends_on = ["kubernetes_role_binding.tiller", + depends_on = [ + "kubernetes_role_binding.tiller", "kubernetes_namespace.forseti", "google_service_account_iam_binding.forseti_server_workload_identity", - "google_service_account_iam_binding.forseti_client_workload_identity"] + "google_service_account_iam_binding.forseti_client_workload_identity" + ] set { name = "database.username" diff --git a/modules/on_gke/variables.tf b/modules/on_gke/variables.tf index 157a78d95..d8c682506 100644 --- a/modules/on_gke/variables.tf +++ b/modules/on_gke/variables.tf @@ -60,12 +60,16 @@ variable "sendgrid_api_key" { #------------# # GKE config # #------------# - variable "gke_node_pool_name" { description = "The name of the GKE node-pool where Forseti is being deployed" default = "default-pool" } +variable "workload_identity_namespace" { + description = "Workload Identity namespace" + default = null +} + #----------------# # Forseti config # #----------------# @@ -710,7 +714,6 @@ variable "inventory_email_summary_enabled" { #---------------------------------------# # Groups Settings scanner configuration # #---------------------------------------# - variable "groups_settings_max_calls" { description = "Maximum calls that can be made to the G Suite Groups API" default = "5" @@ -893,7 +896,6 @@ variable "cloudsql_password" { #-------------# # Helm config # #-------------# - variable "git_sync_image" { description = "The container image used by the config-validator git-sync side-car" default = "gcr.io/google-containers/git-sync" @@ -911,7 +913,7 @@ variable "git_sync_wait" { variable "helm_chart_version" { description = "The version of the Helm chart to use" - default = "2.1.0" + default = "2.2.0-rc1" } variable "helm_repository_url" { diff --git a/test/fixtures/on_gke_end_to_end/main.tf b/test/fixtures/on_gke_end_to_end/main.tf new file mode 100644 index 000000000..11e8f6bc4 --- /dev/null +++ b/test/fixtures/on_gke_end_to_end/main.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "tls_private_key" "main" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "local_file" "gce-keypair-pk" { + content = tls_private_key.main.private_key_pem + filename = "${path.module}/sshkey" +} + +#-------------------------# +# Forseti +#-------------------------# +module "forseti" { + source = "../../../examples/on_gke_end_to_end" + + # Forseti + config_validator_enabled = var.config_validator_enabled + domain = var.domain + gsuite_admin_email = var.gsuite_admin_email + + # Forseti Client + client_instance_metadata = { + sshKeys = "ubuntu:${tls_private_key.main.public_key_openssh}" + } + + # GCP + org_id = var.org_id + project_id = var.gke_project_id + + # GKE + k8s_forseti_orchestrator_image_tag = var.k8s_forseti_orchestrator_image_tag + k8s_forseti_server_image_tag = var.k8s_forseti_server_image_tag + network_description = var.network_description +} diff --git a/test/fixtures/on_gke_end_to_end/outputs.tf b/test/fixtures/on_gke_end_to_end/outputs.tf new file mode 100644 index 000000000..7fe449825 --- /dev/null +++ b/test/fixtures/on_gke_end_to_end/outputs.tf @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "client_token" { + description = "The bearer token for auth" + sensitive = true + value = module.forseti.client_token +} + +output "ca_certificate" { + description = "The cluster CA certificate" + value = module.forseti.ca_certificate +} + +output "forseti-client-vm-ip" { + description = "Forseti Client VM private IP address" + value = module.forseti.forseti-client-vm-ip +} + +output "gke_cluster_location" { + description = "Cluster location (region if regional cluster, zone if zonal cluster)" + value = module.forseti.gke_cluster_location +} + +output "gke_cluster_name" { + description = "The name of the GKE Cluster" + value = module.forseti.gke_cluster_name +} + +output "gke_project_id" { + description = "The ID of an existing Google project where Forseti will be installed" + value = var.gke_project_id +} + +output "kubernetes_endpoint" { + description = "The cluster endpoint" + sensitive = true + value = module.forseti.kubernetes_endpoint +} diff --git a/test/fixtures/on_gke_end_to_end/variables.tf b/test/fixtures/on_gke_end_to_end/variables.tf new file mode 100644 index 000000000..70f734f0c --- /dev/null +++ b/test/fixtures/on_gke_end_to_end/variables.tf @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "config_validator_enabled" { + description = "Config Validator scanner enabled." + type = bool + default = false +} + +variable "domain" { + description = "The domain associated with the GCP Organization ID" +} + +variable "gke_project_id" { + description = "The ID of an existing Google project where Forseti will be installed" +} + +variable "gsuite_admin_email" { + description = "G-Suite administrator email address to manage your Forseti installation" +} + +variable "k8s_forseti_orchestrator_image_tag" { + description = "The tag for the container image for the Forseti orchestrator" + default = "master" +} + +variable "k8s_forseti_server_image_tag" { + description = "The tag for the container image for the Forseti server" + default = "master" +} + +variable "network_description" { + type = string + description = "An optional description of the network. The resource must be recreated to modify this field." + default = "GKE Network" +} + +variable "org_id" { + description = "GCP Organization ID that Forseti will have purview over" +} diff --git a/test/fixtures/on_gke_end_to_end/versions.tf b/test/fixtures/on_gke_end_to_end/versions.tf new file mode 100644 index 000000000..0bad58106 --- /dev/null +++ b/test/fixtures/on_gke_end_to_end/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + required_version = ">= 0.12" +} diff --git a/test/integration/install_simple/controls/client.rb b/test/integration/install_simple/controls/client.rb deleted file mode 100644 index a9214d86e..000000000 --- a/test/integration/install_simple/controls/client.rb +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2018 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require "yaml" - -forseti_server_vm_internal_dns = attribute("forseti-server-vm-internal-dns") -forseti_version = "2.24.0" - -control "client" do - title "Forseti client instance resources" - describe command("forseti") do - it { should exist } - end - - describe command("forseti config show") do - its("exit_status") { should eq 0 } - its("stdout") { should match(/#{forseti_server_vm_internal_dns}:50051/) } - end - - describe command("forseti inventory list") do - its("exit_status") { should eq 0 } - its('stdout') { should_not match(/Error communicating to the Forseti server/) } - end - - describe command("python3 -m pip show forseti-security|grep Version") do - its("exit_status") { should eq 0 } - its("stdout") { should match("Version: #{forseti_version}") } - its("stderr") { should cmp "" } - end - - describe file("/home/ubuntu/forseti-security/configs/forseti_conf_client.yaml") do - it { should exist } - - it "sets the hostname to the Forseti server internal DNS" do - expect(YAML.load(subject.content)).to eq("server_ip" => forseti_server_vm_internal_dns) - end - end -end diff --git a/test/integration/install_simple/controls/client.rb b/test/integration/install_simple/controls/client.rb new file mode 120000 index 000000000..908e40455 --- /dev/null +++ b/test/integration/install_simple/controls/client.rb @@ -0,0 +1 @@ +../../shared/controls/client.rb \ No newline at end of file diff --git a/test/integration/install_simple/controls/forseti.rb b/test/integration/install_simple/controls/forseti.rb deleted file mode 100644 index a22b24d3c..000000000 --- a/test/integration/install_simple/controls/forseti.rb +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright 2018 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# encoding: utf-8 - -org_id = attribute('org_id') -project_id = attribute('project_id') -network_project = attribute('network_project').empty? ? project_id : attribute('network_project') -suffix = attribute('suffix') -forseti_client_vm_name = attribute('forseti-client-vm-name') -forseti_server_vm_name = attribute('forseti-server-vm-name') -forseti_client_storage_bucket = attribute('forseti-client-storage-bucket') -forseti_server_storage_bucket = attribute('forseti-server-storage-bucket') -forseti_client_service_account = attribute('forseti-client-service-account') -forseti_server_service_account = attribute('forseti-server-service-account') - -control 'forseti' do - title "Forseti GCP resources" - - describe google_compute_instance( - project: project_id, - zone: 'us-central1-c', - name: forseti_client_vm_name - ) do - it { should exist } - its('machine_size') { should eq 'n1-standard-2' } - end - - describe google_compute_instance( - project: project_id, - zone: 'us-central1-c', - name: forseti_server_vm_name - ) do - it { should exist } - its('machine_size') { should eq 'n1-standard-8' } - end - - describe google_sql_database_instances(project: project_id) do - its('instance_names') { should include(/forseti-server-db-*/) } - end - - describe google_project_iam_binding(project: project_id, role: "roles/storage.objectViewer") do - it { should exist } - its('members') { should include "serviceAccount:#{forseti_server_service_account}" } - end - describe google_project_iam_binding(project: project_id, role: "roles/storage.objectCreator") do - it { should exist } - its('members') { should include "serviceAccount:#{forseti_server_service_account}" } - end - describe google_project_iam_binding(project: project_id, role: "roles/cloudsql.client") do - it { should exist } - its('members') { should include "serviceAccount:#{forseti_server_service_account}" } - end - describe google_project_iam_binding(project: project_id, role: "roles/logging.logWriter") do - it { should exist } - its('members') { should include "serviceAccount:#{forseti_server_service_account}" } - end - describe google_project_iam_binding(project: project_id, role: "roles/iam.serviceAccountTokenCreator") do - it { should exist } - its('members') { should include "serviceAccount:#{forseti_server_service_account}" } - end - - describe google_storage_buckets(project: project_id) do - its('bucket_names') { should include forseti_server_storage_bucket } - its('bucket_names') { should include forseti_client_storage_bucket } - its('bucket_names') { should include(/forseti-cai-export/) } - end - - describe google_storage_bucket_objects(bucket: forseti_server_storage_bucket) do - - # Enumerate the files that we expect to be present. This fixture ensures that we - # don't silently drop a rules file. - let(:expected_files) do - %w[ - rules/audit_logging_rules.yaml - rules/bigquery_rules.yaml - rules/blacklist_rules.yaml - rules/bucket_rules.yaml - rules/cloudsql_rules.yaml - rules/enabled_apis_rules.yaml - rules/external_project_access_rules.yaml - rules/firewall_rules.yaml - rules/forwarding_rules.yaml - rules/group_rules.yaml - rules/groups_settings_rules.yaml - rules/iam_rules.yaml - rules/iap_rules.yaml - rules/instance_network_interface_rules.yaml - rules/ke_rules.yaml - rules/ke_scanner_rules.yaml - rules/lien_rules.yaml - rules/location_rules.yaml - rules/log_sink_rules.yaml - rules/resource_rules.yaml - rules/retention_rules.yaml - rules/role_rules.yaml - rules/service_account_key_rules.yaml - ] - end - - # Enumerate the files that are present in the rules directory This fixture ensures - # that we don't miss an included rules file. - let(:present_files) do - template_dir = File.expand_path( - "../../../../modules/rules/templates/rules", - __dir__ - ) - Dir.glob("#{template_dir}/*.yaml").map {|file| "rules/#{File.basename(file)}" } - end - - let(:files) { expected_files | present_files } - - its('object_names') { should include(*files) } - end - - describe google_service_account(name: "projects/#{project_id}/serviceAccounts/#{forseti_client_service_account}") do - its(:email) { should eq forseti_client_service_account } - its(:display_name) { should eq "Forseti Client Service Account" } - end - - describe google_service_account(name: "projects/#{project_id}/serviceAccounts/#{forseti_server_service_account}") do - its(:email) { should eq forseti_server_service_account } - its(:display_name) { should eq "Forseti Server Service Account" } - end - - describe google_compute_firewall(project: network_project, name: "forseti-server-allow-grpc-#{suffix}") do - let(:allowed) { subject.allowed.map(&:item) } - - its('source_ranges') { should eq ["10.128.0.0/9"] } - its('direction') { should eq 'INGRESS' } - its('priority') { should eq 100 } - - it "allows gRPC traffic" do - expect(allowed).to contain_exactly({ip_protocol: "tcp", ports: ["50051", "50052"]}) - end - end - - describe google_compute_firewall(project: network_project, name: "forseti-server-deny-all-#{suffix}") do - let(:denied) { subject.denied.map(&:item) } - - its('source_ranges') { should eq ["0.0.0.0/0"] } - its('direction') { should eq 'INGRESS' } - its('priority') { should eq 200 } - - it "denies TCP, UDP, and ICMP" do - expect(denied).to contain_exactly( - {ip_protocol: "icmp"}, - {ip_protocol: "tcp"}, - {ip_protocol: "udp"} - ) - end - end - - describe google_compute_firewall(project: network_project, name: "forseti-client-deny-all-#{suffix}") do - let(:denied) { subject.denied.map(&:item) } - - its('source_ranges') { should eq ["0.0.0.0/0"] } - its('direction') { should eq 'INGRESS' } - its('priority') { should eq 200 } - - it "denies TCP, UDP, and ICMP" do - expect(denied).to contain_exactly( - {ip_protocol: "icmp"}, - {ip_protocol: "tcp"}, - {ip_protocol: "udp"} - ) - end - end -end - -control 'forseti-org-iam' do - title "Validate organization roles of SA" - describe command("gcloud organizations get-iam-policy #{org_id} --filter='bindings.members:#{forseti_server_service_account}' --flatten='bindings[].members' --format='json(bindings.role)'") do - its(:exit_status) { should eq 0 } - its(:stderr) { should eq '' } - - let(:sa_roles) do - JSON.parse(subject.stdout).map { |a| a["bindings"]["role"] } - end - - let(:expected_roles) do - [ - "roles/appengine.appViewer", - "roles/bigquery.metadataViewer", - "roles/browser", - "roles/cloudasset.viewer", - "roles/cloudsql.viewer", - "roles/compute.networkViewer", - "roles/iam.securityReviewer", - "roles/orgpolicy.policyViewer", - "roles/servicemanagement.quotaViewer", - "roles/serviceusage.serviceUsageConsumer", - ] - end - - it 'has all expected org roles' do - expect(sa_roles).to match_array(expected_roles) - end - end -end diff --git a/test/integration/install_simple/controls/forseti.rb b/test/integration/install_simple/controls/forseti.rb new file mode 120000 index 000000000..355ca5b70 --- /dev/null +++ b/test/integration/install_simple/controls/forseti.rb @@ -0,0 +1 @@ +../../shared/controls/forseti.rb \ No newline at end of file diff --git a/test/integration/install_simple/controls/server.rb b/test/integration/install_simple/controls/server.rb deleted file mode 100644 index 3edcbcf71..000000000 --- a/test/integration/install_simple/controls/server.rb +++ /dev/null @@ -1,508 +0,0 @@ -# Copyright 2018 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require "yaml" - -forseti_version = "2.24.0" -suffix = attribute("suffix") - -control "server" do - title "Forseti server instance resources" - describe command("forseti") do - it { should exist } - end - - describe command("forseti server configuration get") do - its("exit_status") { should eq 0 } - end - - describe command("forseti inventory list") do - its("exit_status") { should eq 0 } - end - - describe command("forseti_server") do - it { should exist } - end - - describe command("forseti_enforcer") do - it { should exist } - end - - describe command("python3 -m pip show forseti-security|grep Version") do - its("exit_status") { should eq 0 } - its("stdout") { should match("Version: #{forseti_version}") } - its("stderr") { should cmp "" } - end - - describe file("/home/ubuntu/forseti-security/configs/forseti_conf_server.yaml") do - it { should exist } - it "is valid YAML" do - YAML.load(subject.content) - end - - let(:config) { YAML.load(subject.content) } - - describe "inventory" do - describe "api_quota" do - it "configures admin_max_calls" do - expect(config["inventory"]["api_quota"]["admin"]["max_calls"]).to eq 14 - end - - it "configures admin_period" do - expect(config["inventory"]["api_quota"]["admin"]["period"]).to eq 1.0 - end - - it "configures admin_disable_polling" do - expect(config["inventory"]["api_quota"]["admin"]["disable_polling"]).to eq false - end - - it "configures appengine_max_calls" do - expect(config["inventory"]["api_quota"]["appengine"]["max_calls"]).to eq 18 - end - - it "configures appengine_period" do - expect(config["inventory"]["api_quota"]["appengine"]["period"]).to eq 1.0 - end - - it "configures appengine_disable_polling" do - expect(config["inventory"]["api_quota"]["appengine"]["disable_polling"]).to eq false - end - - it "configures bigquery_max_calls" do - expect(config["inventory"]["api_quota"]["bigquery"]["max_calls"]).to eq 160 - end - - it "configures bigquery_period" do - expect(config["inventory"]["api_quota"]["bigquery"]["period"]).to eq 1.0 - end - - it "configures bigquery_disable_polling" do - expect(config["inventory"]["api_quota"]["bigquery"]["disable_polling"]).to eq false - end - - it "configures cloudasset_max_calls" do - expect(config["inventory"]["api_quota"]["cloudasset"]["max_calls"]).to eq 1 - end - - it "configures cloudasset_period" do - expect(config["inventory"]["api_quota"]["cloudasset"]["period"]).to eq 1.0 - end - - it "configures cloudasset_disable_polling" do - expect(config["inventory"]["api_quota"]["cloudasset"]["disable_polling"]).to eq false - end - - it "configures cloudbilling_max_calls" do - expect(config["inventory"]["api_quota"]["cloudbilling"]["max_calls"]).to eq 5 - end - - it "configures cloudbilling_period" do - expect(config["inventory"]["api_quota"]["cloudbilling"]["period"]).to eq 1.2 - end - - it "configures cloudbilling_disable_polling" do - expect(config["inventory"]["api_quota"]["cloudbilling"]["disable_polling"]).to eq false - end - - it "configures compute_max_calls" do - expect(config["inventory"]["api_quota"]["compute"]["max_calls"]).to eq 18 - end - - it "configures compute_period" do - expect(config["inventory"]["api_quota"]["compute"]["period"]).to eq 1.0 - end - - it "configures compute_disable_polling" do - expect(config["inventory"]["api_quota"]["compute"]["disable_polling"]).to eq false - end - - it "configures container_max_calls" do - expect(config["inventory"]["api_quota"]["container"]["max_calls"]).to eq 9 - end - - it "configures container_period" do - expect(config["inventory"]["api_quota"]["container"]["period"]).to eq 1.0 - end - - it "configures container_disable_polling" do - expect(config["inventory"]["api_quota"]["container"]["disable_polling"]).to eq false - end - - it "configures crm_max_calls" do - expect(config["inventory"]["api_quota"]["crm"]["max_calls"]).to eq 4 - end - - it "configures crm_period" do - expect(config["inventory"]["api_quota"]["crm"]["period"]).to eq 1.2 - end - - it "configures crm_disable_polling" do - expect(config["inventory"]["api_quota"]["crm"]["disable_polling"]).to eq false - end - - it "configures groups_settings_max_calls" do - expect(config["inventory"]["api_quota"]["groupssettings"]["max_calls"]).to eq 5 - end - - it "configures groups_settings_period" do - expect(config["inventory"]["api_quota"]["groupssettings"]["period"]).to eq 1.1 - end - - it "configures groups_settings_disable_polling" do - expect(config["inventory"]["api_quota"]["groupssettings"]["disable_polling"]).to eq false - end - - it "configures iam_max_calls" do - expect(config["inventory"]["api_quota"]["iam"]["max_calls"]).to eq 90 - end - - it "configures iam_period" do - expect(config["inventory"]["api_quota"]["iam"]["period"]).to eq 1.0 - end - - it "configures iam_disable_polling" do - expect(config["inventory"]["api_quota"]["iam"]["disable_polling"]).to eq false - end - - it "configures logging_max_calls" do - expect(config["inventory"]["api_quota"]["logging"]["max_calls"]).to eq 9 - end - - it "configures logging_period" do - expect(config["inventory"]["api_quota"]["logging"]["period"]).to eq 1.0 - end - - it "configures logging_disable_polling" do - expect(config["inventory"]["api_quota"]["logging"]["disable_polling"]).to eq false - end - - it "configures servicemanagement_max_calls" do - expect(config["inventory"]["api_quota"]["servicemanagement"]["max_calls"]).to eq 2 - end - - it "configures servicemanagement_period" do - expect(config["inventory"]["api_quota"]["servicemanagement"]["period"]).to eq 1.1 - end - - it "configures servicemanagement_disable_polling" do - expect(config["inventory"]["api_quota"]["servicemanagement"]["disable_polling"]).to eq false - end - - it "configures serviceusage_max_calls" do - expect(config["inventory"]["api_quota"]["serviceusage"]["max_calls"]).to eq 4 - end - - it "configures serviceusage_period" do - expect(config["inventory"]["api_quota"]["serviceusage"]["period"]).to eq 1.1 - end - - it "configures serviceusage_disable_polling" do - expect(config["inventory"]["api_quota"]["serviceusage"]["disable_polling"]).to eq false - end - - it "configures sqladmin_max_calls" do - expect(config["inventory"]["api_quota"]["sqladmin"]["max_calls"]).to eq 1 - end - - it "configures sqladmin_period" do - expect(config["inventory"]["api_quota"]["sqladmin"]["period"]).to eq 1.1 - end - - it "configures sqladmin_disable_polling" do - expect(config["inventory"]["api_quota"]["sqladmin"]["disable_polling"]).to eq false - end - - it "configures storage_disable_polling" do - expect(config["inventory"]["api_quota"]["storage"]["disable_polling"]).to eq false - end - end - - it "configures cai_api_timeout" do - expect(config["inventory"]["cai"]["api_timeout"]).to eq 3600 - end - - it "configures inventory_retention_days" do - expect(config["inventory"]["retention_days"]).to eq(-1) - end - end - - describe "scanner" do - it "configures audit_logging_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "audit_logging", "enabled" => false) - end - - it "configures bigquery_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "bigquery", "enabled" => true) - end - - it "configures blacklist_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "blacklist", "enabled" => true) - end - - it "configures bucket_acl_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "bucket_acl", "enabled" => true) - end - - it "configures cloudsql_acl_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "cloudsql_acl", "enabled" => true) - end - - it "configures config_validator_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "config_validator", "enabled" => false, "verify_policy_library" => true) - end - - it "configures enabled_apis_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "enabled_apis", "enabled" => false) - end - - it "configures firewall_rule_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "firewall_rule", "enabled" => true) - end - - it "configures forwarding_rule_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "forwarding_rule", "enabled" => false) - end - - it "configures group_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "group", "enabled" => true) - end - - it "configures iam_policy_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "iam_policy", "enabled" => true) - end - - it "configures iap_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "iap", "enabled" => true) - end - - it "configures instance_network_interface_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "instance_network_interface", "enabled" => false) - end - - it "configures ke_scanner_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "ke_scanner", "enabled" => false) - end - - it "configures ke_version_scanner_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "ke_version_scanner", "enabled" => true) - end - - it "configures kms_scanner_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "kms_scanner", "enabled" => true) - end - - it "configures lien_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "lien", "enabled" => true) - end - - it "configures location_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "location", "enabled" => true) - end - - it "configures log_sink_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "log_sink", "enabled" => true) - end - - it "configures resource_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "resource", "enabled" => true) - end - - it "configures role_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "role", "enabled" => false) - end - - it "configures service_account_key_enabled" do - expect(config["scanner"]["scanners"]).to include("name" => "service_account_key", "enabled" => true) - end - end - - describe "notifier" do - describe "api_quota" do - - it "configures securitycenter_max_calls" do - expect(config["notifier"]["api_quota"]["securitycenter"]["max_calls"]).to eq 14 - end - - it "configures securitycenter_period" do - expect(config["notifier"]["api_quota"]["securitycenter"]["period"]).to eq 1.0 - end - end - - describe "resources" do - it "configures iam_policy_violations_should_notify" do - expect(config["notifier"]["resources"]).to include( - including( - "resource" => "iam_policy_violations", - "should_notify" => true, - ) - ) - end - - it "configures audit_logging_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "audit_logging_violations", "should_notify" => true)) - end - - it "configures blacklist_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "blacklist_violations", "should_notify" => true)) - end - - it "configures bigquery_acl_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "bigquery_acl_violations", "should_notify" => true)) - end - - it "configures buckets_acl_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "buckets_acl_violations", "should_notify" => true)) - end - - it "configures cloudsql_acl_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "cloudsql_acl_violations", "should_notify" => true)) - end - - it "configures enabled_apis_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "enabled_apis_violations", "should_notify" => true)) - end - - it "configures firewall_rule_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "firewall_rule_violations", "should_notify" => true)) - end - - it "configures forwarding_rule_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "forwarding_rule_violations", "should_notify" => true)) - end - - it "configures ke_version_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "ke_version_violations", "should_notify" => true)) - end - - it "configures ke_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "ke_violations", "should_notify" => true)) - end - - it "configures kms_violations_should_notify" do - expect(config["notifier"]["resources"]).to include( - including( - "resource" => "kms_violations", - "should_notify" => true, - ) - ) - end - - it "configures groups_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "groups_violations", "should_notify" => true)) - end - - it "configures instance_network_interface_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "instance_network_interface_violations", "should_notify" => true)) - end - - it "configures iap_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "iap_violations", "should_notify" => true)) - end - - it "configures lien_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "lien_violations", "should_notify" => true)) - end - - it "configures location_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "location_violations", "should_notify" => true)) - end - - it "configures log_sink_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "log_sink_violations", "should_notify" => true)) - end - - it "configures resource_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "resource_violations", "should_notify" => true)) - end - - it "configures role_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "role_violations", "should_notify" => true)) - end - - it "configures service_account_key_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "service_account_key_violations", "should_notify" => true)) - end - - it "configures external_project_access_violations_should_notify" do - expect(config["notifier"]["resources"]).to include(including("resource" => "external_project_access_violations", "should_notify" => true)) - end - end - - it "configures cscc_violations_enabled" do - expect(config["notifier"]["violation"]["cscc"]["enabled"]).to eq false - end - - it "configures cscc_source_id" do - expect(config["notifier"]["violation"]["cscc"]["source_id"]).to be_nil - end - - it "configures inventory_gcs_summary_enabled" do - expect(config["notifier"]["inventory"]["gcs_summary"]["enabled"]).to eq true - end - - it "configures inventory_email_summary_enabled" do - expect(config["notifier"]["inventory"]["email_summary"]["enabled"]).to eq false - end - end - end - - # Enumerate the files that we expect to be present. This fixture ensures that we - # don't silently drop a rules file. - expected_files = %w[ - audit_logging_rules.yaml - bigquery_rules.yaml - blacklist_rules.yaml - bucket_rules.yaml - cloudsql_rules.yaml - enabled_apis_rules.yaml - external_project_access_rules.yaml - firewall_rules.yaml - forwarding_rules.yaml - group_rules.yaml - iam_rules.yaml - iap_rules.yaml - instance_network_interface_rules.yaml - ke_rules.yaml - ke_scanner_rules.yaml - lien_rules.yaml - location_rules.yaml - log_sink_rules.yaml - resource_rules.yaml - retention_rules.yaml - role_rules.yaml - service_account_key_rules.yaml - ] - - template_dir = File.expand_path("../../../../modules/rules/templates/rules", __dir__) - - # Enumerate the files that are present in the rules directory. This fixture ensures - # that we don't miss an included rules file. - present_files = Dir.glob("#{template_dir}/*.yaml").map { |file| File.basename(file) } - - files = expected_files | present_files - - files.each do |file| - describe file("/home/ubuntu/forseti-security/rules/#{file}") do - it { should exist } - it "is valid YAML" do - YAML.load(subject.content) - end - end - end - - describe command("sudo gcloud sql instances describe forseti-server-db-#{suffix}") do - its("exit_status") { should eq 0 } - its("stdout") { should match("- name: net_write_timeout\n value: '240'") } - end -end diff --git a/test/integration/install_simple/controls/server.rb b/test/integration/install_simple/controls/server.rb new file mode 120000 index 000000000..dad957039 --- /dev/null +++ b/test/integration/install_simple/controls/server.rb @@ -0,0 +1 @@ +../../shared/controls/server.rb \ No newline at end of file diff --git a/test/integration/on_gke_end_to_end/controls/gcloud.rb b/test/integration/on_gke_end_to_end/controls/gcloud.rb new file mode 100644 index 000000000..590b5bc8b --- /dev/null +++ b/test/integration/on_gke_end_to_end/controls/gcloud.rb @@ -0,0 +1,114 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cluster_name = attribute('gke_cluster_name') +location = attribute('gke_cluster_location') +project_id = attribute('gke_project_id') + +control "gcloud" do + title "Google Compute Engine GKE configuration" + describe command("gcloud beta --project=#{project_id} container clusters --zone=#{location} describe #{cluster_name} --format=json") do + its(:exit_status) { should eq 0 } + its(:stderr) { should eq '' } + + let!(:data) do + if subject.exit_status == 0 + JSON.parse(subject.stdout) + else + {} + end + end + + describe "cluster" do + it "is running" do + expect(data['status']).to eq 'RUNNING' + end + + it "is regional" do + expect(data['location']).to match(/^.*[1-9]$/) + end + + it "is multi zoned" do + expect(data['locations'].size).to eq 3 + end + + it "uses public nodes and master endpoint" do + expect(data['privateClusterConfig']).to eq nil + end + end + + describe "default node pool" do + let(:default_node_pool) { data['nodePools'].select { |p| p['name'] == "default-node-pool" }.first } + + it "has initial node count of 1" do + expect(default_node_pool['initialNodeCount']).to eq 1 + end + + it "has the expected minimum node count" do + expect(default_node_pool).to include( + "autoscaling" => including( + "minNodeCount" => 1, + ), + ) + end + + it "has the expected maximum node count" do + expect(default_node_pool).to include( + "autoscaling" => including( + "maxNodeCount" => 1, + ), + ) + end + + it "has the expected machine type" do + expect(default_node_pool).to include( + "config" => including( + "machineType" => "n1-standard-8", + ), + ) + end + + it "has the expected disk size" do + expect(default_node_pool).to include( + "config" => including( + "diskSizeGb" => 100, + ), + ) + end + + it "has the expected labels" do + expect(default_node_pool).to include( + "config" => including( + "labels" => including( + "cluster_name" => cluster_name, + "node_pool" => "default-node-pool", + ), + ), + ) + end + + it "has autorepair enabled" do + expect(default_node_pool).to include( + "management" => including( + "autoRepair" => true, + ), + ) + end + + it "has the expected status" do + expect(default_node_pool['status']).to eq 'RUNNING' + end + end + end +end diff --git a/test/integration/on_gke_end_to_end/controls/kubectl.rb b/test/integration/on_gke_end_to_end/controls/kubectl.rb new file mode 100644 index 000000000..4ef435049 --- /dev/null +++ b/test/integration/on_gke_end_to_end/controls/kubectl.rb @@ -0,0 +1,109 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'base64' +require 'kubeclient' +require 'rest-client' + +ca_certificate = attribute('ca_certificate') +client_token = attribute('client_token') +kubernetes_endpoint = attribute('kubernetes_endpoint') + +control "kubectl" do + title "Kubernetes configuration" + + describe "kubernetes" do + let(:kubernetes_http_endpoint) { "https://#{kubernetes_endpoint}/api" } + let(:client) do + cert_store = OpenSSL::X509::Store.new + cert_store.add_cert(OpenSSL::X509::Certificate.new(Base64.decode64(ca_certificate))) + Kubeclient::Client.new( + kubernetes_http_endpoint, + "v1", + ssl_options: { + cert_store: cert_store, + verify_ssl: OpenSSL::SSL::VERIFY_PEER, + }, + auth_options: { + bearer_token: Base64.decode64(client_token), + }, + ) + end + + let(:kubernetes_batch_http_endpoint) { "https://#{kubernetes_endpoint}/apis/batch" } + let(:batch_client) do + cert_store = OpenSSL::X509::Store.new + cert_store.add_cert(OpenSSL::X509::Certificate.new(Base64.decode64(ca_certificate))) + Kubeclient::Client.new( + kubernetes_batch_http_endpoint, + "v1", + ssl_options: { + cert_store: cert_store, + verify_ssl: OpenSSL::SSL::VERIFY_PEER, + }, + auth_options: { + bearer_token: Base64.decode64(client_token), + }, + ) + end + + describe "cron-jobs" do + let(:all_jobs) { batch_client.get_jobs } + + describe "forseti-orchestrator" do + let(:jobs) do + all_jobs.select { |n| n.metadata.labels.component == "forseti-orchestrator" } + end + + it "has the expected size" do + expect(jobs.size).to be <= 3 + end + end + end + + describe "pods" do + let(:all_pods) { client.get_pods } + + describe "forseti-database" do + let(:pod) do + all_pods.select { |n| n.metadata.labels.component == "forseti-database" }.first + end + + it "has the expected status" do + expect(pod.status.phase).to eq 'Running' + end + end + + describe "forseti-security" do + let(:pod) do + all_pods.select { |n| n.metadata.labels.component == "forseti-server" }.first + end + + it "has the expected status" do + expect(pod.status.phase).to eq 'Running' + end + end + + describe "tiller-deploy" do + let(:pod) do + all_pods.select { |n| n.metadata.labels.name == "tiller" }.first + end + + it "has the expected status" do + expect(pod.status.phase).to eq 'Running' + end + end + end + end +end diff --git a/test/integration/on_gke_end_to_end/inspec.yml b/test/integration/on_gke_end_to_end/inspec.yml new file mode 100644 index 000000000..50207e094 --- /dev/null +++ b/test/integration/on_gke_end_to_end/inspec.yml @@ -0,0 +1,38 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: on_gke_end_to_end +attributes: +- name: ca_certificate + required: true + type: string +- name: client_token + required: false + type: string +- name: gke_cluster_location + required: true + type: string +- name: gke_cluster_name + required: false + type: string +- name: gke_project_id + required: true + type: string +- name: kubernetes_endpoint + required: true + type: string +depends: + - name: inspec-gcp + git: https://github.com/inspec/inspec-gcp.git + commit: 6b2bfcd0847b69c5ddf6ac674dfab1adea95f0e7 diff --git a/test/integration/shared/controls/client.rb b/test/integration/shared/controls/client.rb new file mode 100644 index 000000000..a9214d86e --- /dev/null +++ b/test/integration/shared/controls/client.rb @@ -0,0 +1,49 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "yaml" + +forseti_server_vm_internal_dns = attribute("forseti-server-vm-internal-dns") +forseti_version = "2.24.0" + +control "client" do + title "Forseti client instance resources" + describe command("forseti") do + it { should exist } + end + + describe command("forseti config show") do + its("exit_status") { should eq 0 } + its("stdout") { should match(/#{forseti_server_vm_internal_dns}:50051/) } + end + + describe command("forseti inventory list") do + its("exit_status") { should eq 0 } + its('stdout') { should_not match(/Error communicating to the Forseti server/) } + end + + describe command("python3 -m pip show forseti-security|grep Version") do + its("exit_status") { should eq 0 } + its("stdout") { should match("Version: #{forseti_version}") } + its("stderr") { should cmp "" } + end + + describe file("/home/ubuntu/forseti-security/configs/forseti_conf_client.yaml") do + it { should exist } + + it "sets the hostname to the Forseti server internal DNS" do + expect(YAML.load(subject.content)).to eq("server_ip" => forseti_server_vm_internal_dns) + end + end +end diff --git a/test/integration/shared/controls/forseti.rb b/test/integration/shared/controls/forseti.rb new file mode 100644 index 000000000..a22b24d3c --- /dev/null +++ b/test/integration/shared/controls/forseti.rb @@ -0,0 +1,210 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# encoding: utf-8 + +org_id = attribute('org_id') +project_id = attribute('project_id') +network_project = attribute('network_project').empty? ? project_id : attribute('network_project') +suffix = attribute('suffix') +forseti_client_vm_name = attribute('forseti-client-vm-name') +forseti_server_vm_name = attribute('forseti-server-vm-name') +forseti_client_storage_bucket = attribute('forseti-client-storage-bucket') +forseti_server_storage_bucket = attribute('forseti-server-storage-bucket') +forseti_client_service_account = attribute('forseti-client-service-account') +forseti_server_service_account = attribute('forseti-server-service-account') + +control 'forseti' do + title "Forseti GCP resources" + + describe google_compute_instance( + project: project_id, + zone: 'us-central1-c', + name: forseti_client_vm_name + ) do + it { should exist } + its('machine_size') { should eq 'n1-standard-2' } + end + + describe google_compute_instance( + project: project_id, + zone: 'us-central1-c', + name: forseti_server_vm_name + ) do + it { should exist } + its('machine_size') { should eq 'n1-standard-8' } + end + + describe google_sql_database_instances(project: project_id) do + its('instance_names') { should include(/forseti-server-db-*/) } + end + + describe google_project_iam_binding(project: project_id, role: "roles/storage.objectViewer") do + it { should exist } + its('members') { should include "serviceAccount:#{forseti_server_service_account}" } + end + describe google_project_iam_binding(project: project_id, role: "roles/storage.objectCreator") do + it { should exist } + its('members') { should include "serviceAccount:#{forseti_server_service_account}" } + end + describe google_project_iam_binding(project: project_id, role: "roles/cloudsql.client") do + it { should exist } + its('members') { should include "serviceAccount:#{forseti_server_service_account}" } + end + describe google_project_iam_binding(project: project_id, role: "roles/logging.logWriter") do + it { should exist } + its('members') { should include "serviceAccount:#{forseti_server_service_account}" } + end + describe google_project_iam_binding(project: project_id, role: "roles/iam.serviceAccountTokenCreator") do + it { should exist } + its('members') { should include "serviceAccount:#{forseti_server_service_account}" } + end + + describe google_storage_buckets(project: project_id) do + its('bucket_names') { should include forseti_server_storage_bucket } + its('bucket_names') { should include forseti_client_storage_bucket } + its('bucket_names') { should include(/forseti-cai-export/) } + end + + describe google_storage_bucket_objects(bucket: forseti_server_storage_bucket) do + + # Enumerate the files that we expect to be present. This fixture ensures that we + # don't silently drop a rules file. + let(:expected_files) do + %w[ + rules/audit_logging_rules.yaml + rules/bigquery_rules.yaml + rules/blacklist_rules.yaml + rules/bucket_rules.yaml + rules/cloudsql_rules.yaml + rules/enabled_apis_rules.yaml + rules/external_project_access_rules.yaml + rules/firewall_rules.yaml + rules/forwarding_rules.yaml + rules/group_rules.yaml + rules/groups_settings_rules.yaml + rules/iam_rules.yaml + rules/iap_rules.yaml + rules/instance_network_interface_rules.yaml + rules/ke_rules.yaml + rules/ke_scanner_rules.yaml + rules/lien_rules.yaml + rules/location_rules.yaml + rules/log_sink_rules.yaml + rules/resource_rules.yaml + rules/retention_rules.yaml + rules/role_rules.yaml + rules/service_account_key_rules.yaml + ] + end + + # Enumerate the files that are present in the rules directory This fixture ensures + # that we don't miss an included rules file. + let(:present_files) do + template_dir = File.expand_path( + "../../../../modules/rules/templates/rules", + __dir__ + ) + Dir.glob("#{template_dir}/*.yaml").map {|file| "rules/#{File.basename(file)}" } + end + + let(:files) { expected_files | present_files } + + its('object_names') { should include(*files) } + end + + describe google_service_account(name: "projects/#{project_id}/serviceAccounts/#{forseti_client_service_account}") do + its(:email) { should eq forseti_client_service_account } + its(:display_name) { should eq "Forseti Client Service Account" } + end + + describe google_service_account(name: "projects/#{project_id}/serviceAccounts/#{forseti_server_service_account}") do + its(:email) { should eq forseti_server_service_account } + its(:display_name) { should eq "Forseti Server Service Account" } + end + + describe google_compute_firewall(project: network_project, name: "forseti-server-allow-grpc-#{suffix}") do + let(:allowed) { subject.allowed.map(&:item) } + + its('source_ranges') { should eq ["10.128.0.0/9"] } + its('direction') { should eq 'INGRESS' } + its('priority') { should eq 100 } + + it "allows gRPC traffic" do + expect(allowed).to contain_exactly({ip_protocol: "tcp", ports: ["50051", "50052"]}) + end + end + + describe google_compute_firewall(project: network_project, name: "forseti-server-deny-all-#{suffix}") do + let(:denied) { subject.denied.map(&:item) } + + its('source_ranges') { should eq ["0.0.0.0/0"] } + its('direction') { should eq 'INGRESS' } + its('priority') { should eq 200 } + + it "denies TCP, UDP, and ICMP" do + expect(denied).to contain_exactly( + {ip_protocol: "icmp"}, + {ip_protocol: "tcp"}, + {ip_protocol: "udp"} + ) + end + end + + describe google_compute_firewall(project: network_project, name: "forseti-client-deny-all-#{suffix}") do + let(:denied) { subject.denied.map(&:item) } + + its('source_ranges') { should eq ["0.0.0.0/0"] } + its('direction') { should eq 'INGRESS' } + its('priority') { should eq 200 } + + it "denies TCP, UDP, and ICMP" do + expect(denied).to contain_exactly( + {ip_protocol: "icmp"}, + {ip_protocol: "tcp"}, + {ip_protocol: "udp"} + ) + end + end +end + +control 'forseti-org-iam' do + title "Validate organization roles of SA" + describe command("gcloud organizations get-iam-policy #{org_id} --filter='bindings.members:#{forseti_server_service_account}' --flatten='bindings[].members' --format='json(bindings.role)'") do + its(:exit_status) { should eq 0 } + its(:stderr) { should eq '' } + + let(:sa_roles) do + JSON.parse(subject.stdout).map { |a| a["bindings"]["role"] } + end + + let(:expected_roles) do + [ + "roles/appengine.appViewer", + "roles/bigquery.metadataViewer", + "roles/browser", + "roles/cloudasset.viewer", + "roles/cloudsql.viewer", + "roles/compute.networkViewer", + "roles/iam.securityReviewer", + "roles/orgpolicy.policyViewer", + "roles/servicemanagement.quotaViewer", + "roles/serviceusage.serviceUsageConsumer", + ] + end + + it 'has all expected org roles' do + expect(sa_roles).to match_array(expected_roles) + end + end +end diff --git a/test/integration/shared/controls/server.rb b/test/integration/shared/controls/server.rb new file mode 100644 index 000000000..3edcbcf71 --- /dev/null +++ b/test/integration/shared/controls/server.rb @@ -0,0 +1,508 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "yaml" + +forseti_version = "2.24.0" +suffix = attribute("suffix") + +control "server" do + title "Forseti server instance resources" + describe command("forseti") do + it { should exist } + end + + describe command("forseti server configuration get") do + its("exit_status") { should eq 0 } + end + + describe command("forseti inventory list") do + its("exit_status") { should eq 0 } + end + + describe command("forseti_server") do + it { should exist } + end + + describe command("forseti_enforcer") do + it { should exist } + end + + describe command("python3 -m pip show forseti-security|grep Version") do + its("exit_status") { should eq 0 } + its("stdout") { should match("Version: #{forseti_version}") } + its("stderr") { should cmp "" } + end + + describe file("/home/ubuntu/forseti-security/configs/forseti_conf_server.yaml") do + it { should exist } + it "is valid YAML" do + YAML.load(subject.content) + end + + let(:config) { YAML.load(subject.content) } + + describe "inventory" do + describe "api_quota" do + it "configures admin_max_calls" do + expect(config["inventory"]["api_quota"]["admin"]["max_calls"]).to eq 14 + end + + it "configures admin_period" do + expect(config["inventory"]["api_quota"]["admin"]["period"]).to eq 1.0 + end + + it "configures admin_disable_polling" do + expect(config["inventory"]["api_quota"]["admin"]["disable_polling"]).to eq false + end + + it "configures appengine_max_calls" do + expect(config["inventory"]["api_quota"]["appengine"]["max_calls"]).to eq 18 + end + + it "configures appengine_period" do + expect(config["inventory"]["api_quota"]["appengine"]["period"]).to eq 1.0 + end + + it "configures appengine_disable_polling" do + expect(config["inventory"]["api_quota"]["appengine"]["disable_polling"]).to eq false + end + + it "configures bigquery_max_calls" do + expect(config["inventory"]["api_quota"]["bigquery"]["max_calls"]).to eq 160 + end + + it "configures bigquery_period" do + expect(config["inventory"]["api_quota"]["bigquery"]["period"]).to eq 1.0 + end + + it "configures bigquery_disable_polling" do + expect(config["inventory"]["api_quota"]["bigquery"]["disable_polling"]).to eq false + end + + it "configures cloudasset_max_calls" do + expect(config["inventory"]["api_quota"]["cloudasset"]["max_calls"]).to eq 1 + end + + it "configures cloudasset_period" do + expect(config["inventory"]["api_quota"]["cloudasset"]["period"]).to eq 1.0 + end + + it "configures cloudasset_disable_polling" do + expect(config["inventory"]["api_quota"]["cloudasset"]["disable_polling"]).to eq false + end + + it "configures cloudbilling_max_calls" do + expect(config["inventory"]["api_quota"]["cloudbilling"]["max_calls"]).to eq 5 + end + + it "configures cloudbilling_period" do + expect(config["inventory"]["api_quota"]["cloudbilling"]["period"]).to eq 1.2 + end + + it "configures cloudbilling_disable_polling" do + expect(config["inventory"]["api_quota"]["cloudbilling"]["disable_polling"]).to eq false + end + + it "configures compute_max_calls" do + expect(config["inventory"]["api_quota"]["compute"]["max_calls"]).to eq 18 + end + + it "configures compute_period" do + expect(config["inventory"]["api_quota"]["compute"]["period"]).to eq 1.0 + end + + it "configures compute_disable_polling" do + expect(config["inventory"]["api_quota"]["compute"]["disable_polling"]).to eq false + end + + it "configures container_max_calls" do + expect(config["inventory"]["api_quota"]["container"]["max_calls"]).to eq 9 + end + + it "configures container_period" do + expect(config["inventory"]["api_quota"]["container"]["period"]).to eq 1.0 + end + + it "configures container_disable_polling" do + expect(config["inventory"]["api_quota"]["container"]["disable_polling"]).to eq false + end + + it "configures crm_max_calls" do + expect(config["inventory"]["api_quota"]["crm"]["max_calls"]).to eq 4 + end + + it "configures crm_period" do + expect(config["inventory"]["api_quota"]["crm"]["period"]).to eq 1.2 + end + + it "configures crm_disable_polling" do + expect(config["inventory"]["api_quota"]["crm"]["disable_polling"]).to eq false + end + + it "configures groups_settings_max_calls" do + expect(config["inventory"]["api_quota"]["groupssettings"]["max_calls"]).to eq 5 + end + + it "configures groups_settings_period" do + expect(config["inventory"]["api_quota"]["groupssettings"]["period"]).to eq 1.1 + end + + it "configures groups_settings_disable_polling" do + expect(config["inventory"]["api_quota"]["groupssettings"]["disable_polling"]).to eq false + end + + it "configures iam_max_calls" do + expect(config["inventory"]["api_quota"]["iam"]["max_calls"]).to eq 90 + end + + it "configures iam_period" do + expect(config["inventory"]["api_quota"]["iam"]["period"]).to eq 1.0 + end + + it "configures iam_disable_polling" do + expect(config["inventory"]["api_quota"]["iam"]["disable_polling"]).to eq false + end + + it "configures logging_max_calls" do + expect(config["inventory"]["api_quota"]["logging"]["max_calls"]).to eq 9 + end + + it "configures logging_period" do + expect(config["inventory"]["api_quota"]["logging"]["period"]).to eq 1.0 + end + + it "configures logging_disable_polling" do + expect(config["inventory"]["api_quota"]["logging"]["disable_polling"]).to eq false + end + + it "configures servicemanagement_max_calls" do + expect(config["inventory"]["api_quota"]["servicemanagement"]["max_calls"]).to eq 2 + end + + it "configures servicemanagement_period" do + expect(config["inventory"]["api_quota"]["servicemanagement"]["period"]).to eq 1.1 + end + + it "configures servicemanagement_disable_polling" do + expect(config["inventory"]["api_quota"]["servicemanagement"]["disable_polling"]).to eq false + end + + it "configures serviceusage_max_calls" do + expect(config["inventory"]["api_quota"]["serviceusage"]["max_calls"]).to eq 4 + end + + it "configures serviceusage_period" do + expect(config["inventory"]["api_quota"]["serviceusage"]["period"]).to eq 1.1 + end + + it "configures serviceusage_disable_polling" do + expect(config["inventory"]["api_quota"]["serviceusage"]["disable_polling"]).to eq false + end + + it "configures sqladmin_max_calls" do + expect(config["inventory"]["api_quota"]["sqladmin"]["max_calls"]).to eq 1 + end + + it "configures sqladmin_period" do + expect(config["inventory"]["api_quota"]["sqladmin"]["period"]).to eq 1.1 + end + + it "configures sqladmin_disable_polling" do + expect(config["inventory"]["api_quota"]["sqladmin"]["disable_polling"]).to eq false + end + + it "configures storage_disable_polling" do + expect(config["inventory"]["api_quota"]["storage"]["disable_polling"]).to eq false + end + end + + it "configures cai_api_timeout" do + expect(config["inventory"]["cai"]["api_timeout"]).to eq 3600 + end + + it "configures inventory_retention_days" do + expect(config["inventory"]["retention_days"]).to eq(-1) + end + end + + describe "scanner" do + it "configures audit_logging_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "audit_logging", "enabled" => false) + end + + it "configures bigquery_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "bigquery", "enabled" => true) + end + + it "configures blacklist_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "blacklist", "enabled" => true) + end + + it "configures bucket_acl_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "bucket_acl", "enabled" => true) + end + + it "configures cloudsql_acl_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "cloudsql_acl", "enabled" => true) + end + + it "configures config_validator_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "config_validator", "enabled" => false, "verify_policy_library" => true) + end + + it "configures enabled_apis_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "enabled_apis", "enabled" => false) + end + + it "configures firewall_rule_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "firewall_rule", "enabled" => true) + end + + it "configures forwarding_rule_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "forwarding_rule", "enabled" => false) + end + + it "configures group_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "group", "enabled" => true) + end + + it "configures iam_policy_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "iam_policy", "enabled" => true) + end + + it "configures iap_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "iap", "enabled" => true) + end + + it "configures instance_network_interface_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "instance_network_interface", "enabled" => false) + end + + it "configures ke_scanner_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "ke_scanner", "enabled" => false) + end + + it "configures ke_version_scanner_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "ke_version_scanner", "enabled" => true) + end + + it "configures kms_scanner_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "kms_scanner", "enabled" => true) + end + + it "configures lien_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "lien", "enabled" => true) + end + + it "configures location_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "location", "enabled" => true) + end + + it "configures log_sink_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "log_sink", "enabled" => true) + end + + it "configures resource_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "resource", "enabled" => true) + end + + it "configures role_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "role", "enabled" => false) + end + + it "configures service_account_key_enabled" do + expect(config["scanner"]["scanners"]).to include("name" => "service_account_key", "enabled" => true) + end + end + + describe "notifier" do + describe "api_quota" do + + it "configures securitycenter_max_calls" do + expect(config["notifier"]["api_quota"]["securitycenter"]["max_calls"]).to eq 14 + end + + it "configures securitycenter_period" do + expect(config["notifier"]["api_quota"]["securitycenter"]["period"]).to eq 1.0 + end + end + + describe "resources" do + it "configures iam_policy_violations_should_notify" do + expect(config["notifier"]["resources"]).to include( + including( + "resource" => "iam_policy_violations", + "should_notify" => true, + ) + ) + end + + it "configures audit_logging_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "audit_logging_violations", "should_notify" => true)) + end + + it "configures blacklist_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "blacklist_violations", "should_notify" => true)) + end + + it "configures bigquery_acl_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "bigquery_acl_violations", "should_notify" => true)) + end + + it "configures buckets_acl_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "buckets_acl_violations", "should_notify" => true)) + end + + it "configures cloudsql_acl_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "cloudsql_acl_violations", "should_notify" => true)) + end + + it "configures enabled_apis_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "enabled_apis_violations", "should_notify" => true)) + end + + it "configures firewall_rule_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "firewall_rule_violations", "should_notify" => true)) + end + + it "configures forwarding_rule_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "forwarding_rule_violations", "should_notify" => true)) + end + + it "configures ke_version_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "ke_version_violations", "should_notify" => true)) + end + + it "configures ke_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "ke_violations", "should_notify" => true)) + end + + it "configures kms_violations_should_notify" do + expect(config["notifier"]["resources"]).to include( + including( + "resource" => "kms_violations", + "should_notify" => true, + ) + ) + end + + it "configures groups_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "groups_violations", "should_notify" => true)) + end + + it "configures instance_network_interface_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "instance_network_interface_violations", "should_notify" => true)) + end + + it "configures iap_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "iap_violations", "should_notify" => true)) + end + + it "configures lien_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "lien_violations", "should_notify" => true)) + end + + it "configures location_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "location_violations", "should_notify" => true)) + end + + it "configures log_sink_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "log_sink_violations", "should_notify" => true)) + end + + it "configures resource_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "resource_violations", "should_notify" => true)) + end + + it "configures role_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "role_violations", "should_notify" => true)) + end + + it "configures service_account_key_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "service_account_key_violations", "should_notify" => true)) + end + + it "configures external_project_access_violations_should_notify" do + expect(config["notifier"]["resources"]).to include(including("resource" => "external_project_access_violations", "should_notify" => true)) + end + end + + it "configures cscc_violations_enabled" do + expect(config["notifier"]["violation"]["cscc"]["enabled"]).to eq false + end + + it "configures cscc_source_id" do + expect(config["notifier"]["violation"]["cscc"]["source_id"]).to be_nil + end + + it "configures inventory_gcs_summary_enabled" do + expect(config["notifier"]["inventory"]["gcs_summary"]["enabled"]).to eq true + end + + it "configures inventory_email_summary_enabled" do + expect(config["notifier"]["inventory"]["email_summary"]["enabled"]).to eq false + end + end + end + + # Enumerate the files that we expect to be present. This fixture ensures that we + # don't silently drop a rules file. + expected_files = %w[ + audit_logging_rules.yaml + bigquery_rules.yaml + blacklist_rules.yaml + bucket_rules.yaml + cloudsql_rules.yaml + enabled_apis_rules.yaml + external_project_access_rules.yaml + firewall_rules.yaml + forwarding_rules.yaml + group_rules.yaml + iam_rules.yaml + iap_rules.yaml + instance_network_interface_rules.yaml + ke_rules.yaml + ke_scanner_rules.yaml + lien_rules.yaml + location_rules.yaml + log_sink_rules.yaml + resource_rules.yaml + retention_rules.yaml + role_rules.yaml + service_account_key_rules.yaml + ] + + template_dir = File.expand_path("../../../../modules/rules/templates/rules", __dir__) + + # Enumerate the files that are present in the rules directory. This fixture ensures + # that we don't miss an included rules file. + present_files = Dir.glob("#{template_dir}/*.yaml").map { |file| File.basename(file) } + + files = expected_files | present_files + + files.each do |file| + describe file("/home/ubuntu/forseti-security/rules/#{file}") do + it { should exist } + it "is valid YAML" do + YAML.load(subject.content) + end + end + end + + describe command("sudo gcloud sql instances describe forseti-server-db-#{suffix}") do + its("exit_status") { should eq 0 } + its("stdout") { should match("- name: net_write_timeout\n value: '240'") } + end +end diff --git a/test/integration/shared_vpc/controls/client.rb b/test/integration/shared_vpc/controls/client.rb index 6e06ed069..908e40455 120000 --- a/test/integration/shared_vpc/controls/client.rb +++ b/test/integration/shared_vpc/controls/client.rb @@ -1 +1 @@ -../../install_simple/controls/client.rb \ No newline at end of file +../../shared/controls/client.rb \ No newline at end of file diff --git a/test/integration/shared_vpc/controls/forseti.rb b/test/integration/shared_vpc/controls/forseti.rb index 7dce7c542..355ca5b70 120000 --- a/test/integration/shared_vpc/controls/forseti.rb +++ b/test/integration/shared_vpc/controls/forseti.rb @@ -1 +1 @@ -../../install_simple/controls/forseti.rb \ No newline at end of file +../../shared/controls/forseti.rb \ No newline at end of file diff --git a/test/integration/shared_vpc/controls/server.rb b/test/integration/shared_vpc/controls/server.rb index 25b62eaed..dad957039 120000 --- a/test/integration/shared_vpc/controls/server.rb +++ b/test/integration/shared_vpc/controls/server.rb @@ -1 +1 @@ -../../install_simple/controls/server.rb \ No newline at end of file +../../shared/controls/server.rb \ No newline at end of file diff --git a/test/setup/main.tf b/test/setup/main.tf index 2edcbc732..82c9567c5 100644 --- a/test/setup/main.tf +++ b/test/setup/main.tf @@ -161,6 +161,7 @@ module "forseti-service-network" { }, ] } + resource "google_compute_router" "forseti_service" { name = "forseti-service" network = module.forseti-service-network.network_self_link @@ -187,3 +188,31 @@ resource "google_compute_router_nat" "forseti_service" { project = module.forseti-service-project.project_id region = google_compute_router.forseti_service.region } + +#----------------# +# Forseti on GKE # +#----------------# +module "forseti-gke-project" { + source = "terraform-google-modules/project-factory/google//modules/shared_vpc" + version = "~> 3.0" + org_id = var.org_id + folder_id = var.folder_id + billing_account = var.billing_account + name = "ci-forseti-gke" + project_id = "ci-forseti-gke-${random_string.project_suffix.result}" + + activate_apis = [ + "cloudresourcemanager.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "iam.googleapis.com", + "logging.googleapis.com", + "monitoring.googleapis.com", + "serviceusage.googleapis.com", + "storage-api.googleapis.com", + "storage-component.googleapis.com", + "sqladmin.googleapis.com", + "sql-component.googleapis.com", + ] + shared_vpc = module.forseti-host-project.project_id +} diff --git a/test/setup/make_source.sh b/test/setup/make_source.sh index fdf95859e..dda2a6f31 100755 --- a/test/setup/make_source.sh +++ b/test/setup/make_source.sh @@ -44,3 +44,7 @@ folder_id=$(terraform output folder_id) echo "export TF_VAR_folder_id='$folder_id'" >> ../source.sh billing_account=$(terraform output billing_account) echo "export TF_VAR_billing_account='$billing_account'" >> ../source.sh + +# GKE +gke_project_id=$(terraform output gke_project_id) +echo "export TF_VAR_gke_project_id='$gke_project_id'" >> ../source.sh diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index 743964d7c..38e3dea76 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -26,6 +26,10 @@ output "project_id" { value = module.forseti-service-project.project_id } +output "gke_project_id" { + value = module.forseti-gke-project.project_id +} + # Temporarily disabled due to issue #285 #output "enforcer_project_id" { # value = module.forseti-enforcer-project.project_id