概要

項目内容
案件IDMAS-206
カテゴリ基盤・セキュリティ
PhaseP1
優先度★★★
所要時間2〜3時間
対象ファイル800_ops/811_audit_checker.js(新規)、appsscript.json(oauthScopes・advancedServices 追記)
前提案件MAS-179(監査証跡の強化・98_audit_log の存在)、MAS-205(特権アカウント分離)
関連案件MAS-205(MFA義務化)、MAS-213(監査ログ保護)

目的

スプレッドシート・ドライブファイルの意図しない組織外共有を検知・通知し、情報漏洩リスクを低減する。Google Workspace 管理コンソールによるポリシー設定(コンポーネント1)と、GAS バッチによる Admin SDK 経由の監査イベント監視(コンポーネント2)を組み合わせ、財務データの不正外部共有をリアルタイムに近い形で検知する体制を構築する。

アーキテクチャ概要

本案件は 2 コンポーネント構成で実装する。

#コンポーネント実装方式担当者
1Google Workspace 管理コンソール設定管理者による一回限りの手動設定(手順書として本仕様書に記述)Workspace 管理者
2GAS 時間トリガー(日次)監査バッチ800_ops/811_audit_checker.js を新規作成し、Admin SDK Reports API で Drive の acl_change イベントを検知・通知GAS 開発者

データフロー(コンポーネント2):

GAS 時間トリガー(日次)
  → checkExternalFileSharing_()
    → PropertiesService から前回チェックタイムスタンプ取得
    → AdminReports.Activities.list() で acl_change イベント取得(ページネーション対応)
    → ホワイトリスト(03_sys_params の SHARING_WHITELIST)と照合
    → 非該当の外部共有を検知リストに追加
    → 検知あり → MailApp.sendEmail() で CFG_ADMIN_EMAIL 宛に通知
    → Utils.auditLog() でバッチ実行を監査証跡に記録
    → PropertiesService にタイムスタンプ更新

修正方針

コンポーネント1: Google Workspace 管理コンソール設定(手動・手順書化)

GAS コードの変更は不要。 Google Workspace 管理者が管理コンソールで設定する運用手順。

設定手順

  1. Google 管理コンソール(admin.google.com)にスーパー管理者でサインイン
  2. アプリGoogle Workspaceドライブとドキュメント を開く
  3. 共有設定 を選択
  4. 組織外との共有 セクションを確認し、以下のいずれかを設定する:
設定値動作推奨度
オフ(完全禁止)組織外のユーザーへの共有操作自体をブロック◎ セキュリティ最優先
オン(警告あり)共有は可能だが、共有前に確認ダイアログを表示○ 外部パートナーへの共有が必要な場合
オン(警告なし)制限なし(現状のデフォルト)× 本案件の目的に反する

どちらの設定を選択するかは「人間が検討すべき事項」に委ねる。外部税理士・会計士への共有を行う場合は「オン(警告あり)」を選択し、コンポーネント2 のホワイトリストで検知除外する方式が現実的。

  1. 対象の組織部門(OU)を選択し、保存 をクリック

推奨設定(ドライブ共有オプション)

項目推奨値備考
組織外との共有オン(警告あり)または オフ運用方針次第
リンクを知っている人への共有(組織外)無効財務データの匿名共有を防止
外部ドメインへのファイル移動無効データの外部流出防止

コンポーネント2: GAS 監査ログ定期チェックバッチ(自動・新規実装)

新規ファイル: 800_ops/811_audit_checker.js

関数名: checkExternalFileSharing_()

設定キー(03_sys_params に追加)

キー名用途
SHARING_WHITELIST文字列(カンマ区切り)tax-accountant.com,[email protected]許可ドメイン/メールアドレスのリスト
CFG_ADMIN_EMAIL文字列[email protected]通知先管理者メールアドレス

Constants.getParam('SHARING_WHITELIST', '') で取得(03_sys_params シートを参照。000_infra/002_constants.js 行 147〜167)。

実装ロジック

/**
 * N-30: Drive 外部共有の定期チェックバッチ
 * Admin SDK Reports API で acl_change イベントを取得し、
 * ホワイトリスト外の外部共有を検知して管理者にメール通知する。
 */
