30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Cloud DLP を使って BigQuery に入っている個人情報を検知する

Cloud Data Loss Prevention (以下 Cloud DLP) は機密データを保護するための Google Cloud のサービス。渡したデータや指定したストレージに入っているデータに対して、機密データが含まれていないか検査したり、含まれていた場合に匿名化を行ったりすることができる。
この記事では、BigQuery に入れているデータに個人情報が含まれていないかを Cloud DLP を使って検査する方法について述べていく。

基本的に gcloud CLI を使って操作していく。
gcloud CLI の基本的な使い方については以下に書いた。

numb86-tech.hatenablog.com

また、作業の都合上、オーナー権限(roles/owner)を持ったユーザーアカウント(numb@example.com)で gcloud CLI の認証を行っている。
途中で認証するアカウントを変えるが、その際は明示する。

プロジェクトのセットアップを行う

今回は、作業用のプロジェクトを一から用意してそこで作業することにする。
sample-pjというプロジェクトを作成する。

$ gcloud projects create sample-pj

sample-pjを gcloud CLI のカレントプロジェクトに設定する。

$ gcloud config set core/project sample-pj

$ gcloud config list
[core]
account = numb@example.com
disable_usage_reporting = True
project = sample-pj

Cloud DLP を利用するためには請求先アカウントを設定する必要があるが、プロジェクト作成直後は設定されていない。

$ gcloud billing projects describe sample-pj
billingAccountName: ''
billingEnabled: false
name: projects/sample-pj/billingInfo
projectId: sample-pj

今回は予め用意しておいた請求先アカウントを使う。既存の請求先アカウントの ID は$ gcloud billing accounts listで確認できる。

$ gcloud billing accounts list
ACCOUNT_ID            NAME                OPEN   MASTER_ACCOUNT_ID
XXXXXX-XXXXXX-XXXXXX  請求先アカウント 1  True

以下のコマンドで請求先アカウントをプロジェクトに設定する。

$ gcloud billing projects link sample-pj --billing-account=XXXXXX-XXXXXX-XXXXXX
billingAccountName: billingAccounts/XXXXXX-XXXXXX-XXXXXX
billingEnabled: true
name: projects/sample-pj/billingInfo
projectId: sample-pj

最後に Cloud DLP の API を有効にすることで、プロジェクトのセットアップは完了。

$ gcloud services enable dlp.googleapis.com

BigQuery にデータを入れる

次は検査対象のデータを用意する。

dlp.sample1というテーブルを用意し、そこにデータを入れることにする。
まずはデータセットdlpを作り、そのなかにテーブルsample1を作る。
sample1valuetimeという 2 つのカラムを持つ。

$ bq --location=asia-northeast1 mk --dataset sample-pj:dlp
Dataset 'sample-pj:dlp' successfully created.

$ bq mk --table dlp.sample1 "value:string, time:timestamp"
Table 'sample-pj:dlp.sample1' successfully created.

$ bq ls
  datasetId
 -----------
  dlp

$ bq ls dlp
  tableId   Type    Labels   Time Partitioning   Clustered Fields
 --------- ------- -------- ------------------- ------------------
  sample1   TABLE

次にdlp.sample1を投入する。
まずは以下の内容のsample.csvを用意する。

value,time
顧客名:伊藤博文 様。商品「新編 日本国憲法逐条解説(第5版)」について、明治憲法との比較に使用したいとの問い合わせあり。,2025-04-01T09:10:00Z
注文番号:240403-HBF。お届け先:山口県熊毛郡。支払方法:後払い。希望納期:4月6日まで。,2025-04-02T11:30:00Z
購入理由欄に「会議で若者に「それ、今の憲法には書いてませんよ」と言われ、悔しくて購入を決意。」との記載あり。,2025-04-02T11:45:00Z
注文者:伊藤博文 様(メール:hirofumi1885@example.com)。配送は通常便を指定。,2025-04-02T13:00:00Z
出荷処理完了。配送業者:日本郵便。追跡番号あり。,2025-04-03T15:30:00Z
配送完了確認済。本人受領。宛名ラベルには「宰相 伊藤 博文 様」と誤って印字され、再発防止対応中。,2025-04-05T10:15:00Z
社内コメント:「氏名欄に旧字体が使われていたが、システム上問題なく通過。今後の明治期対応として様式に追記予定」,2025-04-05T11:20:00Z

