MENU

【VBA→GAS検証】Windowsのイベントログを取得できない?PowerShell×Webアプリ連携で自動送信する

PowerShell×GAS連携でイベントログを自動送信する検証
目次

はじめに

Excel VBAでWindowsのイベント発生時刻を取得したいでは、「イベントビューアーには直接アクセスできない」という職場環境を想定し、VBAからWMIでWindowsのシステムログ(ログイン・ログオフ・電源ON/OFF)の発生時刻を取得する方法を解説しました。

これをGASに移行できないか検証しましたが、結論から言うとGAS単体では不可能です。GASはクラウドサーバーで実行されるスクリプトであり、ローカルPCにはアクセスできません。

そこで今回は、「PowerShellでローカルPC側のイベントログを抽出し、GASのWebアプリ機能(doPost)経由でスプレッドシートへ送信する」方式を検証します。GAS単体の機能検証だったこれまでの記事とは異なり、ローカル側のスクリプトと組み合わせて初めて実現できるアプローチです(会社のポリシーによってはPowerShellの実行そのものが制限されている場合もあるためご注意ください)。

全体像:PowerShellとGASのWebアプリを橋渡しする

「定期実行でログを送り続ける」運用と「指定した日付のログを随時取得する」運用の両方に対応できるよう、次の流れで実装します。

①PowerShell起動時:GASのdoGetへGETで問い合わせ、H2の日付とLAST_SYNCED_TIMEを取得
②H2の日付 > LAST_SYNCED_TIME(続きから) > ポップアップ手入力、の優先順位で起点日を決定
③Get-WinEventでSystemログを抽出しJSON化してPOST送信
④GAS側(doPost):見出し直下に挿入(最新が常に一番上)、LAST_SYNCED_TIME更新、H2をクリア

シート上の「クリア」ボタンでログ本体(A・B列)だけをクリアできます(LAST_SYNCED_TIMEは変更しません)。GASは外部からのHTTPリクエストを直接受け取れる「Webアプリ」としてデプロイする機能を使って「待ち受けるだけ」の役割になり、ログ抽出はPowerShellが担当します。VBAのWMIが「PC内を直接読みに行く」処理だったのに対し、「データを送ってきてもらう」受け身の発想に変わる点が最大の違いです。

準備①:GASをWebアプリとしてデプロイする

doPostのようにHTTPリクエストを受け取る関数を使うには、GASプロジェクトを「Webアプリ」として公開(デプロイ)する必要があります。HtmlServiceのダイアログとは別の公開の仕組みです。

  1. Apps Scriptエディタ右上の「デプロイ」→「新しいデプロイ」
  2. 歯車アイコン→「ウェブアプリ」を選択
  3. 「実行ユーザー」は「自分」のままでOK
  4. 「アクセスできるユーザー」で「全員」を選択する(外部からの未認証アクセスを受け付けるには必須。リスクは後述)
  5. 「デプロイ」をクリック(初回は権限の承認が必要)
  6. 発行された「ウェブアプリのURL」をコピーしてメモしておく
「アクセスできるユーザー」で「全員」を選択する画面

★実機で確認済み: 「全員」以外だとdoPost実行前に「(401) 許可されていません」エラーになります。また、doPost/doGetのコード修正は既存デプロイに自動反映されません。いずれも「デプロイを管理」→鉛筆アイコン→設定変更→「新しいバージョン」で「デプロイ」すればURLを変えずに反映できます。

準備②:シートにH2セル(日付指定用)とクリアボタンを用意する

「イベントログ」シートに、指定日付モード用のセルとリセット用のボタンを用意します。

  1. H1セルに案内文(例:「※取得したい日付を入力(yyyy/m/d、指定日付を取得したい場合のみ)」)を入力(任意)
  2. H2セルは空のままにしておく(日付があれば指定日付モード、空なら続きからモード)
  3. 「挿入」→「図形描画」でボタン図形を作成し、「︙」→「スクリプトを割り当て」→関数名にclearEventLogと入力(GAS最初の記事と同じ方法)

