secrets-scanner について
secrets-scannerは、コードベースの認証情報漏えいを検出するセキュリティスキルです。パターン検査、コミット前とCIでのチェック、独自スキャナー、漏えい時の対応手順を扱います。
主な機能
- 01ファイル内の認証情報パターン検出
- 02Gitleaksとdetect-secretsフックの設定
- 03CIワークフローへのシークレット検査追加
- 04TypeScript製カスタムスキャナーの作成
- 05認証情報漏えいへの対応手順の文書化
活用シーン
- 01コミット前のステージ済み変更の確認
- 02CIでのリポジトリ検査
- 03ソースファイル内の認証情報の発見
- 04検出したシークレット漏えいへの対応
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill secrets-scannerインストール後、Claude Code で/secrets-scannerを入力して読み込まれたか確認します。
詳細
シークレットスキャナー
コードベースに漏えいした認証情報を検出し、防止します。
シークレット検出パターン
# .gitleaks.toml
title = "Gitleaks Configuration"
[[rules]]
id = "aws-access-key"
description = "AWS Access Key"
regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
tags = ["key", "AWS"]
[[rules]]
id = "aws-secret-key"
description = "AWS Secret Key"
regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
tags = ["key", "AWS"]
[[rules]]
id = "github-token"
description = "GitHub Personal Access Token"
regex = '''ghp_[0-9a-zA-Z]{36}'''
tags = ["key", "GitHub"]
[[rules]]
id = "github-oauth"
description = "GitHub OAuth Token"
regex = '''gho_[0-9a-zA-Z]{36}'''
tags = ["key", "GitHub"]
[[rules]]
id = "slack-webhook"
description = "Slack Webhook URL"
regex = '''https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8,10}/B[a-zA-Z0-9_]{8,10}/[a-zA-Z0-9_]{24}'''
tags = ["webhook", "Slack"]
[[rules]]
id = "private-key"
description = "Private Key"
regex = '''-----BEGIN (RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----'''
tags = ["key", "private"]
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(api[_-]?key|apikey|access[_-]?key)(.{0,20})?['"][0-9a-zA-Z]{32,}['"]'''
tags = ["key", "generic"]
[[rules]]
id = "database-connection"
description = "Database Connection String"
regex = '''(?i)(postgresql|mysql|mongodb):\/\/[^\s:]+:[^\s@]+@[^\s\/]+'''
tags = ["database", "credentials"]
[allowlist]
description = "Allowlist"
paths = [
'''node_modules/''',
'''\.git/''',
'''\.lock$''',
]
regexes = [
'''EXAMPLE_KEY_123''',
'''your_api_key_here''',
'''<API_KEY>''',
]
## Pre-commit Hook Setup
```yaml
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: local
hooks:
- id: check-env-files
name: Check for .env files
entry: bash -c 'if git diff --cached --name-only | grep -E "\.env$"; then echo "❌ .env file detected! Add to .gitignore"; exit 1; fi'
language: system
pass_filenames: false
# Install pre-commit
pip install pre-commit
# Install hooks
pre-commit install
# Run on all files
pre-commit run --all-files
CI 連携
# .github/workflows/secrets-scan.yml
name: Secrets Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for scanning
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug --only-verified
カスタムシークレットスキャナー
// scripts/scan-secrets.ts
import * as fs from "fs";
import * as path from "path";
interface SecretPattern {
name: string;
regex: RegExp;
severity: "critical" | "high" | "medium";
}
const SECRET_PATTERNS: SecretPattern[] = [
{
name: "AWS Access Key",
regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/g,
severity: "critical",
},
{
name: "Private Key",
regex: /-----BEGIN (RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----/g,
severity: "critical",
},
{
name: "Generic API Key",
regex:
/['"]?[a-zA-Z0-9_-]*api[_-]?key['"]?\s*[:=]\s*['"][a-zA-Z0-9]{32,}['"]/gi,
severity: "high",
},
{
name: "Database URL",
regex: /(postgresql|mysql|mongodb):\/\/[^\s:]+:[^\s@]+@[^\s\/]+/gi,
severity: "critical",
},
{
name: "JWT Token",
regex: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
severity: "high",
},
];
interface SecretFinding {
file: string;
line: number;
column: number;
pattern: string;
match: string;
severity: string;
}
function scanFile(filePath: string): SecretFinding[] {
const findings: SecretFinding[] = [];
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");
lines.forEach((line, lineIndex) => {
SECRET_PATTERNS.forEach((pattern) => {
const matches = line.matchAll(pattern.regex);
for (const match of matches) {
findings.push({
file: filePath,
line: lineIndex + 1,
column: match.index || 0,
pattern: pattern.name,
match: match[0].substring(0, 50) + "...",
severity: pattern.severity,
});
}
});
});
return findings;
}
function scanDirectory(dir: string): SecretFinding[] {
const findings: SecretFinding[] = [];
const files = fs.readdirSync(dir, { withFileTypes: true });
const ignorePaths = ["node_modules", ".git", "dist", "build"];
files.forEach((file) => {
const fullPath = path.join(dir, file.name);
if (file.isDirectory() && !ignorePaths.includes(file.name)) {
findings.push(...scanDirectory(fullPath));
} else if (file.isFile()) {
findings.push(...scanFile(fullPath));
}
});
return findings;
}
// Run scan
const findings = scanDirectory("./src");
if (findings.length > 0) {
console.error("🚨 Secrets detected!\n");
findings.forEach((f) => {
console.error(
`[${f.severity.toUpperCase()}] ${f.file}:${f.line}:${f.column}`
);
console.error(` Pattern: ${f.pattern}`);
console.error(` Match: ${f.match}\n`);
});
process.exit(1);
} else {
console.log("✅ No secrets detected");
}
修復手順
# Secret Leak Remediation Checklist
## Immediate Actions (< 1 hour)
1. **Revoke the compromised secret**
- [ ] Deactivate API key/token immediately
- [ ] Rotate credentials in production
- [ ] Update all services using the secret
2. **Remove from git history**
```bash
# Using BFG Repo-Cleaner
bfg --replace-text secrets.txt repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push (requires team coordination)
git push --force --all
```
- 関係者に通知する
- セキュリティチーム
- DevOps チーム
- サービス所有者
- 経営陣(パブリックリポジトリの場合)
短期的な対応(24 時間未満)
-
アクセスログを監査する
- CloudWatch/CloudTrail で不審なアクティビティを確認する
- 不正アクセスがないか API の使用状況を確認する
- データの持ち出しがないか確認する
-
シークレット管理を更新する
- Vault(AWS Secrets Manager、HashiCorp Vault)に保存する
- 環境変数を使用する
- ハードコードされたシークレットを削除する
-
スキャンを追加する
- pre-commit フックをインストールする
- CI シークレットスキャンを追加する
- 監視アラートを設定する
長期的な対応(1 週間未満)
- 見直しと改善を行う
- セキュリティトレーニングを実施する
- シークレット管理ポリシーを更新する
- シークレットの定期ローテーションを実施する
- インシデントと得られた教訓を記録する
## Secret Management Best Practices
```typescript
// ❌ BAD: Hardcoded secrets
const API_KEY = 'sk_live_abc123xyz789';
const db = connect('mongodb://admin:password@localhost');
// ✅ GOOD: Environment variables
const API_KEY = process.env.API_KEY;
const db = connect(process.env.DATABASE_URL);
// ✅ BETTER: Secret management service
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
async function getSecret(secretName: string): Promise<string> {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return response.SecretString!;
}
const apiKey = await getSecret('prod/api/stripe-key');
GitHub シークレットスキャン
# Enable GitHub secret scanning (Enterprise)
# Settings → Security & analysis → Secret scanning
# Configure custom patterns
# .github/secret_scanning.yml
patterns:
- name: Company API Key
pattern: "company_[a-zA-Z0-9]{32}"
secret_type: company_api_key
環境変数の検証
// config/env-validation.ts
import { z } from "zod";
const envSchema = z
.object({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
JWT_SECRET: z.string().min(64),
// Never allow default/example values in production
})
.refine((env) => {
if (env.NODE_ENV === "production") {
const invalidValues = ["example", "test", "localhost", "changeme"];
return !invalidValues.some((val) =>
Object.values(env).some((envVal) =>
String(envVal).toLowerCase().includes(val)
)
);
}
return true;
}, "Production environment cannot use example/test values");
// Validate on startup
try {
envSchema.parse(process.env);
} catch (error) {
console.error("❌ Invalid environment configuration:", error);
process.exit(1);
}
監視とアラート
// monitoring/secret-monitoring.ts
import {
CloudWatchClient,
PutMetricDataCommand,
} from "@aws-sdk/client-cloudwatch";
async function monitorSecretUsage(secretName: string) {
const cloudwatch = new CloudWatchClient();
await cloudwatch.send(
new PutMetricDataCommand({
Namespace: "Security/Secrets",
MetricData: [
{
MetricName: "SecretAccess",
Value: 1,
Unit: "Count",
Dimensions: [
{
Name: "SecretName",
Value: secretName,
},
],
},
],
})
);
}
// Alert on unusual secret access patterns
ベストプラクティス
- シークレットをコミットしない。.env ファイルには .gitignore を使用します
- シークレットマネージャーを使用する。AWS Secrets Manager、Vault
- 定期的にローテーションする。90 日ごとのローテーションポリシー
- 継続的にスキャンする。pre-commit + CI + 定期スキャン
- 最小権限の原則。シークレットへのアクセスを最小限にする
- 監査ログ。シークレットへのアクセスを追跡する
- インシデント対応。修復手順書を用意しておく
出力チェックリスト
- Gitleaks の設定を作成した
- pre-commit フックをインストールした
- CI シークレットスキャンを設定した
- カスタムスキャナーを実装した(任意)
- 修復手順書を文書化した
- シークレット管理のベストプラクティスを整備した
- 環境変数の検証を整備した
- 監視とアラートを整備した
- .gitignore に .env ファイルを追加した
- チームにシークレットの取り扱いをトレーニングした
コメント
Markdown 対応