そしてbqコマンドで、sample.csvの内容をdlp.sample1に入れる。

$ bq load --source_format=CSV --skip_leading_rows=1 dlp.sample1 sample.csv

そうすると以下の内容のテーブルが作られる。

value time
顧客名:伊藤博文 様。商品「新編 日本国憲法逐条解説(第5版)」について、明治憲法との比較に使用したいとの問い合わせあり。 2025-04-01 09:10:00
注文番号:240403-HBF。お届け先:山口県熊毛郡。支払方法:後払い。希望納期:4月6日まで。 2025-04-02 11:30:00
購入理由欄に「会議で若者に「それ、今の憲法には書いてませんよ」と言われ、悔しくて購入を決意。」との記載あり。 2025-04-02 11:45:00
注文者:伊藤博文 様(メール:hirofumi1885@example.com)。配送は通常便を指定。 2025-04-02 13:00:00
出荷処理完了。配送業者:日本郵便。追跡番号あり。 2025-04-03 15:30:00
配送完了確認済。本人受領。宛名ラベルには「宰相 伊藤 博文 様」と誤って印字され、再発防止対応中。 2025-04-05 10:15:00
社内コメント:「氏名欄に旧字体が使われていたが、システム上問題なく通過。今後の明治期対応として様式に追記予定」 2025-04-05 11:20:00

このテーブルに対して、 Cloud DLP を使って検査を行っていく。

ジョブを作る

Cloud DLP では、検査などの処理はジョブという単位で行われるので、検査を行いたい場合はジョブを作る必要がある。
ジョブを作る方法はいくつかありクライアントライブラリを使うこともできるが、この記事では REST API を使うことにする。用意されているエンドポイントにcurlで HTTP リクエストを送ることでジョブを作成する。

具体的には以下の URL にPOSTメソッドでリクエストを送るとジョブを作ることができる。

https://dlp.googleapis.com/v2/projects/[PROJECT_ID]/dlpJobs

今回はsample-pjにジョブを作るのでhttps://dlp.googleapis.com/v2/projects/sample-pj/dlpJobsがエンドポイントになる。

そしてリクエストボディで「どのようなジョブを作るのか」を設定する。
この記事ではrequest.jsonというファイルを作成し、そこにリクエストボディを書くことにする。

以下のように書くとsample-pj.dlp.sample1に対して検査を行うジョブを作成することができる。
「何が含まれていないか検査するのか」はinfoTypeで指定する。予め多くのinfoTypeが用意されており、そこから自由に選ぶことができる。以下の例では人名が含まれていないかを検査するPERSON_NAMEを指定している。この記事では扱わないが、infoTypeを自作することもできる。 includeQuoteについては後述する。

{
  "inspectJob": {
    "storageConfig": {
      "bigQueryOptions": {
        "tableReference": {
          "projectId": "sample-pj",
          "datasetId": "dlp",
          "tableId": "sample1"
        }
      }
    },
    "inspectConfig": {
      "infoTypes": [
        { "name": "PERSON_NAME" }
      ],
      "includeQuote": false
    }
  }
}

このままオーナー権限を持ったアカウントで API を叩いてもよいのだが、今回はジョブを作るためのサービスアカウントを用意してそれを使うことにする。

dlp-userというサービスアカウントを作り、それにroles/dlp.userroles/dlp.jobsEditorを付与する。

$ gcloud iam service-accounts create dlp-user

$ gcloud projects add-iam-policy-binding sample-pj --member="serviceAccount:dlp-user@sample-pj.iam.gserviceaccount.com" --role="roles/dlp.user"

