performing-agentless-vulnerability-scanning について
エンドポイントへのエージェント導入なしで脆弱性を調べる方法を扱うスキルです。セキュリティ評価や監査の担当者向けに、SSH、WMI/WinRM、クラウドスナップショット、コンテナイメージの手法と Vuls の設定例を紹介します。
主な機能
- 01SSH 経由で Linux/Unix をスキャン
- 02WMI または WinRM で Windows をスキャン
- 03クラウドスナップショットで VM ディスクを分析
- 04コンテナイメージとクラウド設定をスキャン
- 05Vuls の設定と実行
活用シーン
- 01エンドポイントエージェントを導入できないシステムの評価
- 02スナップショットによるクラウド VM の確認
- 03Linux と Windows ホストの定期的なセキュリティ監査
- 04コンテナイメージの脆弱性検査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill performing-agentless-vulnerability-scanningインストール後、Claude Code で/performing-agentless-vulnerability-scanningを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEassets/template.mdreferences/api-reference.mdstandards.mdworkflows.mdscripts/agent.pyprocess.py詳細
エージェントレス脆弱性スキャンの実施
概要
エージェントレス脆弱性スキャンでは、エンドポイントにエージェントをインストールせずに、システムのセキュリティ上の弱点を評価します。この方法では、既存のネットワークプロトコル(Linux では SSH、Windows では WMI)、スナップショットベースの分析を行うクラウドプロバイダー API、認証済みのリモートチェックを活用します。Microsoft Defender for Cloud、Wiz、Datadog、Tenable などの最新のクラウドプラットフォームは、ディスクスナップショットを取得し、OS の設定やインストール済みパッケージをオフラインで調査することで、帯域外分析を実施します。オープンソースツールの Vuls は、Linux/FreeBSD システムを対象に、NVD および OVAL のデータに基づくエージェントレススキャンを提供します。このスキルでは、オンプレミス、クラウド、コンテナ化された環境にわたるエージェントレススキャンの設定方法を説明します。
使用するタイミング
- エージェントレス脆弱性スキャンを実施するセキュリティ評価を行う場合
- 関連するセキュリティイベントに対するインシデント対応手順に従う場合
- 定期的なセキュリティテストや監査を実施する場合
- 実地テストを通じてセキュリティ制御を検証する場合
前提条件
- Linux/Unix の対象で SSH キーベースの認証が設定されていること
- Windows の対象で、適切な認証情報を使用した WMI/WinRM アクセスが可能であること
- クラウドプロバイダーの API 認証情報(AWS IAM、Azure RBAC、GCP IAM)
- 必要なポートを使用して、スキャナーから対象システムへのネットワークアクセスが可能であること
- 対象システムの設定に対する読み取り専用アクセス権を持つサービスアカウント
- カスタムのスキャン自動化用の Python 3.8+
基本概念
エージェントレススキャンとエージェントベーススキャンの比較
| 項目 | エージェントレス | エージェントベース |
|---|---|---|
| 導入 | ソフトウェアのインストールは不要 | すべてのエンドポイントにエージェントをインストール |
| ネットワーク依存性 | ネットワーク接続が必要 | クラウド同期によりオフラインでも動作 |
| パフォーマンスへの影響 | 対象システムへの影響は最小限 | 軽微な継続的負荷 |
| 対象範囲の深さ | プロトコル/認証情報による | ローカルに深くアクセス |
| クラウドスナップショット分析 | ネイティブで対応 | 該当なし |
| 最適な対象 | クラウド VM、IoT、レガシーシステム、OT | 管理対象エンドポイント、ノート PC |
エージェントレススキャンの方法
| 方法 | プロトコル | 対象 OS | ポート | 用途 |
|---|---|---|---|---|
| SSH リモートコマンド | SSH | Linux/Unix | 22 | パッケージの列挙、設定監査 |
| WMI リモートクエリ | WMI/DCOM | Windows | 135, 445 | ホットフィックスの列挙、レジストリの確認 |
| WinRM PowerShell | WS-Man | Windows | 5985/5986 | リモートコマンドの実行 |
| SNMP コミュニティ | SNMP v2c/v3 | ネットワークデバイス | 161 | デバイスのフィンガープリント取得、ファームウェアの確認 |
| クラウドスナップショット | プロバイダー API | クラウド VM | N/A | ディスクイメージ分析 |
| コンテナレジストリ | HTTPS | コンテナイメージ | 443 | イメージの脆弱性スキャン |
| API ベース | REST/HTTPS | SaaS/クラウド | 443 | 設定評価 |
クラウドスナップショット分析の流れ
1. Scanner requests disk snapshot via cloud API
2. Cloud provider creates snapshot of VM root + data disks
3. Scanner mounts snapshot in isolated analysis environment
4. Scanner examines OS packages, configurations, file system
5. Snapshot is deleted after analysis (no persistent copies)
6. Results sent to central management console
ワークフロー
ステップ 1:SSH ベースのエージェントレススキャン(Linux)
# Create dedicated scan SSH key pair
ssh-keygen -t ed25519 -f /opt/scanner/.ssh/scan_key -N "" \
-C "[email protected]"
# Deploy public key to targets via Ansible
# ansible-playbook deploy_scan_key.yml
# Test connectivity to target
ssh -i /opt/scanner/.ssh/scan_key -o ConnectTimeout=10 \
scanner@target-host "cat /etc/os-release && dpkg -l 2>/dev/null || rpm -qa"
import paramiko
import json
class AgentlessLinuxScanner:
"""SSH-based agentless vulnerability scanner for Linux systems."""
def __init__(self, key_path):
self.key_path = key_path
def connect(self, hostname, username="scanner", port=22):
"""Establish SSH connection to target."""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
key = paramiko.Ed25519Key.from_private_key_file(self.key_path)
client.connect(hostname, port=port, username=username, pkey=key,
timeout=30, banner_timeout=30)
return client
def get_os_info(self, client):
"""Detect OS type and version."""
_, stdout, _ = client.exec_command("cat /etc/os-release", timeout=10)
os_release = stdout.read().decode()
info = {}
for line in os_release.strip().split("\n"):
if "=" in line:
key, val = line.split("=", 1)
info[key] = val.strip('"')
return info
def get_installed_packages(self, client):
"""Enumerate installed packages."""
# Try dpkg (Debian/Ubuntu)
_, stdout, _ = client.exec_command(
"dpkg-query -W -f='${Package}|${Version}|${Architecture}\\n'",
timeout=30
)
output = stdout.read().decode().strip()
if output:
packages = []
for line in output.split("\n"):
parts = line.split("|")
if len(parts) >= 2:
packages.append({
"name": parts[0],
"version": parts[1],
"arch": parts[2] if len(parts) > 2 else "",
"manager": "dpkg"
})
return packages
# Try rpm (RHEL/CentOS/Fedora)
_, stdout, _ = client.exec_command(
"rpm -qa --queryformat '%{NAME}|%{VERSION}-%{RELEASE}|%{ARCH}\\n'",
timeout=30
)
output = stdout.read().decode().strip()
packages = []
for line in output.split("\n"):
parts = line.split("|")
if len(parts) >= 2:
packages.append({
"name": parts[0],
"version": parts[1],
"arch": parts[2] if len(parts) > 2 else "",
"manager": "rpm"
})
return packages
def check_kernel_version(self, client):
"""Get running kernel version."""
_, stdout, _ = client.exec_command("uname -r", timeout=10)
return stdout.read().decode().strip()
def check_listening_ports(self, client):
"""Enumerate listening network services."""
_, stdout, _ = client.exec_command(
"ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null",
timeout=10
)
return stdout.read().decode().strip()
def scan_host(self, hostname, username="scanner"):
"""Perform full agentless scan of a host."""
print(f"[*] Scanning {hostname}...")
client = self.connect(hostname, username)
result = {
"hostname": hostname,
"os_info": self.get_os_info(client),
"kernel": self.check_kernel_version(client),
"packages": self.get_installed_packages(client),
"listening_ports": self.check_listening_ports(client),
}
client.close()
print(f" [+] Found {len(result['packages'])} packages on {hostname}")
return result
ステップ 2:WinRM ベースのエージェントレススキャン(Windows)
import winrm
class AgentlessWindowsScanner:
"""WinRM-based agentless vulnerability scanner for Windows."""
def __init__(self, username, password, domain=None):
self.username = username
self.password = password
self.domain = domain
def connect(self, hostname, use_ssl=True):
"""Create WinRM session."""
port = 5986 if use_ssl else 5985
transport = "ntlm"
user = f"{self.domain}\\{self.username}" if self.domain else self.username
session = winrm.Session(
f"{'https' if use_ssl else 'http'}://{hostname}:{port}/wsman",
auth=(user, self.password),
transport=transport,
server_cert_validation="ignore"
)
return session
def get_installed_hotfixes(self, session):
"""Get installed Windows updates/hotfixes."""
cmd = "Get-HotFix | Select-Object HotFixID,InstalledOn,Description | ConvertTo-Json"
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return []
def get_installed_software(self, session):
"""Enumerate installed software from registry."""
cmd = """
$paths = @(
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
)
Get-ItemProperty $paths -ErrorAction SilentlyContinue |
Where-Object {$_.DisplayName} |
Select-Object DisplayName, DisplayVersion, Publisher |
ConvertTo-Json
"""
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return []
def get_os_info(self, session):
"""Get Windows OS details."""
cmd = "Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture | ConvertTo-Json"
result = session.run_ps(cmd)
if result.status_code == 0:
return json.loads(result.std_out.decode())
return {}
def scan_host(self, hostname):
"""Perform full agentless scan of Windows host."""
print(f"[*] Scanning {hostname} via WinRM...")
session = self.connect(hostname)
result = {
"hostname": hostname,
"os_info": self.get_os_info(session),
"hotfixes": self.get_installed_hotfixes(session),
"software": self.get_installed_software(session),
}
print(f" [+] Found {len(result['hotfixes'])} hotfixes, "
f"{len(result['software'])} software entries")
return result
ステップ 3:クラウドスナップショットスキャン(AWS)
import boto3
import time
class AWSSnapshotScanner:
"""AWS EC2 agentless snapshot-based vulnerability scanner."""
def __init__(self, region="us-east-1"):
self.ec2 = boto3.client("ec2", region_name=region)
def create_snapshot(self, volume_id, description="Security scan snapshot"):
"""Create EBS snapshot for analysis."""
snapshot = self.ec2.create_snapshot(
VolumeId=volume_id,
Description=description,
TagSpecifications=[{
"ResourceType": "snapshot",
"Tags": [
{"Key": "Purpose", "Value": "VulnScan"},
{"Key": "AutoDelete", "Value": "true"},
]
}]
)
snapshot_id = snapshot["SnapshotId"]
print(f" [*] Creating snapshot {snapshot_id} from {volume_id}...")
waiter = self.ec2.get_waiter("snapshot_completed")
waiter.wait(SnapshotIds=[snapshot_id])
print(f" [+] Snapshot {snapshot_id} ready")
return snapshot_id
def delete_snapshot(self, snapshot_id):
"""Clean up snapshot after analysis."""
self.ec2.delete_snapshot(SnapshotId=snapshot_id)
print(f" [+] Deleted snapshot {snapshot_id}")
def scan_instance(self, instance_id):
"""Scan an EC2 instance via snapshot analysis."""
print(f"[*] Agentless scan of instance {instance_id}")
instance = self.ec2.describe_instances(
InstanceIds=[instance_id]
)["Reservations"][0]["Instances"][0]
root_volume = None
for bdm in instance.get("BlockDeviceMappings", []):
if bdm["DeviceName"] == instance.get("RootDeviceName"):
root_volume = bdm["Ebs"]["VolumeId"]
break
if not root_volume:
print(" [!] No root volume found")
return None
snapshot_id = self.create_snapshot(root_volume)
try:
# Analysis would be performed here
# Mount snapshot, examine packages, check configs
result = {
"instance_id": instance_id,
"snapshot_id": snapshot_id,
"root_volume": root_volume,
"platform": instance.get("Platform", "linux"),
"state": instance["State"]["Name"],
}
return result
finally:
self.delete_snapshot(snapshot_id)
ステップ 4:Vuls オープンソースエージェントレススキャナー
# /etc/vuls/config.toml - Vuls configuration for agentless scanning
[servers]
[servers.web-server-01]
host = "192.168.1.10"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast"]
[servers.db-server-01]
host = "192.168.1.20"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast-root"]
[servers.db-server-01.optional]
[servers.db-server-01.optional.sudo]
password = ""
[servers.container-host-01]
host = "192.168.1.30"
port = "22"
user = "vuls"
keyPath = "/opt/vuls/.ssh/scan_key"
scanMode = ["fast"]
containersIncluded = ["${running}"]
# Run Vuls agentless scan
vuls scan
# Generate report
vuls report -format-json -to-localfile
# View results
vuls tui
ベストプラクティス
- Linux のスキャンでは、パスワードではなく SSH キーベースの認証を使用します
- スキャン用に、最小限の読み取り専用権限を持つ専用サービスアカウントを作成します
- ストレージコストやデータ漏えいを避けるため、分析後は必ずクラウドスナップショットを削除します
- 包括的なカバレッジを実現するため、エージェントレススキャンとエージェントベーススキャンを組み合わせます
- パフォーマンスへの影響を最小限に抑えるため、利用の少ない時間帯にスキャンをスケジュールします
- スキャン用認証情報を定期的にローテーションし、シークレット保管庫に保存します
- 本番環境のスキャンをスケジュールする前に、スキャナーの接続性をテストします
- ネットワークデバイスのスキャンでは、認証と暗号化を使用する SNMPv3 を利用します
よくある落とし穴
- 適切に分離せず、複数の環境で共有認証情報を使用する
- クラウド環境で一時スナップショットを削除し忘れる
- エージェントレススキャンはパフォーマンスにまったく影響しないと想定する(ネットワークと CPU を使用します)
- 新しいデプロイで WinRM/SSH のファイアウォールルールが不足し、スキャンに失敗する
- SSH ホストキーの変更によって認証に失敗することを考慮しない
- 安全に処理できないプロトコルで OT/ICS デバイスをスキャンする
関連スキル
- implementing-rapid7-insightvm-for-scanning
- implementing-wazuh-for-vulnerability-detection
- deploying-osquery-for-endpoint-monitoring
- performing-remediation-validation-scanning
コメント
Markdown 対応