feat: add gitleaks secret scanning to quality rails (#5)
This commit was merged in pull request #5.
This commit is contained in:
@@ -33,6 +33,10 @@ Copy-Item -Path "$TemplateDir\.eslintrc.strict.js" -Destination "$TargetDir\.esl
|
||||
Copy-Item -Path "$TemplateDir\tsconfig.strict.json" -Destination "$TargetDir\tsconfig.json" -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path "$TemplateDir\.woodpecker.yml" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Copy shared gitleaks config from templates root
|
||||
$SharedTemplates = Split-Path -Parent $TemplateDir
|
||||
Copy-Item -Path "$SharedTemplates\.gitleaks.toml" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "✓ Files copied"
|
||||
|
||||
if (Test-Path "$TargetDir\package.json") {
|
||||
@@ -50,4 +54,6 @@ Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host "1. Install dependencies: npm install"
|
||||
Write-Host "2. Initialize husky: npx husky install"
|
||||
Write-Host "3. Run verification: ..\quality-rails\scripts\verify.ps1"
|
||||
Write-Host "3. Install gitleaks: winget install gitleaks"
|
||||
Write-Host "4. Run verification: ..\quality-rails\scripts\verify.ps1"
|
||||
Write-Host "5. (Optional) Scan full history: gitleaks git --redact --verbose"
|
||||
|
||||
@@ -53,6 +53,10 @@ cp "$TEMPLATE_DIR/.eslintrc.strict.js" "$TARGET_DIR/.eslintrc.js" 2>/dev/null ||
|
||||
cp "$TEMPLATE_DIR/tsconfig.strict.json" "$TARGET_DIR/tsconfig.json" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/.woodpecker.yml" "$TARGET_DIR/" 2>/dev/null || true
|
||||
|
||||
# Copy shared gitleaks config from templates root
|
||||
SHARED_TEMPLATES="$(dirname "$TEMPLATE_DIR")"
|
||||
cp "$SHARED_TEMPLATES/.gitleaks.toml" "$TARGET_DIR/" 2>/dev/null || true
|
||||
|
||||
echo "✓ Files copied"
|
||||
|
||||
# Check if package.json exists
|
||||
@@ -71,5 +75,7 @@ echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Install dependencies: npm install"
|
||||
echo "2. Initialize husky: npx husky install"
|
||||
echo "3. Run verification: ~/.config/mosaic/bin/mosaic-quality-verify --target $TARGET_DIR"
|
||||
echo "3. Install gitleaks: https://github.com/gitleaks/gitleaks#installing"
|
||||
echo "4. Run verification: ~/.config/mosaic/bin/mosaic-quality-verify --target $TARGET_DIR"
|
||||
echo "5. (Optional) Scan full history: gitleaks git --redact --verbose"
|
||||
echo ""
|
||||
|
||||
@@ -39,6 +39,40 @@ if ($output -match "no-explicit-any") {
|
||||
git reset HEAD test-file.ts 2>$null
|
||||
Remove-Item test-file.ts -ErrorAction SilentlyContinue
|
||||
|
||||
# Test 3a: gitleaks binary must be present
|
||||
Write-Host ""
|
||||
Write-Host "Test 3a: gitleaks must be installed..."
|
||||
$gitleaksPath = Get-Command gitleaks -ErrorAction SilentlyContinue
|
||||
if ($gitleaksPath) {
|
||||
$gitleaksVer = & gitleaks version 2>&1 | Out-String
|
||||
Write-Host "✅ PASS: gitleaks found ($($gitleaksVer.Trim()))" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: gitleaks is NOT installed — secret scanning will not work" -ForegroundColor Red
|
||||
Write-Host " Install: winget install gitleaks"
|
||||
$Failed++
|
||||
}
|
||||
|
||||
# Test 3b: gitleaks detects a planted AWS key
|
||||
Write-Host ""
|
||||
Write-Host "Test 3b: gitleaks should detect planted AWS key..."
|
||||
if ($gitleaksPath) {
|
||||
"aws_access_key_id = AKIAIOSFODNN7REALKEY" | Out-File -FilePath gitleaks-test-secret.txt -Encoding utf8
|
||||
git add gitleaks-test-secret.txt 2>$null
|
||||
$output = & gitleaks git --pre-commit --staged --redact 2>&1 | Out-String
|
||||
if ($output -match "leak|finding") {
|
||||
Write-Host "✅ PASS: gitleaks detected planted secret" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: gitleaks did NOT detect planted secret" -ForegroundColor Red
|
||||
$Failed++
|
||||
}
|
||||
git reset HEAD gitleaks-test-secret.txt 2>$null
|
||||
Remove-Item gitleaks-test-secret.txt -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Write-Host "⚠ SKIP: gitleaks not installed (Test 3a already failed)"
|
||||
}
|
||||
|
||||
# Summary
|
||||
Write-Host ""
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
|
||||
@@ -40,23 +40,35 @@ fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
|
||||
# Test 3: Hardcoded secret blocked (if git-secrets installed)
|
||||
# Test 3a: gitleaks binary must be present
|
||||
echo ""
|
||||
echo "Test 3: Hardcoded secrets should be blocked..."
|
||||
if command -v git-secrets &> /dev/null; then
|
||||
echo "const password = 'SuperSecret123!';" > test-file.ts
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q -i "secret\|password"; then
|
||||
echo "✅ PASS: Secrets blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "⚠ WARN: Secrets NOT blocked (git-secrets may need configuration)"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
echo "Test 3a: gitleaks must be installed..."
|
||||
if command -v gitleaks &> /dev/null; then
|
||||
echo "✅ PASS: gitleaks found ($(gitleaks version 2>/dev/null || echo 'unknown version'))"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "⚠ SKIP: git-secrets not installed"
|
||||
echo "❌ FAIL: gitleaks is NOT installed — secret scanning will not work"
|
||||
echo " Install: https://github.com/gitleaks/gitleaks#installing"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
|
||||
# Test 3b: gitleaks detects a planted AWS key
|
||||
echo ""
|
||||
echo "Test 3b: gitleaks should detect planted AWS key..."
|
||||
if command -v gitleaks &> /dev/null; then
|
||||
echo 'aws_access_key_id = AKIAIOSFODNN7REALKEY' > gitleaks-test-secret.txt
|
||||
git add gitleaks-test-secret.txt 2>/dev/null
|
||||
if gitleaks git --pre-commit --staged --redact 2>&1 | grep -q -i "leak\|finding"; then
|
||||
echo "✅ PASS: gitleaks detected planted secret"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "❌ FAIL: gitleaks did NOT detect planted secret"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
git reset HEAD gitleaks-test-secret.txt 2>/dev/null
|
||||
rm gitleaks-test-secret.txt 2>/dev/null
|
||||
else
|
||||
echo "⚠ SKIP: gitleaks not installed (Test 3a already failed)"
|
||||
fi
|
||||
|
||||
# Test 4: Lint error blocked
|
||||
|
||||
162
tools/quality/templates/.gitleaks.toml
Normal file
162
tools/quality/templates/.gitleaks.toml
Normal file
@@ -0,0 +1,162 @@
|
||||
# Mosaic Quality Rails — gitleaks configuration
|
||||
# Shared across all project templates. Copied to project root by install.sh.
|
||||
# Built-in rules: https://github.com/gitleaks/gitleaks/tree/master/config
|
||||
# This file adds custom rules for patterns the 150+ built-in rules miss.
|
||||
|
||||
title = "Mosaic gitleaks config"
|
||||
|
||||
[allowlist]
|
||||
description = "Global allowlist — skip files that never contain real secrets"
|
||||
paths = [
|
||||
'''node_modules/''',
|
||||
'''dist/''',
|
||||
'''build/''',
|
||||
'''\.next/''',
|
||||
'''\.nuxt/''',
|
||||
'''\.output/''',
|
||||
'''coverage/''',
|
||||
'''__pycache__/''',
|
||||
'''\.venv/''',
|
||||
'''vendor/''',
|
||||
'''pnpm-lock\.yaml$''',
|
||||
'''package-lock\.json$''',
|
||||
'''yarn\.lock$''',
|
||||
'''\.lock$''',
|
||||
'''\.snap$''',
|
||||
'''\.min\.js$''',
|
||||
'''\.min\.css$''',
|
||||
'''\.gitleaks\.toml$''',
|
||||
]
|
||||
stopwords = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"changeme",
|
||||
"placeholder",
|
||||
"example",
|
||||
"example.com",
|
||||
"test",
|
||||
"dummy",
|
||||
"fake",
|
||||
"sample",
|
||||
"your-",
|
||||
"xxx",
|
||||
"CHANGEME",
|
||||
"PLACEHOLDER",
|
||||
"TODO",
|
||||
"REPLACE_ME",
|
||||
]
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Custom rules — patterns the built-in rules miss
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
[[rules]]
|
||||
id = "database-url-with-credentials"
|
||||
description = "Database connection URL with embedded password"
|
||||
regex = '''(?i)(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|amqp)://[^:\s]+:[^@\s]+@[^/\s]+'''
|
||||
tags = ["database", "connection-string"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "password", "example", "test_", "placeholder"]
|
||||
|
||||
[[rules]]
|
||||
id = "alembic-ini-sqlalchemy-url"
|
||||
description = "SQLAlchemy URL in alembic.ini with credentials"
|
||||
regex = '''sqlalchemy\.url\s*=\s*\S+://[^:\s]+:[^@\s]+@\S+'''
|
||||
paths = ['''alembic\.ini$''', '''\.ini$''']
|
||||
tags = ["python", "alembic", "database"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "driver://user:pass"]
|
||||
|
||||
[[rules]]
|
||||
id = "dotenv-secret-value"
|
||||
description = "High-entropy secret value in .env file"
|
||||
regex = '''(?i)(?:SECRET|TOKEN|PASSWORD|KEY|CREDENTIALS|AUTH)[\w]*\s*=\s*['"]?[A-Za-z0-9/+=]{20,}['"]?\s*$'''
|
||||
paths = ['''\.env$''', '''\.env\.\w+$''']
|
||||
tags = ["dotenv", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "your_", "REPLACE", "TODO"]
|
||||
|
||||
[[rules]]
|
||||
id = "jdbc-url-with-password"
|
||||
description = "JDBC connection string with embedded password"
|
||||
regex = '''jdbc:[a-z]+://[^;\s]+password=[^;\s&]+'''
|
||||
tags = ["java", "jdbc", "database"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example"]
|
||||
|
||||
[[rules]]
|
||||
id = "dsn-inline-password"
|
||||
description = "DSN-style connection string with inline password"
|
||||
regex = '''(?i)(?:dsn|connection_string|conn_str)\s*[:=]\s*\S+://[^:\s]+:[^@\s]+@\S+'''
|
||||
tags = ["database", "connection-string"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "example"]
|
||||
|
||||
[[rules]]
|
||||
id = "hardcoded-password-variable"
|
||||
description = "Hardcoded password assignment in source code"
|
||||
regex = '''(?i)(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]'''
|
||||
tags = ["password", "hardcoded"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "test", "dummy", "password123", "your_password"]
|
||||
paths = [
|
||||
'''test[s]?/''',
|
||||
'''spec[s]?/''',
|
||||
'''__test__/''',
|
||||
'''fixture[s]?/''',
|
||||
'''mock[s]?/''',
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "bearer-token-in-code"
|
||||
description = "Hardcoded bearer token in source code"
|
||||
regex = '''(?i)['"]Bearer\s+[A-Za-z0-9\-._~+/]+=*['"]'''
|
||||
tags = ["auth", "bearer", "token"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["example", "test", "dummy", "placeholder", "fake"]
|
||||
|
||||
[[rules]]
|
||||
id = "spring-application-properties-password"
|
||||
description = "Password in Spring Boot application properties"
|
||||
regex = '''(?i)spring\.\w+\.password\s*=\s*\S+'''
|
||||
paths = ['''application\.properties$''', '''application\.yml$''', '''application-\w+\.properties$''', '''application-\w+\.yml$''']
|
||||
tags = ["java", "spring", "password"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "${"]
|
||||
|
||||
[[rules]]
|
||||
id = "docker-compose-env-secret"
|
||||
description = "Hardcoded secret in docker-compose environment"
|
||||
regex = '''(?i)(?:POSTGRES_PASSWORD|MYSQL_ROOT_PASSWORD|MYSQL_PASSWORD|REDIS_PASSWORD|RABBITMQ_DEFAULT_PASS|MONGO_INITDB_ROOT_PASSWORD)\s*[:=]\s*['"]?[^\s'"$]{8,}['"]?'''
|
||||
paths = ['''compose\.ya?ml$''', '''docker-compose\.ya?ml$''']
|
||||
tags = ["docker", "compose", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "${"]
|
||||
|
||||
[[rules]]
|
||||
id = "terraform-variable-secret"
|
||||
description = "Sensitive default value in Terraform variable"
|
||||
regex = '''(?i)default\s*=\s*"[^"]{8,}"'''
|
||||
paths = ['''variables\.tf$''', '''\.tf$''']
|
||||
tags = ["terraform", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "TODO"]
|
||||
|
||||
[[rules]]
|
||||
id = "private-key-pem-inline"
|
||||
description = "PEM-encoded private key in source"
|
||||
regex = '''-----BEGIN\s+(?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'''
|
||||
tags = ["key", "pem", "private-key"]
|
||||
|
||||
[[rules]]
|
||||
id = "base64-encoded-secret"
|
||||
description = "Base64 value assigned to secret-named variable"
|
||||
regex = '''(?i)(?:secret|token|key|password|credentials)[\w]*\s*[:=]\s*['"]?[A-Za-z0-9+/]{40,}={0,2}['"]?'''
|
||||
tags = ["base64", "encoded", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "test"]
|
||||
paths = [
|
||||
'''test[s]?/''',
|
||||
'''spec[s]?/''',
|
||||
'''fixture[s]?/''',
|
||||
]
|
||||
@@ -1,2 +1,15 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
|
||||
@@ -4,11 +4,19 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
@@ -65,3 +73,4 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
|
||||
@@ -4,11 +4,19 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
@@ -65,3 +73,4 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
|
||||
@@ -6,11 +6,19 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
# Stage 1: Install
|
||||
install:
|
||||
image: *node_image
|
||||
@@ -64,3 +72,4 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
Reference in New Issue
Block a user