$ gcloud projects add-iam-policy-binding sample-pj --member="serviceAccount:dlp-user@dsample-pj.iam.gserviceaccount.com" --role="roles/dlp.jobsEditor"

dlp-userのサービスアカウントキーをdlp-user-key.jsonとして発行し、それを使って gcloud CLI の認証を行う。

$ gcloud iam service-accounts keys create dlp-user-key.json --iam-account=dlp-user@sample-pj.iam.gserviceaccount.com

$ gcloud auth login --cred-file=dlp-user-key.json

あとは、curlでリクエストを送ればよい。

$ curl -X POST "https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -H "x-goog-user-project: sample-pj" \
  -d @request.json
{
  "name": "projects/sample-pj/dlpJobs/i-4652535233124657304",
  "type": "INSPECT_JOB",
  "state": "PENDING",
  (中略)
}

これで、「sample-pj.dlp.sample1に人名が含まれていないか検査するジョブ」が作られた。

ちなみにこのタイミングで、roles/dlp.serviceAgentというサービスエージェントが作られる。このサービスエージェントにはroles/dlp.serviceAgentというロールが付与されているが、かなり強い権限を持っている。

https://cloud.google.com/iam/docs/understanding-roles#dlp.serviceAgent

これにより、 BigQuery のテーブルの中身を見たり、後述するように検査結果を BigQuery に書き込んだりすることができるのである。

ジョブの詳細を確認する

以下の URL にGETリクエストを送ることで、ジョブの情報を得ることができる。

https://dlp.googleapis.com/v2/[JOB_NAME]

JOB_NAMEは、ジョブ作成時のレスポンスに含まれている。先程作成したジョブの場合projects/sample-pj/dlpJobs/i-4652535233124657304なので、https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs/i-4652535233124657304にリクエストを送ればよい。

$ curl -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs/i-4652535233124657304" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "name": "projects/sample-pj/dlpJobs/i-4652535233124657304",
  "type": "INSPECT_JOB",
  "state": "DONE",
  "inspectDetails": {
    (中略)
    "result": {
      "processedBytes": "1083",
      "totalEstimatedBytes": "1083",
      "infoTypeStats": [
        {
          "infoType": {
            "name": "PERSON_NAME"
          },
          "count": "5"
        }
      ],
      "numRowsProcessed": "7"
    }
  },
  (中略)
}

inspectDetails.resultキーに、検査の結果が入っている。
numRowsProcessedはスキャンした行数を、infoTypeStatsは検知したデータの件数を、意味している。
つまり今回のジョブでは7行(dlp.sample1の全ての行)をスキャンし、そこからPERSON_NAMEに該当する可能性のあるデータを5件検知した、ということになる。

これが Cloud DLP API の基本的な使い方になる。
これ以降はrequest.jsonの中身を編集し、利用頻度の高そうな設定を紹介していく。
まずは検査結果の詳細を BigQuery に保存する方法を紹介する。保存された情報を見ることで、検査結果についてより詳細な情報を得ることができる。

結果を BigQuery に保存する

request.jsonを書き換えて、inspectJobactionsキーを追加する。
以下のようにしてリクエストを送ると、先ほどと同じ検査内容のジョブが作られ、その結果がsample-pj.dlp.results1に保存される。

{
  "inspectJob": {
    "storageConfig": {
      "bigQueryOptions": {
        "tableReference": {
          "projectId": "sample-pj",
          "datasetId": "dlp",
          "tableId": "sample1"
        }
      }
    },
    "inspectConfig": {
      "infoTypes": [
        { "name": "PERSON_NAME" }
      ],
      "includeQuote": false
    },
    "actions": [
      {
        "saveFindings": {
          "outputConfig": {
            "table": {
              "projectId": "sample-pj",
              "datasetId": "dlp",
              "tableId": "results1"
            }
          }
        }
      }
    ]
  }
}
$ curl -X POST "https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -H "x-goog-user-project: sample-pj" \
  -d @request.json
{
  "name": "projects/sample-pj/dlpJobs/i-799371601794026763",
  "type": "INSPECT_JOB",
  "state": "PENDING",
  (中略)
}

