こんにちは、株式会社インティメート・マージャー 開発本部の @y_kanno です。
1. はじめに
業務で扱うリポジトリの中には、セキュリティポリシー上、ChatGPTなどのクラウドベースのAIサービスにコードを渡せないものが数多く存在します。
しかし、CI/CDパイプラインやpre-commitフック等での自動セキュリティチェック(特にAPIキーやパスワードのハードコード検出)は行いたいものです。
もちろん、trufflehogといった正規表現ベースのツールの利用も考えられますが、テスト用文字列の誤検知等、文脈を考慮しない動作に悩まされることもあります。
例えば password = os.getenv('DB_PASS') は安全ですが、正規表現で password = を探すルールに引っかかるかもしれません。
そこで今回は 「完全オフライン」 で動作するローカルLLM環境を構築し、文脈を理解できるAIにコード内の機密情報を指摘してもらうPythonスクリプトを作成します。
2. アーキテクチャと技術選定
今回は以下の構成で実装します。
- LLMランタイム: Ollama
- モデル: qwen2.5-coder または llama3.2
- 実行言語: Python 3.10+
(環境設定後の)外部通信は一切行わず、
ローカルホスト内 localhost:11434 だけで完結させます。
今回は軽量かつコード理解に向いている qwen2.5-coderを使用します。
事前準備
まず、ローカルLLMサーバーであるOllamaをインストールし、モデルをpullしておきます。
# Ollamaのインストール(Mac/Linuxの場合)
curl -fsSL https://ollama.com/install.sh | sh
# モデルの準備(今回は軽量なqwen2.5-coderを使用)
ollama pull qwen2.5-coder:7b
実装コード (Python)
このスクリプトで
- 指定したディレクトリ内のファイルを走査
- LLMにファイルを投げて「ここに認証情報は含まれているか?」の判定
を実行します。
import os
import json
import requests
import fnmatch
# 設定
TARGET_DIR = "./src" # チェックしたいディレクトリ
OLLAMA_API_URL = "http://localhost:11434/api/generate"
MODEL_NAME = "qwen2.5-coder:7b"
# チェック対象外のファイル/ディレクトリ
IGNORE_PATTERNS = [
"*.pyc", "__pycache__", ".git", "node_modules",
"*.png", "*.jpg", "*.lock"
]
def is_ignored(path):
for pattern in IGNORE_PATTERNS:
if fnmatch.fnmatch(os.path.basename(path), pattern):
return True
return False
def check_code_with_llm(file_path, code_content):
"""
LLMにコードを送信し、認証情報が含まれていないか判定する
"""
# プロンプトエンジニアリング: 役割を与え、出力形式を固定する
system_prompt = (
"あなたはセキュリティ監査の専門家です。渡されたコード内に「ハードコードされた機密情報」"
"(パスワード、APIキー、JWTトークンなど)が含まれていないか分析してください。\n"
"\n"
"【判定基準】\n"
"1. 危険(検出対象): ランダム性の高い文字列や具体的な認証情報が、変数に直接代入されている場合。\n"
" (例: api_key = 'sk-12345abcde...', token = 'eyJhbGciOi...')\n"
"2. 安全(無視): 環境変数からの読み込み、空文字、明らかにダミーと分かるプレースホルダー。\n"
" (例: os.getenv('KEY'), pass = '', token = '<YOUR_TOKEN_HERE>')\n"
"\n"
"【出力形式】\n"
"結果は以下のJSONフォーマットのみを出力してください。Markdownのコードブロックや解説は一切不要です。\n"
"{\n"
" \"detected\": true または false,\n"
" \"offending_code\": \"問題箇所として特定したコード断片(元のソースコードからスペースを含め一字一句変えずに抽出すること)\",\n"
" \"reason\": \"検出理由(日本語で簡潔に)\"\n"
"}"
)
prompt = f"File: {file_path}\n\nCode:\n```\n{code_content}\n```"
try:
response = requests.post(OLLAMA_API_URL, json={
"model": MODEL_NAME,
"prompt": prompt,
"system": system_prompt,
"stream": False,
"format": "json" # JSONモードを強制
})
response.raise_for_status()
result = response.json().get("response", "")
return json.loads(result)
except Exception as e:
print(f"Error checking {file_path}: {e}")
return {"detected": False, "error": str(e)}
def main():
print(f"🔒 Starting Security Scan using {MODEL_NAME}...")
issues_found = 0
for root, dirs, files in os.walk(TARGET_DIR):
# 除外ディレクトリのスキップ
dirs[:] = [d for d in dirs if not is_ignored(d)]
for file in files:
if is_ignored(file):
continue
file_path = os.path.join(root, file)
try:
# テキストファイルとして読み込み
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# コンテキストウィンドウ節約のため、極端に大きなファイルはスキップするか切り詰める
if len(content) > 10000:
print(f"⚠️ Skipping large file: {file_path}")
continue
print(f"Testing: {file_path} ...", end="\r")
result = check_code_with_llm(file_path, content)
if result.get("detected"):
snippet = result.get("offending_code", "")
reason = result.get("reason", "")
# コンソールへのPrintデバッグ出力(人間用)
print(f"\n[!] ALERT : {file_path}")
# 複数行のコードもきれいにインデントして表示する処理
code_lines = snippet.strip().split('\n')
if code_lines:
print(f" Code : {code_lines[0]}")
for line in code_lines[1:]:
print(f" {line}")
print(f" Reason : {reason}")
issues_found += 1
except UnicodeDecodeError:
# バイナリファイル等はスキップ
continue
print("-" * 30)
if issues_found == 0:
print("✅ No hardcoded credentials detected.")
else:
print(f"🚨 Scan finished. {issues_found} potential issues found.")
if __name__ == "__main__":
main()
以下はテスト用に読み込ませるコードです。
src ディレクトリを作成し、以下のファイル名で保存してください。
import os
# ---------------------------------------------------
# 【OK】検出されてはいけないパターン(安全)
# ---------------------------------------------------
# 1. 環境変数からの読み込み(これは安全)
api_key = os.getenv("STRIPE_API_KEY")
# 2. 空文字や初期化(安全)
password_cache = ""
# 3. プレースホルダー(開発用のダミー文字列と文脈でわかるもの)
# ※ LLMによっては迷う場合もありますが、理想はスルー
google_client_id = "<YOUR_CLIENT_ID_HERE>"
# 4. 設定ファイルからのロード関数呼び出し
db_pass = load_config("database.password")
import os
# ---------------------------------------------------
# 【NG】検出されるべきパターン(ハードコード)
# ---------------------------------------------------
# 1. AWSキーに近い文字列
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# 2. ランダム性の高い文字列を直接代入
DB_PASSWORD = "x8z9#mK2$pL0aQw!sT"
# 3. webhookに渡すURLの例
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
# 4. JWTトークンの例
config_val = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
依存ライブラリをインストールしておきます。
$ pip install requests
3. 実行結果
このスクリプトを実行すると、以下のように文脈を読んだ上で指摘をしてくれます。
# クレデンシャルが含まれる場合の例
$ python3 scan_script.py
🔒 Starting Security Scan using qwen2.5-coder:7b...
Testing: ./src/detected_example.py ...
[!] ALERT : ./src/detected_example.py
Code : AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
DB_PASSWORD = "x8z9#mK2$pL0aQw!sT"
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
config_val = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
Reason : ハードコードされた機密情報(AWSキー、データベースパスワード、Slackのwebhook URL、JWTトークン)が直接代入されています。
------------------------------ ...
🚨 Scan finished. 1 potential issues found.
# クレデンシャルが含まれない場合の例
$ python3 scan_script.py
🔒 Starting Security Scan using qwen2.5-coder:7b...
------------------------------ ...
✅ No hardcoded credentials detected.
4. まとめ
ローカルLLM(Ollama + qwen2.5-coder)を使うことで、外部にデータを出さずにAIベースでのセキュリティチェックが可能になりました。
正規表現では難しい「文脈判断」により、誤検知を減らす効果が期待できます。
あくまでAIによる監査補助ですが、オフライン上でもAIを活用する方法の例を記事にしてみました。
今回はシンプルなスクリプトでしたが、これを MCP (Model Context Protocol) サーバーとして実装すれば、Claude DesktopやCursorから『このリポジトリのセキュリティチェックをして』と頼むだけで実行できるようになります。興味がある方はぜひ拡張に挑戦してみてください。
5. おわりに
最後まで読んでいただきありがとうございました!明日のアドベントカレンダーもお楽しみに!
また、インティメート・マージャーでは、新卒から中途採用まで幅広く採用募集中です!
記事を読んで弊社に興味を持ってくれた方は、下記より採用情報をチェック!
弊社では生成AIの活用を推進しております。この記事も生成AIを利用して作成しました。
(目安ですが、本記事では生成されたコードおよび文章の70%程度を利用しています)