📌

GitHub 組織の監査ログをAPI経由で取得し、Azure Blob Storageへ格納する

2025/01/15に公開

はじめに

DevOps エンジニアの皆さん、GitHub の監査ログ (Audit Log) を活用していますでしょうか。
組織(Organization)での操作履歴を追跡できる監査ログは、セキュリティやコンプライアンスの観点から非常に重要なものとなります。しかしながら、標準では90日程度しか保持されないため、長期的なログ保管が必要な場合は、自分たちで外部にエクスポートしておく必要があります。

本記事では、C#でGitHub監査ログをAPI経由で取得し、Azure Blob Storage へ格納する方法を紹介します。
クラウドストレージに保管しておけば、必要に応じて長期保存したり、後から分析に活用できます。


GitHubの監査ログとは?

監査ログの概要

GitHub 監査ログは、組織(Org)内でのさまざまな操作履歴を記録する仕組みです。

  • リポジトリの作成や削除
  • メンバーの追加・削除
  • 組織への招待
  • SAML SSO の連携
  • トークンの承認/取り消し (PAT, OAuth Apps など)
    など多岐にわたるアクションが記録されます。

保持期間に注意

GitHub.com(クラウド版)の標準的なプランでは、過去 90日間 分のみ監査ログが閲覧・取得できます。
それより古いものは GitHub 上では消えてしまいます。
GitHub Enterprise Cloud で「Advanced Security」が有効な場合は、さらに長期 (1年程度) 取得可能な場合もありますが、契約プランによります。
いずれにせよ、監査ログを自分で定期的に取得して外部に保管しておくのがおすすめです。


監査ログを取得する際の注意点

必要な権限

監査ログは、Organization のオーナー権限 を持っているユーザーが取得できます。
メンバーではなく、オーナー (Owner) であることが必須です。

SAML SSOを利用している場合 (Authorize が必要)

組織でSAML SSO が有効化されている場合、PAT (Personal Access Token) を作成するだけでは 403 Forbidden になることがあります。
PATをSAMLに関連付け (Authorize) する手順を踏まないと、Organizationのデータにアクセスできません。
GitHubの設定画面(Personal settings → Developer Settings → Tokens(Classic) Personal access tokens)から、対象のOrgへのアクセスを「Authorize」してください。

PAT の必要なスコープ

監査ログを取得するには、以下のスコープが必要です (Classic PATの場合)

  • audit_log
  • admin:org
    (場合によっては repo なども必要になることがありますが、監査ログ取得には最低限 audit_log + admin:org を付与)

※ 2023年以降では、audit_log スコープは Fine-grained token でも同様に設定可能です。
Enterpriseの監査ログを取得する場合は admin:enterprise が必要になるなど、対象の種類に応じて適切なスコープを設定してください。


C#でのサンプルコード

前提

  • .NET 6 以上(もしくは .NET Core 3.1 以上)を利用している想定。
  • GitHubの監査ログ (Audit Log) API を呼び出せるトークンを用意している
  • Azure Storage アカウントおよびコンテナが作成済み。
  • NuGetパッケージ
    • Azure.Storage.Blobs (Azure Blob Storage へのアクセス用)
    • System.Net.Http.Json (JSON取得をシンプルに行う場合に便利) – オプション
    • 上記以外にも、Newtonsoft.Json を使う方法など用途に応じて変更してください。

ポイントまとめ

GitHub Audit Log 取得

  • エンドポイント: GET /orgs/{org}/audit-log
  • 認証: Personal Access Token (PAT) を使用
  • GitHub APIを叩く際のUser-Agentは必要 (GitHubのAPIポリシー上)
  • Azure Blob Storage へのアップロード

Azure.Storage.Blobs パッケージを利用

  • Audit LogをJSON文字列などの形でそのままアップロード
  • 毎回上書きするのか追記するのか運用設計による

ここでは簡易例として、1ページ分(最大100件) のデータのみ取得するコードを紹介します。
監査ログが100件を超える場合はページング処理が必要ですが、今回は一旦サンプルとしてご覧ください。

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
// using System.Net.Http.Json; // JSONで取得したい場合に使うと便利
using Azure.Storage.Blobs;