セキュリティ面の注意:URLを知っていれば誰でも実行できる

「アクセスできるユーザー」を「全員」にすると、そのURLを知っている人なら誰でもdoPostを呼び出せる状態になります。対策として、リクエストに秘密のトークン文字列を含め、GAS側で一致するか確認する簡易認証を組み込みます(保存先は【GAS】PropertiesServiceとは?ScriptProperties)。厳重なセキュリティが必要な用途では別の認証方式(OAuth等)を検討してください。

Step1:GAS側の実装(doGet/doPost/クリアボタン)

データの書き込み先として「イベントログ」シート(A1「ログID」・B1「イベント発生日時」・H2は準備②の日付指定用セル)を用意します。

// 初回のみ実行:秘密トークンをScriptPropertiesに保存する
function setupSecretToken() {
  PropertiesService.getScriptProperties().setProperty('SECRET_TOKEN', 'ここに任意の文字列を設定');
}

// GET:H2の日付とLAST_SYNCED_TIMEを返す
function doGet(e) {
  const props = PropertiesService.getScriptProperties();
  const secretToken = props.getProperty('SECRET_TOKEN');

  if (e.parameter.token !== secretToken) {
    return jsonOutput({ status: 'error', message: 'unauthorized' });
  }

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('イベントログ');
  const h2Value = sheet.getRange('H2').getValue();

  // Date型ならyyyy/MM/dd文字列に変換
  const dateCellValue = (h2Value instanceof Date)
    ? Utilities.formatDate(h2Value, Session.getScriptTimeZone(), 'yyyy/MM/dd')
    : String(h2Value || '');

  return jsonOutput({
    status: 'ok',
    dateCellValue: dateCellValue,
    lastSyncedTime: props.getProperty('LAST_SYNCED_TIME') // 未設定ならnull
  });
}

// POSTを受け取るたびに呼ばれる
function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);
    const props = PropertiesService.getScriptProperties();

    // トークン確認
    const secretToken = props.getProperty('SECRET_TOKEN');
    if (data.token !== secretToken) {
      return jsonOutput({ status: 'error', message: 'unauthorized' });
    }

    const events = data.events; // 新しい順
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('イベントログ');
    const rows = events.map(item => [item.EventCode, item.TimeCreated]);

    if (rows.length > 0) {
      const lastRow = getLastRowInColumns(sheet, 2);
      const existingData = lastRow >= 2 ? sheet.getRange(2, 1, lastRow - 1, 2).getValues() : [];
      const allData = rows.concat(existingData);
      sheet.getRange(2, 1, allData.length, 2).setValues(allData);

      // 新しい場合だけ更新
      const batchMax = events.map(item => item.TimeCreated).sort().pop();
      const current = props.getProperty('LAST_SYNCED_TIME');
      if (!current || batchMax > current) {
        props.setProperty('LAST_SYNCED_TIME', batchMax);
      }
    }

    sheet.getRange('H2').clearContent();

    return jsonOutput({ status: 'ok', count: rows.length });

  } catch (err) {
    return jsonOutput({ status: 'error', message: err.message });
  }
}

function clearEventLog() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('イベントログ');
  sheet.getRange(2, 1, sheet.getMaxRows() - 1, 2).clearContent();
  SpreadsheetApp.getActiveSpreadsheet().toast('ログをクリアしました。');
}

function getLastRowInColumns(sheet, numColumns) {
  const values = sheet.getRange(1, 1, sheet.getMaxRows(), numColumns).getValues();
  let lastRow = 0;
  for (let i = 0; i < values.length; i++) {
    if (values[i].some(cell => cell !== '')) lastRow = i + 1;
  }
  return lastRow;
}

function jsonOutput(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}

