SUZURI デジタルコンテンツ Vertex AI BigQuery

SUZURIでのOJT、BBQ、その他 〜デジタルコンテンツと行動ログ、類似画像検索を添えて〜

SUZURI デジタルコンテンツ Vertex AI BigQuery

13期の@n01e0@yumuです。11月はSUZURI事業部で働いていました。

  1. SUZURIのデジタルコンテンツ
    1. 新着通知機能の追加
  2. ClamAVの誤検知対応
    1. 検知された原因の特定
    2. 誤検知されないようにする
  3. モバイルアプリのパーソナライズ
    1. 行動ログ収集システムの構築
    2. 類似画像検索機能の拡張
  4. BBQ

SUZURIのデジタルコンテンツ

SUZURIでは昨年よりデジタルコンテンツの取り扱いを開始しています。壁紙や3Dモデル、Podcastなど、データの販売をSUZURI上で行えます。

今回のOJTでは、このデジタルコンテンツ(以下デジコン)周りの機能改善をいくつか行いました。

新着通知機能の追加

SUZURIにはフォロー中のクリエイターが新しいグッズの販売を開始したときにメールで通知を行う機能があります。

しかし、デジコンの新作については通知に含まれていませんでした。

フォロー中のクリエイターがPodcastの新作を公開したことに気づくために、四六時中SNSのタイムラインに張り付いていた方もいらっしゃるんじゃないでしょうか。

今回、デジコンにも新着通知機能を実装することで、そんなみなさんの可処分時間を大幅に増やしています。

こうして余った時間でみなさんもぜひデジタルコンテンツを作成・販売してみてください。

ClamAVの誤検知対応

突然ですがみなさん、普段使っているツールがClamAVによって誤検知されてしまったことはありますか? 私はあります。

というわけで、ClamAVによってツールが誤検知された際の対応手順を書いていこうと思います。

直近であったわかりやすい事例として、mackerel-agentUnix.Malware.Reverseshell-10013098-0として検知されてしまったパターンを紹介します。

検知された原因の特定

まずはmackerel-agentが本当に我々の知っているmackerel-agentであることを確認しましょう。

公式によってリリースされたものか、そのリリースに不審なコミットは無いかを確認したら、次はなぜ検知されたのかを確認します。

ClamAVではfreshclamによって、定期的にルール(シグネチャ)の更新を行っています。

検知時にファイル名と一緒に表示される Unix.Malware.Reverseshell-10013098-0 がそのシグネチャの名前です。

このルールに基づいて検知が行われたということなので、それを確認してみましょう。

sigtoolを用いると、ClamAVのシグネチャを確認できます。

$ sigtool --find-sigs Unix.Malware.Reverseshell-10013098-0
[daily.ldb] Unix.Malware.Reverseshell-10013098-0;Engine:51-255,Target:6;0&1&2&3&4;6769746875622e636f6d2f6d6174746e2f676f2d636f6c6f7261626c652e4e6577436f6c6f7261626c65537464657272;6769746875622e636f6d2f6d6174746e2f676f2d6973617474792e49735465726d696e616c;6d6f72656275663d7b70633a6163636570742d656e636f64696e676163636570742d6c616e6775616765616476657274697365206572726f726170706c69636174696f6e2f7064666173796e63707265656d70746f666661767835313276706f70636e7464716261642063657274696669636174656261642073797374656d20;6164207265636f726420776974682070656e64696e67206170706c69636174696f6e2064617461746c733a206661696c656420746f2073656e6420636c6f73654e6f7469667920616c657274202862757420636f6e6e656374696f6e2077617320636c6f73656420616e79776179293a202577746c733a207365727665722063;706174682f66696c65706174682e566f6c756d654e616d65

こんな感じで、パッと見よくわからない文字列が出力されます。

が、うろたえずによく目を凝らして読んで見ると、