function checkExternalFileSharing_() {
  // 1. 多重起動防止
  var lock = LockService.getScriptLock();
  if (!lock.tryLock(30000)) {
    Utils.logInfo('checkExternalFileSharing_', '別のインスタンスが実行中のためスキップ');
    return;
  }

  try {
    var FUNC = 'checkExternalFileSharing_';

    // 2. ホワイトリスト・通知先取得
    var whitelistRaw = Constants.getParam('SHARING_WHITELIST', '');
    var whitelist = whitelistRaw
      ? whitelistRaw.split(',').map(function(s) { return s.trim().toLowerCase(); })
      : [];
    if (!whitelistRaw) Utils.logInfo(FUNC, 'SHARING_WHITELIST 未設定: 全外部共有を通知対象とします');

    var adminEmail = Constants.getParam('CFG_ADMIN_EMAIL', '');
    if (!adminEmail) {
      Utils.logError(FUNC, new Error('CFG_ADMIN_EMAIL が未設定。メール通知をスキップします'), '03_sys_params を確認してください');
      Utils.auditLog('RUN', '', '', '', FUNC, null, null, 'CFG_ADMIN_EMAIL 未設定によりスキップ');
      return;
    }

    // 3. 前回チェックタイムスタンプ取得(オペレーショナルな状態管理のため PropertiesService 直接使用を許容)
    var props = PropertiesService.getScriptProperties();
    var lastCheckedAt = props.getProperty('N30_LAST_CHECKED_AT');
    var startTime;
    if (lastCheckedAt) {
      startTime = lastCheckedAt;
    } else {
      // 初回実行時は 24 時間前から
      var since = new Date();
      since.setHours(since.getHours() - 24);
      startTime = since.toISOString();
      Utils.logInfo(FUNC, '初回実行: 過去24時間分を処理対象とします');
    }

    // 4. Admin SDK でイベント取得(ページネーション対応)
    var violations = [];
    var pageToken = null;
    do {
      var params = {
        eventName: 'acl_change',
        startTime: startTime,
        maxResults: 1000
      };
      if (pageToken) params.pageToken = pageToken;

      var response = AdminReports.Activities.list('all', 'drive', params);
      var activities = response.items || [];

      activities.forEach(function(activity) {
        var actor = (activity.actor && activity.actor.email) || '';
        // 共有者が組織外の場合はスキップ(組織外アカウントの行動は管理コンソール側で制御)
        if (!actor || actor.indexOf('@') < 0) return;

        var events = activity.events || [];
        events.forEach(function(event) {
          var params_ = event.parameters || [];
          var targetUser = '';
          var targetDomain = '';
          params_.forEach(function(p) {
            if (p.name === 'target_user') targetUser = (p.value || '').toLowerCase();
            if (p.name === 'target_domain') targetDomain = (p.value || '').toLowerCase();
          });

          // ホワイトリスト照合
          var isWhitelisted = whitelist.some(function(w) {
            return targetUser.endsWith('@' + w) || targetUser === w || targetDomain === w;
          });

          // 組織外(ドメイン付きかつホワイトリスト外)のみ検知
          var isExternal = targetUser && targetUser.indexOf('@') >= 0;
          if (isExternal && !isWhitelisted) {
            violations.push({
              actor: actor,
              targetUser: targetUser || targetDomain,
              fileId: activity.id && activity.id.uniqueQualifier ? activity.id.uniqueQualifier : '',
              time: activity.id && activity.id.time ? activity.id.time : ''
            });
          }
        });
      });

      pageToken = response.nextPageToken || null;
    } while (pageToken);

    // 5. 検知リストがあればメール通知
    if (violations.length > 0) {
      var bodyLines = ['Drive 外部共有が検知されました(ホワイトリスト外):\n'];
      violations.forEach(function(v, i) {
        bodyLines.push((i + 1) + '. 共有者: ' + v.actor + ' / 共有先: ' + v.targetUser + ' / ファイルID: ' + v.fileId + ' / 日時: ' + v.time);
      });
      bodyLines.push('\n03_sys_params の SHARING_WHITELIST を確認・更新してください。');

      MailApp.sendEmail({
        to: adminEmail,
        subject: '[N-30 アラート] Drive 外部共有検知: ' + violations.length + '件',
        body: bodyLines.join('\n')
      });
      Utils.logInfo(FUNC, 'メール通知送信: ' + violations.length + '件の外部共有を検知');
    } else {
      Utils.logInfo(FUNC, 'ホワイトリスト外の外部共有は検知されませんでした');
    }

    // 6. 監査証跡
    Utils.auditLog('RUN', '', '', '', FUNC, null, null, '検知: ' + violations.length + '件');

    // 7. タイムスタンプ更新(オペレーショナルな状態管理のため PropertiesService 直接使用を許容)
    props.setProperty('N30_LAST_CHECKED_AT', new Date().toISOString());

  } catch (e) {
    Utils.logError('checkExternalFileSharing_', e, 'Admin SDK 呼び出しまたはメール送信で例外が発生');
  } finally {
    lock.releaseLock();
  }
}

appsscript.json の更新

oauthScopes 配列に以下を追記:

"https://www.googleapis.com/auth/admin.reports.audit.readonly"

advancedServices 配列に以下を追記:

{
  "userSymbol": "AdminReports",
  "version": "reports_v1",
  "serviceId": "admin"
}

重要: clasp push だけでは Advanced Services は有効化されない。GAS エディタで「サービス」→「Admin SDK API」を手動で有効化する必要がある(詳細は「動作確認」手順を参照)。

時間トリガーの登録

checkExternalFileSharing_ は末尾 _ でプライベート関数のため、GAS エディタのトリガー UI から直接選択できない。代わりに プログラム経由で登録するインストール関数 を使用する (MAS-201 installBackupTriggers と同パターン)。

登録手順:

  1. GAS エディタ (prod 環境) を開く
  2. 関数プルダウンから installAuditCheckerTrigger を選択
  3. 実行ボタンをクリック → 初回は権限承認ダイアログに同意
  4. 成功アラート「✅ MAS-206 外部共有監視の日次トリガーを登録しました。...新規 1 件を登録。- 日次: AM6時 (JST)」を確認