dlpデータセットを確認してみると、results1というテーブルが作られている(dlp-userには BigQuery の中身を見る権限がないので、bqコマンドを実行するときは権限を持ったアカウントに切り替える必要がある)。

$ bq ls dlp
  tableId    Type    Labels   Time Partitioning   Clustered Fields
 ---------- ------- -------- ------------------- ------------------
  results1   TABLE
  sample1    TABLE

大量のカラムがあるので、いくつかピックアップしてテーブルの中身を見てみる。

$ bq query --nouse_legacy_sql \
'SELECT
  job_name,
  info_type.name,
  likelihood,
  location.container.full_path,
  location.byte_range.start,
  location.byte_range.end,
  content.record_location.field_id.name,
  quote
FROM
  `sample-pj.dlp.results1`,
  UNNEST(location.content_locations) AS content
LIMIT 1000'
+--------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+-------+
|                              job_name                              |    name     | likelihood |        full_path        | start | end | name_1 | quote |
+--------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+-------+
| projects/sample-pj/locations/global/dlpJobs/i-799371601794026763 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | NULL  |
| projects/sample-pj/locations/global/dlpJobs/i-799371601794026763 | PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    43 |  49 | value  | NULL  |
| projects/sample-pj/locations/global/dlpJobs/i-799371601794026763 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | NULL  |
| projects/sample-pj/locations/global/dlpJobs/i-799371601794026763 | PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    45 |  54 | value  | NULL  |
| projects/sample-pj/locations/global/dlpJobs/i-799371601794026763 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    70 |  87 | value  | NULL  |
+--------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+-------+

検知したデータごとにレコードが作られるので、レコード数は5

  • job_name
    • このレコードを作成したジョブの名前。複数のジョブの結果をひとつのテーブルに入れていく場合、このカラムによって「どのジョブから作られたレコードか」を識別できる。
  • info_type.name
    • infoTypeの名前
  • likelihood
  • location.container.full_path, location.byte_range.start, location.byte_range.end
    • 検知したデータが存在する場所に関する情報。どのテーブルに入っていて、スキャンしたデータ内のどの位置に検知したデータがあるのかをバイト単位で示す。
  • content.record_location.field_id.name
    • 検知したデータが含まれているカラム名
    • location.container.full_pathと組み合わせることで、検知したデータはどのテーブルのどのカラムに入っているのかを特定できる
  • quote
    • includeQuotefalseにしていた場合は必ずNULLになる
    • trueになる場合はどうなるのかは後述する

このように、検知したデータについて詳細な情報を得ることができる。
上述したカラムの詳細、及び紹介していないカラムについては以下の公式ドキュメントを参照。
https://cloud.google.com/sensitive-data-protection/docs/reference/rest/v2/InspectResult

ところで、PERSON_NAME5件見つかったとのことだが、以下に再掲するようにdlp.sample1には人名は3件しかない。

value time
顧客名:伊藤博文 様。商品「新編 日本国憲法逐条解説(第5版)」について、明治憲法との比較に使用したいとの問い合わせあり。 2025-04-01 09:10:00
注文番号:240403-HBF。お届け先:山口県熊毛郡。支払方法:後払い。希望納期:4月6日まで。 2025-04-02 11:30:00
購入理由欄に「会議で若者に「それ、今の憲法には書いてませんよ」と言われ、悔しくて購入を決意。」との記載あり。 2025-04-02 11:45:00
注文者:伊藤博文 様(メール:hirofumi1885@example.com)。配送は通常便を指定。 2025-04-02 13:00:00
出荷処理完了。配送業者:日本郵便。追跡番号あり。 2025-04-03 15:30:00
配送完了確認済。本人受領。宛名ラベルには「宰相 伊藤 博文 様」と誤って印字され、再発防止対応中。 2025-04-05 10:15:00
社内コメント:「氏名欄に旧字体が使われていたが、システム上問題なく通過。今後の明治期対応として様式に追記予定」 2025-04-05 11:20:00

