ServiceNow を AI から自然言語で操作する MCP サーバーに、脆弱性対応(Vulnerability Response / USEM) 専用のツール群を追加しました。
脆弱性アイテム(VI)・修復タスク・NVD・統合フィード・SLA・通知・例外承認まで、SecOps の一連の運用を AI から扱えます。
GitHub: https://github.com/tedorigawa001/ServiceNow-MCP
追加した機能
src/tools/ に USEM 系モジュールを足しました。
secops_analyst というツールパッケージに集約してあります。
| モジュール | 主なツール |
|---|---|
usem.ts |
VI/修復タスク/NVD/脆弱性グループの一覧・取得・作成・更新 |
usem-config.ts |
アサインメント/修復タスク/TTR/承認/自動クローズ/除外の各ルール操作 |
usem-integration.ts |
統合フィード(NVD/Qualys/CSAF…)のカタログ・実行履歴・ログ・有効化 |
usem-sla.ts |
TTR ベースの SLA 状況・コミット日設定・VR 通知の発見 |
usem-approval.ts |
例外申請の可視化と、承認/却下による state 遷移 |
以下は私が実装していく上でハマった落とし穴です。。。
落とし穴1: 「修復タスク」は2つある
USEM のデータモデルを調べ始めて最初に混乱したのがこれです。
USEMには「Remediation Task」と名の付くものが2系統あります。
-
sn_vul_remediation_task… 新しめのモデル。task テーブルを継承していないベーステーブル。人間可読キーはnumberではなくtask_number -
sn_vul_vulnerability… 「Vulnerability Group」。実はsys_classのラベルが文字通り "Remediation Task"、番号プレフィクスはVUL。そしてこちらは task を継承している
最初、私は前者だけを「修復タスク」として実装し、後者の継承を見落としていました。
レビューで「sn_vul_vulnerability は task を継承しているはず」と指摘され、実機で確認:
sn_vul_vulnerability -> task ← task 継承(ご指摘どおり)
sn_vul_app_vulnerability -> task
sn_vul_remediation_task (継承なし)
sn_vul_vulnerable_item (継承なし)
これが重要なのは、task 継承=task_sla が効くからです。
VI/RT 自身は task ではないので task_sla(汎用の get_sla_details)が使えませんが、グループは使えます。record_type に vi/rt/vg の3種を用意し、グループは task_sla とTTR の両方を返すようにしました。
教訓: 「修復タスク」という UI ラベルだけで実テーブルを推測しない。sys_db_object.super_class を辿って継承を確認する。
落とし穴2: VI/RTのstate は直接 PATCH できない
グループの作成・更新ツールを実装し、書き込み権限ももらって実機で create→update を流したところ、state への更新だけが INSUFFICIENT_PRIVILEGES で弾かれました。フィールド単位で切り分けると:
create : OK
update(short_description) : OK
update(state) : FAIL (INSUFFICIENT_PRIVILEGES)
delete : OK
さらに VI の deferral 用フィールド(ignore_reason/ignore_date)を直接書いてみると、「受理されるが業務ルールで即時リバート」(state は Open のまま、ignore も空に戻る)。
つまり VR では state は Table API のフィールド書き込みでは動かせない設計です。
state を動かすのはワークフロー(承認)の役割でした。
教訓: VR レコードは「自動化が主導」。state はワークフロー経由でしか動かない。
落とし穴3: 例外承認は sysapproval を埋めない
「state を動かすのは承認」と分かったので、承認(sysapproval_approver)を一覧するツールを作りました。最初は素直に sysapproval.sys_class_name で VR レコード由来の承認を絞り込む実装に。
ところが実機で Exception Request を起票すると 0 件。
承認レコードを1件ダンプして気づきました:
sysapproval : (空)
source_table : sn_sec_exception_change_approval
document_id : Change Approval: CA0010004
approval_source : sn_vul_vulnerability ← 最終的な VR レコードのクラス
VR の例外承認は sysapproval を埋めません。
代わりに source_table + document_id で例外レコードに紐づき、approval_source に最終的な VR クラスが入ります。フィルタを source_table ベースに直したら、無事に保留中の承認が取れました。
// 修正後: source_table で VR 由来の承認を絞る
const VR_APPROVAL_CLASSES = [
'sn_sec_exception_change_approval',
'sn_vul_vulnerability', 'sn_vul_vulnerable_item', /* ... */
];
query = `source_tableIN${VR_APPROVAL_CLASSES.join(',')}^state=requested`;
そして承認/却下は、検証済みの汎用承認と同じく sysapproval_approver.state を更新するだけ。これがワークフローを進め、結果として VR レコードの state を遷移させます。実機で 承認→state approved→requested に復元 のラウンドトリップまで確認しました。
落とし穴4: 例外申請の本体テーブル
「例外申請テーブルが見当たらない」と一度あきらめていたのですが、False Positive 申請を起票し、承認の document_id を辿ったら見つかりました。
本体は sn_sec_exception_change_approval です。
request_type : False positive (他に Exception / Risk Acceptance)
record + table : Vulnerable Item: VIT0011335
desired_state : 3 (Closed)
desired_substate : 22 (False Positive) ← 承認時の遷移先
approval_state : 0 (In Review)
desired_reason : (申請理由)
「どの VI に対する」「何の種類の(誤検知申請/例外リクエスト)」「承認したら何になる」申請なのかが一覧で見えます。
申請の可視化 → 承認の確認 → 承認/却下 → state 遷移
設計上の工夫: レジストリ方式
設定系テーブル(アサインメントルール/修復タスクルール/TTR/承認ルール/自動クローズ/除外ルール)はスキーマもキー項目もバラバラでした。。。。
これを rule_type のレジストリで吸収し、list/get/create/update/set_active の少数ツールで全種を扱えるようにしました。SLA の record_type(vi/rt/vg)も同じ発想です。テーブルが増えてもレジストリに1行足すだけで済みます。
検証とテストの方針
VR は「実機で叩かないと分からない」ことが多すぎたので、全ツールを実 PDIで検証しました。
- 読み取り: 実データで挙動を確認(NVD ラン、TTR 違反グループ、例外申請など)
- 書き込み: create→update→get→delete のラウンドトリップを流し、テスト用レコードは必ず削除して残留ゼロを確認
-
クエリ安全性: 未来日付は
gs.daysAgo(-N)(allowlist 済み)で注入、source/state等はサニタイズ
ユニットテストは Vitest でモック化。
USEM 系モジュールは func 100% / stmt ほぼ 100% / branch 85〜95%、リポジトリ全体で 457 テスト が green です。
まとめ
VR/USEM を API から触るときに効く実機知見:
- 「Remediation Task」は
sn_vul_remediation_taskと task 継承のsn_vul_vulnerabilityの2系統。継承はsuper_classを辿って確認する - state は直接 PATCH できない。作成/更新後は業務ルールが値を再計算する
- 例外承認は
sysapprovalカラム ではなくsource_table/document_id/approval_sourceカラムで紐づく - 例外申請の本体は
sn_sec_exception_change_approval(request_type / desired_substate を持つ)
