Release v1.7.0: Add repomix-safe-mixer skill
Add new security-focused skill for safely packaging codebases with repomix by automatically detecting and removing hardcoded credentials. New skill: repomix-safe-mixer - Detects 20+ credential patterns (AWS, Supabase, Stripe, OpenAI, etc.) - Scan → Report → Pack workflow with automatic blocking - Standalone security scanner for pre-commit hooks - Environment variable replacement guidance - JSON output for CI/CD integration Also updates: - skill-creator: Simplified path resolution best practices - marketplace.json: Version 1.7.0, added repomix-safe-mixer plugin - README.md: Updated to 14 skills, added repomix-safe-mixer documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
163
repomix-safe-mixer/scripts/safe_pack.py
Normal file
163
repomix-safe-mixer/scripts/safe_pack.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Safe packaging workflow for repomix.
|
||||
|
||||
Scans for secrets, reports findings, and optionally packs after user confirmation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
def run_secret_scan(directory: Path, exclude_patterns: list = None):
|
||||
"""Run secret scanner and return findings."""
|
||||
script_dir = Path(__file__).parent
|
||||
scan_script = script_dir / 'scan_secrets.py'
|
||||
|
||||
cmd = [sys.executable, str(scan_script), str(directory), '--json']
|
||||
|
||||
if exclude_patterns:
|
||||
cmd.extend(['--exclude'] + exclude_patterns)
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
findings = json.loads(result.stdout) if result.stdout.strip() else []
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: Could not parse scan results", file=sys.stderr)
|
||||
print(f"Scanner output: {result.stdout}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return findings
|
||||
|
||||
def print_findings_report(findings: list):
|
||||
"""Print human-readable findings report."""
|
||||
if not findings:
|
||||
print("✅ No secrets detected!\n")
|
||||
return
|
||||
|
||||
print(f"\n⚠️ Security Scan Found {len(findings)} Potential Secrets:\n")
|
||||
|
||||
# Group by type
|
||||
by_type = {}
|
||||
for finding in findings:
|
||||
type_name = finding['type']
|
||||
if type_name not in by_type:
|
||||
by_type[type_name] = []
|
||||
by_type[type_name].append(finding)
|
||||
|
||||
# Print by type
|
||||
for secret_type in sorted(by_type.keys()):
|
||||
count = len(by_type[secret_type])
|
||||
print(f"🔴 {secret_type}: {count} instance(s)")
|
||||
for finding in by_type[secret_type][:3]: # Show first 3
|
||||
print(f" - {finding['file']}:{finding['line']}")
|
||||
print(f" Match: {finding['match']}")
|
||||
if len(by_type[secret_type]) > 3:
|
||||
print(f" ... and {len(by_type[secret_type]) - 3} more\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
def run_repomix(directory: Path, output_file: Path = None, config_file: Path = None):
|
||||
"""Run repomix to package the directory."""
|
||||
cmd = ['repomix']
|
||||
|
||||
if config_file and config_file.exists():
|
||||
cmd.extend(['--config', str(config_file)])
|
||||
|
||||
if output_file:
|
||||
cmd.extend(['--output', str(output_file)])
|
||||
|
||||
# Change to directory before running repomix
|
||||
result = subprocess.run(cmd, cwd=directory, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error: repomix failed", file=sys.stderr)
|
||||
print(result.stderr, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(result.stdout)
|
||||
return result
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: safe_pack.py <directory> [--output file.xml] [--config repomix.config.json] [--force] [--exclude pattern1 pattern2 ...]")
|
||||
print("\nOptions:")
|
||||
print(" --output <file> Output file path for repomix")
|
||||
print(" --config <file> Repomix config file")
|
||||
print(" --force Skip confirmation, pack anyway (dangerous!)")
|
||||
print(" --exclude <patterns> Patterns to exclude from secret scanning")
|
||||
print("\nExamples:")
|
||||
print(" safe_pack.py ./my-project")
|
||||
print(" safe_pack.py ./my-project --output package.xml")
|
||||
print(" safe_pack.py ./my-project --exclude '.*test.*' '.*\.example'")
|
||||
print(" safe_pack.py ./my-project --force # Dangerous! Skip scan")
|
||||
sys.exit(1)
|
||||
|
||||
directory = Path(sys.argv[1]).resolve()
|
||||
|
||||
if not directory.is_dir():
|
||||
print(f"Error: {directory} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse arguments
|
||||
output_file = None
|
||||
config_file = None
|
||||
force = '--force' in sys.argv
|
||||
exclude_patterns = []
|
||||
|
||||
if '--output' in sys.argv:
|
||||
output_idx = sys.argv.index('--output')
|
||||
if output_idx + 1 < len(sys.argv):
|
||||
output_file = Path(sys.argv[output_idx + 1])
|
||||
|
||||
if '--config' in sys.argv:
|
||||
config_idx = sys.argv.index('--config')
|
||||
if config_idx + 1 < len(sys.argv):
|
||||
config_file = Path(sys.argv[config_idx + 1])
|
||||
|
||||
if '--exclude' in sys.argv:
|
||||
exclude_idx = sys.argv.index('--exclude')
|
||||
exclude_patterns = [
|
||||
arg for arg in sys.argv[exclude_idx + 1:]
|
||||
if not arg.startswith('--') and arg != str(directory)
|
||||
]
|
||||
|
||||
print(f"🔍 Scanning {directory} for hardcoded secrets...\n")
|
||||
|
||||
# Step 1: Scan for secrets
|
||||
findings = run_secret_scan(directory, exclude_patterns)
|
||||
|
||||
# Step 2: Report findings
|
||||
print_findings_report(findings)
|
||||
|
||||
# Step 3: Decision point
|
||||
if findings:
|
||||
if force:
|
||||
print("⚠️ WARNING: --force flag set, packing anyway despite secrets found!\n")
|
||||
else:
|
||||
print("❌ Cannot pack: Secrets detected!")
|
||||
print("\nRecommended actions:")
|
||||
print("1. Review the findings above")
|
||||
print("2. Replace hardcoded credentials with environment variables")
|
||||
print("3. Run scan_secrets.py to verify cleanup")
|
||||
print("4. Run this script again")
|
||||
print("\nOr use --force to pack anyway (NOT RECOMMENDED)")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 4: Pack with repomix
|
||||
print(f"📦 Packing {directory} with repomix...\n")
|
||||
run_repomix(directory, output_file, config_file)
|
||||
|
||||
print("\n✅ Packaging complete!")
|
||||
|
||||
if findings:
|
||||
print("\n⚠️ WARNING: Package contains secrets (--force was used)")
|
||||
print(" DO NOT share this package publicly!")
|
||||
else:
|
||||
print(" Package is safe to distribute.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
201
repomix-safe-mixer/scripts/scan_secrets.py
Normal file
201
repomix-safe-mixer/scripts/scan_secrets.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Security scanner for detecting hardcoded credentials in code.
|
||||
|
||||
Scans a directory for common credential patterns and reports findings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
# Common secret patterns (regex)
|
||||
SECRET_PATTERNS = {
|
||||
'aws_access_key': r'(?i)AKIA[0-9A-Z]{16}',
|
||||
'aws_secret_key': r'(?i)(?:aws_secret|aws.{0,20}secret).{0,20}[=:]\s*["\']?([0-9a-zA-Z/+=]{40})["\']?',
|
||||
'supabase_url': r'https://[a-z]{20}\.supabase\.co',
|
||||
'supabase_anon_key': r'eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*',
|
||||
'stripe_key': r'(?:sk|pk)_(live|test)_[0-9a-zA-Z]{24,}',
|
||||
'cloudflare_api_token': r'(?i)(?:cloudflare|cf).{0,20}(?:token|key).{0,20}[=:]\s*["\']?([a-zA-Z0-9_-]{40,})["\']?',
|
||||
'turnstile_key': r'0x[0-9A-F]{22}',
|
||||
'generic_api_key': r'(?i)(?:api[_-]?key|apikey).{0,20}[=:]\s*["\']?([0-9a-zA-Z_\-]{20,})["\']?',
|
||||
'r2_account_id': r'[0-9a-f]{32}(?=\.r2\.cloudflarestorage\.com)',
|
||||
'jwt_token': r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}',
|
||||
'private_key': r'-----BEGIN (?:RSA|EC|OPENSSH|DSA) PRIVATE KEY-----',
|
||||
'oauth_secret': r'(?i)(?:client_secret|oauth).{0,20}[=:]\s*["\']?([0-9a-zA-Z_\-]{20,})["\']?',
|
||||
}
|
||||
|
||||
# File extensions to scan
|
||||
SCANNABLE_EXTENSIONS = {
|
||||
'.ts', '.tsx', '.js', '.jsx', '.py', '.md', '.json', '.yaml', '.yml',
|
||||
'.env', '.env.example', '.env.local', '.env.production', '.env.development',
|
||||
'.sh', '.bash', '.zsh', '.sql', '.go', '.java', '.rb', '.php', '.cs'
|
||||
}
|
||||
|
||||
# Directories to skip
|
||||
SKIP_DIRS = {
|
||||
'node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build',
|
||||
'.next', '.nuxt', 'vendor', 'target', 'bin', 'obj', '.terraform'
|
||||
}
|
||||
|
||||
class SecretFinding:
|
||||
"""Represents a detected secret."""
|
||||
def __init__(self, file_path: str, line_num: int, pattern_name: str,
|
||||
matched_text: str, line_content: str):
|
||||
self.file_path = file_path
|
||||
self.line_num = line_num
|
||||
self.pattern_name = pattern_name
|
||||
self.matched_text = matched_text
|
||||
self.line_content = line_content.strip()
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
'file': self.file_path,
|
||||
'line': self.line_num,
|
||||
'type': self.pattern_name,
|
||||
'match': self.matched_text[:50] + '...' if len(self.matched_text) > 50 else self.matched_text,
|
||||
'context': self.line_content[:100] + '...' if len(self.line_content) > 100 else self.line_content
|
||||
}
|
||||
|
||||
def scan_file(file_path: Path, base_dir: Path) -> List[SecretFinding]:
|
||||
"""Scan a single file for secrets."""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
for pattern_name, pattern in SECRET_PATTERNS.items():
|
||||
matches = re.finditer(pattern, line)
|
||||
for match in matches:
|
||||
# Skip common false positives
|
||||
if should_skip_match(line, match.group()):
|
||||
continue
|
||||
|
||||
findings.append(SecretFinding(
|
||||
file_path=str(file_path.relative_to(base_dir)),
|
||||
line_num=line_num,
|
||||
pattern_name=pattern_name,
|
||||
matched_text=match.group(),
|
||||
line_content=line
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not scan {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
return findings
|
||||
|
||||
def should_skip_match(line: str, match: str) -> bool:
|
||||
"""Check if a match should be skipped (likely false positive)."""
|
||||
# Skip example/placeholder values
|
||||
placeholders = [
|
||||
'your-', 'example', 'placeholder', 'xxx', 'yyy', 'zzz',
|
||||
'test-', 'demo-', 'sample-', '<YOUR_', '${', 'TODO'
|
||||
]
|
||||
|
||||
line_lower = line.lower()
|
||||
match_lower = match.lower()
|
||||
|
||||
for placeholder in placeholders:
|
||||
if placeholder in match_lower or placeholder in line_lower:
|
||||
return True
|
||||
|
||||
# Skip if in a comment
|
||||
if re.search(r'^\s*(?://|#|/\*|\*)', line):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def scan_directory(directory: Path, exclude_patterns: List[str] = None) -> List[SecretFinding]:
|
||||
"""Scan a directory recursively for secrets."""
|
||||
findings = []
|
||||
exclude_patterns = exclude_patterns or []
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# Skip excluded directories
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
||||
|
||||
root_path = Path(root)
|
||||
|
||||
# Skip if matches exclude pattern
|
||||
if any(re.search(pattern, str(root_path)) for pattern in exclude_patterns):
|
||||
continue
|
||||
|
||||
for file in files:
|
||||
file_path = root_path / file
|
||||
|
||||
# Only scan relevant file types
|
||||
if file_path.suffix not in SCANNABLE_EXTENSIONS:
|
||||
continue
|
||||
|
||||
# Skip if matches exclude pattern
|
||||
if any(re.search(pattern, str(file_path)) for pattern in exclude_patterns):
|
||||
continue
|
||||
|
||||
file_findings = scan_file(file_path, directory)
|
||||
findings.extend(file_findings)
|
||||
|
||||
return findings
|
||||
|
||||
def print_report(findings: List[SecretFinding], directory: Path):
|
||||
"""Print a human-readable report."""
|
||||
if not findings:
|
||||
print("✅ No secrets detected!")
|
||||
return
|
||||
|
||||
print(f"\n⚠️ Found {len(findings)} potential secrets in {directory}:\n")
|
||||
|
||||
# Group by file
|
||||
by_file = {}
|
||||
for finding in findings:
|
||||
if finding.file_path not in by_file:
|
||||
by_file[finding.file_path] = []
|
||||
by_file[finding.file_path].append(finding)
|
||||
|
||||
# Print grouped
|
||||
for file_path in sorted(by_file.keys()):
|
||||
print(f"📄 {file_path}")
|
||||
for finding in by_file[file_path]:
|
||||
print(f" Line {finding.line_num}: {finding.pattern_name}")
|
||||
print(f" Match: {finding.matched_text[:80]}")
|
||||
print(f" Context: {finding.line_content[:80]}")
|
||||
print()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: scan_secrets.py <directory> [--json] [--exclude pattern1 pattern2 ...]")
|
||||
print("\nExamples:")
|
||||
print(" scan_secrets.py ./my-project")
|
||||
print(" scan_secrets.py ./my-project --json")
|
||||
print(" scan_secrets.py ./my-project --exclude '.*test.*' '.*example.*'")
|
||||
sys.exit(1)
|
||||
|
||||
directory = Path(sys.argv[1]).resolve()
|
||||
|
||||
if not directory.is_dir():
|
||||
print(f"Error: {directory} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse arguments
|
||||
json_output = '--json' in sys.argv
|
||||
exclude_patterns = []
|
||||
|
||||
if '--exclude' in sys.argv:
|
||||
exclude_idx = sys.argv.index('--exclude')
|
||||
exclude_patterns = [arg for arg in sys.argv[exclude_idx + 1:] if not arg.startswith('--')]
|
||||
|
||||
# Scan
|
||||
findings = scan_directory(directory, exclude_patterns)
|
||||
|
||||
# Output
|
||||
if json_output:
|
||||
print(json.dumps([f.to_dict() for f in findings], indent=2))
|
||||
else:
|
||||
print_report(findings, directory)
|
||||
|
||||
# Exit code
|
||||
sys.exit(1 if findings else 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user