kkamegawa's weblog

Visual Studio,TFS,ALM,VSTS,DevOps関係のことについていろいろと書いていきます。Google Analyticsで解析を行っています

Logic AppsでAzure DevOpsの監査ログをCosmos DBへ保存する

はじめに

Azure DevOpsには監査機能が入っており、https://dev.azure.com/{orgnazation}/_settings/audit というURLでアクセスできます。ただし、この監査ログは90日分しか保持されておらず、それ以上の期間保存したい場合、別途APIアクセスでJSONやCSVに抜き出す必要があります。

docs.microsoft.com

毎日取得するのも面倒なので、LogicAppsを使って自動化してみましょう。

Token取得

f:id:kkamegawa:20190901092840p:plain

最初にPATを作成します。今回の場合、監査ログだけ取れればいいので、Read Audit Logの権限だけあれば問題ありません。

Cosmos DB

f:id:kkamegawa:20190901092847p:plain

次はCosmos DBのコンテナーを作ります。Cosmos DBの作り方については解説しません。ここではSQLを使ってサーバーを作っています。

PartitionKeyは何を対象にするかで変えてください。Cosmos DBの課金に結構影響します。今回の場合、ユーザー単位で監査するという仕様でactorUserIdをPartition Keyにしています。

操作単位で監査したいのであれば、actionIdになるでしょうか。ipAddressだとnullにあることがあるようなので、お勧めできません。

Logic Apps

全体フロー

f:id:kkamegawa:20190901092851p:plain

だいたいこんな感じで作りました。細かいステップを解説します。

トリガー

f:id:kkamegawa:20190901092855p:plain

タイマートリガーで毎日一度実行させます。UTCで0時0分(日本時間の朝9時)に一日一度実行します。

(9/3追記)
0:00としても、正確に0:00(UTC)に動くとは限らないようです。1秒前に実行されるとかおきましたので、少なくとも数分後に動かすようにしたほうがいいですね。

開始終了時刻

f:id:kkamegawa:20190901092800p:plain

REST APIに渡す始点、終点の日付部分を作ります。

formatDaterTime(addDays(utcNow(), -1), 'yyyy-MM-dd')

f:id:kkamegawa:20190901092804p:plain

開始時刻をstartTime,終了をendTimeとして作ります。

開始時刻

concat(outputs('日付作成'), 'T00:00:00Z')

終了時刻

concat(outputs('日付作成'), 'T23:59:59Z')

PAT 作成

f:id:kkamegawa:20190901092837p:plain

Azure DevOpsで作ったPATをbase64ToString()で変換します。

HTTP呼び出し

f:id:kkamegawa:20190901092809p:plain

Azure DevOps HTTP呼び出しというコネクターもあるのですが、こちらのほうがわかりやすいので、標準のHTTPコネクターを使います。

docs.microsoft.com

REST APIのドキュメント通りですが。

  • ヘッダー: Authentication : Basic Base64エンコードしたPAT
  • api-version: 5.1-preview.1
  • skipAggregation: false
  • startTime : outputs('startTime')
  • endTime : outputs('endTime')
  • 認証:なし

JSONの解析

f:id:kkamegawa:20190901092814p:plain

HTTPの出力結果を使うためのJSON Schemaを定義します。APIのページにあるサンプルレスポンスを使ってサンプルペイロードからJSON Schemaを作ればいいです。

(9/5追記)
と思っていたのですが、ipAddressがnullになることがある場合の失敗と、Azure DevOps自身が定期的に実行するジョブの監査ログでsummryがnullになることが多かったので、こんな感じにしました。

{
  "type": "object",
  "properties": {
    "continuationToken": {
      "type": "string"
    },
    "decoratedAuditLogEntries": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "actionId": {
            "type": "string"
          },
          "activityId": {
            "type": "string"
          },
          "actorCUID": {
            "type": "string"
          },
          "actorDisplayName": {
            "type": "string"
          },
          "actorImageUrl": {},
          "actorUserId": {
            "type": "string"
          },
          "area": {
            "type": "string"
          },
          "authenticationMechanism": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "categoryDisplayName": {
            "type": "string"
          },
          "correlationId": {
            "type": "string"
          },
          "data": {
            "type": "object"
          },
          "details": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "ipAddress": {},
          "projectId": {
            "type": "string"
          },
          "projectName": {},
          "scopeDisplayName": {
            "type": "string"
          },
          "scopeId": {
            "type": "string"
          },
          "scopeType": {
            "type": "string"
          },
          "timestamp": {
            "type": "string"
          },
          "userAgent": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "correlationId",
          "activityId",
          "actorCUID",
          "actorUserId",
          "authenticationMechanism",
          "timestamp",
          "scopeType",
          "scopeDisplayName",
          "scopeId",
          "projectId",
          "projectName",
          "ipAddress",
          "userAgent",
          "actionId",
          "data",
          "details",
          "area",
          "category",
          "categoryDisplayName",
          "actorDisplayName",
          "actorImageUrl"
        ]
      }
    },
    "hasMore": {
      "type": "boolean"
    }
  }
}

レコード判定

f:id:kkamegawa:20190901092818p:plain

組織で使うときはたぶんないとは思いますが、日付によっては監査ログがない場合もあります。

empty(body('JSON_の解析'))

これでレコードがある場合のみCosmos DBへの登録処理を呼び出します。

Cosmos DBへの登録

f:id:kkamegawa:20190901092824p:plain

For each制御を使って、レコード単位に登録します。JSONのdecoratedAuditLogEntriesがログエントリーなので、この配列の数だけループさせます。

Logic AppsでデータベースIDやコレクションIDが参照できない場合、Cosmos DBのファイアウォールでAzureデータセンター内からのアクセスを受け入れるを設定する必要があります。

docs.microsoft.com

ドキュメントは現在のアイテム、Partition Keyはユーザー単位で作ることにするので、actorUserIdを指定します。一番はまったのですが、Partition Keyには明示的にダブルクォーテーションを付けないと、Invalid Partition Keyといわれてエラーになります。

Cosmos DBで作ったPartition Keyと一致させればあとはCosmos DBが勝手に入れてくれます。

f:id:kkamegawa:20190901092832p:plain

実行すると、こんな感じでCosmos DBに格納されます。あとは適度に検索してください。

自動削除

監査ログなので、不要になったら消すということもあるかと思います。コンテナーに明示的にTTLを設定すればいいと思いますが、単位が秒なので、一年なら31536000ですかね。

docs.microsoft.com

まとめ

結構苦労しましたが、Logic Apps使ってノンコーディングで監査ログのエクスポートができるようになりました。より高度な監査としては、特定のログが出たらアラートメールを送るようにAzure Functionsをかませてみるとかも面白いと思います。