0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】Webサイトのセキュリティ簡易診断CLIツールを自作する

0
Last updated at Posted at 2026-07-03

はじめに

Webサイトを運営していると、セキュリティ面で見落としがちな問題があります。

  • SSL証明書の期限切れ
  • セキュリティヘッダーの未設定
  • CMSのバージョン露出

これらは公開情報のみで確認できます。

完成イメージ

$ security-scan https://example.com

=== セキュリティ診断結果 ===
対象URL: https://example.com
総合リスク: Medium (スコア: 4)

--- SSL/TLS ---
  有効: はい
  有効期限: 2026-10-25 (残り114日)
  プロトコル: TLS 1.3

--- 検出された問題 ---
1. [Medium] HSTSヘッダーなし
   → HSTSヘッダーを追加してHTTPS接続を強制してください

2. [Medium] X-Frame-Optionsなし
   → X-Frame-Options: SAMEORIGIN を設定してください

チェック項目

項目 説明
SSL証明書 有効性、残り日数、TLSバージョン
HTTPヘッダー HSTS, X-Frame-Options, CSP等
CMS検出 WordPress等のバージョン露出
サーバー情報 Server/X-Powered-By 露出
DNS SPF/DMARC レコード

実装

1. SSL証明書チェック

contextnet.Dialer を組み合わせることで、タイムアウト制御をより厳密に行えます。

func checkSSL(url string) SSLResult {
    result := SSLResult{Risk: "Info"}
    
    if strings.HasPrefix(url, "http://") {
        result.Enabled = false
        result.Risk = "Critical"
        return result
    }
    
    host := getHost(url)
    
    // contextとnet.Dialerで適切なタイムアウト制御
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    dialer := &net.Dialer{}
    netConn, err := dialer.DialContext(ctx, "tcp", host+":443")
    if err != nil {
        result.Enabled = false
        result.Risk = "Critical"
        return result
    }
    
    conn := tls.Client(netConn, &tls.Config{
        ServerName: host,
    })
    conn.SetDeadline(time.Now().Add(10 * time.Second))
    
    if err = conn.Handshake(); err != nil {
        conn.Close()
        result.Enabled = false
        result.Risk = "Critical"
        return result
    }
    defer conn.Close()
    
    result.Enabled = true
    certs := conn.ConnectionState().PeerCertificates
    if len(certs) > 0 {
        cert := certs[0]
        result.ValidUntil = cert.NotAfter
        result.DaysRemaining = int(time.Until(cert.NotAfter).Hours() / 24)
        result.Issuer = cert.Issuer.CommonName
        
        // TLSバージョン
        switch conn.ConnectionState().Version {
        case tls.VersionTLS13:
            result.Protocol = "TLS 1.3"
        case tls.VersionTLS12:
            result.Protocol = "TLS 1.2"
        }
        
        // 残り日数でリスク判定
        if result.DaysRemaining < 14 {
            result.Risk = "High"
        } else if result.DaysRemaining < 30 {
            result.Risk = "Medium"
        }
    }
    
    return result
}

2. HTTPセキュリティヘッダーチェック

func checkHeaders(url string) HeadersResult {
    headers := HeadersResult{Risk: "Info"}
    
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(url)
    if err != nil {
        return headers
    }
    defer resp.Body.Close()
    
    // HSTS
    if resp.Header.Get("Strict-Transport-Security") == "" {
        headers.MissingHeaders = append(headers.MissingHeaders, "HSTS")
    }
    
    // X-Frame-Options
    if resp.Header.Get("X-Frame-Options") == "" {
        headers.MissingHeaders = append(headers.MissingHeaders, "X-Frame-Options")
    }
    
    // CSP
    if resp.Header.Get("Content-Security-Policy") == "" {
        headers.MissingHeaders = append(headers.MissingHeaders, "CSP")
    }
    
    // リスク判定
    if len(headers.MissingHeaders) >= 2 {
        headers.Risk = "Medium"
    }
    
    return headers
}

3. CMS検出

注意: このCMS検出は簡易的なヒューリスティクスに基づいており、確実ではありません。CMS運営者がパスをカスタマイズしている場合、検出できないことがあります。

func detectCMS(url string) CMSResult {
    result := CMSResult{Risk: "Info"}
    
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(url)
    if err != nil {
        return result
    }
    defer resp.Body.Close()
    
    body, _ := io.ReadAll(io.LimitReader(resp.Body, 100*1024))
    bodyStr := string(body)
    
    // WordPress検出
    if strings.Contains(bodyStr, "wp-content") {
        result.Detected = true
        result.Name = "WordPress"
        
        // バージョン露出チェック
        re := regexp.MustCompile(`generator.*WordPress\s*([\d.]+)`)
        if m := re.FindStringSubmatch(bodyStr); len(m) > 1 {
            result.Version = m[1]
            result.VersionExposed = true
            result.Risk = "Medium"
        }
    }
    
    return result
}

4. DNS(SPF/DMARC)チェック

func checkDNS(url string) DNSResult {
    result := DNSResult{Risk: "Info"}
    domain := extractDomain(url)
    
    // MXレコード
    mx, _ := net.LookupMX(domain)
    result.HasMX = len(mx) > 0
    
    // SPFレコード
    txt, _ := net.LookupTXT(domain)
    for _, t := range txt {
        if strings.HasPrefix(t, "v=spf1") {
            result.HasSPF = true
        }
    }
    
    // DMARCレコード
    dmarc, _ := net.LookupTXT("_dmarc." + domain)
    for _, d := range dmarc {
        if strings.HasPrefix(d, "v=DMARC1") {
            result.HasDMARC = true
        }
    }
    
    // MXがあるのにSPF/DMARCがない場合はリスク
    if result.HasMX && !result.HasSPF {
        result.Risk = "High"
    }
    
    return result
}

出力フォーマット

4種類の出力形式に対応しています。

# テキスト(デフォルト)
security-scan https://example.com

# Markdown
security-scan https://example.com -o markdown -f report.md

# JSON
security-scan https://example.com -o json

# HTML(CSS付きレポート)
security-scan https://example.com -o html -f report.html

使用ライブラリ

  • cobra: CLIフレームワーク
  • 標準ライブラリ: context, crypto/tls, net/http, net, encoding/json

外部依存を最小限に抑えています。

補足: Severityの値(Critical, High, Medium, Low, Info)は定数として定義しておくと、型安全性が高まります。

注意事項

  • このツールは公開情報のみを使用した簡易診断です
  • 他者のサイトをスキャンする場合は許可を取得してください
  • 本格的な脆弱性診断には専門ツールを使用してください

まとめ

Go標準ライブラリだけで、実用的なセキュリティ簡易診断ツールを作れます。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?