From 23a8e97a35ff63853472376e29eb51d156fdf8ab Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Tue, 14 Jul 2020 15:26:47 -0700 Subject: [PATCH] chore(samples): move samples and use standard templates for testing (#41) --- securitycenter/AUTHORING_GUIDE.md | 1 + securitycenter/CONTRIBUTING.md | 1 + securitycenter/snippets/noxfile.py | 222 +++++++ securitycenter/snippets/noxfile_config.py | 39 ++ securitycenter/snippets/requirements-test.txt | 1 + securitycenter/snippets/requirements.txt | 2 + securitycenter/snippets/snippets_findings.py | 572 ++++++++++++++++++ .../snippets/snippets_findings_test.py | 133 ++++ .../snippets/snippets_list_assets.py | 205 +++++++ .../snippets/snippets_list_assets_test.py | 64 ++ .../snippets/snippets_notification_configs.py | 138 +++++ .../snippets_notification_receiver.py | 60 ++ .../snippets/snippets_notification_test.py | 144 +++++ securitycenter/snippets/snippets_orgs.py | 58 ++ securitycenter/snippets/snippets_orgs_test.py | 36 ++ .../snippets/snippets_security_marks.py | 187 ++++++ .../snippets/snippets_security_marks_test.py | 137 +++++ 17 files changed, 2000 insertions(+) create mode 100644 securitycenter/AUTHORING_GUIDE.md create mode 100644 securitycenter/CONTRIBUTING.md create mode 100644 securitycenter/snippets/noxfile.py create mode 100644 securitycenter/snippets/noxfile_config.py create mode 100644 securitycenter/snippets/requirements-test.txt create mode 100644 securitycenter/snippets/requirements.txt create mode 100644 securitycenter/snippets/snippets_findings.py create mode 100644 securitycenter/snippets/snippets_findings_test.py create mode 100644 securitycenter/snippets/snippets_list_assets.py create mode 100644 securitycenter/snippets/snippets_list_assets_test.py create mode 100644 securitycenter/snippets/snippets_notification_configs.py create mode 100644 securitycenter/snippets/snippets_notification_receiver.py create mode 100644 securitycenter/snippets/snippets_notification_test.py create mode 100644 securitycenter/snippets/snippets_orgs.py create mode 100644 securitycenter/snippets/snippets_orgs_test.py create mode 100644 securitycenter/snippets/snippets_security_marks.py create mode 100644 securitycenter/snippets/snippets_security_marks_test.py diff --git a/securitycenter/AUTHORING_GUIDE.md b/securitycenter/AUTHORING_GUIDE.md new file mode 100644 index 000000000000..55c97b32f4c1 --- /dev/null +++ b/securitycenter/AUTHORING_GUIDE.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/AUTHORING_GUIDE.md \ No newline at end of file diff --git a/securitycenter/CONTRIBUTING.md b/securitycenter/CONTRIBUTING.md new file mode 100644 index 000000000000..34c882b6f1a3 --- /dev/null +++ b/securitycenter/CONTRIBUTING.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/CONTRIBUTING.md \ No newline at end of file diff --git a/securitycenter/snippets/noxfile.py b/securitycenter/snippets/noxfile.py new file mode 100644 index 000000000000..5660f08be441 --- /dev/null +++ b/securitycenter/snippets/noxfile.py @@ -0,0 +1,222 @@ +# Copyright 2019 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. + +from __future__ import print_function + +import os +from pathlib import Path +import sys + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +# Copy `noxfile_config.py` to your directory and modify it instead. + + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7"], + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars(): + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to tested samples. +ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = bool(os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False)) +# +# Style Checks +# + + +def _determine_local_import_names(start_dir): + """Determines all import names that should be considered "local". + + This is used when running the linter to insure that import order is + properly checked. + """ + file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)] + return [ + basename + for basename, extension in file_ext_pairs + if extension == ".py" + or os.path.isdir(os.path.join(start_dir, basename)) + and basename not in ("__pycache__") + ] + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--import-order-style=google", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session): + session.install("flake8", "flake8-import-order") + + local_names = _determine_local_import_names(".") + args = FLAKE8_COMMON_ARGS + [ + "--application-import-names", + ",".join(local_names), + ".", + ] + session.run("flake8", *args) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests(session, post_install=None): + """Runs py.test for a particular project.""" + if os.path.exists("requirements.txt"): + session.install("-r", "requirements.txt") + + if os.path.exists("requirements-test.txt"): + session.install("-r", "requirements-test.txt") + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars() + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session): + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root(): + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session, path): + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/securitycenter/snippets/noxfile_config.py b/securitycenter/snippets/noxfile_config.py new file mode 100644 index 000000000000..8e6ed132d642 --- /dev/null +++ b/securitycenter/snippets/noxfile_config.py @@ -0,0 +1,39 @@ +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be inported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7"], + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + # 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": { + "GCLOUD_ORGANIZATION": "1081635000895", + "GCLOUD_PROJECT": "project-a-id", + "GCLOUD_PUBSUB_TOPIC": "projects/project-a-id/topics/notifications-sample-topic", + "GCLOUD_PUBSUB_SUBSCRIPTION": "notification-sample-subscription", + }, +} diff --git a/securitycenter/snippets/requirements-test.txt b/securitycenter/snippets/requirements-test.txt new file mode 100644 index 000000000000..55b033e901cd --- /dev/null +++ b/securitycenter/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/securitycenter/snippets/requirements.txt b/securitycenter/snippets/requirements.txt new file mode 100644 index 000000000000..4b59ce29cc1a --- /dev/null +++ b/securitycenter/snippets/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-pubsub==1.6.0 +google-cloud-securitycenter==0.6.0 \ No newline at end of file diff --git a/securitycenter/snippets/snippets_findings.py b/securitycenter/snippets/snippets_findings.py new file mode 100644 index 000000000000..ec465f20e02d --- /dev/null +++ b/securitycenter/snippets/snippets_findings.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. + +"""Examples of working with source and findings in Cloud Security Command Center.""" + + +def create_source(organization_id): + """Create a new findings source. """ + # [START create_source] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + # organization_id is the numeric ID of the organization. e.g.: + # organization_id = "111122222444" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + created = client.create_source( + org_name, + { + "display_name": "Customized Display Name", + "description": "A new custom source that does X", + }, + ) + print("Created Source: {}".format(created.name)) + # [END create_source] + + +def get_source(source_name): + """Gets an existing source.""" + # [START get_source] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + source = client.get_source(source_name) + + print("Source: {}".format(source)) + # [END get_source] + return source + + +def update_source(source_name): + """Updates a source's display name.""" + # [START update_source] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + client = securitycenter.SecurityCenterClient() + + # Field mask to only update the display name. + field_mask = field_mask_pb2.FieldMask(paths=["display_name"]) + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + updated = client.update_source( + {"name": source_name, "display_name": "Updated Display Name"}, + update_mask=field_mask, + ) + print("Updated Source: {}".format(updated)) + # [END update_source] + return updated + + +def add_user_to_source(source_name): + """Gives a user findingsEditor permission to the source.""" + user_email = "csccclienttest@gmail.com" + # [START update_source_iam] + from google.cloud import securitycenter + from google.iam.v1 import policy_pb2 + + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + # Get the old policy so we can do an incremental update. + old_policy = client.get_iam_policy(source_name) + print("Old Policy: {}".format(old_policy)) + + # Setup a new IAM binding. + binding = policy_pb2.Binding() + binding.role = "roles/securitycenter.findingsEditor" + # user_email is an e-mail address known to Cloud IAM (e.g. a gmail address). + # user_mail = user@somedomain.com + binding.members.append("user:{}".format(user_email)) + + # Setting the e-tag avoids over-write existing policy + updated = client.set_iam_policy( + source_name, {"etag": old_policy.etag, "bindings": [binding]} + ) + + print("Updated Policy: {}".format(updated)) + + # [END update_source_iam] + return binding, updated + + +def list_source(organization_id): + """Lists finding sources.""" + i = -1 + # [START list_sources] + from google.cloud import securitycenter + + # Create a new client. + client = securitycenter.SecurityCenterClient() + # organization_id is the numeric ID of the organization. e.g.: + # organization_id = "111122222444" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + # Call the API and print out each existing source. + for i, source in enumerate(client.list_sources(org_name)): + print(i, source) + # [END list_sources] + return i + + +def create_finding(source_name): + """Creates a new finding.""" + # [START create_finding] + from google.cloud import securitycenter + from google.cloud.securitycenter_v1.proto.finding_pb2 import Finding + from google.protobuf.timestamp_pb2 import Timestamp + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # Use the current time as the finding "event time". + now_proto = Timestamp() + now_proto.GetCurrentTime() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + # Controlled by caller. + finding_id = "samplefindingid" + + # The resource this finding applies to. The CSCC UI can link + # the findings for a resource to the corresponding Asset of a resource + # if there are matches. + resource_name = "//cloudresourcemanager.googleapis.com/organizations/11232" + + # Call The API. + created_finding = client.create_finding( + source_name, + finding_id, + { + "state": Finding.ACTIVE, + "resource_name": resource_name, + "category": "MEDIUM_RISK_ONE", + "event_time": now_proto, + }, + ) + print(created_finding) + # [END create_finding] + return created_finding + + +def create_finding_with_source_properties(source_name): + """Demonstrate creating a new finding with source properties. """ + # [START create_finding_with_properties] + from google.cloud import securitycenter + from google.cloud.securitycenter_v1.proto.finding_pb2 import Finding + from google.protobuf.timestamp_pb2 import Timestamp + from google.protobuf.struct_pb2 import Value + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + # Controlled by caller. + finding_id = "samplefindingid2" + + # The resource this finding applies to. The CSCC UI can link + # the findings for a resource to the corresponding Asset of a resource + # if there are matches. + resource_name = "//cloudresourcemanager.googleapis.com/organizations/11232" + + # Define source properties values as protobuf "Value" objects. + str_value = Value() + str_value.string_value = "string_example" + num_value = Value() + num_value.number_value = 1234 + + # Use the current time as the finding "event time". + now_proto = Timestamp() + now_proto.GetCurrentTime() + + created_finding = client.create_finding( + source_name, + finding_id, + { + "state": Finding.ACTIVE, + "resource_name": resource_name, + "category": "MEDIUM_RISK_ONE", + "source_properties": {"s_value": str_value, "n_value": num_value}, + "event_time": now_proto, + }, + ) + print(created_finding) + # [END create_finding_with_properties] + + +def update_finding(source_name): + # [START update_finding] + from google.cloud import securitycenter + from google.protobuf.struct_pb2 import Value + from google.protobuf import field_mask_pb2 + from google.protobuf.timestamp_pb2 import Timestamp + + client = securitycenter.SecurityCenterClient() + # Only update the specific source property and event_time. event_time + # is required for updates. + field_mask = field_mask_pb2.FieldMask( + paths=["source_properties.s_value", "event_time"] + ) + value = Value() + value.string_value = "new_string" + + # Set the update time to Now. This must be some time greater then the + # event_time on the original finding. + now_proto = Timestamp() + now_proto.GetCurrentTime() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + finding_name = "{}/findings/samplefindingid2".format(source_name) + updated_finding = client.update_finding( + { + "name": finding_name, + "source_properties": {"s_value": value}, + "event_time": now_proto, + }, + update_mask=field_mask, + ) + + print( + "New Source properties: {}, Event Time {}".format( + updated_finding.source_properties, updated_finding.event_time.ToDatetime() + ) + ) + # [END update_finding] + + +def update_finding_state(source_name): + """Demonstrate updating only a finding state.""" + # [START update_finding_state] + from google.cloud import securitycenter + from google.cloud.securitycenter_v1.proto.finding_pb2 import Finding + from google.protobuf.timestamp_pb2 import Timestamp + + # Create a client. + client = securitycenter.SecurityCenterClient() + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + finding_name = "{}/findings/samplefindingid2".format(source_name) + + now_proto = Timestamp() + now_proto.GetCurrentTime() + + # Call the API to change the finding state to inactive as of now. + new_finding = client.set_finding_state( + finding_name, Finding.INACTIVE, start_time=now_proto + ) + print("New state: {}".format(Finding.State.Name(new_finding.state))) + # [END update_finding_state] + + +def trouble_shoot(source_name): + """Demonstrate calling test_iam_permissions to determine if the + service account has the correct permisions.""" + # [START test_iam_permissions] + from google.cloud import securitycenter + + # Create a client. + client = securitycenter.SecurityCenterClient() + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + # Check for permssions to call create_finding or update_finding. + permission_response = client.test_iam_permissions( + source_name, ["securitycenter.findings.update"] + ) + + print( + "Permision to create or update findings? {}".format( + len(permission_response.permissions) > 0 + ) + ) + # [END test_iam_permissions] + assert len(permission_response.permissions) > 0 + # [START test_iam_permissions] + # Check for permissions necessary to call set_finding_state. + permission_response = client.test_iam_permissions( + source_name, ["securitycenter.findings.setState"] + ) + print( + "Permision to update state? {}".format(len(permission_response.permissions) > 0) + ) + # [END test_iam_permissions] + return permission_response + assert len(permission_response.permissions) > 0 + + +def list_all_findings(organization_id): + # [START list_all_findings] + from google.cloud import securitycenter + + # Create a client. + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. e.g.: + # organization_id = "111122222444" + org_name = "organizations/{org_id}".format(org_id=organization_id) + # The "sources/-" suffix lists findings across all sources. You + # also use a specific source_name instead. + all_sources = "{org_name}/sources/-".format(org_name=org_name) + finding_result_iterator = client.list_findings(all_sources) + for i, finding_result in enumerate(finding_result_iterator): + print( + "{}: name: {} resource: {}".format( + i, finding_result.finding.name, finding_result.finding.resource_name + ) + ) + # [END list_all_findings] + return i + + +def list_filtered_findings(source_name): + # [START list_filtered_findings] + from google.cloud import securitycenter + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + # You an also use a wild-card "-" for all sources: + # source_name = "organizations/111122222444/sources/-" + finding_result_iterator = client.list_findings( + source_name, filter_='category="MEDIUM_RISK_ONE"' + ) + # Iterate an print all finding names and the resource they are + # in reference to. + for i, finding_result in enumerate(finding_result_iterator): + print( + "{}: name: {} resource: {}".format( + i, finding_result.finding.name, finding_result.finding.resource_name + ) + ) + # [END list_filtered_findings] + return i + + +def list_findings_at_time(source_name): + # [START list_findings_at_a_time] + from google.cloud import securitycenter + from google.protobuf.timestamp_pb2 import Timestamp + from datetime import timedelta, datetime + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + # You an also use a wild-card "-" for all sources: + # source_name = "organizations/111122222444/sources/-" + five_days_ago = Timestamp() + five_days_ago.FromDatetime(datetime.now() - timedelta(days=5)) + # [END list_findings_at_a_time] + i = -1 + five_days_ago.FromDatetime(datetime(2019, 3, 5, 0, 0, 0)) + # [START list_findings_at_a_time] + + finding_result_iterator = client.list_findings(source_name, read_time=five_days_ago) + for i, finding_result in enumerate(finding_result_iterator): + print( + "{}: name: {} resource: {}".format( + i, finding_result.finding.name, finding_result.finding.resource_name + ) + ) + # [END list_findings_at_a_time] + return i + + +def get_iam_policy(source_name): + """Gives a user findingsEditor permission to the source.""" + # [START get_source_iam] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + # Get the old policy so we can do an incremental update. + policy = client.get_iam_policy(source_name) + print("Policy: {}".format(policy)) + # [END get_source_iam] + + +def group_all_findings(organization_id): + """Demonstrates grouping all findings across an organization.""" + i = 0 + # [START group_all_findings] + from google.cloud import securitycenter + + # Create a client. + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. e.g.: + # organization_id = "111122222444" + org_name = "organizations/{org_id}".format(org_id=organization_id) + # The "sources/-" suffix lists findings across all sources. You + # also use a specific source_name instead. + all_sources = "{org_name}/sources/-".format(org_name=org_name) + group_result_iterator = client.group_findings(all_sources, group_by="category") + for i, group_result in enumerate(group_result_iterator): + print((i + 1), group_result) + # [END group_all_findings] + return i + + +def group_filtered_findings(source_name): + """Demonstrates grouping all findings across an organization.""" + i = 0 + # [START group_filtered_findings] + from google.cloud import securitycenter + + # Create a client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + group_result_iterator = client.group_findings( + source_name, group_by="category", filter_='state="ACTIVE"' + ) + for i, group_result in enumerate(group_result_iterator): + print((i + 1), group_result) + # [END group_filtered_findings] + return i + + +def group_findings_at_time(source_name): + """Demonstrates grouping all findings across an organization as of + a specific time.""" + i = -1 + # [START group_findings_at_time] + from datetime import datetime, timedelta + from google.cloud import securitycenter + from google.protobuf.timestamp_pb2 import Timestamp + + # Create a client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + # Group findings as of yesterday. + read_time = datetime.utcnow() - timedelta(days=1) + timestamp_proto = Timestamp() + timestamp_proto.FromDatetime(read_time) + + group_result_iterator = client.group_findings( + source_name, group_by="category", read_time=timestamp_proto + ) + for i, group_result in enumerate(group_result_iterator): + print((i + 1), group_result) + # [END group_filtered_findings_at_time] + return i + + +def group_findings_and_changes(source_name): + """Demonstrates grouping all findings across an organization and + associated changes.""" + i = 0 + # [START group_filtered_findings_with_changes] + from datetime import timedelta + + from google.cloud import securitycenter + from google.protobuf.duration_pb2 import Duration + + # Create a client. + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + + # List assets and their state change the last 30 days + compare_delta = timedelta(days=30) + # Convert the timedelta to a Duration + duration_proto = Duration() + duration_proto.FromTimedelta(compare_delta) + + group_result_iterator = client.group_findings( + source_name, group_by="state_change", compare_duration=duration_proto + ) + for i, group_result in enumerate(group_result_iterator): + print((i + 1), group_result) + # [END group_findings_with_changes] + return i diff --git a/securitycenter/snippets/snippets_findings_test.py b/securitycenter/snippets/snippets_findings_test.py new file mode 100644 index 000000000000..8ac01d8c13c4 --- /dev/null +++ b/securitycenter/snippets/snippets_findings_test.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# +# 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. +from itertools import chain +import os + +import pytest + +import snippets_findings + + +@pytest.fixture(scope="module") +def organization_id(): + """Get Organization ID from the environment variable """ + return os.environ["GCLOUD_ORGANIZATION"] + + +@pytest.fixture(scope="module") +def source_name(organization_id): + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + org_name = "organizations/{org_id}".format(org_id=organization_id) + + source = client.create_source( + org_name, + { + "display_name": "Unit test source", + "description": "A new custom source that does X", + }, + ) + return source.name + + +def test_create_source(organization_id): + snippets_findings.create_source(organization_id) + + +def test_get_source(source_name): + source = snippets_findings.get_source(source_name) + assert source.name == source_name + + +def test_update_source(source_name): + updated = snippets_findings.update_source(source_name) + assert updated.display_name == "Updated Display Name" + + +def test_add_user_to_source(source_name): + binding, updated = snippets_findings.add_user_to_source(source_name) + assert any( + member == "user:csccclienttest@gmail.com" + for member in chain.from_iterable( + binding.members for binding in updated.bindings + ) + ) + + +def test_list_source(organization_id): + count = snippets_findings.list_source(organization_id) + assert count >= 0 + + +def test_create_finding(source_name): + created_finding = snippets_findings.create_finding(source_name) + assert len(created_finding.name) > 0 + + +def test_create_finding_with_source_properties(source_name): + snippets_findings.create_finding_with_source_properties(source_name) + + +def test_update_finding(source_name): + snippets_findings.update_finding(source_name) + + +def test_update_finding_state(source_name): + snippets_findings.update_finding_state(source_name) + + +def test_trouble_shoot(source_name): + snippets_findings.trouble_shoot(source_name) + + +def test_list_all_findings(organization_id): + count = snippets_findings.list_all_findings(organization_id) + assert count > 0 + + +def test_list_filtered_findings(source_name): + count = snippets_findings.list_filtered_findings(source_name) + assert count > 0 + + +def list_findings_at_time(source_name): + count = snippets_findings.list_findings_at_time(source_name) + assert count == -1 + + +def test_get_iam_policy(source_name): + snippets_findings.get_iam_policy(source_name) + + +def test_group_all_findings(organization_id): + count = snippets_findings.group_all_findings(organization_id) + assert count > 0 + + +def test_group_filtered_findings(source_name): + count = snippets_findings.group_filtered_findings(source_name) + assert count == 0 + + +def test_group_findings_at_time(source_name): + count = snippets_findings.group_findings_at_time(source_name) + assert count == -1 + + +def test_group_findings_and_changes(source_name): + count = snippets_findings.group_findings_and_changes(source_name) + assert count == 0 diff --git a/securitycenter/snippets/snippets_list_assets.py b/securitycenter/snippets/snippets_list_assets.py new file mode 100644 index 000000000000..f88638023c16 --- /dev/null +++ b/securitycenter/snippets/snippets_list_assets.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. + +""" Examples of listing assets in Cloud Security Command Center.""" + + +def list_all_assets(organization_id): + """Demonstrate listing and printing all assets.""" + i = 0 + # [START demo_list_all_assets] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + # Call the API and print results. + asset_iterator = client.list_assets(org_name) + for i, asset_result in enumerate(asset_iterator): + print(i, asset_result) + # [END demo_list_all_assets] + return i + + +def list_assets_with_filters(organization_id): + """Demonstrate listing assets with a filter.""" + i = 0 + # [START demo_list_assets_with_filter] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + project_filter = ( + "security_center_properties.resource_type=" + + '"google.cloud.resourcemanager.Project"' + ) + # Call the API and print results. + asset_iterator = client.list_assets(org_name, filter_=project_filter) + for i, asset_result in enumerate(asset_iterator): + print(i, asset_result) + # [END demo_list_assets_with_filter] + return i + + +def list_assets_with_filters_and_read_time(organization_id): + """Demonstrate listing assets with a filter.""" + i = 0 + # [START demo_list_assets_with_filter_and_time] + from datetime import datetime, timedelta + + from google.protobuf.timestamp_pb2 import Timestamp + + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + project_filter = ( + "security_center_properties.resource_type=" + + '"google.cloud.resourcemanager.Project"' + ) + + # Lists assets as of yesterday. + read_time = datetime.utcnow() - timedelta(days=1) + timestamp_proto = Timestamp() + timestamp_proto.FromDatetime(read_time) + + # Call the API and print results. + asset_iterator = client.list_assets( + org_name, filter_=project_filter, read_time=timestamp_proto + ) + for i, asset_result in enumerate(asset_iterator): + print(i, asset_result) + # [END demo_list_assets_with_filter_and_time] + return i + + +def list_point_in_time_changes(organization_id): + """Demonstrate listing assets along with their state changes.""" + i = 0 + # [START demo_list_assets_changes] + from datetime import timedelta + + from google.protobuf.duration_pb2 import Duration + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + project_filter = ( + "security_center_properties.resource_type=" + + '"google.cloud.resourcemanager.Project"' + ) + + # List assets and their state change the last 30 days + compare_delta = timedelta(days=30) + # Convert the timedelta to a Duration + duration_proto = Duration() + duration_proto.FromTimedelta(compare_delta) + # Call the API and print results. + asset_iterator = client.list_assets( + org_name, filter_=project_filter, compare_duration=duration_proto + ) + for i, asset in enumerate(asset_iterator): + print(i, asset) + + # [END demo_list_assets_changes] + return i + + +def group_assets(organization_id): + """Demonstrates grouping all assets by type. """ + i = 0 + # [START group_all_assets] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + group_by_type = "security_center_properties.resource_type" + + result_iterator = client.group_assets(org_name, group_by=group_by_type) + for i, result in enumerate(result_iterator): + print((i + 1), result) + # [END group_all_assets] + return i + + +def group_filtered_assets(organization_id): + """Demonstrates grouping assets by type with a filter. """ + i = 0 + # [START group_all_assets] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + group_by_type = "security_center_properties.resource_type" + only_projects = ( + "security_center_properties.resource_type=" + + '"google.cloud.resourcemanager.Project"' + ) + result_iterator = client.group_assets( + org_name, group_by=group_by_type, filter_=only_projects + ) + for i, result in enumerate(result_iterator): + print((i + 1), result) + # [END group_all_assets] + # only one asset type is a project + return i + + +def group_assets_by_changes(organization_id): + """Demonstrates grouping assets by there changes over a period of time.""" + i = 0 + # [START group_all_assets_by_change] + from datetime import timedelta + + from google.cloud import securitycenter + from google.protobuf.duration_pb2 import Duration + + client = securitycenter.SecurityCenterClient() + + duration_proto = Duration() + duration_proto.FromTimedelta(timedelta(days=5)) + + # organization_id is the numeric ID of the organization. + # organization_id = "1234567777" + org_name = "organizations/{org_id}".format(org_id=organization_id) + result_iterator = client.group_assets( + org_name, group_by="state_change", compare_duration=duration_proto + ) + for i, result in enumerate(result_iterator): + print((i + 1), result) + # [END group_all_assets_by_change] + return i diff --git a/securitycenter/snippets/snippets_list_assets_test.py b/securitycenter/snippets/snippets_list_assets_test.py new file mode 100644 index 000000000000..a2646844262c --- /dev/null +++ b/securitycenter/snippets/snippets_list_assets_test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# +# 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. +"""Tests for snippets.""" + +import os + +import pytest + +import snippets_list_assets + + +@pytest.fixture(scope="module") +def organization_id(): + """Get Organization ID from the environment variable """ + return os.environ["GCLOUD_ORGANIZATION"] + + +def test_list_all_assets(organization_id): + """Demonstrate listing and printing all assets.""" + count = snippets_list_assets.list_all_assets(organization_id) + assert count > 0 + + +def list_assets_with_filters(organization_id): + count = snippets_list_assets.list_all_assets(organization_id) + assert count > 0 + + +def test_list_assets_with_filters_and_read_time(organization_id): + count = snippets_list_assets.list_assets_with_filters_and_read_time(organization_id) + assert count > 0 + + +def test_list_point_in_time_changes(organization_id): + count = snippets_list_assets.list_point_in_time_changes(organization_id) + assert count > 0 + + +def test_group_assets(organization_id): + count = snippets_list_assets.group_assets(organization_id) + assert count >= 8 # 8 different asset types. + + +def test_group_filtered_assets(organization_id): + count = snippets_list_assets.group_filtered_assets(organization_id) + assert count == 0 + + +def test_group_assets_by_changes(organization_id): + count = snippets_list_assets.group_assets_by_changes(organization_id) + assert count >= 0 # only one asset type is a project diff --git a/securitycenter/snippets/snippets_notification_configs.py b/securitycenter/snippets/snippets_notification_configs.py new file mode 100644 index 000000000000..acc4b8ba13c0 --- /dev/null +++ b/securitycenter/snippets/snippets_notification_configs.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# +# 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. +"""Demos for working with notification configs.""" + + +def create_notification_config(organization_id, notification_config_id, pubsub_topic): + + # [START scc_create_notification_config] + from google.cloud import securitycenter as securitycenter + + client = securitycenter.SecurityCenterClient() + + # TODO: organization_id = "your-org-id" + # TODO: notification_config_id = "your-config-id" + # TODO: pubsub_topic = "projects/{your-project-id}/topics/{your-topic-ic}" + # Ensure this ServiceAccount has the "pubsub.topics.setIamPolicy" permission on the new topic. + + org_name = "organizations/{org_id}".format(org_id=organization_id) + + created_notification_config = client.create_notification_config( + org_name, + notification_config_id, + { + "description": "Notification for active findings", + "pubsub_topic": pubsub_topic, + "streaming_config": {"filter": 'state = "ACTIVE"'}, + }, + ) + + print(created_notification_config) + # [END scc_create_notification_config] + return created_notification_config + + +def delete_notification_config(organization_id, notification_config_id): + + # [START scc_delete_notification_config] + from google.cloud import securitycenter as securitycenter + + client = securitycenter.SecurityCenterClient() + + # TODO: organization_id = "your-org-id" + # TODO: notification_config_id = "your-config-id" + + notification_config_name = "organizations/{org_id}/notificationConfigs/{config_id}".format( + org_id=organization_id, config_id=notification_config_id + ) + + client.delete_notification_config(notification_config_name) + print("Deleted notification config: {}".format(notification_config_name)) + # [END scc_delete_notification_config] + return True + + +def get_notification_config(organization_id, notification_config_id): + + # [START scc_get_notification_config] + from google.cloud import securitycenter as securitycenter + + client = securitycenter.SecurityCenterClient() + + # TODO: organization_id = "your-org-id" + # TODO: notification_config_id = "your-config-id" + + notification_config_name = "organizations/{org_id}/notificationConfigs/{config_id}".format( + org_id=organization_id, config_id=notification_config_id + ) + + notification_config = client.get_notification_config(notification_config_name) + print("Got notification config: {}".format(notification_config)) + # [END scc_get_notification_config] + return notification_config + + +def list_notification_configs(organization_id): + + # [START scc_list_notification_configs] + from google.cloud import securitycenter as securitycenter + + client = securitycenter.SecurityCenterClient() + + # TODO: organization_id = "your-org-id" + org_name = "organizations/{org_id}".format(org_id=organization_id) + + notification_configs_iterator = client.list_notification_configs(org_name) + for i, config in enumerate(notification_configs_iterator): + print("{}: notification_config: {}".format(i, config)) + # [END scc_list_notification_configs] + return notification_configs_iterator + + +def update_notification_config(organization_id, notification_config_id, pubsub_topic): + # [START scc_update_notification_config] + from google.cloud import securitycenter as securitycenter + from google.protobuf import field_mask_pb2 + + client = securitycenter.SecurityCenterClient() + + # TODO organization_id = "your-org-id" + # TODO notification_config_id = "config-id-to-update" + # TODO pubsub_topic = "projects/{new-project}/topics/{new-topic}" + # If updating a pubsub_topic, ensure this ServiceAccount has the + # "pubsub.topics.setIamPolicy" permission on the new topic. + + notification_config_name = "organizations/{org_id}/notificationConfigs/{config_id}".format( + org_id=organization_id, config_id=notification_config_id + ) + + updated_description = "New updated description" + + # Only description and pubsub_topic can be updated. + field_mask = field_mask_pb2.FieldMask(paths=["description", "pubsub_topic"]) + + updated_notification_config = client.update_notification_config( + { + "name": notification_config_name, + "description": updated_description, + "pubsub_topic": pubsub_topic, + }, + update_mask=field_mask, + ) + + print(updated_notification_config) + # [END scc_update_notification_config] + return updated_notification_config diff --git a/securitycenter/snippets/snippets_notification_receiver.py b/securitycenter/snippets/snippets_notification_receiver.py new file mode 100644 index 000000000000..aad6ba797bbd --- /dev/null +++ b/securitycenter/snippets/snippets_notification_receiver.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# 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. +"""Demo for receiving notifications.""" + + +def receive_notifications(project_id, subscription_name): + # [START scc_receive_notifications] + # Requires https://cloud.google.com/pubsub/docs/quickstart-client-libraries#pubsub-client-libraries-python + import concurrent + + from google.cloud import pubsub_v1 + from google.cloud.securitycenter_v1.proto.notification_message_pb2 import ( + NotificationMessage, + ) + from google.protobuf import json_format + + # TODO: project_id = "your-project-id" + # TODO: subscription_name = "your-subscription-name" + + def callback(message): + print("Received message") + + notification_msg = NotificationMessage() + json_format.Parse(message.data, notification_msg) + + print( + "Notification config name: {}".format( + notification_msg.notification_config_name + ) + ) + print("Finding: {}".format(notification_msg.finding)) + + # Ack the message to prevent it from being pulled again + message.ack() + + subscriber = pubsub_v1.SubscriberClient() + subscription_path = subscriber.subscription_path(project_id, subscription_name) + + streaming_pull_future = subscriber.subscribe(subscription_path, callback=callback) + + print("Listening for messages on {}...\n".format(subscription_path)) + try: + streaming_pull_future.result(timeout=1) # Block for 1 second + except concurrent.futures.TimeoutError: + streaming_pull_future.cancel() + # [END scc_receive_notifications] + return True diff --git a/securitycenter/snippets/snippets_notification_test.py b/securitycenter/snippets/snippets_notification_test.py new file mode 100644 index 000000000000..73ad00602bc7 --- /dev/null +++ b/securitycenter/snippets/snippets_notification_test.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# +# 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. +"""Tests for snippets.""" + +import os +import uuid + +from google.cloud import securitycenter as securitycenter +import pytest + +import snippets_notification_configs +import snippets_notification_receiver + +ORG_ID = os.environ["GCLOUD_ORGANIZATION"] +PROJECT_ID = os.environ["GCLOUD_PROJECT"] +PUBSUB_TOPIC = os.environ["GCLOUD_PUBSUB_TOPIC"] +PUBSUB_SUBSCRIPTION = os.environ["GCLOUD_PUBSUB_SUBSCRIPTION"] + +CREATE_CONFIG_ID = "new-notification-pytest" + str(uuid.uuid1()) +DELETE_CONFIG_ID = "new-notification-pytest" + str(uuid.uuid1()) +GET_CONFIG_ID = "new-notification-pytest" + str(uuid.uuid1()) +UPDATE_CONFIG_ID = "new-notification-pytest" + str(uuid.uuid1()) + + +def cleanup_notification_config(notification_config_id): + client = securitycenter.SecurityCenterClient() + + notification_config_name = "organizations/{org_id}/notificationConfigs/{config_id}".format( + org_id=ORG_ID, config_id=notification_config_id + ) + client.delete_notification_config(notification_config_name) + + +@pytest.fixture +def new_notification_config_for_update(): + client = securitycenter.SecurityCenterClient() + + org_name = "organizations/{org_id}".format(org_id=ORG_ID) + + created_notification_config = client.create_notification_config( + org_name, + UPDATE_CONFIG_ID, + { + "description": "Notification for active findings", + "pubsub_topic": PUBSUB_TOPIC, + "streaming_config": {"filter": ""}, + }, + ) + yield created_notification_config + cleanup_notification_config(UPDATE_CONFIG_ID) + + +@pytest.fixture +def new_notification_config_for_get(): + client = securitycenter.SecurityCenterClient() + + org_name = "organizations/{org_id}".format(org_id=ORG_ID) + + created_notification_config = client.create_notification_config( + org_name, + GET_CONFIG_ID, + { + "description": "Notification for active findings", + "pubsub_topic": PUBSUB_TOPIC, + "streaming_config": {"filter": ""}, + }, + ) + yield created_notification_config + cleanup_notification_config(GET_CONFIG_ID) + + +@pytest.fixture +def deleted_notification_config(): + client = securitycenter.SecurityCenterClient() + + org_name = "organizations/{org_id}".format(org_id=ORG_ID) + + created_notification_config = client.create_notification_config( + org_name, + DELETE_CONFIG_ID, + { + "description": "Notification for active findings", + "pubsub_topic": PUBSUB_TOPIC, + "streaming_config": {"filter": ""}, + }, + ) + return created_notification_config + + +def test_create_notification_config(): + created_notification_config = snippets_notification_configs.create_notification_config( + ORG_ID, CREATE_CONFIG_ID, PUBSUB_TOPIC + ) + assert created_notification_config is not None + + cleanup_notification_config(CREATE_CONFIG_ID) + + +def test_delete_notification_config(deleted_notification_config): + assert ( + snippets_notification_configs.delete_notification_config( + ORG_ID, DELETE_CONFIG_ID + ) + ) + + +def test_get_notification_config(new_notification_config_for_get): + retrieved_config = snippets_notification_configs.get_notification_config( + ORG_ID, GET_CONFIG_ID + ) + assert retrieved_config is not None + + +def test_list_notification_configs(): + iterator = snippets_notification_configs.list_notification_configs(ORG_ID) + assert iterator is not None + + +def test_update_notification_config(new_notification_config_for_update): + updated_config = snippets_notification_configs.update_notification_config( + ORG_ID, UPDATE_CONFIG_ID, PUBSUB_TOPIC + ) + assert updated_config is not None + + +def test_receive_notifications(): + assert ( + snippets_notification_receiver.receive_notifications( + PROJECT_ID, PUBSUB_SUBSCRIPTION + ) + ) diff --git a/securitycenter/snippets/snippets_orgs.py b/securitycenter/snippets/snippets_orgs.py new file mode 100644 index 000000000000..6b95e49e8601 --- /dev/null +++ b/securitycenter/snippets/snippets_orgs.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. +"""Examples for working with organization settings. """ + + +def get_settings(organization_id): + """Example showing how to retreive current organization settings.""" + # [START get_org_settings] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + # organization_id is numeric ID for the organization. e.g. + # organization_id = "111112223333" + + org_settings_name = client.organization_settings_path(organization_id) + + org_settings = client.get_organization_settings(org_settings_name) + print(org_settings) + # [END get_org_settings] + + +def update_asset_discovery_org_settings(organization_id): + """Example showing how to update the asset discovery configuration + for an organization.""" + # [START update_org_settings] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + # Create the client + client = securitycenter.SecurityCenterClient() + # organization_id is numeric ID for the organization. e.g. + # organization_id = "111112223333" + org_settings_name = "organizations/{org_id}/organizationSettings".format( + org_id=organization_id + ) + # Only update the enable_asset_discovery_value (leave others untouched). + field_mask = field_mask_pb2.FieldMask(paths=["enable_asset_discovery"]) + # Call the service. + updated = client.update_organization_settings( + {"name": org_settings_name, "enable_asset_discovery": True}, + update_mask=field_mask, + ) + print("Asset Discovery Enabled? {}".format(updated.enable_asset_discovery)) + # [END update_org_settings] + return updated diff --git a/securitycenter/snippets/snippets_orgs_test.py b/securitycenter/snippets/snippets_orgs_test.py new file mode 100644 index 000000000000..fc9a3a90663d --- /dev/null +++ b/securitycenter/snippets/snippets_orgs_test.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. +"""Examples for working with organization settings. """ +import os + +import pytest + +import snippets_orgs + + +@pytest.fixture(scope="module") +def organization_id(): + """Get Organization ID from the environment variable """ + return os.environ["GCLOUD_ORGANIZATION"] + + +def test_get_settings(organization_id): + snippets_orgs.get_settings(organization_id) + + +def test_update_asset_discovery_org_settings(organization_id): + updated = snippets_orgs.update_asset_discovery_org_settings(organization_id) + assert updated.enable_asset_discovery diff --git a/securitycenter/snippets/snippets_security_marks.py b/securitycenter/snippets/snippets_security_marks.py new file mode 100644 index 000000000000..885323410c40 --- /dev/null +++ b/securitycenter/snippets/snippets_security_marks.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. +"""Demos for working with security marks.""" + + +def add_to_asset(asset_name): + """Add new security marks to an asset.""" + # [START add_marks_to_asset] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # asset_name is the resource path for an asset that exists in CSCC. + # Its format is "organization/{organization_id}/assets/{asset_id} + # e.g.: + # asset_name = organizations/123123342/assets/12312321 + marks_name = "{}/securityMarks".format(asset_name) + + # Notice the suffix after "marks." in the field mask matches the keys + # in marks. + field_mask = field_mask_pb2.FieldMask(paths=["marks.key_a", "marks.key_b"]) + marks = {"key_a": "value_a", "key_b": "value_b"} + + updated_marks = client.update_security_marks( + {"name": marks_name, "marks": marks}, + # If this field was left empty, all marks would be cleared before adding + # the new values. + update_mask=field_mask, + ) + print(updated_marks) + # [END add_marks_to_asset] + return updated_marks, marks + + +def clear_from_asset(asset_name): + """Removes security marks from an asset.""" + # Make sure they are there first + add_to_asset(asset_name) + # [START clear_marks_asset] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + # Create a new client. + client = securitycenter.SecurityCenterClient() + + # asset_name is the resource path for an asset that exists in CSCC. + # Its format is "organization/{organization_id}/assets/{asset_id} + # e.g.: + # asset_name = organizations/123123342/assets/12312321 + marks_name = "{}/securityMarks".format(asset_name) + + field_mask = field_mask_pb2.FieldMask(paths=["marks.key_a", "marks.key_b"]) + + updated_marks = client.update_security_marks( + { + "name": marks_name + # Note, no marks specified, so the specified values in + # the fields masks will be deleted. + }, + # If this field was left empty, all marks would be cleared. + update_mask=field_mask, + ) + print(updated_marks) + # [END clear_marks_asset] + return updated_marks + + +def delete_and_update_marks(asset_name): + """Updates and deletes security marks from an asset in the same call.""" + # Make sure they are there first + add_to_asset(asset_name) + # [START delete_and_update_marks] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + client = securitycenter.SecurityCenterClient() + # asset_name is the resource path for an asset that exists in CSCC. + # Its format is "organization/{organization_id}/assets/{asset_id} + # e.g.: + # asset_name = organizations/123123342/assets/12312321 + marks_name = "{}/securityMarks".format(asset_name) + + field_mask = field_mask_pb2.FieldMask(paths=["marks.key_a", "marks.key_b"]) + marks = {"key_a": "new_value_for_a"} + + updated_marks = client.update_security_marks( + {"name": marks_name, "marks": marks}, update_mask=field_mask + ) + print(updated_marks) + # [END delete_and_update_marks] + return updated_marks + + +def add_to_finding(finding_name): + """Adds security marks to a finding. """ + # [START add_marks_to_finding] + from google.cloud import securitycenter + from google.protobuf import field_mask_pb2 + + client = securitycenter.SecurityCenterClient() + # finding_name is the resource path for a finding that exists in CSCC. + # Its format is + # "organizations/{org_id}/sources/{source_id}/findings/{finding_id}" + # e.g.: + # finding_name = "organizations/1112/sources/1234/findings/findingid" + finding_marks_name = "{}/securityMarks".format(finding_name) + + # Notice the suffix after "marks." in the field mask matches the keys + # in marks. + field_mask = field_mask_pb2.FieldMask( + paths=["marks.finding_key_a", "marks.finding_key_b"] + ) + marks = {"finding_key_a": "value_a", "finding_key_b": "value_b"} + + updated_marks = client.update_security_marks( + {"name": finding_marks_name, "marks": marks}, update_mask=field_mask + ) + # [END add_marks_to_finding] + return updated_marks, marks + + +def list_assets_with_query_marks(organization_id, asset_name): + """Lists assets with a filter on security marks. """ + add_to_asset(asset_name) + i = -1 + # [START demo_list_assets_with_security_marks] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # organization_id is the numeric ID of the organization. + # organization_id=1234567777 + org_name = "organizations/{org_id}".format(org_id=organization_id) + + marks_filter = 'security_marks.marks.key_a = "value_a"' + # Call the API and print results. + asset_iterator = client.list_assets(org_name, filter_=marks_filter) + + # Call the API and print results. + asset_iterator = client.list_assets(org_name, filter_=marks_filter) + for i, asset_result in enumerate(asset_iterator): + print(i, asset_result) + # [END demo_list_assets_with_security_marks] + return i + + +def list_findings_with_query_marks(source_name, finding_name): + """Lists findings with a filter on security marks.""" + # ensure marks are set on finding. + add_to_finding(finding_name) + i = -1 + # [START demo_list_findings_with_security_marks] + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + + # source_name is the resource path for a source that has been + # created previously (you can use list_sources to find a specific one). + # Its format is: + # source_name = "organizations/{organization_id}/sources/{source_id}" + # e.g.: + # source_name = "organizations/111122222444/sources/1234" + marks_filter = 'NOT security_marks.marks.finding_key_a="value_a"' + + # Call the API and print results. + finding_iterator = client.list_findings(source_name, filter_=marks_filter) + for i, finding_result in enumerate(finding_iterator): + print(i, finding_result) + # [END demo_list_findings_with_security_marks] + # one finding should have been updated with keys, and one should be + # untouched. + return i diff --git a/securitycenter/snippets/snippets_security_marks_test.py b/securitycenter/snippets/snippets_security_marks_test.py new file mode 100644 index 000000000000..18950f86bbfd --- /dev/null +++ b/securitycenter/snippets/snippets_security_marks_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Copyright 2019 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. +"""Demos for working with security marks.""" +import os +import random + +import pytest + +import snippets_security_marks + + +@pytest.fixture(scope="module") +def organization_id(): + """Gets Organization ID from the environment variable """ + return os.environ["GCLOUD_ORGANIZATION"] + + +@pytest.fixture(scope="module") +def asset_name(organization_id): + """Returns a random asset name from existing assets.""" + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + # organization_id is the numeric ID of the organization. + # organization_id=1234567777 + org_name = "organizations/{org_id}".format(org_id=organization_id) + assets = list(client.list_assets(org_name)) + # Select a random asset to avoid collision between integration tests. + asset = (random.sample(assets, 1)[0]).asset.name + + # Set fresh marks. + update = client.update_security_marks( + {"name": "{}/securityMarks".format(asset), "marks": {"other": "other_val"}} + ) + assert update.marks == {"other": "other_val"} + return asset + + +@pytest.fixture(scope="module") +def source_name(organization_id): + """Creates a new source in the organization.""" + from google.cloud import securitycenter + + client = securitycenter.SecurityCenterClient() + org_name = "organizations/{org_id}".format(org_id=organization_id) + source = client.create_source( + org_name, + { + "display_name": "Security marks Unit test source", + "description": "A new custom source that does X", + }, + ) + return source.name + + +@pytest.fixture(scope="module") +def finding_name(source_name): + """Creates a new finding and returns it name.""" + from google.cloud import securitycenter + from google.cloud.securitycenter_v1.proto.finding_pb2 import Finding + from google.protobuf.timestamp_pb2 import Timestamp + + client = securitycenter.SecurityCenterClient() + + now_proto = Timestamp() + now_proto.GetCurrentTime() + + finding = client.create_finding( + source_name, + "scfinding", + { + "state": Finding.ACTIVE, + "category": "C1", + "event_time": now_proto, + "resource_name": "//cloudresourcemanager.googleapis.com/organizations/1234", + }, + ) + client.create_finding( + source_name, + "untouched", + { + "state": Finding.ACTIVE, + "category": "MEDIUM_RISK_ONE", + "event_time": now_proto, + "resource_name": "//cloudresourcemanager.googleapis.com/organizations/1234", + }, + ) + + return finding.name + + +def test_add_to_asset(asset_name): + updated_marks, marks = snippets_security_marks.add_to_asset(asset_name) + assert updated_marks.marks.keys() >= marks.keys() + + +def test_clear_from_asset(asset_name): + updated_marks = snippets_security_marks.clear_from_asset(asset_name) + assert "other" in updated_marks.marks + assert len(updated_marks.marks) == 1 + + +def test_delete_and_update_marks(asset_name): + updated_marks = snippets_security_marks.delete_and_update_marks(asset_name) + assert updated_marks.marks == {"key_a": "new_value_for_a", "other": "other_val"} + + +def test_add_to_finding(finding_name): + updated_marks, marks = snippets_security_marks.add_to_finding(finding_name) + assert updated_marks.marks == marks + + +def test_list_assets_with_query_marks(organization_id, asset_name): + count = snippets_security_marks.list_assets_with_query_marks( + organization_id, asset_name + ) + assert count >= 0 + + +def test_list_findings_with_query_marks(source_name, finding_name): + count = snippets_security_marks.list_findings_with_query_marks( + source_name, finding_name + ) + assert count == 0