rule Unix_Malware_Reverseshell_10013098_0 {
    strings:
        $0 = { 67 69 74 68 75 62 2e 63 6f 6d 2f 6d 61 74 74 6e 2f 67 6f 2d 63 6f 6c 6f 72 61 62 6c 65 2e 4e 65 77 43 6f 6c 6f 72 61 62 6c 65 53 74 64 65 72 72 }
        $1 = { 67 69 74 68 75 62 2e 63 6f 6d 2f 6d 61 74 74 6e 2f 67 6f 2d 69 73 61 74 74 79 2e 49 73 54 65 72 6d 69 6e 61 6c }
        $2 = { 6d 6f 72 65 62 75 66 3d 7b 70 63 3a 61 63 63 65 70 74 2d 65 6e 63 6f 64 69 6e 67 61 63 63 65 70 74 2d 6c 61 6e 67 75 61 67 65 61 64 76 65 72 74 69 73 65 20 65 72 72 6f 72 61 70 70 6c 69 63 61 74 69 6f 6e 2f 70 64 66 61 73 79 6e 63 70 72 65 65 6d 70 74 6f 66 66 61 76 78 35 31 32 76 70 6f 70 63 6e 74 64 71 62 61 64 20 63 65 72 74 69 66 69 63 61 74 65 62 61 64 20 73 79 73 74 65 6d 20 }
        $3 = { 61 64 20 72 65 63 6f 72 64 20 77 69 74 68 20 70 65 6e 64 69 6e 67 20 61 70 70 6c 69 63 61 74 69 6f 6e 20 64 61 74 61 74 6c 73 3a 20 66 61 69 6c 65 64 20 74 6f 20 73 65 6e 64 20 63 6c 6f 73 65 4e 6f 74 69 66 79 20 61 6c 65 72 74 20 28 62 75 74 20 63 6f 6e 6e 65 63 74 69 6f 6e 20 77 61 73 20 63 6c 6f 73 65 64 20 61 6e 79 77 61 79 29 3a 20 25 77 74 6c 73 3a 20 73 65 72 76 65 72 20 63 }
        $4 = { 70 61 74 68 2f 66 69 6c 65 70 61 74 68 2e 56 6f 6c 75 6d 65 4e 61 6d 65 }
    condition:
        all of them
}

こんな感じのyaraルールに見えてきます。

そしてさらに、それぞれのバイト列がASCIIっぽいことに気づくと、

rule Unix_Malware_Reverseshell_10013098_0 {
    strings:
        $0 = "github.com/mattn/go-colorable.NewColorableStderr"
        $1 = "github.com/mattn/go-isatty.IsTerminal"
        $2 = "morebuf={pc:accept-encodingaccept-languageadvertise errorapplication/pdfasyncpreemptoffavx512vpopcntdqbad certificatebad system"
        $3 = "ad record with pending application datatls: failed to send closeNotify alert (but connection was closed anyway): %wtls: server c"
        $4 = "path/filepath.VolumeName"
    condition:
        all of them
}

こう見えてきますね。

もうここまでくれば原因もわかってきます。

特定のライブラリに依存しているGo製のバイナリが検知されているようですね。

もしReverseshellのルールとして、特定の接続先などが含まれていた場合は怪しいですが、今回はやはり誤検知でしょう。

誤検知されないようにする

ClamAVでは、誤検知(false positive)への対応手段として、

  1. パスを指定してスキャン対象から除外する
  2. hashを指定してFalse Positiveとして登録する

の2つの方法があります。

今回は確認が取れたものの、mackerel-agentが置き換えられた場合、パス単位で除外していると気づけないので、hashを登録します。

ここでもsigtoolを用いて

$ sigtool --sha1 /usr/bin/mackerel-agent > /var/lib/clamav/whitelist.fp

で登録できます。

あとはclamdを再起動するだけです。

また、誤検知の原因となったルールについてはClamAVに報告しましょう。

モバイルアプリのパーソナライズ

@yumuです。今月はSUZURIのモバイルアプリチームに参加し、日々のアクティブユーザー数(DAU)を高めるべく、モバイルアプリのパーソナライズプロジェクトに挑戦しました。