解除手順:

  • uninstallAuditCheckerTrigger を実行すれば日次トリガーを一括削除できる

注意:

  • dev 環境で installAuditCheckerTrigger を実行すると、外部共有イベントがほぼ発生しないため通知ノイズ回避のためトリガー登録を スキップ する仕様 (アラートのみ表示)。本番運用は prod のみ
  • isPrivilegedUser_() による権限チェック付き。非特権ユーザーが実行すると拒否される (MAS-205 Part 2 と連動)

影響範囲

対象ファイル変更量既存動作への影響
新規ファイル作成800_ops/811_audit_checker.js約 80 行追加なし(新規追加のみ)
appsscript.jsonoauthScopes + advancedServices 追記2〜3 行追加既存スコープへの影響なし
03_sys_paramsSHARING_WHITELIST / CFG_ADMIN_EMAIL 行追加手動追記 2 行既存キーへの影響なし

注意事項

  1. Constants.getParam03_sys_params シートを参照する。 設定キー(SHARING_WHITELIST, CFG_ADMIN_EMAIL)は 03_sys_params シートに追加すること(01_sys_config ではない)。
  2. Admin SDK Reports API は Google Workspace Business/Enterprise プランが必要。 Google Workspace Essentials や Google Workspace Frontline では利用不可。事前にプランを確認すること。
  3. PropertiesService.getScriptProperties() はオペレーショナルな状態管理(タイムスタンプ保存)に限り直接使用する。 環境設定値(スプレッドシートID・API キー等)の読み取りには引き続き Env モジュールを使用すること(CLAUDE.md コーディング規約に準拠)。
  4. appsscript.json への advancedServices 追記と GAS エディタでの Admin SDK 手動有効化が必須。 clasp push だけでは Advanced Services は有効化されない。有効化を忘れると AdminReports is not defined エラーが発生する。
  5. Admin SDK を使用する実行アカウントは Google Workspace 管理者権限が必要。 時間トリガーの実行アカウントが管理者でない場合、AdminReports.Activities.list() が権限エラーをスローする。トリガーを管理者アカウントで設定すること。

エッジケース

条件動作理由
Admin SDK 権限不足(実行アカウントが管理者でない)try...catch で捕捉し Utils.logError でスタックトレースを記録。MailApp でのエラー通知はスキップ(通知先も不明のため)管理者権限なしでは AdminReports.Activities.list() が例外をスロー
Activities.list() の結果が複数ページにわたるnextPageToken が返る限りループし全件取得してから処理する1 回の API 呼び出しは最大 1000 件。大量イベント時はページネーション必須
SHARING_WHITELIST03_sys_params に未設定または空文字ホワイトリストなし(全外部共有を通知対象)として動作。Utils.logInfo でその旨を記録フォールバックを「全件通知」にすることで見落としを防ぐ(保守的設計)
CFG_ADMIN_EMAIL が未設定または空文字メール送信をスキップし Utils.logError に「通知先メールアドレス未設定」を記録。Utils.auditLog にスキップ理由を記録して終了送信先不明でメール送信するとエラーになるため
前回チェックタイムスタンプ(N30_LAST_CHECKED_AT)が未設定(初回実行)過去 24 時間分のイベントを処理対象とする初回実行時に全履歴を処理すると件数過多になるためウォームアップ期間を設ける
共有者が組織内ユーザーでない(外部ユーザーが共有)ホワイトリスト判定対象外としてスキップ組織外アカウントの行動は Workspace 管理コンソール側で制御すべき範囲
03_sys_params シートが存在しないConstants.getParam() の try-catch(行 150〜163)で握りつぶされ ''(defaultVal)を返すホワイトリストは全件通知モード、通知先未設定でスキップ発動
LockService の取得失敗(30 秒以内に取得できない場合)Utils.logInfo でスキップを記録して早期 return既存インスタンスが実行中の場合、重複起動は不要

実データ検証

実装前に以下を MCP または手動で確認すること。

確認項目確認方法期待値
03_sys_params シートの行構造シートを直接確認A列: キー名、B列: 値(1行目はヘッダー)
SHARING_WHITELIST キーの存在A列を目視確認存在しない場合は手動追加が必要(値: カンマ区切りドメイン/メールアドレス)
CFG_ADMIN_EMAIL キーの存在A列を目視確認存在しない場合は手動追加が必要(値: 管理者のメールアドレス)
Admin SDK Advanced Services の有効化状態GAS エディタ → サービス一覧Admin SDK API が有効化済みであること
実行アカウントの Workspace 管理者権限Google 管理コンソールで確認トリガー実行アカウントが管理者ロールを持つこと
98_audit_log シートの存在タブ一覧を確認存在しない場合は setupAllSchemas を実行して作成

関連ドキュメント