恐らく何かが誤検知されているのだと思うが、具体的に何が検知されたのか把握したい。
そのようなときはincludeQuotetrueにすると、quoteカラムに検知したデータが格納される。

includeQuotetrueにしてresults2に保存してみる。

 @@ -13,7 +13,7 @@
        "infoTypes": [
          { "name": "PERSON_NAME" }
        ],
 -      "includeQuote": false
 +      "includeQuote": true
      },
      "actions": [
        {
 @@ -22,7 +22,7 @@
              "table": {
                "projectId": "sample-pj",
                "datasetId": "dlp",
 -              "tableId": "results1"
 +              "tableId": "results2"
              }
            }
          }

新たに作成されたresults2の中身を見てみると、quoteに値が入っている。

$ bq query --nouse_legacy_sql \
'SELECT
  job_name,
  info_type.name,
  likelihood,
  location.container.full_path,
  location.byte_range.start,
  location.byte_range.end,
  content.record_location.field_id.name,
  quote
FROM
  `sample-pj.dlp.results2`,
  UNNEST(location.content_locations) AS content
LIMIT 1000'
+---------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+--------------+
|                              job_name                               |    name     | likelihood |        full_path        | start | end | name_1 |    quote     |
+---------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+--------------+
| projects/sample-pj/locations/global/dlpJobs/i-2778851594592575886 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 伊藤博文 様  |
| projects/sample-pj/locations/global/dlpJobs/i-2778851594592575886 | PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    43 |  49 | value  | 山口         |
| projects/sample-pj/locations/global/dlpJobs/i-2778851594592575886 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 伊藤博文 様  |
| projects/sample-pj/locations/global/dlpJobs/i-2778851594592575886 | PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    45 |  54 | value  | ラベル       |
| projects/sample-pj/locations/global/dlpJobs/i-2778851594592575886 | PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    70 |  87 | value  | 伊藤 博文 様 |
+---------------------------------------------------------------------+-------------+------------+-------------------------+-------+-----+--------+--------------+

これで、山口ラベルが検知されたことが分かった。そしてどちらもlikelihoodPOSSIBLEであり、正しく検知できているケース(LIKELY)よりも一致度が低く、likelihoodがある程度は機能していそうなことも分かる。

しかし、検査結果を格納するテーブルに機密データを入れたくないケースも多いと思う。そもそも、カラム名やデータの位置(start, end)は分かっているのだから、あとはレコードさえ特定できれば、includeQuoteを有効にしなくても当該データを特定できるはずである。
レコードを特定する情報はデフォルトでは含まれていないが、bigQueryOptionsidentifyingFieldsを設定することで利用可能になる。

identifyingFieldsには、検査対象のテーブルのレコードを一意に絞り込むために使うカラムの名前を指定する。
dlp.sample1の場合、timeの値は全てユニークでありこの値でレコードを特定できるため、timeを指定する。

 @@ -6,7 +6,10 @@
            "projectId": "sample-pj",
            "datasetId": "dlp",
            "tableId": "sample1"
 -        }
 +        },
 +        "identifyingFields": [
 +          { "name": "time" }
 +        ]
        }
      },
      "inspectConfig": {
 @@ -22,7 +25,7 @@
              "table": {
                "projectId": "sample-pj",
                "datasetId": "dlp",
 -              "tableId": "results2"
 +              "tableId": "results3"
              }
            }
          }

results3の中身を見てみる。

$ bq query --nouse_legacy_sql \
'SELECT
  info_type.name,
  likelihood,
  location.container.full_path,
  location.byte_range.start,
  location.byte_range.end,
  content.record_location.field_id.name,
  content.record_location.record_key.id_values[OFFSET(0)],
  quote
FROM
  `sample-pj.dlp.results3`,
  UNNEST(location.content_locations) AS content
