🎯 この章で作るもの

  • ツール1:ログイン失敗を検知するログ解析ツール(正規表現つき)
  • ツール2:パスワード強度チェッカー
  • ツール3:ファイル整合性チェッカー(改ざん検知)
⚠️ 使用範囲の確認

これらのツールは自分が管理する・許可された環境に対してのみ使ってください。他者のシステムのログやファイルを無断で解析する行為は避けましょう(第9章)。

13.1 準備 — 正規表現の速習

ログ解析には正規表現(regular expression)が欠かせません。「文字列のパターン」を表す小さな言語で、Pythonでは re モジュールで使います。実務で必要な最小限だけ押さえます。

パターン意味
\d数字1文字
\d+数字1文字以上
\w英数字・アンダースコア1文字
.任意の1文字
( )グループ化(取り出したい部分を囲む)
\.ドットそのもの(エスケープ)
import re

line = "2026-07-08 09:15:02 LOGIN FAILED user=admin ip=203.0.113.9"

# IPアドレスを取り出す
m = re.search(r"ip=(\d+\.\d+\.\d+\.\d+)", line)
if m:
    print(m.group(1))     # 203.0.113.9(( )で囲んだ部分)

# ユーザー名を取り出す
m = re.search(r"user=(\w+)", line)
print(m.group(1))         # admin
💡 r"..." という書き方

正規表現の文字列には r"..."(raw文字列)を使います。バックスラッシュ \ をそのまま扱えるためです。正規表現を書くときの定番の作法です。

13.2 ツール1 — ログイン失敗検知ツール

「同一IPから短時間に何度もログイン失敗しているものを、ブルートフォース攻撃の疑いとして検出する」ツールです。第3章(ループ・条件)、第4章(辞書)、第5章(関数)、第6章(ファイル)、そして正規表現の集大成です。

# login_watch.py — ログイン失敗検知ツール
import re
from collections import defaultdict

# 攻撃とみなす失敗回数のしきい値
THRESHOLD = 3

# ログ1行からIPとユーザーを取り出す正規表現
FAILED_RE = re.compile(r"LOGIN FAILED user=(\w+) ip=([\d.]+)")

def load_lines(path):
    """ログを読み込む。無ければ空リストを返し、プログラムは止めない"""
    try:
        with open(path, encoding="utf-8") as f:
            return [line.strip() for line in f]
    except FileNotFoundError:
        print(f"[エラー] {path} が見つかりません")
        return []

def detect_bruteforce(lines):
    """IPごとの失敗回数を数え、しきい値を超えたものを返す"""
    counts = defaultdict(int)          # 存在しないキーは自動で0から始まる
    users_by_ip = defaultdict(set)
    for line in lines:
        m = FAILED_RE.search(line)
        if not m:
            continue
        user, ip = m.group(1), m.group(2)
        counts[ip] += 1
        users_by_ip[ip].add(user)

    # しきい値を超えたIPだけ抽出(内包表記)
    suspicious = {
        ip: (n, users_by_ip[ip])
        for ip, n in counts.items()
        if n >= THRESHOLD
    }
    return suspicious

def main():
    lines = load_lines("access.log")
    suspicious = detect_bruteforce(lines)

    if not suspicious:
        print("不審なログイン失敗は検出されませんでした")
        return

    print(f"⚠️  {len(suspicious)} 件の不審なIPを検出しました\n")
    # 失敗回数の多い順に並べる
    for ip, (n, users) in sorted(suspicious.items(),
                                 key=lambda x: x[1][0], reverse=True):
        targets = ", ".join(sorted(users))
        print(f"  {ip}: 失敗{n}回  狙われたアカウント: {targets}")

if __name__ == "__main__":
    main()

第6章で作った access.log を対象に実行すると、203.0.113.9 からの失敗3回が検出されます。

$ python login_watch.py
⚠️  1 件の不審なIPを検出しました

  203.0.113.9: 失敗3回  狙われたアカウント: admin, root
🛡️ セキュリティの視点 — これはSIEMの縮図

企業が使うSIEM(Security Information and Event Management)という高価な監視製品も、原理はこのツールと同じ「ログを集めて、パターンで相関を取り、しきい値で警報を出す」です。defaultdict による集計、正規表現による抽出、しきい値判定──基礎の組み合わせが実務の中核技術になっていることを実感してください。1つのIPから複数アカウントを狙う挙動は、典型的な攻撃パターンです。