ドキュメント関連箇所
MAS-179 監査証跡の強化98_audit_logDDL 定義・Utils.auditLog() の仕様
MAS-201 スプレッドシート定期バックアップ時間トリガー登録パターンの参考(installBackupTriggers
MAS-205 MFA義務化と特権アカウント分離同時期に実施すべきセキュリティ施策。PRIVILEGED_USERS 管理パターンの参考
MAS-213 監査ログ保護98_audit_log の編集権限保護。MAS-206 バッチが記録した監査証跡の改ざん防止
CLAUDE.mdPropertiesService 直接呼び出し制限・Env モジュール経由ルール

人間が検討すべき事項

以下は Workspace 管理者(PO 兼務可)が意思決定する項目。

項目内容判断が必要な理由
ドメイン外共有の運用方針管理コンソールで「完全禁止(オフ)」か「警告のみ(オン+確認ダイアログ)」かを決定する外部税理士・会計士への共有を行う場合は「警告あり」を選択し、コンポーネント2 のホワイトリストで管理する方式が現実的。「完全禁止」を選択した場合、コンポーネント2 は補完的な監査ログの役割になる
外部パートナーへの共有例外ルールホワイトリスト(SHARING_WHITELIST)に登録するドメイン/メールアドレスの洗い出し現在 Drive ファイルを共有している外部関係者(税理士・会計士・外部顧問等)のドメインまたはメールアドレスをリストアップする必要がある
バッチ実行頻度日次か週次か、実行時刻の決定日次の場合は業務開始前(例: 午前 6 時〜7 時)が望ましい。週次でも十分な場合は GAS 実行上限への影響を軽減できる
通知先管理者メールアドレスCFG_ADMIN_EMAIL に設定する値の決定検知通知を受け取る担当者を明示する必要がある。複数名に通知したい場合は MailApp.sendEmailcc フィールドで対応可能
Admin SDK 利用に必要な Workspace プランの確認Google Workspace Business または Enterprise プランが必要Essentials や Frontline プランでは Admin SDK Reports API が利用不可のため、実装前にプランを確認する
GAS 実行アカウントの管理者権限付与トリガーを実行するアカウントに Workspace 管理者権限を付与するか権限付与が難しい場合は、Admin SDK 経由の監査ではなく Drive API の permissions.list を使った代替アプローチが必要になる(実装難度が上がる)

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-206「GASフェーズ: 外部共有の制限と警告設定」を実装してください。

## 実行前タスク
1. `CLAUDE.md` で 800_ops/ 配下の既存ファイル番号を確認し、新規ファイルの番号を確定する(810 は使用済みのため 811 以降)
2. `000_infra/002_constants.js` の `Constants.getParam` 実装を Read し、読み取り対象シートが `03_sys_params` であることを確認する(行 147〜167)
3. `000_infra/004_utils.js` の `Utils.logInfo` / `Utils.logError` / `Utils.auditLog` のシグネチャを確認する(logInfo: 行 232、logError: 行 242、auditLog: 行 273)
4. `appsscript.json` を Read し、現在の oauthScopes リストと advancedServices の有無を確認する
5. `03_sys_params` シートに `SHARING_WHITELIST` と `CFG_ADMIN_EMAIL` の行が存在するか確認し、なければ手動追加する

## 修正対象ファイル(新規作成のみ・既存コード変更最小)
- `800_ops/811_audit_checker.js` — 新規作成
- `appsscript.json` — oauthScopes への追記および advancedServices への Admin SDK 追記

## 実装内容(ステップ順)

### Step 1: 新規ファイル `800_ops/811_audit_checker.js` の作成
`checkExternalFileSharing_()` 関数を以下のロジックで実装する:
1. `LockService.getScriptLock()` で排他制御(tryLock(30000)、取得失敗時は Utils.logInfo でスキップを記録して早期 return)
2. `Constants.getParam('SHARING_WHITELIST', '')` でホワイトリストを取得し、カンマ分割・trim して配列化(空の場合は空配列として全件通知モード)
3. `Constants.getParam('CFG_ADMIN_EMAIL', '')` で通知先アドレスを取得(空の場合は Utils.logError で記録して return)
4. `PropertiesService.getScriptProperties().getProperty('N30_LAST_CHECKED_AT')` で前回タイムスタンプを取得(未設定時は 24 時間前の ISO 文字列を使用)
5. `AdminReports.Activities.list('all', 'drive', { eventName: 'acl_change', startTime: startTime, maxResults: 1000 })` でイベント取得
6. `nextPageToken` がある間はループして全件取得(ページネーション)
7. 各イベントの `actor.email` を確認し、組織外共有先(`target_user` / `target_domain` パラメータ)とホワイトリストを照合し、非該当のものを violations 配列に追加
8. violations が空でなければ `MailApp.sendEmail()` で adminEmail 宛に通知(件名・本文に「誰が・いつ・ファイルID・誰に」を含める)
9. `Utils.auditLog('RUN', '', '', '', 'checkExternalFileSharing_', null, null, '検知: N件')` を記録
10. `PropertiesService.getScriptProperties().setProperty('N30_LAST_CHECKED_AT', new Date().toISOString())` でタイムスタンプ更新
11. finally ブロックで `lock.releaseLock()`

### Step 2: `appsscript.json` の更新
- `oauthScopes` 配列に `"https://www.googleapis.com/auth/admin.reports.audit.readonly"` を追記
- `advancedServices` 配列(なければ新規追加)に以下を追記:
  ```json
  {
    "userSymbol": "AdminReports",
    "version": "reports_v1",
    "serviceId": "admin"
  }
  ```

### Step 3: GAS エディタでの Admin SDK 手動有効化(コード実装後)
- GAS エディタ → 「サービス」(+ボタン) → 「Admin SDK API」を検索して追加
- `clasp push` 後に必ず実施する

## 制約
- 既存ファイルへのロジック変更は `appsscript.json` 以外禁止
- `01_sys_config` に設定キーを追加しない(`03_sys_params` のみ)
- `PropertiesService.getScriptProperties()` はタイムスタンプ保存(キー: `N30_LAST_CHECKED_AT`)にのみ使用し、環境設定値の読み取りには使用しない
- 列番号のハードコード禁止(ヘッダー名ベースでのアクセスを徹底)
- メニューへの登録は不要(時間トリガーで自動実行するバッチのため)

## エッジケース
(仕様書の「エッジケース」セクション参照)

## 動作確認
1. `03_sys_params` シートに `SHARING_WHITELIST`(値: テスト用ドメインを除いた文字列)と `CFG_ADMIN_EMAIL`(自身のメールアドレス)を手動追記する
2. `npm run push:dev` でデプロイ
3. GAS エディタで「サービス」→「Admin SDK API」が有効化されていることを確認する
4. `checkExternalFileSharing_()` を GAS エディタから手動実行し、エラーなく完了することを確認する
5. `98_audit_log` シートに `RUN / checkExternalFileSharing_ / 検知: 0件` の記録が書き込まれていることを確認する
6. テスト用の外部共有を実施し、バッチを再実行して通知メールが届くことを確認する(`N30_LAST_CHECKED_AT` を削除してから再実行すること)
7. 重複実行防止の確認: 連続実行時に同一イベントが二重通知されないことを確認(`N30_LAST_CHECKED_AT` のスクリプトプロパティが更新されていること)
8. 動作確認後 `git push` → PR → main マージ

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル番号確定・設定キー設計 |
| Phase 2(実装) | なし | Phase 1 確定内容の実装に徹する |

推奨実行モデル

工程推奨モデル理由
ファイル作成・appsscript.json 更新Claude Sonnet 4.6Admin SDK の挿入位置特定・既存パターン適用に中程度の判断が必要

変更履歴

日付変更内容
2026-04-20初版作成
2026-04-21時間トリガー登録方法を手動 UI 登録からプログラム登録 (installAuditCheckerTrigger / uninstallAuditCheckerTrigger) に変更。checkExternalFileSharing_ が末尾 _ でプライベート扱いとなり GAS エディタのトリガー UI から選択できないため、MAS-201 バックアップと同じ install*Trigger パターンを採用 (prod 限定・権限チェック付き)

仕様書作成プロンプト(再現性・監査性のため必ず記録)

展開して表示

【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】

  1. 拡張思考の使い分け: Phase 1(設計)では拡張思考をフル活用し、ファイル配置先・関数名・エッジケース一覧・各 Step の記述内容を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
  2. テキスト報告の禁止: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
  3. 4-5 分割の Write/Edit 実行: Step 2-1(骨格 ~20行)/ 2-2(概要〜注意事項 ~300行)/ 2-3a(エッジケース〜人間検討事項 ~200行)/ 2-3b(実装プロンプト〜変更履歴 ~250行)/ 2-4(<details> 記録)に分割。1 回の Write/Edit は 300 行以内を目安にする。
  4. 各 Step で何を書くかを具体指示: Phase 1 で確定した内容を Phase 2 に持ち込む。出力時に設計判断を再考しない。

====================================================================== あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。 案件 MAS-206「GASフェーズ: 外部共有の制限と警告設定」の開発仕様書を作成してください。 仕様書を新規作成した後は、docs/_config.jsonnav 配列の適切なセクションに必ず追記してください。


Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

1-A: 案件定義の読み込み

  • docs/_internal/TODO_future.md を検索し、MAS-206 の案件名・概要・人間が検討すべき事項を取得する。

1-B: プロジェクト規約の読み込み

  • CLAUDE.md を読み込み、以下を確認する:
    • GAS ファイル番号体系(百の位=レイヤー、現在の 800_ops の割り当て済みファイル番号一覧)
    • 700_batch/ ディレクトリは CLAUDE.md の番号体系に存在しない。800_ops 配下の既存ファイル(801〜809)の番号を確認し、新規ファイルの適切な番号と配置(例: 800_ops/810_audit_checker.js)を Phase 1 で確定させること。700_batch/701_audit_checker.js という配置は採用しない。
    • PropertiesService.getScriptProperties() の直接呼び出し制限(環境依存値の取得には Env モジュール経由が必須)。本案件で使用するバッチ実行タイムスタンプの保存はオペレーショナルな状態管理であり、環境設定値とは異なるため直接使用を許容するが、その旨を仕様書の注意事項に明記すること。

1-C: 既存コードの読み込み(Grep → Read の順。名前からの推測禁止)

  1. 000_infra/002_constants.js を Read し、以下を実際のコードで確認する:

    • Constants.getParam(key, defaultVal) の実装(特に読み取り対象シート名を確認する。実コードでは '03_sys_params' を参照している)
    • Constants.CONFIG_SHEET の値(= '01_sys_config'
    • SHEET_DEFAULTS の構造(型・フィールド名)
    • 重要: Constants.getParam03_sys_params を参照する。SHARING_WHITELISTCFG_ADMIN_EMAIL 等の設定キーは 03_sys_params に追加する設計とする。01_sys_config に追加するよう指示しているドキュメントは誤りのため修正すること。
  2. 000_infra/004_utils.js を Read し、以下を確認する:

    • Utils.logInfo(funcName, message) のシグネチャ
    • Utils.logError(funcName, error, context) のシグネチャ
    • Utils.auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note) のシグネチャ
  3. 000_infra/001_env.js を Read し、Env モジュールが提供するメソッド一覧を確認する(PropertiesService 直接呼び出しの代替として利用可能なものを把握する)。

  4. 100_config/101_sys_config.js を Read し、以下を確認する:

    • onOpen()createMenu 呼び出しで定義されている既存メニュー名(仕様書の動作確認手順に記載するメニュー名は、ここで実在する文字列のみ引用する)
    • 時間トリガー登録に使っている既存パターン(あれば)
  5. 800_ops/ 配下のファイル一覧を Bash で確認し、使用済みファイル番号の最大値を特定して新規ファイル番号を確定する。

1-D: 参考仕様書の読み込み

  • docs/dev/dev_mas-179_audit_trail.md(監査ログ系の仕様書構成を確認)
  • docs/dev/dev_mas-201_sheet_backup.md(基盤バッチ系の仕様書構成を確認)
  • 上記ファイルが存在しない場合は docs/dev/ 配下のファイル一覧を確認し、基盤系で最も近い仕様書を 1 件選ぶ。

1-E: Phase 2 で記述する内容の確定(拡張思考フル活用)

Phase 1 の調査結果をもとに、以下を確定させてから Phase 2 に進む:

  • 新規ファイルの正確なパス(例: 800_ops/810_audit_checker.js)と関数名(例: checkExternalFileSharing_()
  • 03_sys_params に追加する設定キーの名前と型(SHARING_WHITELIST, CFG_ADMIN_EMAIL 等)
  • appsscript.json に追加する oauthScopes と Advanced Services の記述(Admin SDK Reports API のスコープ: https://www.googleapis.com/auth/admin.reports.audit.readonly
  • AdminReports.Activities.list() の正確な呼び出しシグネチャ(Advanced Services 有効化後の GAS オブジェクト名を確認)
  • 時間トリガーの登録方法(既存パターンに倣う)
  • 仕様書出力先ファイルパス: docs/dev/dev_mas-206_external_sharing_policy.md

Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-206_external_sharing_policy.md

Step 2-1: 骨格の作成(Write・~20行)

以下の全セクション見出しのみを含む骨格ファイルを作成する(本文は空で可):

# N-30: GASフェーズ: 外部共有の制限と警告設定
## 概要
## 目的
## アーキテクチャ概要
## 修正方針
### コンポーネント1: Google Workspace 管理コンソール設定(手動・手順書化)
### コンポーネント2: GAS 監査ログ定期チェックバッチ(自動・新規実装)
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト(再現性・監査性のため必ず記録)

Step 2-2: 前半セクションの追記(Edit または Bash heredoc・~300行)

以下の内容を各セクションに追記する:

「概要」: テーブル形式(案件ID=MAS-206, カテゴリ=基盤・セキュリティ, Phase=未着手, 優先度=TODO_future.mdから転記, 所要時間=調査値, 対象ファイル=Phase 1 で確定したファイルパス, 前提案件=なし)

「目的」: スプレッドシートの意図しない組織外共有を検知・通知し、情報漏洩リスクを低減する。

「アーキテクチャ概要」: 2 コンポーネント構成を明記。

  • コンポーネント1: Workspace 管理コンソールでの組織外共有ポリシー設定(管理者による一回限りの手動設定。手順書として仕様書に記述)
  • コンポーネント2: GAS 時間トリガー(日次)で Admin SDK Reports API を呼び出し、Drive の acl_change イベントを監査し、ホワイトリスト外の共有を検知したら管理者にメール通知するバッチ処理(Phase 1 で確定したファイルパスに新規実装)

「修正方針 / コンポーネント1」: Google Workspace 管理コンソールの操作手順(管理コンソール → アプリ → Google Workspace → ドライブとドキュメント → 共有設定 → 組織外との共有 の設定箇所と選択肢の説明。どちらを選ぶかは「人間が検討すべき事項」に委ねることを明記)

「修正方針 / コンポーネント2」: 以下を含む設計を記述:

  • Phase 1 で確定した新規ファイルパスと関数名
  • AdminReports.Activities.list() による Drive audit events 取得ロジック(applicationName='drive', eventName='acl_change')
  • ホワイトリスト管理: 許可ドメイン/メールアドレスのリストを 03_sys_paramsSHARING_WHITELIST キー(カンマ区切り文字列)に定義し、Constants.getParam('SHARING_WHITELIST', '') で読み込む(01_sys_config ではない)
  • 重複通知防止: PropertiesService.getScriptProperties() で前回チェック済みタイムスタンプ(キー: N30_LAST_CHECKED_AT)を保存・参照し、新規イベントのみ処理する(環境設定値ではなくオペレーショナルな状態管理であるため直接使用を許容)
  • 多重起動防止: LockService.getScriptLock() を関数冒頭で取得するパターン
  • 通知機能: MailApp.sendEmail()Constants.getParam('CFG_ADMIN_EMAIL', '') 宛に通知(「誰が・いつ・どのファイルを・誰に」共有したかをメール本文に含める)
  • 既存関数の再利用:
    • Utils.logInfo(funcName, message) — 正常実行ログ
    • Utils.logError(funcName, error, context) — 例外時ログ
    • Utils.auditLog('RUN', '', '', '', 'checkExternalFileSharing_', null, null, '検知件数: N件') — バッチ実行の監査証跡

「影響範囲」: 新規ファイル追加のみ。既存ファイルへの変更なし(appsscript.json の oauthScopes 追記と Advanced Services 有効化を除く)。

「注意事項」(番号付きリスト):

  1. Constants.getParam03_sys_params シートを参照する。設定キーは 03_sys_params に追加すること(01_sys_config ではない)
  2. Admin SDK Reports API は Google Workspace Business/Enterprise プランが必要。Google Workspace Essentials では利用不可
  3. PropertiesService.getScriptProperties() はオペレーショナルな状態管理(タイムスタンプ保存)に使用する。環境設定値の読み取りには引き続き Env モジュールを使用すること
  4. appsscript.json への advancedServices 追記と GAS エディタでの Admin SDK 手動有効化が必須(clasp push だけでは Advanced Services は有効化されない)
  5. Admin SDK を使用する実行アカウントは Google Workspace 管理者権限が必要

Step 2-3a: エッジケース〜人間検討事項の追記(Edit または Bash・~200行)

「エッジケース」(テーブル形式: 条件 | 動作 | 理由):

条件動作理由
Admin SDK 権限不足(実行アカウントが管理者でない)try...catch で捕捉し Utils.logError でスタックトレースを記録。MailApp でのエラー通知はスキップ(通知先も不明のため)管理者権限なしでは AdminReports.Activities.list() が例外をスロー
Activities.list() の結果が複数ページにわたるnextPageToken が返る限りループし全件取得してから処理する1 回の API 呼び出しは最大 1000 件。大量イベント時はページネーション必須
SHARING_WHITELIST03_sys_params に未設定または空文字ホワイトリストなし(全外部共有を通知対象)として動作。Utils.logInfo でその旨を記録フォールバックを「全件通知」にすることで見落としを防ぐ(保守的設計)
CFG_ADMIN_EMAIL が未設定または空文字メール送信をスキップし Utils.logError に「通知先メールアドレス未設定」を記録送信先不明でメール送信するとエラーになるため
前回チェックタイムスタンプが未設定(初回実行)過去 24 時間分のイベントを処理対象とする初回実行時に全履歴を処理すると件数過多になるためウォームアップ期間を設ける
共有者が組織内ユーザーでない(外部ユーザーが共有)ホワイトリスト判定対象外としてスキップ組織外アカウントの行動は Workspace 管理コンソール側で制御すべき範囲

「実データ検証」:

  • 03_sys_paramsSHARING_WHITELIST(値例: example.com,[email protected])と CFG_ADMIN_EMAIL の行が存在するか実装前に MCP で確認する
  • Admin SDK Advanced Services が GAS エディタで有効化済みか確認する

「関連ドキュメント」(テーブル: 仕様書リンク | 関連箇所)

「人間が検討すべき事項」: docs/_internal/TODO_future.md の MAS-206 行から転記した内容に加えて、以下を追記する:

  • ドメイン外共有を「完全禁止」か「警告のみ」かの運用方針決定(コンポーネント1 の設定値が変わる)
  • 外部税理士・会計士への共有の例外ルール(ホワイトリストに登録するドメイン/メールアドレスの洗い出し)
  • バッチ実行頻度の決定(日次 or 週次。日次の場合は実行時刻も指定)
  • 通知先管理者メールアドレス(CFG_ADMIN_EMAIL に設定する値)
  • Admin SDK 利用に必要な Google Workspace プランの確認(Business/Enterprise が必要)

Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash・~250行)

「実装プロンプト」 セクションは以下フォーマット(行頭スペース 4 つのインデント、バッククォートで囲まない):

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-206「GASフェーズ: 外部共有の制限と警告設定」を実装してください。

## 実行前タスク
1. `CLAUDE.md` で 800_ops/ 配下の既存ファイル番号を確認し、新規ファイルの番号を確定する
2. `000_infra/002_constants.js` の `Constants.getParam` 実装を Read し、読み取り対象シートが `03_sys_params` であることを確認する
3. `000_infra/004_utils.js` の `Utils.logInfo` / `Utils.logError` / `Utils.auditLog` のシグネチャを確認する
4. `100_config/101_sys_config.js` の `onOpen()` を Read し、時間トリガー登録の既存パターンを確認する
5. `appsscript.json` を Read し、現在の oauthScopes リストを確認する

## 修正対象ファイル(新規作成のみ)
- `800_ops/8XX_audit_checker.js`(XX は Phase 1 で確定した番号)— 新規作成
- `appsscript.json` — oauthScopes への `https://www.googleapis.com/auth/admin.reports.audit.readonly` 追記、および advancedServices への Admin SDK 追記

## 実装内容(ステップ順)

### Step 1: 新規ファイル作成
`checkExternalFileSharing_()` 関数を以下のロジックで実装する:
1. `LockService.getScriptLock()` で排他制御(tryLock(30000)、取得失敗時は早期 return)
2. `Constants.getParam('SHARING_WHITELIST', '')` でホワイトリストを取得し、カンマ分割して配列化
3. `Constants.getParam('CFG_ADMIN_EMAIL', '')` で通知先アドレスを取得
4. `PropertiesService.getScriptProperties().getProperty('N30_LAST_CHECKED_AT')` で前回タイムスタンプを取得(未設定時は 24 時間前を使用)
5. `AdminReports.Activities.list('all', 'drive', { eventName: 'acl_change', startTime: lastCheckedAt, maxResults: 1000 })` でイベント取得
6. `nextPageToken` がある間はループして全件取得
7. 各イベントの外部共有先アドレス/ドメインをホワイトリストと照合し、非該当のものを検知リストに追加
8. 検知リストが空でなければ `MailApp.sendEmail()` で管理者に通知(件名・本文に「誰が・いつ・ファイルID/リンク・誰に」を含める)
9. `Utils.auditLog('RUN', '', '', '', 'checkExternalFileSharing_', null, null, '検知:N件')` を記録
10. `PropertiesService.getScriptProperties().setProperty('N30_LAST_CHECKED_AT', new Date().toISOString())` でタイムスタンプを更新
11. LockService を release()

### Step 2: appsscript.json の更新
- `oauthScopes` 配列に `"https://www.googleapis.com/auth/admin.reports.audit.readonly"` を追記
- `advancedServices` 配列に Admin SDK Reports API エントリを追記

### Step 3: 時間トリガーの登録(101_sys_config.js のメニューまたは別途手動)
- 101_sys_config.js の既存パターンに倣い、日次トリガー登録関数を追加するか、手動登録手順を動作確認に記載する

## 制約
- 既存ファイルへのロジック変更は appsscript.json 以外禁止
- `01_sys_config` に設定キーを追加しない(`03_sys_params` のみ)
- `PropertiesService.getScriptProperties()` はタイムスタンプ保存にのみ使用し、環境設定値の読み取りには使用しない
- 列番号のハードコード禁止(ヘッダー名ベースでのアクセスを徹底)

## エッジケース
(仕様書の「エッジケース」セクション参照)

## 動作確認
1. `03_sys_params` シートに `SHARING_WHITELIST`(値: テスト用ドメインを除いた文字列)と `CFG_ADMIN_EMAIL`(自身のメールアドレス)を手動追記する
2. GAS エディタで「サービス」→「Admin SDK API」が有効化されていることを確認する
3. `checkExternalFileSharing_()` を GAS エディタから手動実行し、エラーなく完了することを確認する
4. テスト用の外部共有を実施し、バッチを再実行して通知メールが届くことを確認する
5. 重複実行時に同一イベントが二重通知されないことを確認する(`N30_LAST_CHECKED_AT` のスクリプトプロパティが更新されていること)
6. `98_audit_log` シートに実行記録が書き込まれていることを確認する
7. `npm run push:dev` → GAS 動作確認後 `git push` → PR → main マージ

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル番号確定・設定キー設計 |
| Phase 2(実装) | なし | Phase 1 確定内容の実装に徹する |

「推奨実行モデル」(テーブル):

工程推奨モデル理由
ファイル作成・appsscript.json 更新Claude Sonnet 4.6Admin SDK の挿入位置特定・既存パターン適用に中程度の判断が必要

「変更履歴」:

日付変更内容
2026-04-20初版作成

Step 2-4: 仕様書作成プロンプトの記録(Edit または Bash)

末尾の「仕様書作成プロンプト」セクションに、<details><summary>展開して表示</summary> ブロックで本 <instruction> 全文を囲んで追記する。


Phase 3: 後処理(順に実行)

3-A: docs/_config.json への追記

docs/_config.json を Read し、nav 配列の「§E.1 基盤・DevOps」セクション末尾に以下を追記する(セクション番号 X は既存エントリ数を数えて決定する。推測で番号を付けない):

{ "file": "dev/dev_mas-206_external_sharing_policy.md", "title": "E.1.X N-30 外部共有の制限と警告" }

3-B: changelog への追記

docs/_internal/changelog.md の先頭(ヘッダー直後)に以下を追記する:

| 2026-04-20 | [dev_mas-206_external_sharing_policy.md](dev_mas-206_external_sharing_policy.md) | 初版作成。外部共有制限・GAS監査バッチの設計 |

3-C: コミット&プッシュ

git add docs/dev/dev_mas-206_external_sharing_policy.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: N-30 外部共有の制限と警告設定の開発仕様書を作成

2コンポーネント構成(Workspace管理コンソール手順書 + GAS監査バッチ)。
Admin SDK Reports API を使用した acl_change イベント検知ロジックを設計。
設定キーは03_sys_paramsに配置(Constants.getParam経由)。"
git push -u origin {現在のブランチ}