このプロジェクトでは、アプリ内の各画面で表示されるグッズをユーザごとにカスタマイズし、最適化することを目指しています。その最初のステップとして、複数の推薦アルゴリズムを効率的に比較検討できる基盤を構築しました。

行動ログ収集システムの構築

bigfoot-banditは、多腕バンディットアルゴリズムを駆使して自動A/Bテストを可能にするgemです。

今回はこのgemを使って複数の推薦アルゴリズムをA/Bテストしようと考えました。これまでSUZURIではこのgemをWebアプリで利用してきましたが、モバイルアプリへの導入は初の試みです。

bigfoot-banditを活用したA/BテストではBigQueryに蓄積されたユーザの行動ログが重要な評価指標となりますが、これまでモバイルアプリユーザの詳細な行動ログを収集する方法はありませんでした。

そこで今回、BigQueryに行動ログを送信できる新たなエンドポイントを開発しました。これにより、モバイルアプリやフロントエンドからも詳細な行動ログを収集できるようになり、推薦アルゴリズムの評価ができることはもちろん、ユーザの行動データの幅広い活用の可能性が広がりました。

類似画像検索機能の拡張

比較検討する推薦アルゴリズムを増やすために、類似画像検索機能の拡張を行いました。

SUZURIのWebアプリには閲覧中のTシャツ画像を基に似た画像のグッズを推薦する機能があります。今回はモバイルアプリにおいてTシャツ以外のアイテムに関してもパーソナライズを行いたいので、類似画像検索の対象範囲を広げることにしました。

この機能では、Vertex AI Vector Searchを活用しています。さらに多くのアイテムに対応させるためには、検索の基盤となるインデックスの更新が必要でした。

インデックスの更新は、元となる画像のURLを含むTSVの用意、画像のダウンロード、特徴量の抽出、インデックスの作成・デプロイの4段階に分けられます。

画像のダウンロードと特徴量の抽出には、Apache AirflowのDAG(Directed Acyclic Graph)とGoogle CloudのBatchを使用しています。TSVはこれまで手動で用意する必要がありましたが、今回DAGの中でTSVを自動生成できるようにしたので効率的にindexを更新できるようになりました。

さらに、類似画像検索を行う際にアイテムの種類による絞り込みを可能にするため、特徴量のTSVにアイテムを判別できるitem=t-shirtのような列を追加しました。これにより、以下のように検索時のクエリでアイテムを指定でき、より実用的な検索が可能になりました。

connection = Faraday.new(
  url: "https://us-central1-aiplatform.googleapis.com",
  headers: {'Content-Type' => 'application/json'},
)
response = connection.post("v1beta1/projects/#{@project_id}/locations/us-central1/indexEndpoints/#{ENDPOINT_ID}:findNeighbors") do |req|
  req.headers['Authorization'] = "Bearer #{ACCESS_TOKEN}"
  query = {
    datapoint: {
      datapointId: DATAPOINT_ID,
      restricts: [{'namespace': 'item', 'allow_list': ['t-shirt']}]
    },
    neighbor_count: 10
  }
  req.body = {
    deployed_index_id: DEPLOYED_INDEX_ID,
    queries: [query]
  }.to_json
end

現在はインデックスの更新を都度手動で行っていますが、今後は定期的な差分更新を導入し、毎日追加される新しいグッズも類似画像検索の範囲に含める計画です。

BBQ

ここで少し趣向を変えて、私たちのチームビルディング活動について触れたいと思います。最近、11/11(土)に、ペパボでBBQイベントが開催されました。会場は有明の開放的な空間で、肉や海鮮、焼きおにぎりといった美味しい食べ物を楽しむ時間でした。

技術の世界は常に前進していますが、時にはキーボードを離れてリラックスする時間も必要です。このイベントは、チームメンバー間の絆を深める絶好の機会となり、新しいアイデアやインスピレーションを刺激する、楽しく有意義な時間となりました!