Attack Surface Managementは、組織の外部から攻撃可能な「攻撃対象面」を継続的に発見・監視・評価する手法です。
S³のASMモジュールは、従来のSAST/SCAに加えてシークレット漏洩・SSL/TLS設定・公開資産を統合的にスキャンし、リスクを一元管理します。
17種の正規表現パターンでハードコードされた認証情報を検出します。
検出後、シャノンエントロピー(情報量計算)でランダム性を評価。entropy < 3.0 の文字列はダミー値として誤検知フィルタリング。
シャノンエントロピー H = -Σ p(x) log₂ p(x) で文字列のランダム性を定量化。
3.0 ≤ H < 4.0 中エントロピー → パスワード等の可能性
H ≥ 4.0 高エントロピー → ランダム生成キーの可能性大
パターンマッチなしでも高エントロピー文字列(H≥4.0, 20文字以上)を追加検出するscan_high_entropy()モードあり。
Python ssl + cryptography ライブラリでTLSハンドシェイク → x509証明書パースを実行。
検査項目: 有効期限 チェーン検証 TLSバージョン 暗号スイート HSTS SAN
ネゴシエートされたTLSバージョンと暗号スイートをWEAK_SETと照合。
WEAK_CIPHERS RC4, DES, 3DES, NULL, EXPORT, anon
CA証明書バンドルは certifi を使用。macOS/LinuxのシステムCA差異を吸収し、一貫した証明書チェーン検証を保証。
全検出結果(SAST/SCA/Secret/SSL)にカテゴリ別重み付けを適用した統一スコア。
DAST Finding ×2.5(実環境確認済み)
Open Port ×2.0(攻撃面露出)
SAST Critical ×1.5
SSL/TLS Issue ×1.0
検出後30日未対応で×1.2、90日未対応で×1.5の減衰因子を適用。パッチ適用済みは×0.0。
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保存時にマスク済み)
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
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)
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段階の検出パイプラインを実行します。
re.finditer(pattern, line) → 各行に対して17パターンを順次照合。マッチしたら即 calculate_entropy() で情報量評価。
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 KEYConnection 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) = -Σ p(xᵢ) × log₂(p(xᵢ))
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パターンで既知の安全な文字列を除外します。
is_likely_false_positive = Trueexample, dummy, placeholder, test, TODO, FIXME,
your[-_]?api[-_]?key, insert[-_]?token, xxx, sample
FPフラグは検出結果から除外しない(ユーザー判断用)。UIで FP_LIKELY / CONFIRMED と表示。
検出されたシークレットは即座にマスク処理してDBに保存されます。
mask_secret(value) → value[:4] + "***" + value[-4:]
例: AKIAIOSFODNN7EXAMPLE → AKIA***MPLE
DB: asm_secrets テーブルに masked_value として保存。生の値はメモリ上のみで、永続化されません。
パターンのカテゴリとエントロピーの組み合わせで判定。
HIGH GitHub/GitLab Token, Connection String, Stripe Key
MEDIUM Google API Key, JWT, Generic Secret
LOW Bearer Token, Webhook URL, Base64
INFO FP_LIKELY のもの全般
| 深刻度 | 種別 | ファイル | 行 | マスク値 | エントロピー | 判定 |
|---|---|---|---|---|---|---|
🔑 シークレットスキャン未実行 | ||||||
SSLChecker.check_domain() の実行フロー:
gethostbyname()
→
TCP接続create_connection()
→
TLS Wrapwrap_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の証明書ストア差分を解消。
cryptography.x509.load_der_x509_certificate() で抽出する情報:
NameOID.COMMON_NAME 発行者名Subject CN サーバー名(コモンネーム)
SAN
ExtensionOID.SUBJECT_ALTERNATIVE_NAME → DNSName[]Serial Number hex形式
Signature Algorithm
signature_algorithm_oid._nameNot Before / Not After
not_valid_before_utc / not_valid_after_utcDays Remaining
(not_after - now).days
_evaluate_status() で以下をチェック:
SSLv2, SSLv3, TLSv1, TLSv1.1 → HIGHWEAK_CIPHERS
RC4, DES, 3DES, NULL, EXPORT, anon → HIGH推奨 TLSv1.2+ / AES-256-GCM / ECDHE
_check_hsts() は http.client.HTTPSConnection で HEAD / を送信。
応答ヘッダーに Strict-Transport-Security が存在すれば
→ HSTS ✓
存在しなければ → NO HSTS (severity を LOW に引き上げ)
HSTSはブラウザにHTTPS強制を指示するヘッダー。ダウングレード攻撃(SSL stripping)を防止。
HIGH 7日以内 / 弱プロトコル / 弱暗号
MEDIUM 30日以内 / チェーン検証失敗
LOW HSTS未設定のみ
INFO 問題なし (OK)
複数問題がある場合は最も高い深刻度を採用。issuesリストに全問題を列挙。
| ドメイン | 発行者 | 有効期限 | 残日数 | TLS | HSTS | チェーン | ステータス |
|---|---|---|---|---|---|---|---|
🔒 証明書チェック未実行 | |||||||
AssetDiscovery.discover_all() は2つの独立したソースを統合します。
CT Log API
+
DNS Brute50 prefixes
→
マージdedup by name
→
IP補完cross-fill
→
資産登録asm_assets
crt.sh で見つかったサブドメインにDNSのIP情報を補完。両方で見つかれば信頼度が高い。
証明書透過性ログ(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はパッシブ手法であり、対象サーバーにアクセスしません。過去に発行された証明書の履歴から検出。
50個の一般的プレフィックスをドメインに結合し、DNSのAレコード解決を試行。
開発系 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
resolve_ip() は2段階フォールバック:
優先 dns.resolver.resolve(hostname, "A") (dnspython)
→ タイムアウト制御、NXDOMAIN正確判定
フォールバック socket.getaddrinfo(hostname, None, AF_INET)
→ dnspython未インストール時に使用。OS標準リゾルバ経由。
解決失敗 = そのサブドメインは存在しない(レスポンスとして除外)
まだディスカバリが実行されていません
| サブドメイン | IP | 検出ソース | 証明書発行者 | 証明書有効期限 |
|---|---|---|---|---|
🔍 ディスカバリ未実行 | ||||
PortScanner.scan_port() は socket.connect_ex() によるフルTCP 3-way handshakeでポート状態を判定。
connect_ex() == 0 → OPENconnect_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ホスト内のポートのみ並列。
21 FTP
22 SSH
23 Telnet
25 SMTP53 DNS
80 HTTP
110 POP3
143 IMAP443 HTTPS
445 SMB
993 IMAPS
995 POP3S1433 MSSQL
1521 Oracle
3306 MySQL
3389 RDP5432 PostgreSQL
5672 RabbitMQ
5900 VNC
6379 Redis8080 HTTP-Alt
8443 HTTPS-Alt
9200 Elasticsearch9090 Prometheus
11211 Memcached
27017 MongoDB
インターネットに公開されている場合に攻撃対象になりやすい13ポート:
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() の判定ルール:
HIGH SSH(:22), RDP(:3389), VNC(:5900), Redis(:6379), MongoDB(:27017)
MEDIUM その他のRISKY_PORTS (FTP, SMB等)
LOW Webポート (:80, :443, :8080, :8443)
INFO その他
まだスキャンが実行されていません
| ポート | サービス | 状態 | バナー | リスク | 深刻度 |
|---|---|---|---|---|---|
🔌 ポートスキャン未実行 | |||||
DB: asm.db → asm_assets
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検出から自動登録
各資産の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 になるとスコアから除外。
| 種別 | 値 | 発見方法 | ステータス | リスクスコア | 最終確認 |
|---|---|---|---|---|---|
🌐 資産未登録 | |||||
asm_findings テーブルは全検出ソースからの結果を正規化して一元管理します。
Scanner → SSL/TLS
Checker → Asset
Discovery → Port
Scanner ⇒ asm_findings
(統合)
各エンジンが add_finding() を呼び出し、ソース種別・深刻度・カテゴリ・リスクスコアを統一フォーマットで保存。
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つのステータスを遷移します:
acknowledged 認識済み — 対応中/リスク受容
resolved 解決済み — リスクスコアから除外
false_positive 誤検知 — リスクスコアから除外
ステータス変更は PATCH /asm/api/findings/:id で即時反映。サマリーカードのリスクスコアも再計算。
各ソースの検出結果にカテゴリ別基準スコアを適用:
10.0 即座に悪用可能HIGH →
7.0 高リスクMEDIUM →
4.0 中リスクLOW →
2.0 低リスクINFO →
1.0 情報のみ
サマリーのtotal_risk_scoreは全openなfindingのスコア合計。100上限でキャップ。
| 深刻度 | ソース | カテゴリ | タイトル | 対象 | スコア | ステータス | 検出日 |
|---|---|---|---|---|---|---|---|
📋 findings未検出 | |||||||