LIMIT 1000'
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+
|    name     | likelihood |        full_path        | start | end | name_1 |         f0_          |    quote     |
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+
| PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 2025-04-01T09:10:00Z | 伊藤博文 様  |
| PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    43 |  49 | value  | 2025-04-02T11:30:00Z | 山口         |
| PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 2025-04-02T13:00:00Z | 伊藤博文 様  |
| PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    45 |  54 | value  | 2025-04-05T10:15:00Z | ラベル       |
| PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    70 |  87 | value  | 2025-04-05T10:15:00Z | 伊藤 博文 様 |
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+

content.record_location.record_key.id_values[OFFSET(0)]timeカラムの値が入っている。
これで、includeQuotefalseにしたとしても、他のカラムのデータと組み合わせることで、検知したデータがどこに存在するのかを特定できる。

複数の infoType を使う

infoTypeは複数指定することもできる。試しに、PERSON_NAMEに加えてEMAIL_ADDRESSも指定してみる。

 @@ -14,7 +14,8 @@
      },
      "inspectConfig": {
        "infoTypes": [
 -        { "name": "PERSON_NAME" }
 +        { "name": "PERSON_NAME" },
 +        { "name": "EMAIL_ADDRESS" }
        ],
        "includeQuote": true
      },
 @@ -25,7 +26,7 @@
              "table": {
                "projectId": "sample-pj",
                "datasetId": "dlp",
 -              "tableId": "results3"
 +              "tableId": "results4"
              }
            }
          }

EMAIL_ADDRESSも検知されている。

$ bq query --nouse_legacy_sql \
'SELECT
  info_type.name,
  likelihood,
  location.container.full_path,
  location.byte_range.start,
  location.byte_range.end,
  content.record_location.field_id.name,
  content.record_location.record_key.id_values[OFFSET(0)],
  quote
FROM
  `sample-pj.dlp.results4`,
  UNNEST(location.content_locations) AS content
LIMIT 1000'
+---------------+------------+-------------------------+-------+-----+--------+----------------------+--------------------------+
|     name      | likelihood |        full_path        | start | end | name_1 |         f0_          |          quote           |
+---------------+------------+-------------------------+-------+-----+--------+----------------------+--------------------------+
| PERSON_NAME   | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 2025-04-01T09:10:00Z | 伊藤博文 様              |
| PERSON_NAME   | POSSIBLE   | sample-pj:dlp:sample1 |    43 |  49 | value  | 2025-04-02T11:30:00Z | 山口                     |
| PERSON_NAME   | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 2025-04-02T13:00:00Z | 伊藤博文 様              |
| EMAIL_ADDRESS | LIKELY     | sample-pj:dlp:sample1 |    43 |  67 | value  | 2025-04-02T13:00:00Z | hirofumi1885@example.com |
| PERSON_NAME   | POSSIBLE   | sample-pj:dlp:sample1 |    45 |  54 | value  | 2025-04-05T10:15:00Z | ラベル                   |
| PERSON_NAME   | LIKELY     | sample-pj:dlp:sample1 |    70 |  87 | value  | 2025-04-05T10:15:00Z | 伊藤 博文 様             |
+---------------+------------+-------------------------+-------+-----+--------+----------------------+--------------------------+

検査対象のレコードを絞り込む

ここまではdlp.sample1にあるレコードを全てスキャンしていたが、一部のレコードのみをスキャンさせることもできる。
方法はいくつかあるが、ここではtimespanConfigを使った方法を紹介する。

timespanConfigを以下のように設定すると、timeカラムの値が2025-04-02T12:00:00Z以降のレコードのみがスキャン対象になる。

"timespanConfig": {
  "timestampField": {
    "name": "time"
  },
  "startTime": "2025-04-02T12:00:00Z"
}