setupSecretToken()はコードを保存したあと実行ボタンで一度だけ実行し、トークンを保存する関数です(実行後は削除してもかまいません)。

ポイント①:末尾追記ではなく「読み取り→結合→書き戻し」で先頭に差し込む。 イベントログは新しい順のため、末尾追記だと「最新が一番下」になり元のVBAと逆になります。当初insertRowsBefore(2, ...)で見出し直下に挿入していましたが、この関数は行全体を挿入するためE列の凡例やH列の日付入力欄までズレる不具合を実機で確認getLastRow()と同じくGASの行操作関数は列スコープを持たないため)。既存のA・B列データを読み取り、新しいデータを先頭に結合してA・B列だけに書き戻す方式に変更して解決しました。

ポイント②:H2は使用後に自動クリアする(残すと次回の定期実行でも指定日付モードのままになるため)。

ポイント③:LAST_SYNCED_TIMEは新しい場合だけ更新する(過去日の後追い取得で基準日が巻き戻らないようにするため)。いずれも実機で動作確認済みです。

Step2:PowerShell側の実装(指定日付・続きから・手入力の3パターンに対応)

VBAのようにH2セルへの日付入力だけで完結させつつ、定期実行にも対応できるよう、PowerShell側では「H2の日付」→「LAST_SYNCED_TIME(続きから)」→「ポップアップ手入力」の優先順位で起点日を決めます(-ge/-gtの使い分けは後述)。

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8

# ここにStep1でデプロイしたWebアプリのURLを設定
$webAppUrl = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxx/exec"
# setupSecretToken()で設定したのと同じ文字列を設定
$secretToken = "ここに任意の文字列を設定"
$tokenEncoded = [uri]::EscapeDataString($secretToken)

# H2とLAST_SYNCED_TIMEを問い合わせる
try {
    $statusResponse = Invoke-RestMethod -Uri "$($webAppUrl)?token=$tokenEncoded" -Method Get -ErrorAction Stop
} catch {
    Write-Host "GASへの問い合わせに失敗しました。`$webAppUrlの値を確認してください:$webAppUrl"
    Write-Host "エラー内容:$($_.Exception.Message)"
    exit
}

if ($statusResponse.dateCellValue) {
    # ①H2の日付を最優先
    try {
        $targetDate = Get-Date $statusResponse.dateCellValue -ErrorAction Stop
    } catch {
        Write-Host "H2の日付を認識できませんでした。処理を中断します。"
        exit
    }
    $includeTargetDate = $true

} elseif ($statusResponse.lastSyncedTime) {
    # ②続きから(重複防止のためこの日時自体は含めない)
    $targetDate = Get-Date $statusResponse.lastSyncedTime
    $includeTargetDate = $false

} else {
    # ③初回:ポップアップで手入力
    Add-Type -AssemblyName Microsoft.VisualBasic
    $inputStr = [Microsoft.VisualBasic.Interaction]::InputBox(
        "取得したい日付を入力してください`n(例: 2026/09/06)",
        "日付指定",
        (Get-Date).ToString("yyyy/MM/dd")
    )

    if ([string]::IsNullOrWhiteSpace($inputStr)) {
        Write-Host "日付が入力されなかったため処理を中断します。"
        exit
    }

    try {
        $targetDate = Get-Date $inputStr -ErrorAction Stop
    } catch {
        [Microsoft.VisualBasic.Interaction]::MsgBox("「$inputStr」を日付として認識できませんでした。処理を中断します。", "OKOnly,Critical", "入力エラー")
        exit
    }
    $includeTargetDate = $true
}

