diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..3b4e6804 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax + + +/samples/ @tdh911 @googleapis/python-samples-owners \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index dd4f09ef..758afabf 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,28 +14,33 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Generated by synthtool. DO NOT EDIT! + from __future__ import absolute_import import os -import shutil +import shutil import nox +BLACK_VERSION = "black==19.10b0" +BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] + +DEFAULT_PYTHON_VERSION = "3.8" +SYSTEM_TEST_PYTHON_VERSIONS = ["2.7", "3.8"] +UNIT_TEST_PYTHON_VERSIONS = ["2.7", "3.5", "3.6", "3.7", "3.8"] + -@nox.session(python="3.7") +@nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): """Run linters. Returns a failure if the linters find linting errors or sufficiently serious code quality issues. """ - session.install("flake8", "black") + session.install("flake8", BLACK_VERSION) session.run( - "black", - "--check", - "google", - "tests", - "docs", + "black", "--check", *BLACK_PATHS, ) session.run("flake8", "google", "tests") @@ -45,21 +50,18 @@ def blacken(session): """Run black. Format code to uniform standard. - + This currently uses Python 3.6 due to the automated Kokoro run of synthtool. That run uses an image that doesn't have 3.6 installed. Before updating this check the state of the `gcp_ubuntu_config` we use for that Kokoro run. """ - session.install("black") + session.install(BLACK_VERSION) session.run( - "black", - "google", - "tests", - "docs", + "black", *BLACK_PATHS, ) -@nox.session(python="3.7") +@nox.session(python=DEFAULT_PYTHON_VERSION) def lint_setup_py(session): """Verify that setup.py is valid (including RST check).""" session.install("docutils", "pygments") @@ -75,6 +77,7 @@ def default(session): session.run( "py.test", "--quiet", + "--cov=google.cloud.securitycenter", "--cov=google.cloud", "--cov=tests.unit", "--cov-append", @@ -86,13 +89,13 @@ def default(session): ) -@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8"]) +@nox.session(python=UNIT_TEST_PYTHON_VERSIONS) def unit(session): """Run the unit test suite.""" default(session) -@nox.session(python=["2.7", "3.7"]) +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run the system test suite.""" system_test_path = os.path.join("tests", "system.py") @@ -112,7 +115,9 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. - session.install("mock", "pytest", "google-cloud-testutils") + session.install( + "mock", "pytest", "google-cloud-testutils", + ) session.install("-e", ".") # Run py.test against the system tests. @@ -122,7 +127,7 @@ def system(session): session.run("py.test", "--quiet", system_test_folder_path, *session.posargs) -@nox.session(python="3.7") +@nox.session(python=DEFAULT_PYTHON_VERSION) def cover(session): """Run the final coverage report. @@ -135,69 +140,23 @@ def cover(session): session.run("coverage", "erase") -@nox.session(python=["2.7", "3.5", "3.6", "3.7"]) -def snippets(session): - """Run the documentation example snippets.""" - # Sanity check: Only run snippets system tests if the environment variable - # is set. - if not os.environ.get('GOOGLE_APPLICATION_CREDENTIALS', ''): - session.skip('Credentials must be set via environment variable.') - if not os.environ.get('GCLOUD_ORGANIZATION', ''): - if 'KOKORO_GFILE_DIR' in os.environ: - session.env['GCLOUD_ORGANIZATION'] = '1081635000895' - else: - session.skip('Credentials must be set via environment variable.') - if not os.environ.get('GCLOUD_PROJECT', ''): - if 'KOKORO_GFILE_DIR' in os.environ: - session.env['GCLOUD_PROJECT'] = 'project-a-id' - else: - session.skip('Credentials must be set via environment variable.') - if not os.environ.get('GCLOUD_PUBSUB_TOPIC', ''): - if 'KOKORO_GFILE_DIR' in os.environ: - session.env['GCLOUD_PUBSUB_TOPIC'] = 'projects/project-a-id/topics/notifications-sample-topic' - else: - session.skip('Credentials must be set via environment variable.') - if not os.environ.get('GCLOUD_PUBSUB_SUBSCRIPTION', ''): - if 'KOKORO_GFILE_DIR' in os.environ: - session.env['GCLOUD_PUBSUB_SUBSCRIPTION'] = 'notification_sample_subscription' - else: - session.skip('Credentials must be set via environment variable.') - - - # Install all test dependencies, then install local packages in place. - session.install('mock', 'pytest') - session.install("-r", "docs/requirements.txt") - session.install('-e', '.') - session.run( - 'py.test', - '--quiet', - os.path.join('docs', 'snippets_list_assets.py'), - os.path.join('docs', 'snippets_security_marks.py'), - os.path.join('docs', 'snippets_orgs.py'), - os.path.join('docs', 'snippets_findings.py'), - os.path.join('docs', 'snippets_security_marks.py'), - os.path.join('docs', 'snippets_notification_test.py'), - - - *session.posargs - ) - - -@nox.session(python="3.7") +@nox.session(python=DEFAULT_PYTHON_VERSION) def docs(session): """Build the docs for this library.""" - session.install('-e', '.') - session.install('sphinx<3.0.0', 'alabaster', 'recommonmark') + session.install("-e", ".") + session.install("sphinx<3.0.0", "alabaster", "recommonmark") - shutil.rmtree(os.path.join('docs', '_build'), ignore_errors=True) + shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) session.run( - 'sphinx-build', - '-W', # warnings as errors - '-T', # show full traceback on exception - '-N', # no colors - '-b', 'html', - '-d', os.path.join('docs', '_build', 'doctrees', ''), - os.path.join('docs', ''), - os.path.join('docs', '_build', 'html', ''), + "sphinx-build", + "-W", # warnings as errors + "-T", # show full traceback on exception + "-N", # no colors + "-b", + "html", + "-d", + os.path.join("docs", "_build", "doctrees", ""), + os.path.join("docs", ""), + os.path.join("docs", "_build", "html", ""), ) diff --git a/samples/AUTHORING_GUIDE.md b/samples/AUTHORING_GUIDE.md new file mode 100644 index 00000000..55c97b32 --- /dev/null +++ b/samples/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/samples/CONTRIBUTING.md b/samples/CONTRIBUTING.md new file mode 100644 index 00000000..34c882b6 --- /dev/null +++ b/samples/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/samples/snippets/noxfile.py b/samples/snippets/noxfile.py new file mode 100644 index 00000000..5660f08b --- /dev/null +++ b/samples/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/samples/snippets/noxfile_config.py b/samples/snippets/noxfile_config.py new file mode 100644 index 00000000..8e6ed132 --- /dev/null +++ b/samples/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/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt new file mode 100644 index 00000000..55b033e9 --- /dev/null +++ b/samples/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt new file mode 100644 index 00000000..4b59ce29 --- /dev/null +++ b/samples/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/samples/snippets/snippets_findings.py b/samples/snippets/snippets_findings.py new file mode 100644 index 00000000..ec465f20 --- /dev/null +++ b/samples/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/samples/snippets/snippets_findings_test.py b/samples/snippets/snippets_findings_test.py new file mode 100644 index 00000000..8ac01d8c --- /dev/null +++ b/samples/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/samples/snippets/snippets_list_assets.py b/samples/snippets/snippets_list_assets.py new file mode 100644 index 00000000..f8863802 --- /dev/null +++ b/samples/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/samples/snippets/snippets_list_assets_test.py b/samples/snippets/snippets_list_assets_test.py new file mode 100644 index 00000000..a2646844 --- /dev/null +++ b/samples/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/samples/snippets/snippets_notification_configs.py b/samples/snippets/snippets_notification_configs.py new file mode 100644 index 00000000..acc4b8ba --- /dev/null +++ b/samples/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/samples/snippets/snippets_notification_receiver.py b/samples/snippets/snippets_notification_receiver.py new file mode 100644 index 00000000..aad6ba79 --- /dev/null +++ b/samples/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/samples/snippets/snippets_notification_test.py b/samples/snippets/snippets_notification_test.py new file mode 100644 index 00000000..73ad0060 --- /dev/null +++ b/samples/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/samples/snippets/snippets_orgs.py b/samples/snippets/snippets_orgs.py new file mode 100644 index 00000000..6b95e49e --- /dev/null +++ b/samples/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/samples/snippets/snippets_orgs_test.py b/samples/snippets/snippets_orgs_test.py new file mode 100644 index 00000000..fc9a3a90 --- /dev/null +++ b/samples/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/samples/snippets/snippets_security_marks.py b/samples/snippets/snippets_security_marks.py new file mode 100644 index 00000000..88532341 --- /dev/null +++ b/samples/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/samples/snippets/snippets_security_marks_test.py b/samples/snippets/snippets_security_marks_test.py new file mode 100644 index 00000000..18950f86 --- /dev/null +++ b/samples/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 diff --git a/setup.py b/setup.py index d052d865..5a2cf044 100644 --- a/setup.py +++ b/setup.py @@ -19,59 +19,58 @@ import setuptools -name = 'google-cloud-securitycenter' -description = 'Cloud Security Command Center API API client library' +name = "google-cloud-securitycenter" +description = "Cloud Security Command Center API API client library" version = "0.6.0" -release_status = 'Development Status :: 3 - Alpha' +release_status = "Development Status :: 3 - Alpha" dependencies = [ - 'google-api-core[grpc] >= 1.14.0, < 2.0.0dev', - 'grpc-google-iam-v1 >= 0.12.3, < 0.13dev', + "google-api-core[grpc] >= 1.14.0, < 2.0.0dev", + "grpc-google-iam-v1 >= 0.12.3, < 0.13dev", 'enum34; python_version < "3.4"', ] package_root = os.path.abspath(os.path.dirname(__file__)) -readme_filename = os.path.join(package_root, 'README.rst') -with io.open(readme_filename, encoding='utf-8') as readme_file: +readme_filename = os.path.join(package_root, "README.rst") +with io.open(readme_filename, encoding="utf-8") as readme_file: readme = readme_file.read() packages = [ - package for package in setuptools.find_packages() - if package.startswith('google') + package for package in setuptools.find_packages() if package.startswith("google") ] -namespaces = ['google'] -if 'google.cloud' in packages: - namespaces.append('google.cloud') +namespaces = ["google"] +if "google.cloud" in packages: + namespaces.append("google.cloud") setuptools.setup( name=name, version=version, description=description, long_description=readme, - author='Google LLC', - author_email='googleapis-packages@google.com', - license='Apache 2.0', - url='https://github.com/googleapis/python-securitycenter', + author="Google LLC", + author_email="googleapis-packages@google.com", + license="Apache 2.0", + url="https://github.com/googleapis/python-securitycenter", classifiers=[ release_status, - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Operating System :: OS Independent', - 'Topic :: Internet', + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Operating System :: OS Independent", + "Topic :: Internet", ], - platforms='Posix; MacOS X; Windows', + platforms="Posix; MacOS X; Windows", packages=packages, namespace_packages=namespaces, install_requires=dependencies, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", include_package_data=True, zip_safe=False, ) diff --git a/synth.metadata b/synth.metadata index 602cd308..c9656a59 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,23 +3,30 @@ { "git": { "name": ".", - "remote": "https://github.com/googleapis/python-securitycenter.git", - "sha": "93853b4b33a15b7fa59e89b0d498d1a10af7748d" + "remote": "git@github.com:googleapis/python-securitycenter.git", + "sha": "a30a996cafb8dd9fed3c86ef641d42ab959febe3" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "db69b46790b55a82ab7cfa473d031da787bc7591", - "internalRef": "320411362" + "sha": "50ae1c72fd94a3ae4269394b09e4b7fbb9251146", + "internalRef": "320484049" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "303271797a360f8a439203413f13a160f2f5b3b4" + "sha": "799d8e6522c1ef7cb55a70d9ea0b15e045c3d00b" + } + }, + { + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "799d8e6522c1ef7cb55a70d9ea0b15e045c3d00b" } } ], diff --git a/synth.py b/synth.py index 8f230a46..576b74a6 100644 --- a/synth.py +++ b/synth.py @@ -15,6 +15,7 @@ """This script is used to synthesize generated parts of this library.""" import synthtool as s from synthtool import gcp +from synthtool.languages import python gapic = gcp.GAPICBazel() common = gcp.CommonTemplates() @@ -48,8 +49,10 @@ # ---------------------------------------------------------------------------- # Add templated files # ---------------------------------------------------------------------------- -templated_files = common.py_library(cov_level=88) -s.move(templated_files, excludes=['noxfile.py']) +templated_files = common.py_library(cov_level=88, samples=True) +s.move(templated_files) + +python.py_samples(root="samples", skip_readmes=True) # TODO(busunkim): Use latest sphinx after microgenerator transition s.replace("noxfile.py", """['"]sphinx['"]""", '"sphinx<3.0.0"')