namespace GitHubAuditLogToBlob
{
    class Program
    {
        // 例: 環境変数やセキュアな方法で取得することを推奨
        private static readonly string githubToken = "<YOUR_GITHUB_TOKEN>";
        private static readonly string organizationName = "<YOUR_ORG_NAME>";

        // Azure Blob Storage の接続文字列
        private static readonly string azureStorageConnectionString = "<YOUR_AZURE_BLOB_CONNECTION_STRING>";
        // 保存先のコンテナ名
        private static readonly string containerName = "<YOUR_CONTAINER_NAME>";
        // アップロード時のBlob名(ファイル名)
        private static readonly string blobName = "auditlog.json";

        static async Task Main(string[] args)
        {
            try
            {
                // 1. GitHubから監査ログを取得
                string auditLogJson = await GetGitHubAuditLogAsync(organizationName);

                // 2. Azure Blob Storageにアップロード
                await UploadToAzureBlobAsync(auditLogJson);

                Console.WriteLine("処理が完了しました。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"エラーが発生しました: {ex.Message}");
            }
        }

        /// <summary>
        /// GitHub APIを利用して監査ログを取得するメソッド
        /// </summary>
        /// <param name="orgName">監査ログを取得したいOrganization名</param>
        /// <returns>取得したログ(JSON形式)</returns>
        private static async Task<string> GetGitHubAuditLogAsync(string orgName)
        {
            // 例: per_pageや後続ページの対応などは運用次第で調整
            string url = $"https://api.github.com/orgs/{orgName}/audit-log?per_page=100";

            using (HttpClient client = new HttpClient())
            {
                // 必須ヘッダー設定 (User-Agentが必要)
                client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNetApp", "1.0"));
                // 認証ヘッダー (token 認証)
                client.DefaultRequestHeaders.Authorization 
                    = new AuthenticationHeaderValue("token", githubToken);

                HttpResponseMessage response = await client.GetAsync(url);
                response.EnsureSuccessStatusCode();

                // JSON文字列をそのまま取得
                string resultJson = await response.Content.ReadAsStringAsync();
                return resultJson;
            }
        }

        /// <summary>
        /// Azure Blob Storage にJSON文字列をアップロードするメソッド
        /// </summary>
        /// <param name="jsonData">アップロードしたいJSON文字列</param>
        private static async Task UploadToAzureBlobAsync(string jsonData)
        {
            // BlobServiceClientの生成
            BlobServiceClient blobServiceClient = new BlobServiceClient(azureStorageConnectionString);
            // コンテナへの参照取得
            BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName);

            // コンテナが存在しなければ作成
            await containerClient.CreateIfNotExistsAsync();

            // アップロード先のBlobを指定
            BlobClient blobClient = containerClient.GetBlobClient(blobName);

            // 文字列をストリームに変換
            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonData)))
            {
                // 既に存在する場合は上書き
                await blobClient.UploadAsync(ms, overwrite: true);
            }
        }
    }
}

簡単な解説

  1. GetGitHubAuditLogAsync

    • GET https://api.github.com/orgs/{org}/audit-log へリクエストし、JSON文字列として受け取っています。
    • User-Agent ヘッダと、トークン認証ヘッダ (Authorization) を設定しているのがポイントです。
    • per_page=100 と指定しているため、1回のリクエストで最大100件の監査ログを取得可能。
  2. UploadToAzureBlobAsync

    • 受け取った JSON 文字列を MemoryStream に変換し、Azure Blob Storage へアップロードしています。
    • createIfNotExistsAsync でコンテナが未作成なら自動で作成し、同名ファイルがあれば overwrite: true で上書きします。

ページングの考慮について

監査ログが多い場合は100件では足りないかもしれません。
その場合はLinkヘッダーpage=2 以降のパラメータを利用して複数ページにわたり取得する必要があります。
ページング方法については、別途対応が必要なのでご注意ください。


