From 0f1b30d31386c9963f7bd34f38843affebc6d87b Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Thu, 9 Mar 2017 15:41:51 -0600 Subject: [PATCH] DLP samples (#322) * First draft of DLP samples * Fix DLP tests * Add README * Fix README bugs --- dlp/README.md | 120 +++++++++ dlp/inspect.js | 401 +++++++++++++++++++++++++++++++ dlp/metadata.js | 116 +++++++++ dlp/package.json | 40 +++ dlp/redact.js | 130 ++++++++++ dlp/resources/accounts.txt | 1 + dlp/resources/harmless.txt | 1 + dlp/resources/test.png | Bin 0 -> 21438 bytes dlp/resources/test.txt | 1 + dlp/system-test/inspect.test.js | 166 +++++++++++++ dlp/system-test/metadata.test.js | 47 ++++ dlp/system-test/redact.test.js | 54 +++++ 12 files changed, 1077 insertions(+) create mode 100644 dlp/README.md create mode 100644 dlp/inspect.js create mode 100644 dlp/metadata.js create mode 100644 dlp/package.json create mode 100644 dlp/redact.js create mode 100644 dlp/resources/accounts.txt create mode 100644 dlp/resources/harmless.txt create mode 100644 dlp/resources/test.png create mode 100644 dlp/resources/test.txt create mode 100644 dlp/system-test/inspect.test.js create mode 100644 dlp/system-test/metadata.test.js create mode 100644 dlp/system-test/redact.test.js diff --git a/dlp/README.md b/dlp/README.md new file mode 100644 index 0000000000..91ae4d9e27 --- /dev/null +++ b/dlp/README.md @@ -0,0 +1,120 @@ +Google Cloud Platform logo + +# Data Loss Prevention API Node.js Samples + +The [Data Loss Prevention][dlp_docs] (DLP) API provides programmatic access to a powerful detection engine for personally identifiable information and other privacy-sensitive data in unstructured data streams. + +This code provides a demonstration of the DLP API's functionality via REST in Node.js. It is intended for developers who want to be early adopters of the API. + +[gRPC](https://grpc.io)-based samples/client libraries for [several languages](https://cloud.google.com/docs/) are under active development, and will be released shortly. + +## Table of Contents + +* [Setup](#setup) +* [Samples](#samples) + * [Getting started with the Data Loss Prevention API](#getting-started-with-data-loss-prevention-api) + +## Setup + +1. Read [Prerequisites][prereq] and [How to run a sample][run] first. +1. Install dependencies: + + npm install + +[prereq]: ../README.md#prerequisities +[run]: ../README.md#how-to-run-a-sample + +## Samples + +### Getting started with the Data Loss Prevention API + +View the [DLP documentation][dlp_docs] or the [samples][dlp_samples]. + +__Run the samples:__ + +```sh +node inspect.js --help +``` + +``` +Commands: + Commands: + string Inspect a string using the Data Loss Prevention API. + file Inspects a local text, PNG, or JPEG file using the Data Loss Prevention API. + gcsFile Inspects a text file stored on Google Cloud Storage using the Data Loss Prevention + API. + datastore Inspect a Datastore instance using the Data Loss Prevention API. + +Options: + --help Show help [boolean] + -m, --minLikelihood + [string] [choices: "LIKELIHOOD_UNSPECIFIED", "VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"] + [default: "LIKELIHOOD_UNSPECIFIED"] + -f, --maxFindings [default: 0] + -q, --includeQuote [boolean] [default: true] + -a, --authToken [string] [default: + "ab97.XXX..."] + -t, --infoTypes [array] [default: []] + +Examples: + node inspect.js string "My phone number is (123) 456-7890 + and my email address is me@somedomain.com" + node inspect.js file resources/test.txt + node inspect.js gcsFile my-bucket my-file.txt + +For more information, see https://cloud.google.com/dlp/docs. Optional flags are explained at +https://cloud.google.com/dlp/docs/reference/rest/v2beta1/content/inspect#InspectConfig +``` + +```sh +node metadata.js --help +``` + +``` +Commands: + infoTypes List types of sensitive information within a category. + categories List root categories of sensitive information. + +Options: + --help Show help [boolean] + -a, --authToken [string] [default: + "ab97.XXX..."] + oz0146E86Lk"] + +Examples: + node metadata.js infoTypes GOVERNMENT + node metadata.js categories + +For more information, see https://cloud.google.com/dlp/docs +``` + +```sh +node redact.js --help +``` + +``` +Commands: + string Redact sensitive data from a string using the Data Loss Prevention API. + +Options: + --help Show help [boolean] + -t, --infoTypes [array] [required] + -m, --minLikelihood + [string] [choices: "LIKELIHOOD_UNSPECIFIED", "VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"] + [default: "LIKELIHOOD_UNSPECIFIED"] + -a, --authToken [string] [default: + "ab97.XXX..."] + htPfG_eIy04"] + +Examples: + node redact.js string "My name is Gary" "REDACTED" -t + US_MALE_NAME + +For more information, see https://cloud.google.com/dlp/docs. Optional flags are explained at +https://cloud.google.com/dlp/docs/reference/rest/v2beta1/content/inspect#InspectConfig +``` + +For more information, see [the docs][dlp_docs]. + +[dlp_samples]: ../dlp +[dlp_docs]: https://cloud.google.com/dlp/docs/ \ No newline at end of file diff --git a/dlp/inspect.js b/dlp/inspect.js new file mode 100644 index 0000000000..267d63bc18 --- /dev/null +++ b/dlp/inspect.js @@ -0,0 +1,401 @@ +/** + * 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. + */ + +'use strict'; + +const API_URL = 'https://dlp.googleapis.com/v2beta1'; +const fs = require('fs'); +const requestPromise = require('request-promise'); +const mime = require('mime'); + +// Helper function to poll the rest API using exponential backoff +function pollJob (body, initialTimeout, tries, authToken) { + const jobName = body.name.split('/')[2]; + + // Construct polling function + const doPoll = (timeout, tries, resolve, reject) => { + // Construct REST request for polling an inspect job + const options = { + url: `${API_URL}/inspect/operations/${jobName}`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: true + }; + + // Poll the inspect job + setTimeout(() => { + requestPromise.get(options) + .then((body) => { + if (tries <= 0) { + reject('polling timed out'); + } + + // Job not finished - try again if possible + if (!(body && body.done)) { + return doPoll(timeout * 2, tries - 1, resolve, reject); + } + + // Job finished successfully! + return resolve(jobName); + }) + .catch((err) => { + reject(err); + }); + }, timeout); + }; + + // Return job-polling REST request as a Promise + return new Promise((resolve, reject) => { + doPoll(initialTimeout, tries, resolve, reject); + }); +} + +// Helper function to get results of a long-running (polling-required) job +function getJobResults (authToken, jobName) { + // Construct REST request to get results of finished inspect job + const options = { + url: `${API_URL}/inspect/results/${jobName}/findings`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: true + }; + + // Run job-results-fetching REST request + return requestPromise.get(options); +} + +function inspectString (authToken, string, inspectConfig) { + // [START inspect_string] + // Your gcloud auth token + // const authToken = 'YOUR_AUTH_TOKEN'; + + // The string to inspect + // const string = 'My name is Gary and my email is gary@example.com'; + + // Construct items to inspect + const items = [{ type: 'text/plain', value: string }]; + + // Construct REST request body + const requestBody = { + inspectConfig: { + infoTypes: inspectConfig.infoTypes, + minLikelihood: inspectConfig.minLikelihood, + maxFindings: inspectConfig.maxFindings, + includeQuote: inspectConfig.includeQuote + }, + items: items + }; + + // Construct REST request + const options = { + url: `${API_URL}/content:inspect`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: requestBody + }; + + // Run REST request + requestPromise.post(options) + .then((body) => { + const results = body.results[0].findings; + console.log(JSON.stringify(results, null, 2)); + }) + .catch((err) => { + console.log('Error in inspectString:', err); + }); + // [END inspect_string] +} + +function inspectFile (authToken, filepath, inspectConfig) { + // [START inspect_file] + // Your gcloud auth token. + // const authToken = 'YOUR_AUTH_TOKEN'; + + // The path to a local file to inspect. Can be a text, JPG, or PNG file. + // const fileName = 'path/to/image.png'; + + // Construct file data to inspect + const fileItems = [{ + type: mime.lookup(filepath) || 'application/octet-stream', + data: new Buffer(fs.readFileSync(filepath)).toString('base64') + }]; + + // Construct REST request body + const requestBody = { + inspectConfig: { + infoTypes: inspectConfig.infoTypes, + minLikelihood: inspectConfig.minLikelihood, + maxFindings: inspectConfig.maxFindings, + includeQuote: inspectConfig.includeQuote + }, + items: fileItems + }; + + // Construct REST request + const options = { + url: `${API_URL}/content:inspect`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: requestBody + }; + + // Run REST request + requestPromise.post(options) + .then((body) => { + const results = body.results[0].findings; + console.log(JSON.stringify(results, null, 2)); + }) + .catch((err) => { + console.log('Error in inspectFile:', err); + }); + // [END inspect_file] +} + +function inspectGCSFile (authToken, bucketName, fileName, inspectConfig) { + // [START inspect_gcs_file] + // Your gcloud auth token. + // const authToken = 'YOUR_AUTH_TOKEN'; + + // The name of the bucket where the file resides. + // const bucketName = 'YOUR-BUCKET'; + + // The path to the file within the bucket to inspect. + // Can contain wildcards, e.g. "my-image.*" + // const fileName = 'my-image.png'; + + // Get reference to the file to be inspected + const storageItems = { + cloudStorageOptions: { + fileSet: { url: `gs://${bucketName}/${fileName}` } + } + }; + + // Construct REST request body for creating an inspect job + const requestBody = { + inspectConfig: { + infoTypes: inspectConfig.infoTypes, + minLikelihood: inspectConfig.minLikelihood, + maxFindings: inspectConfig.maxFindings + }, + storageConfig: storageItems + }; + + // Construct REST request for creating an inspect job + let options = { + url: `${API_URL}/inspect/operations`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: requestBody + }; + + // Run inspect-job creation REST request + requestPromise.post(options) + .then((createBody) => pollJob(createBody, inspectConfig.initialTimeout, inspectConfig.tries, authToken)) + .then((jobName) => getJobResults(authToken, jobName)) + .then((findingsBody) => { + const findings = findingsBody.result.findings; + console.log(JSON.stringify(findings, null, 2)); + }) + .catch((err) => { + console.log('Error in inspectGCSFile:', err); + }); + // [END inspect_gcs_file] +} + +function inspectDatastore (authToken, namespaceId, kind, inspectConfig) { + // [START inspect_datastore] + // Your gcloud auth token + // const authToken = 'YOUR_AUTH_TOKEN'; + + // (Optional) The ID namespace of the Datastore document to inspect. + // To ignore Datastore namespaces, set this to an empty string ('') + // const namespace = ''; + + // The kind of the Datastore entity to inspect. + // const kind = 'Person'; + + // Get reference to the file to be inspected + const storageItems = { + datastoreOptions: { + partitionId: { + projectId: inspectConfig.projectId, + namespaceId: namespaceId + }, + kind: { + name: kind + } + } + }; + + // Construct REST request body for creating an inspect job + const requestBody = { + inspectConfig: { + infoTypes: inspectConfig.infoTypes, + minLikelihood: inspectConfig.minLikelihood, + maxFindings: inspectConfig.maxFindings + }, + storageConfig: storageItems + }; + + // Construct REST request for creating an inspect job + let options = { + url: `${API_URL}/inspect/operations`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: requestBody + }; + + // Run inspect-job creation REST request + requestPromise.post(options) + .then((createBody) => pollJob(createBody, inspectConfig.initialTimeout, inspectConfig.tries, authToken)) + .then((jobName) => getJobResults(authToken, jobName)) + .then((findingsBody) => { + const findings = findingsBody.result.findings; + console.log(JSON.stringify(findings, null, 2)); + }) + .catch((err) => { + console.log('Error in inspectDatastore:', err); + }); + // [END inspect_datastore] +} + +if (module === require.main) { + const auth = require('google-auto-auth')({ + keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS, + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + auth.getToken((err, token) => { + if (err) { + console.err('Error fetching auth token:', err); + process.exit(1); + } + + const cli = require(`yargs`) + .demand(1) + .command( + `string `, + `Inspect a string using the Data Loss Prevention API.`, + {}, + (opts) => inspectString(opts.authToken, opts.string, opts) + ) + .command( + `file `, + `Inspects a local text, PNG, or JPEG file using the Data Loss Prevention API.`, + {}, + (opts) => inspectFile(opts.authToken, opts.filepath, opts) + ) + .command( + `gcsFile `, + `Inspects a text file stored on Google Cloud Storage using the Data Loss Prevention API.`, + { + initialTimeout: { + type: 'integer', + alias: '-i', + default: 5000 + }, + tries: { + type: 'integer', + default: 5 + } + }, + (opts) => inspectGCSFile(opts.authToken, opts.bucketName, opts.fileName, opts) + ) + .command( + `datastore `, + `Inspect a Datastore instance using the Data Loss Prevention API.`, + { + projectId: { + type: 'string', + default: process.env.GCLOUD_PROJECT + }, + namespaceId: { + type: 'string', + default: '' + }, + initialTimeout: { + type: 'integer', + alias: '-i', + default: 5000 + }, + tries: { + type: 'integer', + default: 5 + } + }, + (opts) => inspectDatastore(opts.authToken, opts.namespaceId, opts.kind, opts) + ) + .option('m', { + alias: 'minLikelihood', + default: 'LIKELIHOOD_UNSPECIFIED', + type: 'string', + choices: [ + 'LIKELIHOOD_UNSPECIFIED', + 'VERY_UNLIKELY', + 'UNLIKELY', + 'POSSIBLE', + 'LIKELY', + 'VERY_LIKELY' + ], + global: true + }) + .option('f', { + alias: 'maxFindings', + default: 0, + type: 'integer', + global: true + }) + .option('q', { + alias: 'includeQuote', + default: true, + type: 'boolean', + global: true + }) + .option('a', { + alias: 'authToken', + default: token, + type: 'string', + global: true + }) + .option('t', { + alias: 'infoTypes', + default: [], + type: 'array', + global: true, + coerce: (infoTypes) => infoTypes.map((type) => { + return { name: type }; + }) + }) + .example(`node $0 string "My phone number is (123) 456-7890 and my email address is me@somedomain.com"`) + .example(`node $0 file resources/test.txt`) + .example(`node $0 gcsFile my-bucket my-file.txt`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/dlp/docs. Optional flags are explained at https://cloud.google.com/dlp/docs/reference/rest/v2beta1/content/inspect#InspectConfig`); + + cli.help().strict().argv; + }); +} diff --git a/dlp/metadata.js b/dlp/metadata.js new file mode 100644 index 0000000000..377344072c --- /dev/null +++ b/dlp/metadata.js @@ -0,0 +1,116 @@ +/** + * 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. + */ + +'use strict'; + +const API_URL = 'https://dlp.googleapis.com/v2beta1'; +const requestPromise = require('request-promise'); + +function listInfoTypes (authToken, category) { + // [START list_info_types] + // Your gcloud auth token. + // const authToken = 'YOUR_AUTH_TOKEN'; + + // The category of info types to list. + // const category = 'CATEGORY_TO_LIST'; + + // Construct REST request + const options = { + url: `${API_URL}/rootCategories/${category}/infoTypes`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: true + }; + + // Run REST request + requestPromise.get(options) + .then((body) => { + console.log(body); + }) + .catch((err) => { + console.log('Error in listInfoTypes:', err); + }); + // [END list_info_types] +} + +function listCategories (authToken) { + // [START list_categories] + // Your gcloud auth token. + // const authToken = 'YOUR_AUTH_TOKEN'; + + // Construct REST request + const options = { + url: `${API_URL}/rootCategories`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: true + }; + + // Run REST request + requestPromise.get(options) + .then((body) => { + const categories = body.categories; + console.log(categories); + }) + .catch((err) => { + console.log('Error in listCategories:', err); + }); + // [END list_categories] +} + +if (module === require.main) { + const auth = require('google-auto-auth')({ + keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS, + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + auth.getToken((err, token) => { + if (err) { + console.err('Error fetching auth token:', err); + process.exit(1); + } + + const cli = require(`yargs`) + .demand(1) + .command( + `infoTypes `, + `List types of sensitive information within a category.`, + {}, + (opts) => listInfoTypes(opts.authToken, opts.category) + ) + .command( + `categories`, + `List root categories of sensitive information.`, + {}, + (opts) => listCategories(opts.authToken) + ) + .option('a', { + alias: 'authToken', + default: token, + type: 'string', + global: true + }) + .example(`node $0 infoTypes GOVERNMENT`) + .example(`node $0 categories`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/dlp/docs`); + + cli.help().strict().argv; + }); +} diff --git a/dlp/package.json b/dlp/package.json new file mode 100644 index 0000000000..5e27385a2d --- /dev/null +++ b/dlp/package.json @@ -0,0 +1,40 @@ +{ + "name": "dlp-cli", + "description": "Command-line interface for Google Cloud Platform's Data Loss Prevention API", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google Inc.", + "contributors": [ + { + "name": "Ace Nassri", + "email": "anassri@google.com" + }, + { + "name": "Jason Dobry", + "email": "jason.dobry@gmail.com" + }, + { + "name": "Jon Wayne Parrott", + "email": "jonwayne@google.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "scripts": { + "test": "ava system-test/*.test.js -c 20 -T 240s" + }, + "engines": { + "node": ">=4.3.2" + }, + "dependencies": { + "google-auth-library": "^0.10.0", + "google-auto-auth": "^0.5.2", + "mime": "1.3.4", + "request": "2.79.0", + "request-promise": "4.1.1", + "yargs": "6.6.0" + } +} diff --git a/dlp/redact.js b/dlp/redact.js new file mode 100644 index 0000000000..02ea1dfa54 --- /dev/null +++ b/dlp/redact.js @@ -0,0 +1,130 @@ +/** + * 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. + */ + +'use strict'; + +const API_URL = 'https://dlp.googleapis.com/v2beta1'; +const requestPromise = require('request-promise'); + +function redactString (authToken, string, replaceString, inspectConfig) { + // [START redact_string] + // Your gcloud auth token + // const authToken = 'YOUR_AUTH_TOKEN'; + + // The string to inspect + // const string = 'My name is Gary and my email is gary@example.com'; + + // The string to replace sensitive data with + // const replaceString = 'REDACTED'; + + // Construct items to inspect + const items = [{ type: 'text/plain', value: string }]; + + // Construct info types + replacement configs + const replaceConfigs = inspectConfig.infoTypes.map((infoType) => { + return { + infoType: infoType, + replaceWith: replaceString + }; + }); + + // Construct REST request body + const requestBody = { + inspectConfig: { + infoTypes: inspectConfig.infoTypes, + minLikelihood: inspectConfig.minLikelihood + }, + items: items, + replaceConfigs: replaceConfigs + }; + + // Construct REST request + const options = { + url: `${API_URL}/content:redact`, + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + json: requestBody + }; + + // Run REST request + requestPromise.post(options) + .then((body) => { + const results = body.items[0].value; + console.log(results); + }) + .catch((err) => { + console.log('Error in redactString:', err); + }); + // [END redact_string] +} + +if (module === require.main) { + const auth = require('google-auto-auth')({ + keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS, + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + auth.getToken((err, token) => { + if (err) { + console.err('Error fetching auth token:', err); + process.exit(1); + } + + const cli = require(`yargs`) + .demand(1) + .command( + `string `, + `Redact sensitive data from a string using the Data Loss Prevention API.`, + {}, + (opts) => redactString(opts.authToken, opts.string, opts.replaceString, opts) + ) + .option('m', { + alias: 'minLikelihood', + default: 'LIKELIHOOD_UNSPECIFIED', + type: 'string', + choices: [ + 'LIKELIHOOD_UNSPECIFIED', + 'VERY_UNLIKELY', + 'UNLIKELY', + 'POSSIBLE', + 'LIKELY', + 'VERY_LIKELY' + ], + global: true + }) + .option('a', { + alias: 'authToken', + default: token, + type: 'string', + global: true + }) + .option('t', { + alias: 'infoTypes', + required: true, + type: 'array', + global: true, + coerce: (infoTypes) => infoTypes.map((type) => { + return { name: type }; + }) + }) + .example(`node $0 string "My name is Gary" "REDACTED" -t US_MALE_NAME`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/dlp/docs. Optional flags are explained at https://cloud.google.com/dlp/docs/reference/rest/v2beta1/content/inspect#InspectConfig`); + + cli.help().strict().argv; + }); +} diff --git a/dlp/resources/accounts.txt b/dlp/resources/accounts.txt new file mode 100644 index 0000000000..2763cd0ab8 --- /dev/null +++ b/dlp/resources/accounts.txt @@ -0,0 +1 @@ +My credit card number is 1234 5678 9012 3456, and my CVV is 789. \ No newline at end of file diff --git a/dlp/resources/harmless.txt b/dlp/resources/harmless.txt new file mode 100644 index 0000000000..5666de37ab --- /dev/null +++ b/dlp/resources/harmless.txt @@ -0,0 +1 @@ +This file is mostly harmless. diff --git a/dlp/resources/test.png b/dlp/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/resources/test.txt b/dlp/resources/test.txt new file mode 100644 index 0000000000..c2ee3815bc --- /dev/null +++ b/dlp/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/dlp/system-test/inspect.test.js b/dlp/system-test/inspect.test.js new file mode 100644 index 0000000000..9dd76c8af2 --- /dev/null +++ b/dlp/system-test/inspect.test.js @@ -0,0 +1,166 @@ +/** + * 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. + */ + +'use strict'; + +require(`../../system-test/_setup`); + +const cmd = 'node inspect'; + +// inspect_string +test(`should inspect a string`, async (t) => { + const output = await runAsync(`${cmd} string "I'm Gary and my email is gary@example.com"`); + t.regex(output, /"name": "EMAIL_ADDRESS"/); +}); + +test(`should handle a string with no sensitive data`, async (t) => { + const output = await runAsync(`${cmd} string "foo"`); + t.is(output, 'undefined'); +}); + +test(`should report string inspection handling errors`, async (t) => { + const output = await runAsync(`${cmd} string "I'm Gary and my email is gary@example.com" -a foo`); + t.regex(output, /Error in inspectString/); +}); + +// inspect_file +test(`should inspect a local text file`, async (t) => { + const output = await runAsync(`${cmd} file resources/test.txt`); + t.regex(output, /"name": "PHONE_NUMBER"/); + t.regex(output, /"name": "EMAIL_ADDRESS"/); +}); + +test(`should inspect a local image file`, async (t) => { + const output = await runAsync(`${cmd} file resources/test.png`); + t.regex(output, /"name": "PHONE_NUMBER"/); +}); + +test(`should handle a local file with no sensitive data`, async (t) => { + const output = await runAsync(`${cmd} file resources/harmless.txt`); + t.is(output, 'undefined'); +}); + +test(`should report local file handling errors`, async (t) => { + const output = await runAsync(`${cmd} file resources/harmless.txt -a foo`); + t.regex(output, /Error in inspectFile/); +}); + +// inspect_gcs_file +test.serial(`should inspect a GCS text file`, async (t) => { + const output = await runAsync(`${cmd} gcsFile nodejs-docs-samples-dlp test.txt`); + t.regex(output, /"name": "PHONE_NUMBER"/); + t.regex(output, /"name": "EMAIL_ADDRESS"/); +}); + +test.serial(`should inspect multiple GCS text files`, async (t) => { + const output = await runAsync(`${cmd} gcsFile nodejs-docs-samples-dlp *.txt`); + t.regex(output, /"name": "PHONE_NUMBER"/); + t.regex(output, /"name": "EMAIL_ADDRESS"/); + t.regex(output, /"name": "CREDIT_CARD_NUMBER"/); +}); + +test.serial(`should accept try limits for inspecting GCS files`, async (t) => { + const output = await runAsync(`${cmd} gcsFile nodejs-docs-samples-dlp test.txt --tries 0`); + t.regex(output, /polling timed out/); +}); + +test.serial(`should handle a GCS file with no sensitive data`, async (t) => { + const output = await runAsync(`${cmd} gcsFile nodejs-docs-samples-dlp harmless.txt`); + t.is(output, 'undefined'); +}); + +test.serial(`should report GCS file handling errors`, async (t) => { + const output = await runAsync(`${cmd} gcsFile nodejs-docs-samples-dlp harmless.txt -a foo`); + t.regex(output, /Error in inspectGCSFile/); +}); + +// inspect_datastore +test.serial(`should inspect Datastore`, async (t) => { + const output = await runAsync(`${cmd} datastore Person --namespaceId DLP`); + t.regex(output, /"name": "PHONE_NUMBER"/); + t.regex(output, /"name": "EMAIL_ADDRESS"/); +}); + +test.serial(`should accept try limits for inspecting Datastore`, async (t) => { + const output = await runAsync(`${cmd} datastore Person --namespaceId DLP --tries 0`); + t.regex(output, /polling timed out/); +}); + +test.serial(`should handle Datastore with no sensitive data`, async (t) => { + const output = await runAsync(`${cmd} datastore Harmless --namespaceId DLP`); + t.is(output, 'undefined'); +}); + +test.serial(`should report Datastore file handling errors`, async (t) => { + const output = await runAsync(`${cmd} datastore Harmless --namespaceId DLP -a foo`); + t.regex(output, /Error in inspectDatastore/); +}); + +// CLI options +test(`should have a minLikelihood option`, async (t) => { + const promiseA = runAsync(`${cmd} string "My phone number is (123) 456-7890." -m POSSIBLE`); + const promiseB = runAsync(`${cmd} string "My phone number is (123) 456-7890." -m UNLIKELY`); + + const outputA = await promiseA; + t.truthy(outputA); + t.notRegex(outputA, /PHONE_NUMBER/); + + const outputB = await promiseB; + t.regex(outputB, /PHONE_NUMBER/); +}); + +test(`should have a maxFindings option`, async (t) => { + const promiseA = runAsync(`${cmd} string "My email is gary@example.com and my phone number is (223) 456-7890." -f 1`); + const promiseB = runAsync(`${cmd} string "My email is gary@example.com and my phone number is (223) 456-7890." -f 2`); + + const outputA = await promiseA; + t.not(outputA.includes('PHONE_NUMBER'), outputA.includes('EMAIL_ADDRESS')); // Exactly one of these should be included + + const outputB = await promiseB; + t.regex(outputB, /PHONE_NUMBER/); + t.regex(outputB, /EMAIL_ADDRESS/); +}); + +test(`should have an option to include quotes`, async (t) => { + const promiseA = runAsync(`${cmd} string "My phone number is (223) 456-7890." -q false`); + const promiseB = runAsync(`${cmd} string "My phone number is (223) 456-7890."`); + + const outputA = await promiseA; + t.truthy(outputA); + t.notRegex(outputA, /\(223\) 456-7890/); + + const outputB = await promiseB; + t.regex(outputB, /\(223\) 456-7890/); +}); + +test(`should have an option to filter results by infoType`, async (t) => { + const promiseA = runAsync(`${cmd} string "My email is gary@example.com and my phone number is (223) 456-7890."`); + const promiseB = runAsync(`${cmd} string "My email is gary@example.com and my phone number is (223) 456-7890." -t PHONE_NUMBER`); + + const outputA = await promiseA; + t.regex(outputA, /EMAIL_ADDRESS/); + t.regex(outputA, /PHONE_NUMBER/); + + const outputB = await promiseB; + t.notRegex(outputB, /EMAIL_ADDRESS/); + t.regex(outputB, /PHONE_NUMBER/); +}); + +test(`should have an option for custom auth tokens`, async (t) => { + const output = await runAsync(`${cmd} string "My name is Gary and my phone number is (223) 456-7890." -a foo`); + t.regex(output, /Error in inspectString/); + t.regex(output, /invalid authentication/); +}); + diff --git a/dlp/system-test/metadata.test.js b/dlp/system-test/metadata.test.js new file mode 100644 index 0000000000..967630d675 --- /dev/null +++ b/dlp/system-test/metadata.test.js @@ -0,0 +1,47 @@ +/** + * 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. + */ + +'use strict'; + +require(`../../system-test/_setup`); + +const cmd = `node metadata`; + +test(`should list info types for a given category`, async (t) => { + const output = await runAsync(`${cmd} infoTypes GOVERNMENT`); + t.regex(output, /name: 'US_DRIVERS_LICENSE_NUMBER'/); +}); + +test(`should inspect categories`, async (t) => { + const output = await runAsync(`${cmd} categories`); + t.regex(output, /name: 'FINANCE'/); +}); + +test(`should have an option for custom auth tokens`, async (t) => { + const output = await runAsync(`${cmd} categories -a foo`); + t.regex(output, /Error in listCategories/); + t.regex(output, /invalid authentication/); +}); + +// Error handling +test(`should report info type listing handling errors`, async (t) => { + const output = await runAsync(`${cmd} infoTypes GOVERNMENT -a foo`); + t.regex(output, /Error in listInfoTypes/); +}); + +test(`should report category listing handling errors`, async (t) => { + const output = await runAsync(`${cmd} categories -a foo`); + t.regex(output, /Error in listCategories/); +}); diff --git a/dlp/system-test/redact.test.js b/dlp/system-test/redact.test.js new file mode 100644 index 0000000000..d55c0a6f0f --- /dev/null +++ b/dlp/system-test/redact.test.js @@ -0,0 +1,54 @@ +/** + * 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. + */ + +'use strict'; + +require(`../../system-test/_setup`); + +const cmd = 'node redact'; + +// redact_string +test(`should redact sensitive data from a string`, async (t) => { + const output = await runAsync(`${cmd} string "I am Gary and my phone number is (123) 456-7890." REDACTED -t US_MALE_NAME PHONE_NUMBER`); + t.is(output, 'I am REDACTED and my phone number is REDACTED.'); +}); + +test(`should ignore unspecified type names when redacting from a string`, async (t) => { + const output = await runAsync(`${cmd} string "I am Gary and my phone number is (123) 456-7890." REDACTED -t PHONE_NUMBER`); + t.is(output, 'I am Gary and my phone number is REDACTED.'); +}); + +test(`should report string redaction handling errors`, async (t) => { + const output = await runAsync(`${cmd} string "My name is Gary and my phone number is (123) 456-7890." REDACTED -t PHONE_NUMBER -a foo`); + t.regex(output, /Error in redactString/); +}); + +// CLI options +test(`should have a minLikelihood option`, async (t) => { + const promiseA = runAsync(`${cmd} string "My phone number is (123) 456-7890." REDACTED -t PHONE_NUMBER -m VERY_LIKELY`); + const promiseB = runAsync(`${cmd} string "My phone number is (123) 456-7890." REDACTED -t PHONE_NUMBER -m UNLIKELY`); + + const outputA = await promiseA; + t.is(outputA, 'My phone number is (123) 456-7890.'); + + const outputB = await promiseB; + t.is(outputB, 'My phone number is REDACTED.'); +}); + +test(`should have an option for custom auth tokens`, async (t) => { + const output = await runAsync(`${cmd} string "My name is Gary and my phone number is (123) 456-7890." REDACTED -t PHONE_NUMBER -a foo`); + t.regex(output, /Error in redactString/); + t.regex(output, /invalid authentication/); +});