S³ ASM
ATTACK SURFACE MANAGEMENT
/* ASSET_INVENTORY */
登録資産
-
ドメイン / IP
enum: domain | subdomain | ip
/* CREDENTIAL_LEAK */
シークレット検出
-
要確認
17 regex patterns + entropy
/* TLS_MONITOR */
SSL/TLS問題
-
証明書チェック
x509 + HSTS + chain verify
/* UNIFIED_FINDINGS */
統合findings
-
open
source: sast|sca|secret|ssl
/* RISK_POSTURE */
ASMリスクスコア
-
/100
Σ(severity × weight)
ASMとは?

Attack Surface Managementは、組織の外部から攻撃可能な「攻撃対象面」を継続的に発見・監視・評価する手法です。

S³のASMモジュールは、従来のSAST/SCAに加えてシークレット漏洩・SSL/TLS設定・公開資産を統合的にスキャンし、リスクを一元管理します。

シークレット検出エンジン

17種の正規表現パターンでハードコードされた認証情報を検出します。

AWS Access Key GitHub Token Google API Key Private Key (RSA/EC) JWT Token Connection String Slack/Discord Webhook Stripe/SendGrid Key

検出後、シャノンエントロピー(情報量計算)でランダム性を評価。entropy < 3.0 の文字列はダミー値として誤検知フィルタリング。

エントロピー分析

シャノンエントロピー H = -Σ p(x) log₂ p(x) で文字列のランダム性を定量化。

H < 3.0 低エントロピー → 辞書語・ダミー値の可能性大
3.0 ≤ H < 4.0 中エントロピー → パスワード等の可能性
H ≥ 4.0 高エントロピー → ランダム生成キーの可能性大

パターンマッチなしでも高エントロピー文字列(H≥4.0, 20文字以上)を追加検出するscan_high_entropy()モードあり。

SSL/TLS証明書検査

Python ssl + cryptography ライブラリでTLSハンドシェイク → x509証明書パースを実行。

DNS解決 TLS接続 証明書取得 チェーン検証 HSTS確認 評価

検査項目: 有効期限 チェーン検証 TLSバージョン 暗号スイート HSTS SAN

弱暗号・弱プロトコル検出

ネゴシエートされたTLSバージョンと暗号スイートをWEAK_SETと照合。

WEAK_PROTOCOLS TLSv1, TLSv1.1, SSLv2, SSLv3
WEAK_CIPHERS RC4, DES, 3DES, NULL, EXPORT, anon

CA証明書バンドルは certifi を使用。macOS/LinuxのシステムCA差異を吸収し、一貫した証明書チェーン検証を保証。

統一リスクスコアリング

全検出結果(SAST/SCA/Secret/SSL)にカテゴリ別重み付けを適用した統一スコア。

Exposed Secret ×3.0(即座に悪用可能)
DAST Finding ×2.5(実環境確認済み)
Open Port ×2.0(攻撃面露出)
SAST Critical ×1.5
SSL/TLS Issue ×1.0

検出後30日未対応で×1.290日未対応で×1.5の減衰因子を適用。パッチ適用済みは×0.0。

Secret Scanner Pipeline

Pattern Match: re.finditer() で17パターンを行単位でスキャン
Entropy Calc: H = -Σ (count/len) × log₂(count/len)
FP Filter: Allowlist 10パターン(example, dummy, placeholder, test, TODO, etc.)
Masking: value[:4] + '***' + value[-4:](DB保存時にマスク済み)

SSL/TLS Handshake Flow

1st attempt: ssl.create_default_context(cafile=certifi.where()) + check_hostname=True
On SSLCertVerificationError: verify_mode=CERT_NONE で再接続 → cert取得のみ、chain_valid=False
HSTS: http.client.HTTPSConnection.request('HEAD')Strict-Transport-Security ヘッダー存在確認
Parse: x509.load_der_x509_certificate() → issuer/subject/SAN/serial/signature_algorithm

ASM Database Schema