# 秒精度に丸めてから比較する(ミリ秒差での重複送信を防ぐ)
$filter = if ($includeTargetDate) {
    { (Get-Date $_.TimeCreated.ToString('yyyy/MM/dd HH:mm:ss')) -ge $targetDate }
} else {
    { (Get-Date $_.TimeCreated.ToString('yyyy/MM/dd HH:mm:ss')) -gt $targetDate }
}
$events = @(Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 6005, 6006, 7001, 7002 } |
    Where-Object $filter |
    Sort-Object TimeCreated -Descending |
    Select-Object `
        @{ Name = 'EventCode'; Expression = { $_.Id } },
        @{ Name = 'TimeCreated'; Expression = { $_.TimeCreated.ToString('yyyy/MM/dd HH:mm:ss') } })

if ($events.Count -eq 0) {
    Write-Host "新しいイベントはありませんでした。"
    exit
}

# GASへ送信するJSONを組み立て
$body = @{
    token  = $secretToken
    events = $events
} | ConvertTo-Json -Depth 3

# GASのWebアプリへPOST送信
$response = Invoke-RestMethod -Uri $webAppUrl -Method Post -Body $body -ContentType "application/json; charset=utf-8"
if ($response.status -eq 'ok') {
    Write-Host "送信結果:$($response.status)($($response.count)件)"
} else {
    Write-Host "送信結果:$($response.status) - $($response.message)"
}

VBAとの違いは主に3点です。①UTC→JST変換が不要Get-WinEventはローカルタイムゾーンで返るため)。-ge-gtの使い分け(境界日を含めるH2指定・初回手入力は-ge、重複防止が必要な続きからモードは-gt)。Sort-Object -Descendingで新しい順を保証(既定順に頼りすぎない安全策)。ConvertTo-JsonでのJSON化もVBAに無い標準機能です。

実機検証しながら発見して改善したPowerShellの改善点が次の3つです。
"$webAppUrl?token=..."のように変数直後に?が続くと変数名ごと空文字列になる(VBAの&結合にはない落とし穴。$(...)で囲んで解決。あわせてGET失敗時に-ErrorAction Stopが無く静かにフォールバックしていた問題も修正)。
②続きからモードで直前送信済みの1件を重複送信するLAST_SYNCED_TIMEは秒精度、TimeCreatedはミリ秒精度のため-gt比較をすり抜けていた。比較側を秒精度に丸めて解決)。
③新着イベントがちょうど1件だと送信に失敗する(PowerShellは結果1件だとパイプラインが配列でなく単体オブジェクトを返しJSON化が壊れるため。@( ... )で常に配列化して解決)。

既知の制約: H2で過去の期間を後追い取得すると常に一番上に挿入される仕様にしたため並び順が崩れる可能性があります。トークンをURLのクエリ文字列に含めるためアクセスログに記録される可能性もあります。

保存・実行の注意点

文字コードは「UTF-8(BOM付き)」で保存する。 メモ帳の「UTF-8」(BOMなし)だと、Windows PowerShell 5.1が日本語コメントをShift-JISとして誤読し、文字化けやGet-Dateの構文エラーが起きることを実機で確認しました(Windows 10以降のメモ帳は「UTF-8」=BOMなしの仕様)。

実行方法: 保存後、右クリック→「PowerShellで実行」。ウィンドウが一瞬で閉じる場合は実行ポリシーがRestrictedの可能性があるのでSet-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedを一度だけ実行してください。ダブルクリックに近い操作感にしたい場合は、ショートカットのリンク先をpowershell.exe -ExecutionPolicy Bypass -NoExit -File "C:\path\to\gas-09.ps1"にします。

タスクスケジューラでの定期実行: 「操作」タブ→「プログラムの開始」で、プログラムにpowershell.exe、引数に-ExecutionPolicy Bypass -File "C:\path\to\gas-09.ps1"を設定すれば動作します(実機確認済み)。

タスクスケジューラの「操作の編集」画面(プログラムと引数の設定)

処理速度: 285件で起動から約2秒(Windows Updateでログがリセットされ、それ以上は未検証)。元のVBA版では数千件でもタイムアウトはありませんでした。

⚠️未検証: イベントビューアー制限環境でPowerShell自体が実行できるか。会社のポリシー次第では使えない可能性があります。

PowerShell実行結果(送信結果:ok)とスプレッドシートへの反映(新しい順に正しく挿入され、E・H列も影響なし)

できること・できないことの整理

項目結果
GAS単体でのイベントログ取得❌ 不可能(クラウド実行)
PowerShellでの抽出・UTC→JST変換✅ 変換不要、動作確認済み
doPostでの受信・書き込み✅ 確認済み(詳細はStep1)
「アクセスできるユーザー」設定・再デプロイ要否✅ 「全員」以外は401エラー。修正のたびに再デプロイ要
.ps1の保存時の文字コード✅ BOM付きUTF-8で保存(詳細は後述)
セキュリティ(URL漏洩時のリスク)⚠️ トークン確認のみ
続きから/H2指定/クリアボタン✅ すべて実機確認済み(詳細はStep1・2)
タスクスケジューラでの定期実行・処理速度✅ 確認済み。285件で約2秒
イベントビューアー制限環境での動作⚠️ 未検証。環境次第で実行不可の可能性

よくある質問

Q. PowerShellを定期的に自動実行させることはできる?

→ タスクスケジューラに登録すれば可能です(詳しくは前述)。H2を空のままにしておけば続きからモードで動くため日付管理は不要です。

Q. 特定の日付のログだけを取得/すべて消してやり直したい場合は?

→ H2に日付(例:2026/09/06)を入力すれば、送信成功後に自動クリアされ続きからモードに戻ります。ログのクリアは「クリア」ボタンでA・B列のみ(LAST_SYNCED_TIMEは変更されないため、最初から取り直す場合はH2に開始日を入力)。

Q. 複数のPCから同じスプレッドシートに送信できる?

→ 同じURL・トークンを配布すれば可能です。送信元の識別が必要なら$env:COMPUTERNAMEをJSONに含めてください。

まとめ

  • GAS単体ではローカルPCのイベントログを直接取得できない。「PowerShellで抽出しGASのWebアプリへPOSTする」橋渡し方式で対応する
  • 「定期実行」と「指定日付での随時取得」の両方に対応するため、H2の日付指定→続きから同期→ポップアップ手入力、の3段階で起点日を決める設計にした
  • 実機検証でGAS側・PowerShell側あわせて4件の不具合(行操作関数の列スコープ、$変数?展開、単一要素の配列化漏れ、ミリ秒精度による重複送信)を発見・修正した(詳細はStep1・2参照)
  • 修正後は全機能・定期実行・処理速度(285件で約2秒)とも実機で正常動作を確認済み。唯一、イベントビューアー制限環境での動作だけは検証できていない

次回予告

主要な機能・運用面は「イベントビューアー制限環境でのPowerShell実行可否」を除いてすべて実機検証を完了しました。

次回記事の予定は「変換できないExcelのスピルする関数をGASのカスタム関数で対応する」というような内容にしたいと思います。

サンプルファイルについて

GASのスクリプトはスプレッドシートに紐づいた状態(コンテナバインド型)で保存されているため、シートをコピーすればStep1のコード・シート構成(見出し・H2セル・ボタン)もそのまま複製されます。

サンプルスプレッドシートを開く(コピーして使用)

コピー後はsetupSecretToken()の実行とWebアプリとしてのデプロイをご自身で行ってください(トークン・URLは個人ごとに異なるため、コピーしても引き継がれません)。ローカルPC側で動かすStep2のPowerShellコードはスプレッドシートには含められないため、上記のコードを別途.ps1ファイルとして保存してお使いください。

関連記事

当サイトの記事で使用したVBAなどのサンプルをDLできます

この記事のサンプルファイルは「サンプルファイルについて」からコピーしてください!

ダウンロードページへは下のカードをクリックすればジャンプできます。
よろしければご利用ください!

よかったらシェアしてね!
  • URLをコピーしました!
目次