13.3 ツール2 — パスワード強度チェッカー

第5章の演習を発展させ、パスワードを多角的に評価してスコアとアドバイスを返すツールです。

# password_strength.py — パスワード強度チェッカー
import re

# 過去の漏えいで頻出する「使ってはいけない」パスワード(抜粋)
COMMON = {"password", "123456", "qwerty", "admin", "welcome", "letmein"}

def check_strength(password):
    """パスワードを評価し、(スコア0-5, アドバイスのリスト) を返す"""
    score = 0
    tips = []

    if len(password) >= 12:
        score += 2
    elif len(password) >= 8:
        score += 1
        tips.append("12文字以上にするとより安全です")
    else:
        tips.append("短すぎます。最低でも12文字にしましょう")

    if re.search(r"[a-z]", password) and re.search(r"[A-Z]", password):
        score += 1
    else:
        tips.append("大文字と小文字を混ぜましょう")

    if re.search(r"\d", password):
        score += 1
    else:
        tips.append("数字を含めましょう")

    if re.search(r"[^A-Za-z0-9]", password):
        score += 1
    else:
        tips.append("記号(!?#$など)を含めましょう")

    if password.lower() in COMMON:
        score = 0
        tips = ["よく使われる危険なパスワードです。今すぐ変更してください"]

    return score, tips

def label(score):
    return ["危険", "とても弱い", "弱い", "普通", "強い", "とても強い"][score]

def main():
    pw = input("パスワードを入力(画面に表示されます / 本番では getpass を使う): ")
    score, tips = check_strength(pw)
    print(f"\n強度: {label(score)}({score}/5)")
    if tips:
        print("改善のヒント:")
        for t in tips:
            print(f"  - {t}")
    else:
        print("素晴らしいパスワードです!")

if __name__ == "__main__":
    main()
$ python password_strength.py
パスワードを入力(...): abc123

