今週末から北海道オフィスに出張でワクワクしている瀧川です。 私はデータ分析基盤の構築をする機会がよくあり、FluentdやEmbulk、Digdag、BigQueryを好んで使っています。 構築する際に気をつけることというと、冪等性やログ欠損(リカバリ)などいろいろあるかと思いますが、その中でも重要になるのが 個人情報などの見せられないデータ(機密情報) の扱いかな思っています。 構造化されたデータの個人情報であれば、そもそも分析基盤に転送しないことや、マスキングして送るなど対策は容易 *1 ですが、 例えば「バグでログの可変な箇所に個人情報が入ってしまった」とか「アンケートの集計をしたいが電話番号など入ってしまっていて隠したい」 などの場合、検出や秘匿化はかなり難しいと感じています。 その課題を解決してくれるのが GoogleCloud DLP(Data Loss Prevention) API ではないかなと思っています! 本記事ではGoogleCloud DLP APIをGolangのクライアントを使って、簡単なサンプルを作っていきます。 なかなか機能が多く、とっつきにくいところもあるので、その辺り参考になればと思います! Google Cloud DLP(Data Loss Prevention) とは 使ってみる 準備 認証 機密データ検出(Inspect) 秘匿化(Deidentify) コード全体 最後に Google Cloud DLP(Data Loss Prevention) とは 機密データ保護に有用な DLP API の新機能 | Google Cloud Blog 上記の公式Blogを見ていただくと、わかりやすいアニメーションもあるのでイメージしやすいかと思います。 ざっくりと説明すると、 「あらゆる場所に存在するデータの中から、90種類以上の機密情報を検出し、機密情報に応じた秘匿化を実施できる」 サービスです。 ワークフローへの組み込み 対象のデータはテキストまたは画像で、Cloud Storage、BigQuery、Cloud DatastoreといったGCPのストアに配置するか、直接APIにPOSTすることで実行することができます。 また、リアルタイムでの実行や、PubSubへの通知なども対応しているようです。 検出可能な機密情報 機密情報は、「クレジット カード番号、氏名、社会保障番号、電話番号、認証情報」などがすでに機械学習によってモデル化されており、その中から必要に応じて検出項目を指定することができます。 infoType 検出器リファレンス | Data Loss Prevention のドキュメント | Google Cloud 柔軟な秘匿化方法 機密情報によっては秘匿化方法をしっかりと考える必要があるかもしれません。 例えば、「携帯電話の番号は先頭3桁だけ残してマスキングしてほしい(@080- - @)」とか「Emailごと集計したいので同一Emailは同一の文字列になるようにしてほしい(仮名化、ハッシュ化)」などなど。 そういったこともCloud DLPであれば実現することができるようです。 使ってみる 早速プログラムから使ってみたいと思います! APIに対して、Node, Golang, Java, Pythonのクライアントライブラリが提供されています。 本記事ではGolangでやっていきます。 APIドキュメントは以下なので適宜参考にしてください。 cloud - GoDoc 準備 事前に以下を準備しておいてください。 Golang depなど依存解決ツール サービスアカウントのJSONキー Cloud DLPへのアクセス権を忘れずに 以下のような1ファイルにつらつら書いていきます! main.go package main func main() { } 認証 まずはサービスを利用するために認証を通す必要があります。 DLPのクライアントにJSONキーを渡しています。 この辺りはGCPのどのサービスでも共通しているお決まりな書き方ですね! 特にエラーがでなければ先に進みましょう。 エラーが出る場合、サービスアカウントの権限やJSONキーが正しいか、依存解決がうまくできているかなど確認してみてください! ( dep ensure や go get などは実行してください) package main import ( "context" "log" dlp "cloud.google.com/go/dlp/apiv2" "google.golang.org/api/option" ) func main() { credJSON := "ここはJSONキーで置き換えてください" ctx := context.Background() cred := option.WithCredentialsJSON([] byte (credJSON)) client, err := dlp.NewClient(ctx, cred) if err != nil { log.Fatal(err) } defer client.Close() } 機密データ検出(Inspect) それでは実際に機密データを検出してみたいと思います! 以下のように呼び出す inspectString 関数を実装していきます。 infoTypes が検出する機密情報の種類で、 message が検出対象ですね。 新しくimportされている dlppb はDLP APIと通信する際のデータ構造定義になります。(Protocol Bufferで定義していますね) こちらがdlppbのドキュメントになります。 dlp - GoDoc 設定データを構築する際には参考にするといいと思います! package main import ( "context" "fmt" "log" dlp "cloud.google.com/go/dlp/apiv2" "google.golang.org/api/option" dlppb "google.golang.org/genproto/googleapis/privacy/dlp/v2" ) func main() { ... projectID := "サービスアカウントを発行したProject ID" infoTypes := [] string { "PHONE_NUMBER" , "EMAIL_ADDRESS" } message := ` TO:hoge@hoge.com こんにちわ、瀧川です。 080-0000-0000 までご連絡ください。 ` inspectString(projectID, client, infoTypes, message) } 検出を実行し、結果を標準出力するコードが以下のようになります。 リクエストのデータ構造は若干難しいですね。 func inspectString(projectID string , client *dlp.Client, infoTypes [] string , input string ) { // 検出機密種別をdlppb.InfoTypeに変換 var i []*dlppb.InfoType for _, it := range infoTypes { i = append (i, &dlppb.InfoType{Name: it}) } // リクエストのデータを構築 req := &dlppb.InspectContentRequest{ Parent: "projects/" + projectID, // 必ずprojects/を前につける InspectConfig: &dlppb.InspectConfig{ // 検出条件など InfoTypes: i, }, Item: &dlppb.ContentItem{ // 検出対象 DataItem: &dlppb.ContentItem_Value{ Value: input, }, }, } resp, err := client.InspectContent(context.Background(), req) // 検出を実行 if err != nil { log.Fatal(err) } // 以下は結果表示のみ resultFormatter := func (inputStr string , result *dlppb.Finding) string { tmp := ` 機密種別: %s もっともらしさ(尤度): %s 範囲: %d ~ %d 検出文字列: %s ` start := result.GetLocation().GetCodepointRange().GetStart() end := result.GetLocation().GetCodepointRange().GetEnd() return fmt.Sprintf(tmp, result.GetInfoType().GetName(), result.GetLikelihood(), start, end, string ([] rune (input)[start:end])) } fmt.Printf( "対象文字列: %s \n " , input) for _, result := range resp.GetResult().GetFindings() { // 検出されたものはFinding fmt.Println(resultFormatter(input, result)) } } 実行すると以下のようになります! 検出できてそうですね! 検出結果は dlppb.Finding というデータになり、機密種別(InfoType)や精度の指標である尤度(Likelihood)、検出された箇所(Location)などが取得できるのがわかるかと思います。 $ go run main.go 対象文字列: TO:hoge@hoge.com こんにちわ、瀧川です。 080-0000-0000 までご連絡ください。 機密種別: EMAIL_ADDRESS もっともらしさ(尤度): LIKELY 範囲: 4 ~ 17 検出文字列: hoge@hoge.com 機密種別: PHONE_NUMBER もっともらしさ(尤度): POSSIBLE 範囲: 31 ~ 44 検出文字列: 080-0000-0000 秘匿化(Deidentify) 検出された機密情報を秘匿化するコードは以下のようになります! (急にデータ構造が複雑になった気がしますね...型名が冗長だったりするだけなのでそこまで複雑ではないですよ!) PHONE_NUMBER は「末尾から9文字をアスタリスクに変換」するようにしています。 EMAIL_ADDRESS は「 abcdefghijklmnop をキーにしてハッシュ化」するようにしています。 func deidentifyString(projectID string , client *dlp.Client, infoTypes [] string , input string ) { var i []*dlppb.InfoType for _, it := range infoTypes { i = append (i, &dlppb.InfoType{Name: it}) } req := &dlppb.DeidentifyContentRequest{ // Deidentifyリクエスト Parent: "projects/" + projectID, InspectConfig: &dlppb.InspectConfig{ InfoTypes: i, }, DeidentifyConfig: &dlppb.DeidentifyConfig{ Transformation: &dlppb.DeidentifyConfig_InfoTypeTransformations{ InfoTypeTransformations: &dlppb.InfoTypeTransformations{ Transformations: []*dlppb.InfoTypeTransformations_InfoTypeTransformation{ // 変換方法の配列 { InfoTypes: []*dlppb.InfoType{&dlppb.InfoType{Name: "PHONE_NUMBER" }}, PrimitiveTransformation: &dlppb.PrimitiveTransformation{ Transformation: &dlppb.PrimitiveTransformation_CharacterMaskConfig{ // 変換方法 CharacterMaskConfig: &dlppb.CharacterMaskConfig{ MaskingCharacter: "*" , NumberToMask: 9 , ReverseOrder: true , }, }, }, }, { InfoTypes: []*dlppb.InfoType{&dlppb.InfoType{Name: "EMAIL_ADDRESS" }}, PrimitiveTransformation: &dlppb.PrimitiveTransformation{ Transformation: &dlppb.PrimitiveTransformation_CryptoHashConfig{ CryptoHashConfig: &dlppb.CryptoHashConfig{ CryptoKey: &dlppb.CryptoKey{ // 変換方法 Source: &dlppb.CryptoKey_Unwrapped{ Unwrapped: &dlppb.UnwrappedCryptoKey{ Key: [] byte ( "abcdefghijklmnop" ), }, }, }, }, }, }, }, }, }, }, }, Item: &dlppb.ContentItem{ DataItem: &dlppb.ContentItem_Value{ Value: input, }, }, } r, err := client.DeidentifyContent(context.Background(), req) // Deidentify実行 if err != nil { log.Fatal(err) } fmt.Println(r.GetItem().GetValue()) } 実行結果はこちら! よさそうですね。 $ go run main.go TO:9ELfD7kXS0HfGr8KjitHndGCWSO8ReSM1l8GwLKqjok= こんにちわ、瀧川です。 080-********* までご連絡ください。 ※ ドキュメントを見ると、 インタフェース名_具象クラス名 のような形でポリモフィックに定義されているようです。(以下が変換方法一覧) type PrimitiveTransformation_BucketingConfig type PrimitiveTransformation_CharacterMaskConfig type PrimitiveTransformation_CryptoDeterministicConfig type PrimitiveTransformation_CryptoHashConfig type PrimitiveTransformation_CryptoReplaceFfxFpeConfig type PrimitiveTransformation_DateShiftConfig type PrimitiveTransformation_FixedSizeBucketingConfig type PrimitiveTransformation_RedactConfig type PrimitiveTransformation_ReplaceConfig type PrimitiveTransformation_ReplaceWithInfoTypeConfig type PrimitiveTransformation_TimePartConfig コード全体 package main import ( "context" "fmt" "log" dlp "cloud.google.com/go/dlp/apiv2" "google.golang.org/api/option" dlppb "google.golang.org/genproto/googleapis/privacy/dlp/v2" ) func main() { credJSON := "JSONキー" ctx := context.Background() cred := option.WithCredentialsJSON([] byte (credJSON)) client, _ := dlp.NewClient(ctx, cred) defer client.Close() infoTypes := [] string { "PHONE_NUMBER" , "EMAIL_ADDRESS" } message := ` TO:hoge@hoge.com こんにちわ、瀧川です。 080-0000-0000 までご連絡ください。 ` inspectString( "プロジェクトID" , client, infoTypes, message) deidentifyString( "プロジェクトID" , client, infoTypes, message) } func inspectString(projectID string , client *dlp.Client, infoTypes [] string , input string ) { var i []*dlppb.InfoType for _, it := range infoTypes { i = append (i, &dlppb.InfoType{Name: it}) } req := &dlppb.InspectContentRequest{ Parent: "projects/" + projectID, InspectConfig: &dlppb.InspectConfig{ InfoTypes: i, }, Item: &dlppb.ContentItem{ DataItem: &dlppb.ContentItem_Value{ Value: input, }, }, } resp, err := client.InspectContent(context.Background(), req) if err != nil { log.Fatal(err) } resultFormatter := func (inputStr string , result *dlppb.Finding) string { tmp := ` 機密種別: %s もっともらしさ(尤度): %s 範囲: %d ~ %d 検出文字列: %s ` start := result.GetLocation().GetCodepointRange().GetStart() end := result.GetLocation().GetCodepointRange().GetEnd() return fmt.Sprintf(tmp, result.GetInfoType().GetName(), result.GetLikelihood(), start, end, string ([] rune (input)[start:end])) } fmt.Printf( "対象文字列: %s \n " , input) for _, result := range resp.GetResult().GetFindings() { fmt.Println(resultFormatter(input, result)) } } func deidentifyString(projectID string , client *dlp.Client, infoTypes [] string , input string ) { var i []*dlppb.InfoType for _, it := range infoTypes { i = append (i, &dlppb.InfoType{Name: it}) } req := &dlppb.DeidentifyContentRequest{ Parent: "projects/" + projectID, InspectConfig: &dlppb.InspectConfig{ InfoTypes: i, }, DeidentifyConfig: &dlppb.DeidentifyConfig{ Transformation: &dlppb.DeidentifyConfig_InfoTypeTransformations{ InfoTypeTransformations: &dlppb.InfoTypeTransformations{ Transformations: []*dlppb.InfoTypeTransformations_InfoTypeTransformation{ { InfoTypes: []*dlppb.InfoType{&dlppb.InfoType{Name: "PHONE_NUMBER" }}, PrimitiveTransformation: &dlppb.PrimitiveTransformation{ Transformation: &dlppb.PrimitiveTransformation_CharacterMaskConfig{ CharacterMaskConfig: &dlppb.CharacterMaskConfig{ MaskingCharacter: "*" , NumberToMask: 9 , ReverseOrder: true , }, }, }, }, { InfoTypes: []*dlppb.InfoType{&dlppb.InfoType{Name: "EMAIL_ADDRESS" }}, PrimitiveTransformation: &dlppb.PrimitiveTransformation{ Transformation: &dlppb.PrimitiveTransformation_CryptoHashConfig{ CryptoHashConfig: &dlppb.CryptoHashConfig{ CryptoKey: &dlppb.CryptoKey{ Source: &dlppb.CryptoKey_Unwrapped{ Unwrapped: &dlppb.UnwrappedCryptoKey{ Key: [] byte ( "abcdefghijklmnop" ), }, }, }, }, }, }, }, }, }, }, }, Item: &dlppb.ContentItem{ DataItem: &dlppb.ContentItem_Value{ Value: input, }, }, } r, err := client.DeidentifyContent(context.Background(), req) if err != nil { log.Fatal(err) } fmt.Println(r.GetItem().GetValue()) } 最後に Google Cloud DLPがどのようなサービスで、なにができるのか伝わりましたでしょうか。 機械学習なので検出は100%とはいきませんが、潜在的なリスクを減らしたり、重大なセキュリティインシデントにすばやく気づくなど今まで見過ごされてきたような場所で活躍すると思います。 また、先日WebConsoleもベータで発表されて今後、さらに使いやすく、信頼性も高まっていくかなとも思います。 Cloud Console での Cloud DLP | Data Loss Prevention のドキュメント ぜひ利用してみてください! 近々Cloud DLPでBigQueryのテーブルを秘匿化して出力するちょっと実践に近い記事も書こうと思うのでそちらも見ていただければ嬉しいです! *1 : 一例を別記事に書いてありますので参考にしてください tech.smartcamp.co.jp