監査ログレスポンスの例と解説

実際に取得した監査ログは、JSON配列の形で返却されます。
以下のような例を見てみましょう:

[
    {
        "@timestamp": 1735651879982,
        "_document_id": "3HPzlqPgM2tOlSFSWo4fEQ",
        "action": "org_credential_authorization.grant",
        "actor": "yutaka-art",
        "actor_id": 53134237,
        "actor_is_bot": false,
        "actor_location": {
            "country_code": "JP"
        },
        "business": "antemode",
        "business_id": 480,
        "created_at": 1735651879982,
        "external_identity_nameid": "yutaka.osada@antemode.holy.jp",
        "external_identity_username": null,
        "hashed_token": "tX6QliOpIF58i7cAHp8tlz9189NYCRU6U/0EwK0Z3YQ=",
        "oauth_credential_type": "Personal Access Token",
        "operation_type": "create",
        "org": "Ante-Mode",
        "org_id": 135647888,
        "request_access_security_header": null,
        "token_id": 2045063237,
        "token_scopes": "admin:enterprise,admin:org,audit_log,public_repo,read:user",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    },
    {
        "@timestamp": 1735563771186,
        "_document_id": "59xMfpCSvG76ilFIA9aVKg",
        "action": "org_credential_authorization.deauthorize",
        "actor": "hogeoge",
        "actor_id": 154479788,
        "actor_is_bot": false,
        "business": "antemode",
        "business_id": 480,
        "created_at": 1735563771186,
        "external_identity_nameid": "hoge.oge@antemode.holy.jp",
        "hashed_token": null,
        "operation_type": "remove",
        "org": "Ante-Mode",
        "org_id": 135647888,
        "token_id": 1438587423,
        "token_scopes": null
    }
]

それぞれの項目が、いつ・誰が・どんな操作をしたかを表しています。

  • @timestamp / created_at: イベントが発生した時刻 (UNIXミリ秒)
  • action: 行われた操作の種類。たとえば
    • org_credential_authorization.grant: パーソナルアクセストークン (PAT) がOrgで使用できるように承認された
    • org_credential_authorization.deauthorize: PATが無効化された (Org から取り消された)
  • actor: その操作を行ったGitHubユーザー名
  • actor_id: GitHubユーザーのID (数値)
  • business: Enterprise アカウント名 (エンタープライズ管理下のOrgなら含まれる)
  • org: どのOrganizationか (例: Ante-Mode)
  • hashed_token: トークン自体はわからないようにハッシュ化
  • token_id: トークンを特定するID (数字)
  • token_scopes: PATに割り当てられたスコープ一覧
  • external_identity_nameid: SAML SSOを利用している場合、外部IDP上でのユーザーIDやメールアドレスが記載される

このように、誰がいつ PAT を承認/解除したのかなど、詳細な履歴を確認できます。


まとめ

  • GitHub Organization の監査ログは、デフォルトで 過去90日間 しか保持されません。
  • セキュリティやコンプライアンスの観点から、必要があれば定期的にAPI経由で取得し、外部ストレージに保管するのがおすすめです。
  • 監査ログAPIを利用するには、OrgのOwner である必要があり、PATに audit_log + admin:org など適切なスコープを付与する必要があります。
  • SAML SSOを利用している場合は、発行したトークンをSAMLに関連付け(Authorize) しなければ 403 Forbidden が返る点に注意しましょう。

本記事で紹介したコードは、1ページのみのシンプルサンプルです。
複数ページのログをすべて取得したい場合は、リンクヘッダーを辿るページング処理を追加実装してください。
監査ログをしっかりエクスポートし、長期保管や分析に役立てましょう!


おわりに

DevOpsの現場では、ログ収集や監査証跡の可視化がますます重要になっています。
GitHub 監査ログの活用で、自社の開発体制における変更やアクセス状況を把握し、セキュリティとコンプライアンスをより強固にしていきましょう。

参考になりましたら、幸いです。
何か質問があれば、コメントなどでお気軽にどうぞ。

GitHubで編集を提案

Discussion