強度: 弱い(2/5)
改善のヒント:
  - 短すぎます。最低でも12文字にしましょう
  - 大文字と小文字を混ぜましょう
  - 記号(!?#$など)を含めましょう
💡 パスワード入力は getpass で

実際のツールでは、入力を画面に表示しないよう標準ライブラリの getpass.getpass() を使います。上の例は結果を見せるために input() にしていますが、本番では from getpass import getpass に置き換えてください。

✅ 現代のパスワード指針

米国NISTの指針では、「定期的な強制変更」よりも「長いこと(パスフレーズ)」と「漏えい済みパスワードの拒否」が重視されています。4つの無関係な単語をつなぐような長いパスフレーズは、覚えやすく破られにくい良い方法です。そして最も確実なのはパスワードマネージャ + MFAの併用です。

13.4 ツール3 — ファイル整合性チェッカー

第12章のハッシュを応用し、「重要ファイルが改ざん・変更されていないか」を検知するツールです。設定ファイルやWebコンテンツの不正な書き換え検知に使えます。

# integrity_check.py — ファイル整合性チェッカー
import hashlib
import json
import os
import sys

BASELINE_FILE = "baseline.json"

def file_hash(path):
    """ファイルのSHA-256を計算する(大きなファイルにも対応)"""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

def create_baseline(paths):
    """監視対象の現在のハッシュを基準(baseline)として記録する"""
    baseline = {}
    for path in paths:
        if os.path.isfile(path):
            baseline[path] = file_hash(path)
    with open(BASELINE_FILE, "w", encoding="utf-8") as f:
        json.dump(baseline, f, indent=2)
    print(f"{len(baseline)} 件の基準を {BASELINE_FILE} に保存しました")

def check(paths):
    """現在のハッシュを基準と比較し、変更・削除・追加を報告する"""
    try:
        with open(BASELINE_FILE, encoding="utf-8") as f:
            baseline = json.load(f)
    except FileNotFoundError:
        print("基準がありません。先に create を実行してください")
        return

    alerts = 0
    for path, old_hash in baseline.items():
        if not os.path.isfile(path):
            print(f"[削除] {path} が存在しません")
            alerts += 1
        elif file_hash(path) != old_hash:
            print(f"[変更] {path} が変更されています!")
            alerts += 1

    # 基準に無い新規ファイル
    for path in paths:
        if path not in baseline and os.path.isfile(path):
            print(f"[追加] {path} は基準に登録されていません")
            alerts += 1

    if alerts == 0:
        print("✅ すべてのファイルは基準と一致しています")
    else:
        print(f"\n⚠️  {alerts} 件の変化を検出しました")

def main():
    # 使い方: python integrity_check.py create file1 file2 ...
    #         python integrity_check.py check  file1 file2 ...
    if len(sys.argv) < 3:
        print("使い方: python integrity_check.py [create|check] ファイル...")
        return
    mode, paths = sys.argv[1], sys.argv[2:]
    if mode == "create":
        create_baseline(paths)
    elif mode == "check":
        check(paths)
    else:
        print("モードは create か check を指定してください")

if __name__ == "__main__":
    main()
# 1) 現在の状態を基準として記録
$ python integrity_check.py create config.ini index.html
2 件の基準を baseline.json に保存しました

# 2) (config.ini を誰かが書き換えた後)
$ python integrity_check.py check config.ini index.html
[変更] config.ini が変更されています!

⚠️  1 件の変化を検出しました
🛡️ セキュリティの視点 — FIMという実在のカテゴリ

このツールはFIM(File Integrity Monitoring:ファイル整合性監視)という、実在する製品カテゴリの縮図です(Tripwire、AIDEなどが有名)。Webサーバーの改ざん、重要な設定ファイルへの不正な変更、マルウェアによるシステムファイルの書き換えを検知します。sys.argv によるコマンドライン引数の受け取り、json での状態保存、ハッシュ比較──実務ツールの骨格がすべて詰まっています。実運用では基準ファイル自体の保護(改ざん防止)も重要な論点になります。

13.5 学びを実務へ — 自動化の心得

まとめ

練習問題

問題 13-1

ツール1(login_watch.py)を改造し、「検出結果をCSVファイル alerts.csv に出力する」機能を追加してください(列:ip, failed_count, targeted_users)。

解答を見る
import csv

def export_csv(suspicious, path="alerts.csv"):
    with open(path, "w", encoding="utf-8", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["ip", "failed_count", "targeted_users"])
        for ip, (n, users) in sorted(suspicious.items(),
                                     key=lambda x: x[1][0], reverse=True):
            writer.writerow([ip, n, " ".join(sorted(users))])
    print(f"{path} に出力しました")

# main() の中で detect_bruteforce の後に呼ぶ
# export_csv(suspicious)

第6章のCSV書き込みがそのまま活きています。検知結果をファイルにすることで、他のツールへの受け渡しや定期レポート化が可能になります。

問題 13-2

ツール2(パスワード強度チェッカー)に、「ユーザー名と同じ/一部を含むパスワードを弱いと判定する」機能を追加する方針を説明してください(コードは擬似で可)。

解答を見る
def check_strength(password, username=""):
    # ...既存の判定...
    if username and username.lower() in password.lower():
        score = max(0, score - 2)
        tips.append("ユーザー名を含むパスワードは推測されやすく危険です")
    return score, tips

ユーザー名・氏名・生年月日・サービス名などの「本人に紐づく情報」は攻撃者が最初に試す候補です。それらを含むパスワードを減点する、というのは実際のパスワードポリシーでも使われる考え方です。

問題 13-3

ツール3(整合性チェッカー)を「毎日自動実行して、変化があればメール通知する」運用にしたいと考えました。設計上、どんな点に注意すべきか3つ挙げてください。

解答を見る
  1. 基準ファイル(baseline.json)自体の保護 — これが書き換えられると改ざんを見逃します。読み取り専用にする、別の安全な場所に保管する、基準にも署名するなどの対策が必要です。
  2. 正当な変更との区別(誤検知対策) — 正規の更新作業でもファイルは変わります。変更管理と連携し、承認済みの変更時は基準を更新する運用が要ります。通知が多すぎると「オオカミ少年」化して無視されます。
  3. 認証情報の安全な管理 — メール送信のパスワードやAPIキーをコードに直書きしない(第10章)。環境変数やシークレットマネージャを使い、ツールは最小権限で動かします。

定期実行はcronやタスクスケジューラ、あるいはCI/CDで実現できます。