DB: SQLite asm.db(S³ Core の scan.db / settings.db と分離)
Tables:
asm_assets (tenant_id, asset_type, value, discovery_method, risk_score)
asm_certificates (asset_id FK, domain, issuer, days_remaining, tls_version, hsts, chain_valid)
asm_secrets (repo_id, file_path, line_number, secret_type, masked_value, entropy)
asm_findings (source, severity, category, title, risk_score, status)

ASM Roadmap

Phase 1 (NOW) シークレット検出 + SSL/TLS監視
Phase 2 資産ディスカバリ(crt.sh/DNS列挙)+ ポートスキャン
Phase 3 DAST(Nuclei連携)+ クラウド設定監査(IaC解析)

Engine: FastAPI(非同期)+ Redis/Celery ジョブキュー
UI: S³統合ダッシュボード(Flask/Jinja2)

検出パイプライン概要

SecretScanner.scan_content() は各ファイルに対して3段階の検出パイプラインを実行します。

入力ファイル 行分割 17パターンマッチ Allowlistフィルタ エントロピー計算 マスク処理 結果出力

re.finditer(pattern, line) → 各行に対して17パターンを順次照合。マッチしたら即 calculate_entropy() で情報量評価。

正規表現パターン一覧(17種)
AWS Access Key AKIA[0-9A-Z]{16}
AWS Secret Key [0-9a-zA-Z/+=]{40}
GitHub Token gh[ps]_[A-Za-z0-9_]{36,}
GitLab Token glpat-[A-Za-z0-9\-]{20,}
Google API Key AIza[0-9A-Za-z\-_]{35}
JWT Token eyJ[A-Za-z0-9_-]+\.eyJ...
Private Key (PEM) BEGIN (RSA|EC|DSA|OPENSSH) PRIVATE KEY
Connection String (?:mysql|postgres|mongodb)://.*:.*@
Slack Webhook hooks\.slack\.com/services/T[A-Z0-9]+/B...
Discord Webhook discord\.com/api/webhooks/[0-9]+/...
Stripe Key sk_live_[0-9a-zA-Z]{24,}
SendGrid Key SG\.[A-Za-z0-9_-]{22}\.
Twilio Key SK[0-9a-fA-F]{32}
Heroku API Key [0-9a-fA-F]{8}-...-[0-9a-fA-F]{12}
Generic Secret (?:password|secret|token|api_key)\s*[=:]\s*["']...
Generic Bearer Bearer\s+[A-Za-z0-9_\-.]{20,}
Base64 Long [A-Za-z0-9+/]{64,}={0,2}
シャノンエントロピー H(X)

情報理論のシャノンエントロピーを使い、文字列のランダム性を定量評価します。

H(X) = -Σ p(xᵢ) × log₂(p(xᵢ))

H < 2.0 極低 → 反復文字列 ("aaaa", "1111")
H = 2.0〜3.0 低 → 辞書語・ダミー値 ("password", "example")
H = 3.0〜4.0 中 → 簡易パスワード・短縮ID
H ≥ 4.0 高 → ランダム生成キー・暗号学的トークン

HIGH_ENTROPY_THRESHOLD = 4.0 — これ以上はパターンなしでも scan_high_entropy() で検出可能。

誤検知フィルタリング

10個のAllowlistパターンで既知の安全な文字列を除外します。

ALLOWLIST 以下に該当→ is_likely_false_positive = True
example, dummy, placeholder, test, TODO, FIXME,
your[-_]?api[-_]?key, insert[-_]?token, xxx, sample

FPフラグは検出結果から除外しない(ユーザー判断用)。UIで FP_LIKELY / CONFIRMED と表示。

マスク処理 & DB保存

検出されたシークレットは即座にマスク処理してDBに保存されます。

mask_secret(value) → value[:4] + "***" + value[-4:]

例: AKIAIOSFODNN7EXAMPLEAKIA***MPLE

DB: asm_secrets テーブルに masked_value として保存。生の値はメモリ上のみで、永続化されません。

深刻度判定ロジック

パターンのカテゴリエントロピーの組み合わせで判定。

CRITICAL AWS Key, Private Key (H≥4.0)
HIGH GitHub/GitLab Token, Connection String, Stripe Key
MEDIUM Google API Key, JWT, Generic Secret
LOW Bearer Token, Webhook URL, Base64
INFO FP_LIKELY のもの全般
/* SECRET_SCANNER::init() */
シークレットスキャン
// regex_patterns: 17 | entropy_threshold: 4.0 | allowlist: 10 patterns | shannon_entropy + false_positive_filter
/* DISTRIBUTION::type */
シークレット種別分布
/* DISTRIBUTION::severity */
深刻度分布
/* FINDINGS::secrets */
検出されたシークレット
// columns: severity | type | path | line | masked_value | shannon_entropy | verdict
深刻度 種別 ファイル マスク値 エントロピー 判定
🔑
シークレットスキャン未実行
TLSハンドシェイクフロー

SSLChecker.check_domain() の実行フロー:

DNS解決
gethostbyname()
TCP接続
create_connection()
TLS Wrap
wrap_socket()
証明書取得
getpeercert(binary)
x509パース
load_der_x509()
HSTS確認
HEAD / STS
二段階接続戦略

_get_certificate()2フェーズで証明書を取得します。

Phase 1 ssl.create_default_context(cafile=certifi.where())
check_hostname=True, verify_mode=CERT_REQUIRED
→ 成功時: chain_valid=True

Phase 2 (fallback) SSLCertVerificationError 発生時
check_hostname=False, verify_mode=CERT_NONE で再接続
→ 証明書は取得できるが chain_valid=False

certifi バンドル使用でOS間のCA差異を吸収。macOS/Linuxの証明書ストア差分を解消。

x509証明書パース項目

cryptography.x509.load_der_x509_certificate() で抽出する情報:

Issuer CN NameOID.COMMON_NAME 発行者名
Subject CN サーバー名(コモンネーム)
SAN ExtensionOID.SUBJECT_ALTERNATIVE_NAME → DNSName[]
Serial Number hex形式
Signature Algorithm signature_algorithm_oid._name
Not Before / Not After not_valid_before_utc / not_valid_after_utc
Days Remaining (not_after - now).days
弱暗号・弱プロトコル検出

_evaluate_status() で以下をチェック:

WEAK_PROTOCOLS
SSLv2, SSLv3, TLSv1, TLSv1.1HIGH
WEAK_CIPHERS
RC4, DES, 3DES, NULL, EXPORT, anonHIGH
推奨 TLSv1.2+ / AES-256-GCM / ECDHE
HSTS (HTTP Strict Transport Security)

_check_hsts()http.client.HTTPSConnectionHEAD / を送信。

応答ヘッダーに Strict-Transport-Security が存在すれば
HSTS ✓
存在しなければ → NO HSTS (severity を LOW に引き上げ)

HSTSはブラウザにHTTPS強制を指示するヘッダー。ダウングレード攻撃(SSL stripping)を防止。

ステータス評価マトリクス
CRITICAL 有効期限切れ (days ≤ 0)
HIGH 7日以内 / 弱プロトコル / 弱暗号
MEDIUM 30日以内 / チェーン検証失敗
LOW HSTS未設定のみ
INFO 問題なし (OK)

複数問題がある場合は最も高い深刻度を採用。issuesリストに全問題を列挙。

/* SSL_CHECKER::handshake() */
SSL/TLS証明書チェック
// x509.load_der → issuer/subject/SAN/validity | ssl.create_default_context(cafile=certifi) | HSTS via HEAD
/* CERT_STATUS_MATRIX */
証明書ステータス一覧
// check: chain_valid | days_remaining | tls_version ∈ {1.2, 1.3} | cipher ∉ WEAK_SET | HSTS header
ドメイン 発行者 有効期限 残日数 TLS HSTS チェーン ステータス
🔒
証明書チェック未実行
デュアルソース列挙

AssetDiscovery.discover_all() は2つの独立したソースを統合します。

crt.sh
CT Log API
+ DNS Brute
50 prefixes
マージ
dedup by name
IP補完
cross-fill
資産登録
asm_assets

crt.sh で見つかったサブドメインにDNSのIP情報を補完。両方で見つかれば信頼度が高い。

crt.sh — Certificate Transparency

証明書透過性ログ(CT Log)は、CAが発行した全証明書を公開ログに記録する仕組みです。

API https://crt.sh/?q=%25.{domain}&output=json
Parse name_value フィールドから改行分割でFQDN抽出
Filter ワイルドカード (*.) 除外、ドメイン外除外
Metadata issuer_name, not_before, not_after 保持

CT Logはパッシブ手法であり、対象サーバーにアクセスしません。過去に発行された証明書の履歴から検出。

DNSブルートフォース

50個の一般的プレフィックスをドメインに結合し、DNSのAレコード解決を試行。

インフラ系 www, api, app, cdn, static, assets, docs
開発系 dev, staging, test, ci, cd, git, jenkins
監視系 monitoring, grafana, prometheus, kibana, elastic
DB系 db, mysql, postgres, mongo, redis, cache
認証系 auth, sso, login, oauth, vpn, remote
内部系 internal, intranet, admin, portal
DNS解決エンジン

resolve_ip()2段階フォールバック:

優先 dns.resolver.resolve(hostname, "A") (dnspython)
→ タイムアウト制御、NXDOMAIN正確判定

フォールバック socket.getaddrinfo(hostname, None, AF_INET)
→ dnspython未インストール時に使用。OS標準リゾルバ経由。

解決失敗 = そのサブドメインは存在しない(レスポンスとして除外)

/* ASSET_DISCOVERY::discover_all() */
サブドメイン列挙
// crt.sh CT logs + DNS brute_force (50 prefixes) | merge + dedup | resolve_ip(dnspython → socket fallback)
/* SOURCES::breakdown */
検出ソース内訳
ディスカバリ統計

まだディスカバリが実行されていません

/* SUBDOMAIN_MATRIX */
検出サブドメイン一覧
// subdomain | ip | source | issuer | cert_validity
サブドメイン IP 検出ソース 証明書発行者 証明書有効期限
🔍
ディスカバリ未実行
TCPコネクトスキャン

PortScanner.scan_port()socket.connect_ex() によるフルTCP 3-way handshakeでポート状態を判定。

SYN送信 SYN-ACK受信 ACK送信 接続確立 状態判定
connect_ex() == 0OPEN
connect_ex() != 0 → CLOSED (RST received)
socket.timeout → FILTERED (パケットドロップ/FW)
並列実行アーキテクチャ

scan_host()ThreadPoolExecutor で最大50スレッド並列実行。

max_workers 50 — I/Oバウンドなのでスレッドが適切
timeout 2.0s / ポート — 応答なしはfiltered判定
実行時間 26ポート × 2s timeout = 理論最大52s → 並列で数秒
as_completed() 完了順に結果収集(先着順表示)

複数ホスト (scan_hosts()) はレート制御のため直列実行。1ホスト内のポートのみ並列。

TOP 26ポート一覧
21 FTP 22 SSH 23 Telnet 25 SMTP
53 DNS 80 HTTP 110 POP3 143 IMAP
443 HTTPS 445 SMB 993 IMAPS 995 POP3S
1433 MSSQL 1521 Oracle 3306 MySQL 3389 RDP
5432 PostgreSQL 5672 RabbitMQ 5900 VNC 6379 Redis
8080 HTTP-Alt 8443 HTTPS-Alt 9200 Elasticsearch
9090 Prometheus 11211 Memcached 27017 MongoDB
危険ポート(RISKY_PORTS)

インターネットに公開されている場合に攻撃対象になりやすい13ポート:

21 FTP — 平文認証、匿名アクセスの可能性
22 SSH — ブルートフォース攻撃のターゲット
23 Telnet — 平文通信、即CRITICAL
445 SMB — WannaCry等のワーム攻撃ベクター
3306 MySQL 5432 PostgreSQL — DB直接露出
3389 RDP — リモートデスクトップ攻撃
6379 Redis — 認証なしデフォルト設定が多い
27017 MongoDB — 認証なしデフォルト
バナーグラブ

_grab_banner() はオープンポートに接続し、サービス識別情報を取得。

手順 connect() → send("\r\n") → recv(1024)
timeout 2.0秒(応答なし = banner取得失敗 → N/A)
用途 サーバーソフト名・バージョン検出
例: SSH-2.0-OpenSSH_8.9p1
例: 220 smtp.example.com ESMTP Postfix

バナーが取得できた暗号化なしDBポート(MySQL, PostgreSQL, MSSQL, Oracle)は CRITICAL に昇格。

深刻度判定ロジック

_determine_severity() の判定ルール:

CRITICAL Telnet(:23) / バナー付きDBポート
HIGH SSH(:22), RDP(:3389), VNC(:5900), Redis(:6379), MongoDB(:27017)
MEDIUM その他のRISKY_PORTS (FTP, SMB等)
LOW Webポート (:80, :443, :8080, :8443)
INFO その他
/* PORT_SCANNER::scan_host() */
TCPポートスキャン
// tcp_connect | ThreadPoolExecutor(max_workers=50) | TOP_PORTS: 26 | banner_grab | severity: CRITICAL→INFO
/* PORT_SEVERITY::distribution */
ポートリスク分布
スキャン結果サマリー

まだスキャンが実行されていません

/* OPEN_PORT_MATRIX */
オープンポート一覧
// port | service | state | banner | is_risky | severity
ポート サービス 状態 バナー リスク 深刻度
🔌
ポートスキャン未実行
資産管理データモデル

DB: asm.db → asm_assets

tenant_id マルチテナント対応キー
asset_type domain | subdomain | ip | host
value UNIQUE制約(同一テナント内で重複防止)
discovery_method manual / manual_ssl_check / port_scan / crt.sh / dns_brute
risk_score 0.0〜10.0(関連findingsから自動算出)
status active / inactive / decommissioned

UPSERT(INSERT OR REPLACE)で同一値の資産は最終確認日のみ更新。

資産発見経路

資産は以下の5つの経路から登録されます:

手動登録 このタブから直接入力
SSLチェック ドメイン入力時に自動登録
アセットディスカバリ crt.sh / DNS列挙の結果を一括登録
ポートスキャン スキャン対象ホストを自動登録
リポジトリスキャン 将来的にコード内のURL/IP検出から自動登録
統合findingsとの関係

各資産のrisk_scoreは、関連するfindingsの深刻度から集計されます。

asm_findings.asset_value → 資産との紐付け
CRITICAL=10.0, HIGH=7.0, MEDIUM=4.0, LOW=2.0, INFO=1.0
findingが resolved / false_positive になるとスコアから除外。

資産登録
登録済み資産
種別 発見方法 ステータス リスクスコア 最終確認
🌐
資産未登録
統合findingsアーキテクチャ

asm_findings テーブルは全検出ソースからの結果を正規化して一元管理します。

Secret
Scanner
SSL/TLS
Checker
Asset
Discovery
Port
Scanner
asm_findings
(統合)

各エンジンが add_finding() を呼び出し、ソース種別・深刻度・カテゴリ・リスクスコアを統一フォーマットで保存。

findingスキーマ
source secret / ssl / discovery / port_scan / sast / sca
source_finding_id 元テーブルのID(追跡用)
severity CRITICAL / HIGH / MEDIUM / LOW / INFO
category 検出カテゴリ (Secret Type, SSL/TLS, Open Port等)
title 人間可読な要約
description 詳細情報
asset_value 関連資産のキー(ドメイン名/ファイルパス/host:port)
risk_score 0.0〜10.0(ソース別重み付け)
ステータスライフサイクル

findingは4つのステータスを遷移します:

open acknowledged resolved
open 新規検出 — 対応が必要
acknowledged 認識済み — 対応中/リスク受容
resolved 解決済み — リスクスコアから除外
false_positive 誤検知 — リスクスコアから除外

ステータス変更は PATCH /asm/api/findings/:id で即時反映。サマリーカードのリスクスコアも再計算。

リスクスコア計算

各ソースの検出結果にカテゴリ別基準スコアを適用:

CRITICAL10.0 即座に悪用可能
HIGH7.0 高リスク
MEDIUM4.0 中リスク
LOW2.0 低リスク
INFO1.0 情報のみ

サマリーのtotal_risk_scoreは全openなfindingのスコア合計。100上限でキャップ。

統合findings
深刻度 ソース カテゴリ タイトル 対象 スコア ステータス 検出日
📋
findings未検出
S³ ASM — Attack Surface Management | DIVX Inc.