request.jsonを以下のように変更してリクエストしてみる。

 @@ -10,12 +10,17 @@
          "identifyingFields": [
            { "name": "time" }
          ]
 +      },
 +      "timespanConfig": {
 +        "timestampField": {
 +          "name": "time"
 +        },
 +        "startTime": "2025-04-02T12:00:00Z"
        }
      },
      "inspectConfig": {
        "infoTypes": [
 -        { "name": "PERSON_NAME" },
 -        { "name": "EMAIL_ADDRESS" }
 +        { "name": "PERSON_NAME" }
        ],
        "includeQuote": true
      },
 @@ -26,7 +31,7 @@
              "table": {
                "projectId": "sample-pj",
                "datasetId": "dlp",
 -              "tableId": "results4"
 +              "tableId": "results5"
              }
            }
          }
$ curl -X POST "https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -H "x-goog-user-project: sample-pj" \
  -d @request.json
{
  "name": "projects/sample-pj/dlpJobs/i-4755260208488542553",
  "type": "INSPECT_JOB",
  "state": "PENDING",
  (中略)
}

projects/sample-pj/dlpJobs/i-4755260208488542553の詳細を確認する。

$ curl -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/dlpJobs/i-4755260208488542553" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "x-goog-user-project: sample-pj"

{
  "name": "projects/sample-pj/dlpJobs/i-4755260208488542553",
  "type": "INSPECT_JOB",
  "state": "DONE",
  "inspectDetails": {
    (中略)
    "result": {
      "processedBytes": "561",
      "totalEstimatedBytes": "1013",
      "infoTypeStats": [
        {
          "infoType": {
            "name": "PERSON_NAME"
          },
          "count": "3"
        }
      ],
      "numRowsProcessed": "4"
    }
  },
  (中略)
}

4行をスキャンして3件検知したことが分かった。

dlp.sample1dlp.results5の内容を示して、確認してみる。

value time
顧客名:伊藤博文 様。商品「新編 日本国憲法逐条解説(第5版)」について、明治憲法との比較に使用したいとの問い合わせあり。 2025-04-01 09:10:00
注文番号:240403-HBF。お届け先:山口県熊毛郡。支払方法:後払い。希望納期:4月6日まで。 2025-04-02 11:30:00
購入理由欄に「会議で若者に「それ、今の憲法には書いてませんよ」と言われ、悔しくて購入を決意。」との記載あり。 2025-04-02 11:45:00
注文者:伊藤博文 様(メール:hirofumi1885@example.com)。配送は通常便を指定。 2025-04-02 13:00:00
出荷処理完了。配送業者:日本郵便。追跡番号あり。 2025-04-03 15:30:00
配送完了確認済。本人受領。宛名ラベルには「宰相 伊藤 博文 様」と誤って印字され、再発防止対応中。 2025-04-05 10:15:00
社内コメント:「氏名欄に旧字体が使われていたが、システム上問題なく通過。今後の明治期対応として様式に追記予定」 2025-04-05 11:20:00
$ bq query --nouse_legacy_sql \
'SELECT
  info_type.name,
  likelihood,
  location.container.full_path,
  location.byte_range.start,
  location.byte_range.end,
  content.record_location.field_id.name,
  content.record_location.record_key.id_values[OFFSET(0)],
  quote
FROM
  `sample-pj.dlp.results5`,
  UNNEST(location.content_locations) AS content
LIMIT 1000'
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+
|    name     | likelihood |        full_path        | start | end | name_1 |         f0_          |    quote     |
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+
| PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    12 |  28 | value  | 2025-04-02T13:00:00Z | 伊藤博文 様  |
| PERSON_NAME | POSSIBLE   | sample-pj:dlp:sample1 |    45 |  54 | value  | 2025-04-05T10:15:00Z | ラベル       |
| PERSON_NAME | LIKELY     | sample-pj:dlp:sample1 |    70 |  87 | value  | 2025-04-05T10:15:00Z | 伊藤 博文 様 |
+-------------+------------+-------------------------+-------+-----+--------+----------------------+--------------+

time2025-04-02T12:00:00Z以降である後半4行をスキャンし、そこから3件を検知したことが分かる。
startTimeではなくendTimeを指定することもできるし、startTimeendTimeの両方を指定すればその期間の値のレコードのみがスキャンの対象になる。