From f081bf51d66bb7d9a99490a4d60f3b802a558414 Mon Sep 17 00:00:00 2001 From: Jisha Abubaker Date: Tue, 18 Jul 2017 09:25:23 -0700 Subject: [PATCH] DLP samples (#752) --- dlp/README.md | 115 +++++ dlp/pom.xml | 101 ++++ .../main/java/com/example/dlp/Inspect.java | 444 ++++++++++++++++++ .../main/java/com/example/dlp/Metadata.java | 96 ++++ dlp/src/main/java/com/example/dlp/Redact.java | 142 ++++++ .../test/java/com/example/dlp/InspectIT.java | 103 ++++ .../test/java/com/example/dlp/MetadataIT.java | 61 +++ .../test/java/com/example/dlp/RedactIT.java | 55 +++ dlp/src/test/resources/test.png | Bin 0 -> 21438 bytes dlp/src/test/resources/test.txt | 1 + pom.xml | 1 + 11 files changed, 1119 insertions(+) create mode 100644 dlp/README.md create mode 100644 dlp/pom.xml create mode 100644 dlp/src/main/java/com/example/dlp/Inspect.java create mode 100644 dlp/src/main/java/com/example/dlp/Metadata.java create mode 100644 dlp/src/main/java/com/example/dlp/Redact.java create mode 100644 dlp/src/test/java/com/example/dlp/InspectIT.java create mode 100644 dlp/src/test/java/com/example/dlp/MetadataIT.java create mode 100644 dlp/src/test/java/com/example/dlp/RedactIT.java create mode 100644 dlp/src/test/resources/test.png create mode 100644 dlp/src/test/resources/test.txt diff --git a/dlp/README.md b/dlp/README.md new file mode 100644 index 00000000000..8715bb5770c --- /dev/null +++ b/dlp/README.md @@ -0,0 +1,115 @@ +# Cloud Data Loss Prevention (DLP) API Samples +The [Data Loss Prevention API](https://cloud.google.com/dlp/docs/) provides programmatic access to +a powerful detection engine for personally identifiable information and other privacy-sensitive data + in unstructured data streams. + +## Setup +- A Google Cloud project with billing enabled +- [Enable](https://console.cloud.google.com/launcher/details/google/dlp.googleapis.com) the DLP API. +- (Local testing)[Create a service account](https://cloud.google.com/docs/authentication/getting-started) +and set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to the downloaded credentials file. + +## Build +This project uses the [Assembly Plugin](https://maven.apache.org/plugins/maven-assembly-plugin/usage.html) to build an uber jar. +Run: +``` + mvn clean package +``` + +## Retrieve InfoTypes +An [InfoType identifier](https://cloud.google.com/dlp/docs/infotypes-categories) represents an element of sensitive data. + +[Info types](https://cloud.google.com/dlp/docs/infotypes-reference#global) are updated periodically. Use the API to retrieve the most current +info types for a given category. eg. HEALTH or GOVERNMENT. + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Metadata -category GOVERNMENT + ``` + +## Retrieve Categories +[Categories](https://cloud.google.com/dlp/docs/infotypes-categories) provide a way to easily access a group of related InfoTypes. +``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Metadata +``` + +## Inspect data for sensitive elements +Inspect strings, files locally and on Google Cloud Storage and Cloud Datastore kinds with the DLP API. + +Note: image scanning is not currently supported on Google Cloud Storage. +For more information, refer to the [API documentation](https://cloud.google.com/dlp/docs). +Optional flags are explained in [this resource](https://cloud.google.com/dlp/docs/reference/rest/v2beta1/content/inspect#InspectConfig). +``` +Commands: + -s Inspect a string using the Data Loss Prevention API. + -f Inspects a local text, PNG, or JPEG file using the Data Loss Prevention API. + -gcs -bucketName -fileName Inspects a text file stored on Google Cloud Storage using the Data Loss + Prevention API. + -ds -projectId [projectId] -namespace [namespace] - kind Inspect a Datastore instance using the Data Loss Prevention API. + +Options: + --help Show help + -minLikelihood [string] [choices: "LIKELIHOOD_UNSPECIFIED", "VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"] + [default: "LIKELIHOOD_UNSPECIFIED"] + specifies the minimum reporting likelihood threshold. + -f, --maxFindings [number] [default: 0] + maximum number of results to retrieve + -q, --includeQuote [boolean] [default: true] include matching string in results + -t, --infoTypes restrict to limited set of infoTypes [ default: []] + [ eg. PHONE_NUMBER US_PASSPORT] +``` +### Examples + - Inspect a string: + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Inspect -s "My phone number is (123) 456-7890 and my email address is me@somedomain.com" + ``` + - Inspect a local file (text / image): + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Inspect -f resources/test.txt + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Inspect -f resources/test.png + ``` +- Inspect a file on Google Cloud Storage: + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Inspect -gcs -bucketName my-bucket -fileName my-file.txt + ``` +- Inspect a Google Cloud Datastore kind: + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Inspect -ds -kind my-kind + ``` + +## Automatic redaction of sensitive data +[Automatic redaction](https://cloud.google.com/dlp/docs/classification-redaction) produces an output with sensitive data matches removed. + +``` +Commands: + -s Source input string + -r String to replace detected info types + Options: + --help Show help + -minLikelihood choices: "LIKELIHOOD_UNSPECIFIED", "VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"] + [default: "LIKELIHOOD_UNSPECIFIED"] + specifies the minimum reporting likelihood threshold. + + -infoTypes restrict operation to limited set of info types [ default: []] + [ eg. PHONE_NUMBER US_PASSPORT] +``` + +### Example +- Replace sensitive data in text with `_REDACTED_`: + ``` + java -cp target/dlp-samples-1.0-jar-with-dependencies.jar com.example.dlp.Redact -s "My phone number is (123) 456-7890 and my email address is me@somedomain.com" -r "_REDACTED_" + ``` + +## Integration tests +### Setup +- [Create a Google Cloud Storage bucket](https://console.cloud.google.com/storage) and upload [test.txt](src/test/resources/test.txt). +- [Create a Google Cloud Datastore](https://console.cloud.google.com/datastore) kind and add an entity with properties: + - `property1` : john@doe.com + - `property2` : 343-343-3435 +- Update the Google Cloud Storage path and Datastore kind in [InspectIT.java](src/test/java/com/example/dlp/InspectIT.java). +- Ensure that `GOOGLE_APPLICATION_CREDENTIALS` points to authorized service account credentials file. + +## Run +Run all tests: + ``` + mvn clean verify + ``` + diff --git a/dlp/pom.xml b/dlp/pom.xml new file mode 100644 index 00000000000..95f15f638f3 --- /dev/null +++ b/dlp/pom.xml @@ -0,0 +1,101 @@ + + + + + 4.0.0 + jar + com.example + dlp-samples + 1.0 + + + + doc-samples + com.google.cloud + 1.0.0 + .. + + + + 1.8 + 1.8 + 0.7.0 + UTF-8 + + + + + + + com.google.auth + google-auth-library-credentials + ${google.auth.version} + + + com.google.auth + google-auth-library-oauth2-http + ${google.auth.version} + + + + + + + + + com.google.cloud + google-cloud-dlp + 0.20.2-alpha + + + + commons-cli + commons-cli + 1.4 + + + + junit + junit + 4.12 + + + + + + + maven-assembly-plugin + 3.0.0 + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + diff --git a/dlp/src/main/java/com/example/dlp/Inspect.java b/dlp/src/main/java/com/example/dlp/Inspect.java new file mode 100644 index 00000000000..4b8750f3e76 --- /dev/null +++ b/dlp/src/main/java/com/example/dlp/Inspect.java @@ -0,0 +1,444 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +package com.example.dlp; + +import com.google.api.gax.grpc.OperationFuture; +import com.google.cloud.ServiceOptions; +import com.google.cloud.dlp.v2beta1.DlpServiceClient; +import com.google.privacy.dlp.v2beta1.CloudStorageOptions; +import com.google.privacy.dlp.v2beta1.CloudStorageOptions.FileSet; +import com.google.privacy.dlp.v2beta1.ContentItem; +import com.google.privacy.dlp.v2beta1.DatastoreOptions; +import com.google.privacy.dlp.v2beta1.Finding; +import com.google.privacy.dlp.v2beta1.InfoType; +import com.google.privacy.dlp.v2beta1.InspectConfig; +import com.google.privacy.dlp.v2beta1.InspectContentRequest; +import com.google.privacy.dlp.v2beta1.InspectContentResponse; +import com.google.privacy.dlp.v2beta1.InspectOperationMetadata; +import com.google.privacy.dlp.v2beta1.InspectOperationResult; +import com.google.privacy.dlp.v2beta1.InspectResult; +import com.google.privacy.dlp.v2beta1.KindExpression; +import com.google.privacy.dlp.v2beta1.Likelihood; +import com.google.privacy.dlp.v2beta1.OutputStorageConfig; +import com.google.privacy.dlp.v2beta1.PartitionId; +import com.google.privacy.dlp.v2beta1.ResultName; +import com.google.privacy.dlp.v2beta1.StorageConfig; +import com.google.protobuf.ByteString; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.activation.MimetypesFileTypeMap; + +public class Inspect { + + private static void inspectString(String string, Likelihood minLikelihood, int maxFindings, + List infoTypes, boolean includeQuote) { + // [START dlp_inspect_string] + // instantiate a client + try (DlpServiceClient dlpServiceClient = DlpServiceClient.create()) { + + // The minimum likelihood required before returning a match + // minLikelihood = LIKELIHOOD_UNSPECIFIED; + + // The maximum number of findings to report (0 = server maximum) + // maxFindings = 0; + + // The infoTypes of information to match + // infoTypes = ['US_MALE_NAME', 'US_FEMALE_NAME']; + + // Whether to include the matching string + // includeQuote = true; + InspectConfig inspectConfig = InspectConfig.newBuilder() + .addAllInfoTypes(infoTypes) + .setMinLikelihood(minLikelihood) + .setMaxFindings(maxFindings) + .setIncludeQuote(includeQuote) + .build(); + + // The string to inspect + // string = 'My name is Gary and my email is gary@example.com'; + ContentItem contentItem = ContentItem.newBuilder() + .setType("text/plain") + .setValue(string) + .build(); + + InspectContentRequest request = InspectContentRequest.newBuilder() + .setInspectConfig(inspectConfig) + .addItems(contentItem) + .build(); + InspectContentResponse response = dlpServiceClient.inspectContent(request); + + for (InspectResult result : response.getResultsList()) { + if (result.getFindingsCount() > 0) { + System.out.println("Findings: "); + for (Finding finding : result.getFindingsList()) { + if (includeQuote) { + System.out.print("Quote: " + finding.getQuote()); + } + System.out.print("\tInfo type: " + finding.getInfoType().getName()); + System.out.println("\tLikelihood: " + finding.getLikelihood()); + } + } else { + System.out.println("No findings."); + } + } + } catch (Exception e) { + System.out.println("Error in inspectString: " + e.getMessage()); + } + // [END dlp_inspect_string] + } + + private static void inspectFile(String filePath, Likelihood minLikelihood, int maxFindings, + List infoTypes, boolean includeQuote) { + // [START dlp_inspect_file] + // Instantiates a client + try (DlpServiceClient dlpServiceClient = DlpServiceClient.create()) { + // The path to a local file to inspect. Can be a text, JPG, or PNG file. + // fileName = 'path/to/image.png'; + + // The minimum likelihood required before returning a match + // minLikelihood = LIKELIHOOD_UNSPECIFIED; + + // The maximum number of findings to report (0 = server maximum) + // maxFindings = 0; + + // The infoTypes of information to match + // infoTypes = ['US_MALE_NAME', 'US_FEMALE_NAME']; + + // Whether to include the matching string + // includeQuote = true; + Path path = Paths.get(filePath); + + // detect file mime type, default to application/octet-stream + String mimeType = URLConnection.guessContentTypeFromName(filePath); + if (mimeType == null) { + mimeType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(filePath); + } + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + + byte[] data = Files.readAllBytes(path); + ContentItem contentItem = ContentItem.newBuilder() + .setType(mimeType) + .setData(ByteString.copyFrom(data)) + .build(); + + InspectConfig inspectConfig = InspectConfig.newBuilder() + .addAllInfoTypes(infoTypes) + .setMinLikelihood(minLikelihood) + .setMaxFindings(maxFindings) + .setIncludeQuote(includeQuote) + .build(); + + InspectContentRequest request = InspectContentRequest.newBuilder() + .setInspectConfig(inspectConfig) + .addItems(contentItem) + .build(); + InspectContentResponse response = dlpServiceClient.inspectContent(request); + + for (InspectResult result : response.getResultsList()) { + if (result.getFindingsCount() > 0) { + System.out.println("Findings: "); + for (Finding finding : result.getFindingsList()) { + if (includeQuote) { + System.out.print("Quote: " + finding.getQuote()); + } + System.out.print("\tInfo type: " + finding.getInfoType().getName()); + System.out.println("\tLikelihood: " + finding.getLikelihood()); + } + } else { + System.out.println("No findings."); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error in inspectFile: " + e.getMessage()); + } + // [END dlp_inspect_file] + } + + private static void inspectGcsFile(String bucketName, String fileName, + Likelihood minLikelihood, List infoTypes) + throws Exception { + // [START dlp_inspect_gcs] + // Instantiates a client + try (DlpServiceClient dlpServiceClient = DlpServiceClient.create()) { +// The name of the bucket where the file resides. + // bucketName = 'YOUR-BUCKET'; + + // The path to the file within the bucket to inspect. + // Can contain wildcards, e.g. "my-image.*" + // fileName = 'my-image.png'; + + // The minimum likelihood required before returning a match + // minLikelihood = LIKELIHOOD_UNSPECIFIED; + + // The maximum number of findings to report (0 = server maximum) + // maxFindings = 0; + + // The infoTypes of information to match + // infoTypes = ['US_MALE_NAME', 'US_FEMALE_NAME']; + + CloudStorageOptions cloudStorageOptions = CloudStorageOptions + .newBuilder() + .setFileSet(FileSet.newBuilder().setUrl( + "gs://" + bucketName + "/" + fileName + )) + .build(); + + StorageConfig storageConfig = StorageConfig.newBuilder() + .setCloudStorageOptions(cloudStorageOptions) + .build(); + + InspectConfig inspectConfig = InspectConfig.newBuilder() + .addAllInfoTypes(infoTypes) + .setMinLikelihood(minLikelihood) + .build(); + + // optionally provide an output configuration to store results, default : none + OutputStorageConfig outputConfig = OutputStorageConfig.getDefaultInstance(); + + // asynchronously submit an inspect operation + OperationFuture responseFuture = + dlpServiceClient.createInspectOperationAsync(inspectConfig, storageConfig, outputConfig); + + // ... + // block on response, returning job id of the operation + InspectOperationResult inspectOperationResult = responseFuture.get(); + ResultName resultName = inspectOperationResult.getNameAsResultName(); + InspectResult inspectResult = dlpServiceClient.listInspectFindings(resultName).getResult(); + + if (inspectResult.getFindingsCount() > 0) { + System.out.println("Findings: "); + for (Finding finding : inspectResult.getFindingsList()) { + System.out.print("\tInfo type: " + finding.getInfoType().getName()); + System.out.println("\tLikelihood: " + finding.getLikelihood()); + } + } else { + System.out.println("No findings."); + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error in inspectGCSFileAsync: " + e.getMessage()); + } + // [END dlp_inspect_gcs] + } + + private static void inspectDatastore(String projectId, String namespaceId, String kind, + Likelihood minLikelihood, List infoTypes) { + // [START dlp_inspect_datastore] + // Instantiates a client + try (DlpServiceClient dlpServiceClient = DlpServiceClient.create()) { + + // (Optional) The project ID containing the target Datastore + // projectId = my-project-id + + // (Optional) The ID namespace of the Datastore document to inspect. + // To ignore Datastore namespaces, set this to an empty string ('') + // namespaceId = ''; + + // The kind of the Datastore entity to inspect. + // kind = 'Person'; + + // The minimum likelihood required before returning a match + // minLikelihood = LIKELIHOOD_UNSPECIFIED; + + // The infoTypes of information to match + // infoTypes = ['US_MALE_NAME', 'US_FEMALE_NAME']; + + // Get reference to the file to be inspected + PartitionId partitionId = PartitionId.newBuilder().setProjectId(projectId) + .setNamespaceId(namespaceId).build(); + KindExpression kindExpression = KindExpression.newBuilder().setName(kind).build(); + DatastoreOptions datastoreOptions = DatastoreOptions.newBuilder() + .setKind(kindExpression).setPartitionId(partitionId).build(); + StorageConfig storageConfig = StorageConfig.newBuilder() + .setDatastoreOptions(datastoreOptions).build(); + + InspectConfig inspectConfig = InspectConfig.newBuilder() + .addAllInfoTypes(infoTypes) + .setMinLikelihood(minLikelihood) + .build(); + + // optionally provide an output configuration to store results, default : none + OutputStorageConfig outputConfig = OutputStorageConfig.getDefaultInstance(); + + // asynchronously submit an inspect operation + OperationFuture responseFuture = + dlpServiceClient.createInspectOperationAsync(inspectConfig, storageConfig, outputConfig); + + // ... + // block on response, returning job id of the operation + InspectOperationResult inspectOperationResult = responseFuture.get(); + ResultName resultName = inspectOperationResult.getNameAsResultName(); + InspectResult inspectResult = dlpServiceClient.listInspectFindings(resultName).getResult(); + + if (inspectResult.getFindingsCount() > 0) { + System.out.println("Findings: "); + for (Finding finding : inspectResult.getFindingsList()) { + System.out.print("\tInfo type: " + finding.getInfoType().getName()); + System.out.println("\tLikelihood: " + finding.getLikelihood()); + } + } else { + System.out.println("No findings."); + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error in inspectDatastore: " + e.getMessage()); + } + // [END dlp_inspect_datastore] + } + + public static void main(String[] args) throws Exception { + + OptionGroup optionsGroup = new OptionGroup(); + optionsGroup.setRequired(true); + Option stringOption = new Option("s", "string", true, "inspect string"); + optionsGroup.addOption(stringOption); + + Option fileOption = new Option("f", "file path", true, "inspect input file path"); + optionsGroup.addOption(fileOption); + + Option gcsOption = new Option("gcs", "Google Cloud Storage", false, "inspect GCS file"); + optionsGroup.addOption(gcsOption); + + Option datastoreOption = new Option("ds", "Google Datastore", false, "inspect Datastore kind"); + optionsGroup.addOption(datastoreOption); + + Options commandLineOptions = new Options(); + commandLineOptions.addOptionGroup(optionsGroup); + + Option minLikelihoodOption = Option.builder("minLikelihood") + .hasArg(true) + .required(false) + .build(); + + commandLineOptions.addOption(minLikelihoodOption); + + Option maxFindingsOption = Option.builder("maxFindings") + .hasArg(true) + .required(false) + .build(); + + commandLineOptions.addOption(maxFindingsOption); + + Option infoTypesOption = Option.builder("infoTypes") + .hasArg(true) + .required(false) + .build(); + infoTypesOption.setArgs(Option.UNLIMITED_VALUES); + commandLineOptions.addOption(infoTypesOption); + + Option includeQuoteOption = Option.builder("includeQuote") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(includeQuoteOption); + + Option bucketNameOption = Option.builder("bucketName") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(bucketNameOption); + + Option gcsFileNameOption = Option.builder("fileName") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(gcsFileNameOption); + + Option datastoreProjectIdOption = Option.builder("projectId") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(datastoreProjectIdOption); + + Option datastoreNamespaceOption = Option.builder("namespace") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(datastoreNamespaceOption); + + Option datastoreKindOption = Option.builder("kind") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(datastoreKindOption); + + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + CommandLine cmd; + + try { + cmd = parser.parse(commandLineOptions, args); + } catch (ParseException e) { + System.out.println(e.getMessage()); + formatter.printHelp(Inspect.class.getName(), commandLineOptions); + System.exit(1); + return; + } + + Likelihood minLikelihood = Likelihood.valueOf(cmd.getOptionValue(minLikelihoodOption.getOpt(), + Likelihood.LIKELIHOOD_UNSPECIFIED.name())); + int maxFindings = Integer.parseInt(cmd.getOptionValue(maxFindingsOption.getOpt(), "0")); + boolean includeQuote = Boolean + .parseBoolean(cmd.getOptionValue(includeQuoteOption.getOpt(), "true")); + + List infoTypesList = Collections.emptyList(); + if (cmd.hasOption(infoTypesOption.getOpt())) { + infoTypesList = new ArrayList<>(); + String[] infoTypes = cmd.getOptionValues(infoTypesOption.getOpt()); + for (String infoType : infoTypes) { + infoTypesList.add(InfoType.newBuilder().setName(infoType).build()); + } + } + // string inspection + if (cmd.hasOption("s")) { + String val = cmd.getOptionValue(stringOption.getOpt()); + inspectString(val, minLikelihood, maxFindings, infoTypesList, includeQuote); + } else if (cmd.hasOption("f")) { + String filePath = cmd.getOptionValue(fileOption.getOpt()); + inspectFile(filePath, minLikelihood, maxFindings, infoTypesList, includeQuote); + // gcs file inspection + } else if (cmd.hasOption("gcs")) { + String bucketName = cmd.getOptionValue(bucketNameOption.getOpt()); + String fileName = cmd.getOptionValue(gcsFileNameOption.getOpt()); + inspectGcsFile(bucketName, fileName, minLikelihood, infoTypesList); + // datastore kind inspection + } else if (cmd.hasOption("ds")) { + String namespaceId = cmd.getOptionValue(datastoreNamespaceOption.getOpt(), ""); + String kind = cmd.getOptionValue(datastoreKindOption.getOpt()); + // use default project id when project id is not specified + String projectId = cmd.getOptionValue(datastoreProjectIdOption.getOpt(), + ServiceOptions.getDefaultProjectId()); + inspectDatastore(projectId, namespaceId, kind, minLikelihood, infoTypesList); + } + } +} diff --git a/dlp/src/main/java/com/example/dlp/Metadata.java b/dlp/src/main/java/com/example/dlp/Metadata.java new file mode 100644 index 00000000000..8045a22ef35 --- /dev/null +++ b/dlp/src/main/java/com/example/dlp/Metadata.java @@ -0,0 +1,96 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +package com.example.dlp; + +import com.google.cloud.dlp.v2beta1.DlpServiceClient; +import com.google.privacy.dlp.v2beta1.CategoryDescription; +import com.google.privacy.dlp.v2beta1.InfoTypeDescription; +import com.google.privacy.dlp.v2beta1.ListInfoTypesResponse; +import com.google.privacy.dlp.v2beta1.ListRootCategoriesResponse; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.util.List; + +public class Metadata { + + private static void listInfoTypes(String category, String languageCode) throws Exception { + // [START dlp_list_info_types] + // Instantiate a DLP client + try (DlpServiceClient dlpClient = DlpServiceClient.create()) { + // The category of info types to list, e.g. category = 'GOVERNMENT'; + // Optional BCP-47 language code for localized info type friendly names, e.g. 'en-US' + ListInfoTypesResponse infoTypesResponse = dlpClient.listInfoTypes(category, languageCode); + List infoTypeDescriptions = infoTypesResponse.getInfoTypesList(); + for (InfoTypeDescription infoTypeDescription : infoTypeDescriptions) { + System.out.println("Name : " + infoTypeDescription.getName()); + System.out.println("Display name : " + infoTypeDescription.getDisplayName()); + } + } + // [END dlp_list_info_types] + } + + private static void listRootCategories(String languageCode) throws Exception { + // [START dlp_list_root_categories] + // Instantiate a DLP client + try (DlpServiceClient dlpClient = DlpServiceClient.create()) { + // The BCP-47 language code to use, e.g. 'en-US' + // languageCode = 'en-US' + ListRootCategoriesResponse rootCategoriesResponse = dlpClient + .listRootCategories(languageCode); + for (CategoryDescription categoryDescription : rootCategoriesResponse.getCategoriesList()) { + System.out.println("Name : " + categoryDescription.getName()); + System.out.println("Display name : " + categoryDescription.getDisplayName()); + } + } + // [END dlp_list_root_categories] + } + + public static void main(String[] args) throws Exception { + Options options = new Options(); + Option languageCodeOption = new Option("language", null, true, "BCP-47 language code"); + languageCodeOption.setRequired(false); + options.addOption(languageCodeOption); + + Option categoryOption = new Option("category", null, true, "Category of info types to list."); + categoryOption.setRequired(false); + options.addOption(categoryOption); + + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + CommandLine cmd; + try { + cmd = parser.parse(options, args); + } catch (ParseException e) { + System.out.println(e.getMessage()); + formatter.printHelp(Metadata.class.getName(), options); + System.exit(1); + return; + } + String languageCode = cmd.getOptionValue(languageCodeOption.getOpt(), "en-US"); + if (cmd.hasOption(categoryOption.getOpt())) { + String category = cmd.getOptionValue(categoryOption.getOpt()); + listInfoTypes(category, languageCode); + } else { + listRootCategories(languageCode); + } + } +} diff --git a/dlp/src/main/java/com/example/dlp/Redact.java b/dlp/src/main/java/com/example/dlp/Redact.java new file mode 100644 index 00000000000..780c34dd3ff --- /dev/null +++ b/dlp/src/main/java/com/example/dlp/Redact.java @@ -0,0 +1,142 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +package com.example.dlp; + +import com.google.cloud.dlp.v2beta1.DlpServiceClient; +import com.google.privacy.dlp.v2beta1.ContentItem; +import com.google.privacy.dlp.v2beta1.InfoType; +import com.google.privacy.dlp.v2beta1.InspectConfig; +import com.google.privacy.dlp.v2beta1.Likelihood; +import com.google.privacy.dlp.v2beta1.RedactContentRequest.ReplaceConfig; +import com.google.privacy.dlp.v2beta1.RedactContentResponse; +import com.google.protobuf.ByteString; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Redact { + + private static void redactString(String string, String replacement, Likelihood minLikelihood, + List infoTypes) throws Exception { + // [START dlp_redact_string] + // Instantiate the DLP client + try (DlpServiceClient dlpClient = DlpServiceClient.create()) { + // The minimum likelihood required before returning a match + // eg.minLikelihood = LIKELIHOOD_VERY_LIKELY; + InspectConfig inspectConfig = InspectConfig.newBuilder() + .addAllInfoTypes(infoTypes) + .setMinLikelihood(minLikelihood) + .build(); + + ContentItem contentItem = ContentItem.newBuilder() + .setType("text/plain") + .setData(ByteString.copyFrom(string.getBytes())) + .build(); + + List replaceConfigs = new ArrayList<>(); + + if (infoTypes.isEmpty()) { + // replace all detected sensitive elements with replacement string + replaceConfigs.add( + ReplaceConfig.newBuilder() + .setReplaceWith(replacement) + .build()); + } else { + // Replace select info types with chosen replacement string + for (InfoType infoType : infoTypes) { + replaceConfigs.add( + ReplaceConfig.newBuilder() + .setInfoType(infoType) + .setReplaceWith(replacement) + .build()); + } + } + + RedactContentResponse contentResponse = dlpClient.redactContent( + inspectConfig, Collections.singletonList(contentItem), replaceConfigs); + for (ContentItem responseItem : contentResponse.getItemsList()) { + // print out string with redacted content + System.out.println(responseItem.getData().toStringUtf8()); + } + } + // [END dlp_redact_string] + } + + // Command line application to redact strings using the Data Loss Prevention API + public static void main(String[] args) throws Exception { + Options commandLineOptions = new Options(); + + Option stringOption = Option.builder("s") + .longOpt("source string") + .hasArg(true) + .required(true) + .build(); + commandLineOptions.addOption(stringOption); + + Option replaceOption = Option.builder("r") + .longOpt("replace string") + .hasArg(true) + .required(true) + .build(); + commandLineOptions.addOption(replaceOption); + + Option minLikelihoodOption = Option.builder("minLikelihood") + .hasArg(true) + .required(false) + .build(); + commandLineOptions.addOption(minLikelihoodOption); + + Option infoTypesOption = Option.builder("infoTypes") + .hasArg(true) + .required(false) + .build(); + infoTypesOption.setArgs(Option.UNLIMITED_VALUES); + commandLineOptions.addOption(infoTypesOption); + + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + CommandLine cmd; + + try { + cmd = parser.parse(commandLineOptions, args); + } catch (ParseException e) { + System.out.println(e.getMessage()); + formatter.printHelp(Redact.class.getName(), commandLineOptions); + System.exit(1); + return; + } + + String source = cmd.getOptionValue(stringOption.getOpt()); + String replacement = cmd.getOptionValue(replaceOption.getOpt()); + + List infoTypesList = new ArrayList<>(); + String[] infoTypes = cmd.getOptionValues(infoTypesOption.getOpt()); + if (infoTypes != null) { + for (String infoType : infoTypes) { + infoTypesList.add(InfoType.newBuilder().setName(infoType).build()); + } + } + redactString(source, replacement, Likelihood.LIKELIHOOD_UNSPECIFIED, infoTypesList); + } +} diff --git a/dlp/src/test/java/com/example/dlp/InspectIT.java b/dlp/src/test/java/com/example/dlp/InspectIT.java new file mode 100644 index 00000000000..fff56b734b0 --- /dev/null +++ b/dlp/src/test/java/com/example/dlp/InspectIT.java @@ -0,0 +1,103 @@ +/** + * Copyright 2017, Google, Inc. 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. + */ +package com.example.dlp; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; + +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class InspectIT { + private ByteArrayOutputStream bout; + private PrintStream out; + + // Update to Google Cloud Storage path containing test.txt + private String bucketName = System.getenv("GOOGLE_CLOUD_PROJECT") + "/dlp"; + + // Update to Google Cloud Datastore Kind containing an entity + // with phone number and email address properties. + private String datastoreKind = "dlp"; + + @Before + public void setUp() { + bout = new ByteArrayOutputStream(); + out = new PrintStream(bout); + System.setOut(out); + assertNotNull(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")); + } + + @Test + public void testStringInspectionReturnsInfoTypes() throws Exception { + String text = + "\"My phone number is (234) 456-7890 and my email address is gary@somedomain.com\""; + Inspect.main(new String[] {"-s", text}); + String output = bout.toString(); + assertTrue(output.contains("PHONE_NUMBER")); + assertTrue(output.contains("EMAIL_ADDRESS")); + } + + @Test + public void testTextFileInspectionReturnsInfoTypes() throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("test.txt").getFile()); + Inspect.main(new String[] {"-f", file.getAbsolutePath()}); + String output = bout.toString(); + assertTrue(output.contains("PHONE_NUMBER")); + assertTrue(output.contains("EMAIL_ADDRESS")); + } + + @Test + public void testImageFileInspectionReturnsInfoTypes() throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("test.png").getFile()); + Inspect.main(new String[] {"-f", file.getAbsolutePath()}); + String output = bout.toString(); + assertTrue(output.contains("PHONE_NUMBER")); + assertTrue(output.contains("EMAIL_ADDRESS")); + } + + // Requires that bucket by the specified name exists + @Test + public void testGcsFileInspectionReturnsInfoTypes() throws Exception { + Inspect.main(new String[] {"-gcs", "-bucketName", bucketName, "-fileName", "test.txt"}); + String output = bout.toString(); + assertTrue(output.contains("PHONE_NUMBER")); + assertTrue(output.contains("EMAIL_ADDRESS")); + } + + // Requires a Datastore kind containing an entity + // with phone number and email address properties. + @Test + public void testDatastoreInspectionReturnsInfoTypes() throws Exception { + Inspect.main(new String[] {"-ds", "-kind", datastoreKind}); + String output = bout.toString(); + assertTrue(output.contains("PHONE_NUMBER")); + assertTrue(output.contains("EMAIL_ADDRESS")); + } + + @After + public void tearDown() { + System.setOut(null); + bout.reset(); + } +} diff --git a/dlp/src/test/java/com/example/dlp/MetadataIT.java b/dlp/src/test/java/com/example/dlp/MetadataIT.java new file mode 100644 index 00000000000..ebd0d1a2e9e --- /dev/null +++ b/dlp/src/test/java/com/example/dlp/MetadataIT.java @@ -0,0 +1,61 @@ +/** + * Copyright 2017, Google, Inc. 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. + */ +package com.example.dlp; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class MetadataIT { + + private ByteArrayOutputStream bout; + private PrintStream out; + + @Before + public void setUp() { + bout = new ByteArrayOutputStream(); + out = new PrintStream(bout); + System.setOut(out); + assertNotNull(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")); + } + + @Test + public void testRootCategoriesAreRetrieved() throws Exception { + Metadata.main(new String[] {}); + String output = bout.toString(); + assertTrue(output.contains("GOVERNMENT")); + assertTrue(output.contains("HEALTH")); + } + + @Test + public void testInfoTypesAreRetrieved() throws Exception { + Metadata.main(new String[] {"-category", "GOVERNMENT"}); + String output = bout.toString(); + assertTrue(output.contains("AUSTRALIA_TAX_FILE_NUMBER")); + } + + @After + public void tearDown() { + System.setOut(null); + } +} diff --git a/dlp/src/test/java/com/example/dlp/RedactIT.java b/dlp/src/test/java/com/example/dlp/RedactIT.java new file mode 100644 index 00000000000..6e768a1cedf --- /dev/null +++ b/dlp/src/test/java/com/example/dlp/RedactIT.java @@ -0,0 +1,55 @@ +/** + * Copyright 2017, Google, Inc. 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. + */ +package com.example.dlp; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class RedactIT { + private ByteArrayOutputStream bout; + private PrintStream out; + + @Before + public void setUp() { + bout = new ByteArrayOutputStream(); + out = new PrintStream(bout); + System.setOut(out); + assertNotNull(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")); + } + + @Test + public void testInfoTypesInStringAreReplaced() throws Exception { + String text = + "\"My phone number is (234) 456-7890 and my email address is gary@somedomain.com\""; + Redact.main(new String[] {"-s", text, "-r", "_REDACTED_"}); + String output = bout.toString(); + assertTrue(output.contains("My phone number is _REDACTED_ and my email address is _REDACTED_")); + } + + @After + public void tearDown() { + System.setOut(null); + bout.reset(); + } +} diff --git a/dlp/src/test/resources/test.png b/dlp/src/test/resources/test.png new file mode 100644 index 0000000000000000000000000000000000000000..8f32c825884261083b7d731676375303d49ca6f6 GIT binary patch literal 21438 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%0Dk_JRQOqzBh$Z^~#JvUs5?gsH6m;e=MJg zj)gWg^b(F}&Dgt!iGqlNs_qe+fDTUiQbRYOjT~%x%1TzZ-%04Qnf*=nKZ~VOG)V}u zHe!DuOLA?g{{|^>v$zF?1E(U}HGYYx@j5Agw7{sF!wQ z4wr6mIbisG7j|62703?(w~YEouD)^d+)W*nB{BWqiTY339I042YUD;=`l;ntsN2#Gj7lw8Z; z4`C`)M8l-xP*|opM*2?oDVdEM$IepnzpK{-2_hhxWgwSSv3FFMhGgC>NojG5AdHoJnLW_}GCo{oOi~*>$ z2qNgZ;jm7)RjNWDtf%XYhnNY#LWvTNjqMfl@&U5FgMg6&w}k({KmNCGb{vqo6h5RH zc13&2;tlbF!-)~xu$VC0f3F+8y`E3zqU|O5qu3i2CwW`%JsDo%DtxEV{{{5xpAT9z M)pS)Wl`TL24{iKN9{>OV literal 0 HcmV?d00001 diff --git a/dlp/src/test/resources/test.txt b/dlp/src/test/resources/test.txt new file mode 100644 index 00000000000..c2ee3815bc9 --- /dev/null +++ b/dlp/src/test/resources/test.txt @@ -0,0 +1 @@ +My phone number is (223) 456-7890 and my email address is gary@somedomain.com. \ No newline at end of file diff --git a/pom.xml b/pom.xml index f2a4b975147..6042874f607 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ compute/sendgrid datastore datastore/cloud-client + dlp iap kms language/analysis