# カスタムジョブ統合ガイド

## カスタムジョブ統合ガイド <a href="#custom-job-integration-guide" id="custom-job-integration-guide"></a>

本ガイドでは、カスタム実行ファイルをKEPMのジョブタスクとして統合する流れを、バイナリのビルドとデプロイ、ジョブJSONの作成、MQTT接続と構造化ログのパブリッシュまで順に説明します。スケジュール実行やエージェント起動時に動くツールが、本番環境で正しく動作するために必要な事項を扱います。

[統合の概要](https://github.com/Keeper-Security/gitbook-jp-secrets-manager/blob/main/endpoint-privilege-manager/integrations/endpoint-privilege-manager/integrations/overview.md)をまだ読んでいない場合は、そちらから始めてください。用語の整理、各要素のつながり、ジョブタスクがユースケースに適するパターンについて説明しています。

{% stepper %}
{% step %}

#### バイナリのビルドとデプロイ <a href="#build-and-deploy-your-binary" id="build-and-deploy-your-binary"></a>

**ビルドの方針**

バイナリの実装言語は任意です。エージェントはフレームワークやランタイムを強制せず、子プロセスとして実行ファイルを起動し、終了コードを追跡し、出力を取り込みます。想定されるパターンは単純で、起動して処理を行い、意味のある終了コードで終了します。

**最初から押さえておきたい点:**

* **OSごとに1ビルド:** Windows、Linux、macOSをサポートする場合はプラットフォームごとに別バイナリを用意します。OSごとにジョブJSONを分ける方法と、1つのジョブに複数タスクと条件を載せる方法がありますが、プラットフォーム別ビルドの方がシンプルで保守しやすくなります。
* **単一の目的:** スキャン、レポート、メンテナンスなど、処理したら終了する設計にします。ジョブタスク用のバイナリを常駐デーモンとして設計しないでください。その用途はプラグインパターン向けです。
* **MQTTでログ出力:** 標準出力に頼らず、構造化ログメッセージを `KeeperLogger` MQTTトピックへパブリッシュする計画にします。詳細は[手順3](#author-the-job-json)をご参照ください。

**エンドポイントへのバイナリの配置**

バイナリはエージェントのアプリケーションルート配下に配置します。推奨レイアウトでは、フルパスを明示しなくても名前でバイナリを解決できます。

```
{AgentRoot}/Jobs/bin/{CommandName}/{CommandName}.exe    (Windows)
{AgentRoot}/Jobs/bin/{CommandName}/{CommandName}        (Linux / macOS)
```

ジョブJSONではタスクの `command` フィールドに `{CommandName}` のみを指定します (パスなし)。ジョブランナーは `Jobs/bin/{CommandName}/` を先に検索し、その後に `PATH` を検索します。

このレイアウトが使えない場合は、`executablePath` に絶対パスを指定します。ジョブスキーマ上 `command` は必須のままですが、解決には `executablePath` が優先されます。

**クロスプラットフォームジョブと `osFilter`**

ジョブのルートに `osFilter` を設定すると、各エンドポイントではOSに一致するタスクだけが実行されます。`osFilter` で現在のOSが除外されている場合、バリデータはそのタスクのバイナリがディスクに存在するかどうかのチェックも行いません。つまり、Linuxエージェントを含むフリートにWindows用ジョブを配布しても検証エラーにはなりません。

```json
"osFilter": {
  "windows": true,
  "linux": false,
  "macOS": false
}
```

複数OSを扱う場合、最も単純なのはプラットフォームごとに別のジョブJSONファイル (例: `my-tool-windows.json`、`my-tool-linux.json`) を用意し、それぞれにバイナリパスと `osFilter` を書く方法です。1つのジョブに複数タスクを載せ、各タスクに `osFilter` 条件を付けることもできますが、保守は複雑になりがちです。

**実行コンテキスト**

タスクの `ExecutionType` で、プロセス起動時にエージェントが使うアカウントを制御します。

<table><thead><tr><th width="144.54534912109375">ExecutionType</th><th>用途</th></tr></thead><tbody><tr><td><code>Service</code></td><td>デフォルト。エージェントのサービスアカウントで実行します。マシン全体へのアクセスが必要で対話ユーザーセッションは不要なバックグラウンドタスク (セキュリティスキャン、メンテナンスジョブ、コンプライアンスレポートなど) に使います。</td></tr><tr><td><code>User</code></td><td>ログオン中のユーザーセッションで実行します。ツールが対話的にユーザープロファイルへアクセスする必要がある場合に限って使います。ユーザーセッションのプロセスは、次節で述べる追加の信頼チェックの対象になります。</td></tr><tr><td><code>UserDesktop</code></td><td>ユーザーのデスクトップセッションで実行します。注意点は <code>User</code> と同様です。</td></tr></tbody></table>

多くのカスタムツールでは `Service` が適切です。`User` または `UserDesktop` を使う場合は、署名とプロセス信頼の節をよく読んでください。管理者以外のユーザーセッションでは追加の制限がかかります。
{% endstep %}

{% step %}

#### コード署名とプロセス信頼 <a href="#code-signing-and-process-trust" id="code-signing-and-process-trust"></a>

ジョブJSONを一行も書く前に理解しておくべき重要な内容です。エージェントがバイナリをどう信頼するかによって、MQTT接続の成否や、プラグイン階層のHTTPS呼び出しがデータを返すか、 `403` になるかが決まります。

**エージェントによるプロセス登録の仕組み**

ジョブランナーがタスクとしてバイナリを起動すると、起動直後にそのプロセスがエージェントの起動済みプロセス登録簿に追加されます。この登録により、次が付与されます。

* ローカルMQTTブローカーへのアクセス
* `/api/PluginSettings/...` などプラグイン階層HTTPSエンドポイントの呼び出し許可

登録はバイナリが最初の接続を行う前に行われるため、通常どおりジョブランナーから起動されたジョブタスクが、MQTT接続やプラグイン設定呼び出しのために証明書チェックをパスする必要はありません。

**証明書チェックが走るタイミング**

証明書チェックは、**すでに**起動済みプロセス登録簿にないプロセスをエージェントが扱うときに実行されます。次の2ケースがあります。

1. **ジョブランナーの登録が完了する前にバイナリが起動している** (起動時の競合)。エージェントは証明書ベースのチェックにフォールバックします。対策として、最初のMQTT接続で短いリトライループを入れます。
2. **ジョブではなく手動でバイナリが起動されている** (シェル、スクリプト、エクスプローラーなど)。この場合、実行ファイルの署名をKeeper特権マネージャーの証明書、または `Settings:AlternativeSignatures` に列挙されたサムプリントのいずれかと照合する必要があります。未署名のバイナリはこの経路では失敗します。

**証明書サムプリントの追加**

ジョブ外で起動したバイナリからもMQTTやプラグイン設定に接続する必要がある場合 (開発やテスト中など)、コード署名証明書のサムプリントを `appsettings.json` の `Settings:AlternativeSignatures` に追加します。

```json
{
  "Settings": {
    "AlternativeSignatures": [
      "A1B2C3D4E5F6789012345678901234567890ABCD"
    ]
  }
}
```

Windowsでサムプリントを取得する例:

```powershell
Get-AuthenticodeSignature -FilePath "C:\Path\To\YourTool.exe" |
  Select-Object -ExpandProperty SignerCertificate |
  Select-Object -ExpandProperty Thumbprint
```

サムプリントはスペースなしの40文字の16進文字列として指定します。署名証明書が複数ある場合は複数列挙できます。

`appsettings.json` を保存したらKeeper特権マネージャーサービスを再起動します。これらの設定は起動時に読み込まれます。

**ユーザーセッション向け `AllowedNonAdminExecutables`**

バイナリがユーザーセッションで動作し (`ExecutionType: User` または `UserDesktop`)、かつプロセス所有者が管理者でない場合、証明書チェックの後に追加の条件があります。実行ファイルのベース名 (パスなし、`.exe` なし) が `Settings:AllowedNonAdminExecutables` に含まれている必要があります。

```json
{
  "Settings": {
    "AllowedNonAdminExecutables": [
      "MyTool"
    ]
  }
}
```

このチェックは、エージェントのサービスアカウントで動く `Service` タスクには適用されず、システムセッションの信頼ルールに従います。ツールを常に `Service` のジョブタスクとしてだけ実行する場合は、この設定は不要です。

**推奨運用**

1. **本番ビルドはすべて署名する** (WindowsはAuthenticode、macOSはApple Developer ID、Linuxはデプロイで強制する場合はGPG署名パッケージやIMA署名など)。
   1. Linuxではエージェントのプロセス信頼は主にジョブランナーによる起動に依存します。証明書チェック経路を避けるため、バイナリは常にジョブタスク経由で起動するようにします。
2. ジョブ起動以外の文脈でもMQTTやプラグイン設定へのアクセスが必要なときは、サムプリントを **`Settings:AlternativeSignatures`** に追加する。
3. **ジョブ用バイナリは `Jobs/` 配下に置き、`Plugins/` 配下には置かない。** プラグイン用ディレクトリのパスにはより厳しい検証ルールが適用されますが、それは `Jobs/bin/` のバイナリには当てはまりません。
4. **特別な理由がない限り `Service` で実行する。**
   {% endstep %}

{% step %}

#### ジョブJSONの作成 <a href="#author-the-job-json" id="author-the-job-json"></a>

**ジョブファイルとその配置**

ジョブは次の場所に保存するJSONファイルです。

```
{AgentRoot}/Jobs/{job-id}.json
```

ファイル内の `id` フィールドは、拡張子 `.json` を除いたファイル名と完全一致する必要があります。例: `secrets-scanner.json` には `"id": "secrets-scanner"` を含めます。

**重要:** ジョブIDにはハイフンを使い、アンダースコアは使わないでください。MQTTブローカーはクライアントIDからジョブIDを取り出す際にアンダースコアで分割します。`secrets_scanner` のようなIDはクライアントIDの解釈を壊し、パブリッシュ許可エラーの原因になります。

**Last Known Good と安全なデプロイ**

多くのデプロイでは、エージェントがLast Known Good (`ConfigurationLkg`) を有効にした状態で動作します。各ジョブのJSONの暗号化された参照コピーを保持し、`Jobs/` ディレクトリが参照と食い違っていないか監視します。手編集などでファイルが参照と一致しないと検出された場合、参照コピーからファイルを復元し、変更は取り消されます。

そのため、**ジョブを `Jobs/` に直接ファイルを置いてデプロイしないでください。** 代わりに、信頼できる書き込み経路のいずれかを使います。

**方法1: ローカルHTTPS API。** ジョブJSONをエージェントの `POST /api/Jobs` にPOSTします。ファイルとLast Known Goodの参照を原子的に更新します。APIの詳細は[手順4](#deploy-the-job)をご参照ください。

**方法2: `JobUpdate` ポリシー。** 管理フリートでは、Keeperコンソール経由で配信される `JobUpdate` ポリシーが標準です。構成ポリシープロセッサがジョブファイルと参照コピーを許可された操作として書き込みます。現在のポリシースキーマとコンソール上の手順は、Keeper管理者に確認してください。

**ジョブJSONの構造**

Windows向けの完全な注釈付き例です。

```json
{
  "id": "secrets-scanner",
  "name": "Secrets Scanner",
  "description": "Scans for exposed credentials and publishes results to KeeperLogger.",
  "enabled": true,

  "schedule": {
    "intervalMinutes": 120
  },

  "events": [
    { "eventType": "Startup" }
  ],

  "osFilter": {
    "windows": true,
    "linux": false,
    "macOS": false
  },

  "mqttTopics": {
    "allowedPublications": ["KeeperLogger"],
    "allowedSubscriptions": []
  },

  "parameters": [],

  "tasks": [
    {
      "id": "run-scanner",
      "name": "Run scanner",
      "ExecutionType": "Service",
      "command": "SecretScanner",
      "executablePath": "C:\\Program Files\\KeeperPrivilegeManager\\Jobs\\bin\\SecretScanner\\SecretScanner.exe",
      "arguments": "--scan --keeper-api-base={KeeperApiBaseUrl}",
      "timeoutSeconds": 3600,
      "continueOnFailure": false,
      "scriptType": "Auto"
    }
  ]
}
```

主なフィールド:

**`id`** — ファイル名と一致させる。ハイフンのみ。アンダースコアは不可。

**`enabled`** — `true` にする。`enabled: false` のジョブは手動トリガーがあっても実行されません。

**`schedule`** — タイマートリガー。`intervalMinutes` は実行の**間隔** (分) であり、時刻に揃えた周期ではありません。`120` は2時間ごとです。その他のモードは以下の[スケジュールオプション](#schedule-options)をご参照ください。

**`events`** — イベントトリガー。`Startup` はエージェント起動時に1回、ジョブ読み込み後に実行されます。`schedule` と `events` の両方を持てます。どちらも独立して実行を引き起こし得ます。

**`osFilter`** — ジョブを実行するOS。対象プラットフォームだけ `true` にします。

**`mqttTopics`** — タスクがパブリッシュ・サブスクライブしてよいMQTTトピック。ブローカーがこのリストを強制します。ここに `KeeperLogger` がないと、MQTT接続に成功してもパブリッシュは拒否されます。

**`tasks`** — 実行するステップの配列。多くのカスタムツールでは1タスクで足ります。タスクは順番に実行されます。

**`arguments`** — 変数置換に対応。`{KeeperApiBaseUrl}` はエージェントがローカルHTTPS APIのベースURL (例: `https://127.0.0.1:6889`) に置き換えます。バイナリはこのフラグを解釈し、実行時にプラグイン設定を呼び出します。コード例は[概要ページ](https://github.com/Keeper-Security/gitbook-jp-secrets-manager/blob/main/endpoint-privilege-manager/integrations/endpoint-privilege-manager/integrations/overview.md)をご参照ください。

**スケジュールオプション**

ジョブは4種類のスケジュールモードをサポートします。1ジョブで有効にできるモードは1つだけです。

**間隔** — スケジューラ起動からN分ごと:

```json
"schedule": { "intervalMinutes": 120 }
```

**Cron** — 標準的な5フィールド (分、時、日、月、曜日):

```json
"schedule": { "cronExpression": "0 3 * * *" }
```

**1回限り** — 指定したUTC時刻に1回だけ:

```json
"schedule": { "runAt": "2025-06-01T02:00:00Z" }
```

**カレンダー** — 週または月の特定曜日・特定時刻:

```json
"schedule": {
  "calendar": [
    { "time": "03:00", "daysOfWeek": ["Monday", "Wednesday", "Friday"] }
  ]
}
```

**起動時と間隔のタイミング**

`Startup` イベントと `intervalMinutes` の両方があるジョブは、エージェント起動時に1回実行されたあと、そこから間隔どおりに再実行されます。間隔タイマーは独立して動作するため、起動時実行と最初の間隔実行の関係は、対象エージェントのバージョンで実測せずに固定関係とみなさないでください。

エージェント起動時は、ジョブの読み込みと `KeeperLogger` がメッセージを受け付け可能になった後に、Startupジョブがトリガーされます。間隔タイマーの最初の刻みは、Startup実行の完了時刻ではなく、スケジューラ初期化から設定された分数後です。
{% endstep %}

{% step %}

#### ジョブのデプロイ <a href="#deploy-the-job" id="deploy-the-job"></a>

**ローカルHTTPS APIの利用**

エージェントはループバック上でジョブ管理用のREST APIを公開します。HTTPSリスナーは `Settings:KestrelHttpsPort` (多くの場合 `6889`) です。ジョブ管理の各エンドポイントには**管理用階層**の認可が必要です。認証方式 (多くの場合、環境用にプロビジョニングされたクライアント証明書による相互TLS) はデプロイチームから案内されます。

ジョブの作成または更新:

```powershell
Invoke-RestMethod -Method Post `
  -Uri "https://127.0.0.1:6889/api/Jobs" `
  -ContentType "application/json" `
  -Certificate $adminClientCert `
  -Body (Get-Content -Raw .\secrets-scanner.json)
```

```bash
curl -s -X POST https://127.0.0.1:6889/api/Jobs \
  --cert /path/to/client.pem \
  --key /path/to/client.key \
  --cacert /path/to/ca.pem \
  -H "Content-Type: application/json" \
  -d @secrets-scanner.json
```

主なジョブ管理エンドポイント:

<table><thead><tr><th width="98.45452880859375">メソッド</th><th width="240.39398193359375">パス</th><th>用途</th></tr></thead><tbody><tr><td><code>POST</code></td><td><code>/api/Jobs</code></td><td>ジョブの作成</td></tr><tr><td><code>PUT</code></td><td><code>/api/Jobs/{jobId}</code></td><td>既存ジョブの置換</td></tr><tr><td><code>DELETE</code></td><td><code>/api/Jobs/{jobId}</code></td><td>ジョブの削除</td></tr><tr><td><code>POST</code></td><td><code>/api/Jobs/validate</code></td><td>保存せずにJSONの検証</td></tr><tr><td><code>GET</code></td><td><code>/api/Jobs</code></td><td>ジョブ一覧</td></tr><tr><td><code>GET</code></td><td><code>/api/Jobs/{jobId}</code></td><td>1件のジョブ定義の取得</td></tr><tr><td><code>POST</code></td><td><code>/api/Jobs/{jobId}/trigger</code></td><td>手動での実行依頼</td></tr></tbody></table>

プラグイン設定エンドポイントや認可の詳細を含む完全なAPIリファレンスは、[HTTPリファレンス](/keeperpam/jp/endpoint-privilege-manager/integrations/http-reference-guide.md)をご参照ください。

**検証にはバイナリの存在が必要**

`POST /api/Jobs` と `POST /api/Jobs/validate` はジョブバリデータを実行し、呼び出し時点でタスクの実行ファイルがディスク上に存在するかを確認します。つまり、APIでジョブを登録する**前に**エンドポイントへバイナリを配置するか、バイナリが想定パスに既にあるマシンで検証する必要があります。

ジョブの `osFilter` が現在のOSを除外している場合、その検証実行ではバイナリ存在チェックはスキップされます。WindowsホストからLinux専用ジョブだけを登録するといった運用が可能です。

**`JobUpdate` ポリシーの利用**

管理フリートでは、Keeperコンソール経由の `JobUpdate` ポリシーが典型的なデプロイ経路です。Keeper管理者がポリシー封筒を構成してエンドポイントへ配信し、こちらはジョブJSONを用意します。現在のポリシースキーマとコンソール手順は管理者に確認してください。関連: [ジョブの作成、変更、または削除](/keeperpam/jp/endpoint-privilege-manager/policies/policy-examples/advanced-examples/policy-create-modify-or-delete-job.md)
{% endstep %}

{% step %}

#### MQTT接続とログのパブリッシュ <a href="#connect-to-mqtt-and-publish-logs" id="connect-to-mqtt-and-publish-logs"></a>

バイナリをデプロイしジョブを登録したら、ログがエージェントのログパイプラインへ流れるよう、MQTT接続を設定します。

**手順1: ジョブでMQTT権限を宣言する**

ジョブのルートオブジェクトに `mqttTopics` を追加します。ブローカーがこのリストを強制するため、トピックが欠けているとMQTT接続自体は成功してもパブリッシュは拒否されます。

```json
"mqttTopics": {
  "allowedPublications": ["KeeperLogger"],
  "allowedSubscriptions": []
}
```

カスタムイベントトピックへもパブリッシュする場合は、[MQTTでのジョブ進捗](#optional-job-progress-over-mqtt)をご参照ください。そのトピックも `allowedPublications` に追加します。

`allowedSubscriptions` はMQTTトピックを購読する場合にのみ必要です。多くのジョブタスク用バイナリでは不要です。

**手順2: プラグイン設定からブローカーアドレスを読む**

ジョブが少なくとも1エントリを含む `mqttTopics` を定義している場合、エージェントはタスク起動前に次の環境変数を設定します。

| 変数名               | 値                     |
| ----------------- | --------------------- |
| `KEEPER_JOB_ID`   | ジョブJSONの `id` フィールド   |
| `KEEPER_JOB_NAME` | ジョブJSONの `name` フィールド |

バイナリはMQTTクライアントIDのために `KEEPER_JOB_ID` が必要で、ブローカーアドレスはプラグイン設定から取得します。起動時の例:

```python
import os
import argparse
import json
import ssl
import urllib.request

FALLBACK_HOST = "127.0.0.1"
FALLBACK_PORT = 8675

def load_broker_settings(keeper_api_base: str) -> tuple[str, int]:
    url = f"{keeper_api_base}/api/PluginSettings/KeeperPrivilegeManager"
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE  # ループバック自己署名; CAバンドルがあれば利用
    try:
        with urllib.request.urlopen(url, context=ctx, timeout=10) as resp:
            settings = json.loads(resp.read().decode())
        return settings.get("broker.host", FALLBACK_HOST), int(settings.get("broker.port", FALLBACK_PORT))
    except Exception as exc:
        print(f"[WARNING] Plugin Settings unavailable ({exc}). Using defaults.")
        return FALLBACK_HOST, FALLBACK_PORT

parser = argparse.ArgumentParser()
parser.add_argument("--keeper-api-base", required=True)
args = parser.parse_args()

job_id   = os.environ.get("KEEPER_JOB_ID", "")
job_name = os.environ.get("KEEPER_JOB_NAME", "")
broker_host, broker_port = load_broker_settings(args.keeper_api_base)
```

ここで覚えておきたい点:

* **この呼び出しにはプラグイン階層の認証が必要です。** エージェントが起動したプロセスからのみ成功します。手動起動のコピーは `403` になります。
* **エージェントはループバックに自己署名TLS証明書を使うことがあります。** 組織がエージェント証明書用のCAバンドルを配布している場合はHTTPクライアントに設定します。ない場合は、セキュリティポリシーに従い、ループバック限定で証明書検証を無効にします。
* **失敗時はデフォルトにフォールバックしつつ警告ログを残す。** 本番でプラグイン設定の取得に失敗したことは、運用者が把握すべき構成上の問題です。

**手順3: MQTTクライアントIDを組み立てる**

ブローカーはクライアントIDをジョブと突き合わせて検証します。ジョブタスク向けの形式は次のとおりです。

```
{KEEPER_JOB_ID}_{ExecutableToken}_{ProcessId}
```

意味は次のとおりです。

* `{KEEPER_JOB_ID}` — エージェントが設定した環境変数の値
* `{ExecutableToken}` — バイナリの短い固定名 (パス文字・スペース・アンダースコアなし)
* `{ProcessId}` — 現在のOSプロセスID (10進整数)

```python
import os

job_id = os.environ.get("KEEPER_JOB_ID", "unknown")
pid    = os.getpid()
client_id = f"{job_id}_SecretScanner_{pid}"
# 例: "secrets-scanner_SecretScanner_48292"
```

**ジョブIDにアンダースコアを含めないでください。** ブローカーは `_` でクライアントIDを分割してジョブID部分を取り出します。ジョブIDに `secrets_scanner` のようなアンダースコアがあると誤って解釈され、接続とジョブの対応付けやパブリッシュの許可判定に失敗します。ハイフンを使います (例: `secrets-scanner`)。

**手順4: MQTTブローカーに接続する**

プラグイン設定で取得したホストとポートに対し、TLSで接続します。ブローカーは暗号化接続を要求し、平文TCPは受け付けません。

```python
import ssl
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        print("Connected to MQTT broker")
    else:
        print(f"MQTT connection failed: {reason_code}")

mqttc = mqtt.Client(
    mqtt.CallbackAPIVersion.VERSION2,
    client_id=client_id,
    protocol=mqtt.MQTTv5
)

# TLS — CAバンドルがあれば利用、なければループバック証明書を信頼
tls_ctx = ssl.create_default_context()
tls_ctx.check_hostname = False
tls_ctx.verify_mode = ssl.CERT_NONE  # 利用可能ならCAバンドルに置き換え
mqttc.tls_set_context(tls_ctx)

mqttc.on_connect = on_connect
mqttc.connect(broker_host, broker_port, keepalive=60)
mqttc.loop_start()
```

**タイミング:** 起動直後にMQTTへ接続し接続拒否になる場合、接続試行とエージェント側のプロセス登録完了との競合の可能性があります。失敗とみなす前に1〜2秒程度の短いリトライループを入れます。

クライアントライブラリが対応していればMQTT v5を優先します。エージェント内部コンポーネントはMQTT v5を使用します。

**手順5: KeeperLoggerへログメッセージをパブリッシュする**

トピック `KeeperLogger` に対し、QoS 1、`retain: false` でパブリッシュします。各ペイロードは次の形の単一JSONオブジェクトにします。プロパティ名の大文字小文字は区別されます。

```json
{
  "Id": "b2e7f4a8-6c2d-4b1e-9f3a-8c1d2e3f4a5b",
  "Version": 1,
  "RespondToTopic": null,
  "MetaData": {
    "MessageType": 4,
    "LogLevel": 1,
    "Source": "SecretScanner",
    "Category": "SecretsScan",
    "Message": "Scan complete: 0 issues detected.",
    "CorrelationId": "",
    "Context": ""
  }
}
```

フィールドの説明:

<table><thead><tr><th width="223.5333251953125">フィールド</th><th>値</th></tr></thead><tbody><tr><td><code>Id</code></td><td>メッセージごとに新しいランダムUUID文字列</td></tr><tr><td><code>Version</code></td><td><code>1</code></td></tr><tr><td><code>RespondToTopic</code></td><td><code>null</code></td></tr><tr><td><code>MetaData.MessageType</code></td><td><code>4</code> (または文字列 <code>"Log"</code>)</td></tr><tr><td><code>MetaData.LogLevel</code></td><td><code>0</code> Debug、<code>1</code> Info、<code>2</code> Warning、<code>3</code> Error、<code>4</code> Critical、<code>5</code> Verbose、または対応する文字列</td></tr><tr><td><code>MetaData.Source</code></td><td>ログに表示されるツール名</td></tr><tr><td><code>MetaData.Category</code></td><td>フェーズやコンポーネントの短いラベル</td></tr><tr><td><code>MetaData.Message</code></td><td>人が読めるテキスト。空不可。生のシークレット値は含めず、件数やパスなどに留める</td></tr><tr><td><code>MetaData.CorrelationId</code></td><td>任意のトレースID。未使用は <code>""</code></td></tr><tr><td><code>MetaData.Context</code></td><td>任意の追加テキスト。未使用は <code>""</code></td></tr></tbody></table>

Pythonでのパブリッシュ例:

```python
import uuid
import json

def publish_log(client, source: str, category: str, message: str, level: int = 1):
    payload = {
        "Id": str(uuid.uuid4()),
        "Version": 1,
        "RespondToTopic": None,
        "MetaData": {
            "MessageType": 4,
            "LogLevel": level,
            "Source": source,
            "Category": category,
            "Message": message,
            "CorrelationId": "",
            "Context": ""
        }
    }
    client.publish("KeeperLogger", json.dumps(payload), qos=1, retain=False)

# 使用例
publish_log(mqttc, "SecretScanner", "SecretsScan", "Scan complete: 0 issues detected.")
publish_log(mqttc, "SecretScanner", "SecretsScan", "3 files could not be read.", level=2)
```

注意点:

* **`Message` は空にできません。** ロガーは空メッセージを拒否します。
* **ログに生のシークレットを含めない。** 件数、ファイルパス、状態のみを記録します。
* **`Message` や `Context` 内の生の改行は避ける。** スクレイプしたファイル内容をログに出す場合はスペースに置き換えます。
  {% endstep %}
  {% endstepper %}

## 任意: MQTTでのジョブ進捗 <a href="#optional-job-progress-over-mqtt" id="optional-job-progress-over-mqtt"></a>

構造化ログを `KeeperLogger` に送るのとは別に、下流の購読者がジョブ実行イベントを観測する必要がある場合、ジョブのイベントトピックへパブリッシュできます。

デフォルトのイベントトピックのパターンは `Jobs/{jobId}/events` です。ジョブのルートに `eventTopic` フィールドでカスタムトピックを指定することもできます。

```json
{
  "id": "secrets-scanner",
  "eventTopic": "Jobs/secrets-scanner/events",
  "mqttTopics": {
    "allowedPublications": [
      "KeeperLogger",
      "Jobs/secrets-scanner/events"
    ],
    "allowedSubscriptions": []
  }
}
```

**イベントトピックは `allowedPublications` にも含める必要があります。** リストにないと接続に成功してもパブリッシュは拒否されます。`eventTopic` を追加するときは、`eventTopic` と `allowedPublications` の両方に同じトピック文字列があることを確認してください。

## 起動前チェックリスト <a href="#pre-launch-checklist" id="pre-launch-checklist"></a>

パイロットエンドポイントで全項目を確認してからフリートへ展開してください。

<table><thead><tr><th width="59.18182373046875">#</th><th>確認項目</th></tr></thead><tbody><tr><td>1</td><td><code>POST /api/Jobs</code> 呼び出し前に、エンドポイント上で解決された <code>command</code> / <code>executablePath</code> にバイナリが存在する</td></tr><tr><td>2</td><td>ジョブの <code>id</code> がファイル名と一致し、<code>id</code> にアンダースコアがない</td></tr><tr><td>3</td><td><code>enabled</code> が <code>true</code></td></tr><tr><td>4</td><td><code>osFilter</code> が対象プラットフォームと一致する</td></tr><tr><td>5</td><td><code>mqttTopics.allowedPublications</code> に <code>KeeperLogger</code> が含まれる。<code>eventTopic</code> を使う場合はそのトピックも含まれる</td></tr><tr><td>6</td><td>デプロイ後 <code>GET /api/Jobs</code> にジョブが表示される</td></tr><tr><td>7</td><td><code>POST /api/Jobs/{jobId}/trigger</code> でタスクが成功する (管理用認可が必要)</td></tr><tr><td>8</td><td>実行中タスクに環境変数 <code>KEEPER_JOB_ID</code> と <code>KEEPER_JOB_NAME</code> が渡る</td></tr><tr><td>9</td><td>実行中タスク内から <code>GET /api/PluginSettings/KeeperPrivilegeManager</code> が <code>broker.host</code> と <code>broker.port</code> を返す</td></tr><tr><td>10</td><td>MQTTがTLSで接続でき、クライアントIDが <code>{jobId}_Token_{pid}</code> 形式である</td></tr><tr><td>11</td><td><code>KeeperLogger</code> へのパブリッシュが成功し、オペレーターのログ画面にメッセージが出る</td></tr><tr><td>12</td><td><code>eventTopic</code> を使う場合、そのトピックへのパブリッシュが成功する</td></tr><tr><td>13</td><td>コード署名: 本番バイナリは署名済み。必要ならサムプリントが <code>AlternativeSignatures</code> にある</td></tr></tbody></table>

## トラブルシューティング <a href="#troubleshooting" id="troubleshooting"></a>

| 症状                                           | 確認先                                                |
| -------------------------------------------- | -------------------------------------------------- |
| `POST /api/Jobs` が `403` を返す                 | 管理用認可。証明書が誤っている、または呼び出しプロセスが管理用として認可されていない         |
| `POST /api/Jobs` が検証エラーで `400` を返す           | 検証ホスト上のパスにバイナリがない。先にバイナリをデプロイする                    |
| `GET /api/Jobs` に出るが実行されない                   | `enabled: true` か確認。`osFilter` がエンドポイントのOSと一致するか確認 |
| 手編集した `Jobs/*.json` が元に戻る                    | Last Known Good が有効。APIまたは `JobUpdate` ポリシーを使う     |
| タスク内から `GET /api/PluginSettings/...` が `403` | プロセスが信頼済みとして登録されていない。手動ではなくジョブランナーから起動されているか確認     |
| MQTT接続が拒否される                                 | 起動済みプロセス登録簿に載っていない。起動時にリトライロジックを追加                 |
| MQTTは接続するがパブリッシュが拒否される                       | ジョブJSONの `mqttTopics.allowedPublications` にトピックがない |
| クライアントIDの解析エラー / ジョブの取り違え                    | ジョブ `id` にアンダースコアがある。ハイフンに変更                       |
| オペレーター画面にログが出ない                              | KeeperLogger が未起動または未購読の可能性。管理者に確認                 |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/jp/endpoint-privilege-manager/integrations/custom-job-guide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
