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:
315
repomix-safe-mixer/SKILL.md
Normal file
315
repomix-safe-mixer/SKILL.md
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
name: repomix-safe-mixer
|
||||
description: Safely package codebases with repomix by automatically detecting and removing hardcoded credentials before packing. Use when packaging code for distribution, creating reference packages, or when the user mentions security concerns about sharing code with repomix.
|
||||
---
|
||||
|
||||
# Repomix Safe Mixer
|
||||
|
||||
## Overview
|
||||
|
||||
Safely package codebases with repomix by automatically detecting and removing hardcoded credentials.
|
||||
|
||||
This skill prevents accidental credential exposure when packaging code with repomix. It scans for hardcoded secrets (API keys, database credentials, tokens), reports findings, and ensures safe packaging.
|
||||
|
||||
**When to use**: When packaging code with repomix for distribution, creating shareable reference packages, or whenever security concerns exist about hardcoded credentials in code.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### Standard Safe Packaging
|
||||
|
||||
Use `safe_pack.py` from this skill's `scripts/` directory for the complete workflow: scan → report → pack.
|
||||
|
||||
```bash
|
||||
python3 scripts/safe_pack.py <directory>
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
1. Scans directory for hardcoded credentials
|
||||
2. Reports findings with file/line details
|
||||
3. Blocks packaging if secrets found
|
||||
4. Packs with repomix only if scan is clean
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py ./my-project
|
||||
```
|
||||
|
||||
**Output if clean**:
|
||||
```
|
||||
🔍 Scanning ./my-project for hardcoded secrets...
|
||||
✅ No secrets detected!
|
||||
📦 Packing ./my-project with repomix...
|
||||
✅ Packaging complete!
|
||||
Package is safe to distribute.
|
||||
```
|
||||
|
||||
**Output if secrets found**:
|
||||
```
|
||||
🔍 Scanning ./my-project for hardcoded secrets...
|
||||
⚠️ Security Scan Found 3 Potential Secrets:
|
||||
|
||||
🔴 supabase_url: 1 instance(s)
|
||||
- src/client.ts:5
|
||||
Match: https://ghyttjckzmzdxumxcixe.supabase.co
|
||||
|
||||
❌ Cannot pack: Secrets detected!
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
**Custom output file**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./my-project \
|
||||
--output package.xml
|
||||
```
|
||||
|
||||
**With repomix config**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./my-project \
|
||||
--config repomix.config.json
|
||||
```
|
||||
|
||||
**Exclude patterns from scanning**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./my-project \
|
||||
--exclude '.*test.*' '.*\.example'
|
||||
```
|
||||
|
||||
**Force pack (dangerous, skip scan)**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./my-project \
|
||||
--force # ⚠️ NOT RECOMMENDED
|
||||
```
|
||||
|
||||
## Standalone Secret Scanning
|
||||
|
||||
Use `scan_secrets.py` from this skill's `scripts/` directory for scanning only (without packing).
|
||||
|
||||
```bash
|
||||
python3 scripts/scan_secrets.py <directory>
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Verify cleanup after removing credentials
|
||||
- Pre-commit security checks
|
||||
- Audit existing codebases
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
python3 scripts/scan_secrets.py ./my-project
|
||||
```
|
||||
|
||||
**JSON output for programmatic use**:
|
||||
```bash
|
||||
python3 scripts/scan_secrets.py \
|
||||
./my-project \
|
||||
--json
|
||||
```
|
||||
|
||||
**Exclude patterns**:
|
||||
```bash
|
||||
python3 scripts/scan_secrets.py \
|
||||
./my-project \
|
||||
--exclude '.*test.*' '.*example.*' '.*SECURITY_AUDIT\.md'
|
||||
```
|
||||
|
||||
## Detected Secret Types
|
||||
|
||||
The scanner detects common credential patterns including:
|
||||
|
||||
**Cloud Providers**:
|
||||
- AWS Access Keys (`AKIA...`)
|
||||
- Cloudflare R2 Account IDs and Access Keys
|
||||
- Supabase Project URLs and Anon Keys
|
||||
|
||||
**API Keys**:
|
||||
- Stripe Keys (`sk_live_...`, `pk_live_...`)
|
||||
- OpenAI API Keys (`sk-...`)
|
||||
- Google Gemini API Keys (`AIza...`)
|
||||
- Generic API Keys
|
||||
|
||||
**Authentication**:
|
||||
- JWT Tokens (`eyJ...`)
|
||||
- OAuth Client Secrets
|
||||
- Private Keys (`-----BEGIN PRIVATE KEY-----`)
|
||||
- Turnstile Keys (`0x...`)
|
||||
|
||||
See `references/common_secrets.md` for complete list and patterns.
|
||||
|
||||
## Handling Detected Secrets
|
||||
|
||||
When secrets are found:
|
||||
|
||||
### Step 1: Review Findings
|
||||
|
||||
Examine each finding to verify it's a real credential (not a placeholder or example).
|
||||
|
||||
### Step 2: Replace with Environment Variables
|
||||
|
||||
**Before**:
|
||||
```javascript
|
||||
const SUPABASE_URL = "https://ghyttjckzmzdxumxcixe.supabase.co";
|
||||
const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
|
||||
```
|
||||
|
||||
**After**:
|
||||
```javascript
|
||||
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "https://your-project-ref.supabase.co";
|
||||
const API_KEY = import.meta.env.VITE_API_KEY || "your-api-key-here";
|
||||
|
||||
// Validation
|
||||
if (!import.meta.env.VITE_SUPABASE_URL) {
|
||||
console.error("⚠️ Missing VITE_SUPABASE_URL environment variable");
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create .env.example
|
||||
|
||||
```bash
|
||||
# Example environment variables
|
||||
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
VITE_API_KEY=your-api-key-here
|
||||
|
||||
# Instructions:
|
||||
# 1. Copy this file to .env
|
||||
# 2. Replace placeholders with real values
|
||||
# 3. Never commit .env to version control
|
||||
```
|
||||
|
||||
### Step 4: Verify Cleanup
|
||||
|
||||
Run scanner again to confirm secrets removed:
|
||||
```bash
|
||||
python3 scripts/scan_secrets.py ./my-project
|
||||
```
|
||||
|
||||
### Step 5: Safe Pack
|
||||
|
||||
Once clean, package safely:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py ./my-project
|
||||
```
|
||||
|
||||
## Post-Exposure Actions
|
||||
|
||||
If credentials were already exposed (e.g., committed to git, shared publicly):
|
||||
|
||||
1. **Rotate credentials immediately** - Generate new keys/tokens
|
||||
2. **Revoke old credentials** - Disable compromised credentials
|
||||
3. **Audit usage** - Check logs for unauthorized access
|
||||
4. **Monitor** - Set up alerts for unusual activity
|
||||
5. **Update deployment** - Deploy code with new credentials
|
||||
6. **Document incident** - Record what was exposed and actions taken
|
||||
|
||||
## Common False Positives
|
||||
|
||||
The scanner skips common false positives:
|
||||
|
||||
**Placeholders**:
|
||||
- `your-api-key`, `example-key`, `placeholder-value`
|
||||
- `<YOUR_API_KEY>`, `${API_KEY}`, `TODO: add key`
|
||||
|
||||
**Test/Example files**:
|
||||
- Files matching `.*test.*`, `.*example.*`, `.*sample.*`
|
||||
|
||||
**Comments**:
|
||||
- Lines starting with `//`, `#`, `/*`, `*`
|
||||
|
||||
**Environment variable references** (correct usage):
|
||||
- `process.env.API_KEY`
|
||||
- `import.meta.env.VITE_API_KEY`
|
||||
- `Deno.env.get('API_KEY')`
|
||||
|
||||
Use `--exclude` to skip additional patterns if needed.
|
||||
|
||||
## Integration with Repomix
|
||||
|
||||
This skill works with standard repomix:
|
||||
|
||||
**Default usage** (no config):
|
||||
```bash
|
||||
python3 scripts/safe_pack.py ./project
|
||||
```
|
||||
|
||||
**With repomix config**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./project \
|
||||
--config repomix.config.json
|
||||
```
|
||||
|
||||
**Custom output location**:
|
||||
```bash
|
||||
python3 scripts/safe_pack.py \
|
||||
./project \
|
||||
--output ~/Downloads/package-clean.xml
|
||||
```
|
||||
|
||||
The skill runs repomix internally after security validation, passing through config and output options.
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### Workflow 1: Package a Clean Project
|
||||
|
||||
```bash
|
||||
# Scan and pack in one command
|
||||
python3 scripts/safe_pack.py \
|
||||
~/workspace/my-project \
|
||||
--output ~/Downloads/my-project-package.xml
|
||||
```
|
||||
|
||||
### Workflow 2: Clean and Package a Project with Secrets
|
||||
|
||||
```bash
|
||||
# Step 1: Scan to discover secrets
|
||||
python3 scripts/scan_secrets.py ~/workspace/my-project
|
||||
|
||||
# Step 2: Review findings and replace credentials with env vars
|
||||
# (Edit files manually or with automation)
|
||||
|
||||
# Step 3: Verify cleanup
|
||||
python3 scripts/scan_secrets.py ~/workspace/my-project
|
||||
|
||||
# Step 4: Package safely
|
||||
python3 scripts/safe_pack.py \
|
||||
~/workspace/my-project \
|
||||
--output ~/Downloads/my-project-clean.xml
|
||||
```
|
||||
|
||||
### Workflow 3: Audit Before Commit
|
||||
|
||||
```bash
|
||||
# Pre-commit hook: scan for secrets
|
||||
python3 scripts/scan_secrets.py . --json
|
||||
|
||||
# Exit code 1 if secrets found (blocks commit)
|
||||
# Exit code 0 if clean (allows commit)
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**References**:
|
||||
- `references/common_secrets.md` - Complete credential pattern catalog
|
||||
|
||||
**Scripts**:
|
||||
- `scripts/scan_secrets.py` - Standalone security scanner
|
||||
- `scripts/safe_pack.py` - Complete scan → pack workflow
|
||||
|
||||
**Related Skills**:
|
||||
- `repomix-unmixer` - Extracts files from repomix packages
|
||||
- `skill-creator` - Creates new Claude Code skills
|
||||
|
||||
## Security Note
|
||||
|
||||
This skill detects common patterns but may not catch all credential types. Always:
|
||||
- Review findings manually
|
||||
- Rotate exposed credentials
|
||||
- Use .env.example templates
|
||||
- Validate environment variables
|
||||
- Monitor for unauthorized access
|
||||
|
||||
**Not a replacement for**: Secret scanning in CI/CD, git history scanning, or comprehensive security audits.
|
||||
252
repomix-safe-mixer/references/common_secrets.md
Normal file
252
repomix-safe-mixer/references/common_secrets.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Common Secret Patterns Reference
|
||||
|
||||
This document catalogs common credential types detected by the security scanner.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Cloud Provider Credentials](#cloud-provider-credentials)
|
||||
- [Database Credentials](#database-credentials)
|
||||
- [API Keys and Tokens](#api-keys-and-tokens)
|
||||
- [Authentication Secrets](#authentication-secrets)
|
||||
- [Common False Positives](#common-false-positives)
|
||||
|
||||
---
|
||||
|
||||
## Cloud Provider Credentials
|
||||
|
||||
### AWS Credentials
|
||||
|
||||
**AWS Access Key ID**:
|
||||
- Pattern: `AKIA[0-9A-Z]{16}`
|
||||
- Example: `AKIAIOSFODNN7EXAMPLE`
|
||||
- Location: Often in `.env`, config files, or infrastructure code
|
||||
- Risk: Full AWS account access
|
||||
|
||||
**AWS Secret Access Key**:
|
||||
- Pattern: `[0-9a-zA-Z/+=]{40}`
|
||||
- Context: Usually follows `aws_secret` or similar variable names
|
||||
- Example: `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`
|
||||
- Risk: Account compromise, data breach, cost abuse
|
||||
|
||||
### Cloudflare R2
|
||||
|
||||
**R2 Account ID**:
|
||||
- Pattern: `[0-9a-f]{32}` (in R2 URLs)
|
||||
- Example: `89ff427005e1767943b5ac257905a280` in `https://89ff427005e1767943b5ac257905a280.r2.cloudflarestorage.com`
|
||||
- Risk: Account identification, targeted attacks
|
||||
|
||||
**R2 Access Keys**:
|
||||
- Similar to AWS S3 credentials
|
||||
- Pattern: Standard access key + secret key pair
|
||||
- Risk: Bucket access, file manipulation, cost abuse
|
||||
|
||||
---
|
||||
|
||||
## Database Credentials
|
||||
|
||||
### Supabase
|
||||
|
||||
**Project URL**:
|
||||
- Pattern: `https://[a-z]{20}.supabase.co`
|
||||
- Example: `https://ghyttjckzmzdxumxcixe.supabase.co`
|
||||
- Risk: Project identification
|
||||
|
||||
**Anon/Public Key**:
|
||||
- Pattern: JWT token starting with `eyJ`
|
||||
- Example: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`
|
||||
- Risk: Public data access, edge function invocation, quota abuse
|
||||
|
||||
**Service Role Key**:
|
||||
- Pattern: JWT token (longer than anon key)
|
||||
- Risk: **CRITICAL** - Full database admin access, bypasses RLS
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
**Connection String**:
|
||||
- Pattern: `postgresql://user:password@host:port/database`
|
||||
- Risk: Direct database access
|
||||
|
||||
---
|
||||
|
||||
## API Keys and Tokens
|
||||
|
||||
### Stripe
|
||||
|
||||
**Publishable Key**:
|
||||
- Pattern: `pk_(live|test)_[0-9a-zA-Z]{24,}`
|
||||
- Example: `pk_live_51AbC...` (truncated for security)
|
||||
- Risk: Low (public by design, but reveals account)
|
||||
|
||||
**Secret Key**:
|
||||
- Pattern: `sk_(live|test)_[0-9a-zA-Z]{24,}`
|
||||
- Example: `sk_live_51AbC...` (truncated for security)
|
||||
- Risk: **CRITICAL** - Payment processing, refunds, customer data
|
||||
|
||||
### OpenAI / Gemini / LLM Providers
|
||||
|
||||
**OpenAI API Key**:
|
||||
- Pattern: `sk-[A-Za-z0-9]{48}`
|
||||
- Risk: API abuse, cost accumulation
|
||||
|
||||
**Google Gemini API Key**:
|
||||
- Pattern: `AIza[0-9A-Za-z_-]{35}`
|
||||
- Risk: API abuse, quota exhaustion
|
||||
|
||||
**OpenRouter API Key**:
|
||||
- Pattern: `sk-or-v1-[0-9a-f]{64}`
|
||||
- Risk: API abuse via OpenRouter
|
||||
|
||||
### Cloudflare Turnstile
|
||||
|
||||
**Site Key**:
|
||||
- Pattern: `0x[0-9A-F]{22}`
|
||||
- Example: `0x4AAAAAABvH03QZ3BpnHR7p`
|
||||
- Risk: Low (public by design), but enables testing
|
||||
|
||||
**Secret Key**:
|
||||
- Pattern: `0x[0-9A-F]{40}`
|
||||
- Risk: Bot protection bypass
|
||||
|
||||
---
|
||||
|
||||
## Authentication Secrets
|
||||
|
||||
### JWT Tokens
|
||||
|
||||
**Format**:
|
||||
- Pattern: `eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+`
|
||||
- Three base64url-encoded parts separated by dots
|
||||
- Risk: Session hijacking, impersonation
|
||||
|
||||
### OAuth Secrets
|
||||
|
||||
**Client Secret**:
|
||||
- Pattern: Variable, often `[0-9a-zA-Z_-]{20,}`
|
||||
- Context: Near `client_id`, `oauth`, `app_secret`
|
||||
- Risk: Application impersonation
|
||||
|
||||
### Private Keys
|
||||
|
||||
**RSA/EC Private Keys**:
|
||||
- Pattern: `-----BEGIN (RSA|EC|OPENSSH|DSA) PRIVATE KEY-----`
|
||||
- Risk: **CRITICAL** - Complete identity compromise
|
||||
|
||||
---
|
||||
|
||||
## Common False Positives
|
||||
|
||||
### Example/Placeholder Values
|
||||
|
||||
Safe to ignore when matching:
|
||||
- Strings containing: `example`, `placeholder`, `test`, `demo`, `sample`
|
||||
- Template variables: `<YOUR_API_KEY>`, `${API_KEY}`, `${...}`
|
||||
- Documentation examples: `xxx`, `yyy`, `zzz`
|
||||
- TODO markers: `TODO`, `FIXME`, `CHANGEME`
|
||||
|
||||
### Environment Variable References
|
||||
|
||||
Safe patterns (these are correct usage):
|
||||
```javascript
|
||||
// JavaScript/TypeScript
|
||||
const apiKey = process.env.API_KEY;
|
||||
const apiKey = import.meta.env.VITE_API_KEY;
|
||||
|
||||
// Python
|
||||
api_key = os.getenv('API_KEY')
|
||||
api_key = os.environ.get('API_KEY')
|
||||
|
||||
// Deno
|
||||
const apiKey = Deno.env.get('API_KEY');
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
Lines starting with comment markers are often documentation:
|
||||
- `//` - JavaScript/TypeScript
|
||||
- `#` - Python/Shell/YAML
|
||||
- `/* */` - Multi-line comments
|
||||
|
||||
---
|
||||
|
||||
## Detection Strategies
|
||||
|
||||
### Context-Aware Scanning
|
||||
|
||||
Look for credential indicators:
|
||||
- Variable names: `API_KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `PRIVATE_KEY`
|
||||
- Assignment operators: `=`, `:`, `=>`
|
||||
- Quote patterns: `"..."`, `'...'`, `` `...` ``
|
||||
|
||||
### File Type Priorities
|
||||
|
||||
**High Risk**:
|
||||
- `.env`, `.env.local`, `.env.production`
|
||||
- Configuration files: `config.json`, `settings.py`
|
||||
- Infrastructure code: `.tf`, `.yaml` (Terraform, K8s)
|
||||
|
||||
**Medium Risk**:
|
||||
- Source code: `.js`, `.ts`, `.py`, `.go`
|
||||
- Documentation: `.md` (may contain examples)
|
||||
|
||||
**Low Risk**:
|
||||
- Test files: `*.test.js`, `*.spec.ts`
|
||||
- Example files: `*.example.*`
|
||||
|
||||
---
|
||||
|
||||
## Remediation Patterns
|
||||
|
||||
### Convert to Environment Variables
|
||||
|
||||
**Before** (hardcoded):
|
||||
```javascript
|
||||
const SUPABASE_URL = "https://ghyttjckzmzdxumxcixe.supabase.co";
|
||||
const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
|
||||
```
|
||||
|
||||
**After** (environment variables):
|
||||
```javascript
|
||||
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "https://your-project-ref.supabase.co";
|
||||
const SUPABASE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || "your-anon-key-here";
|
||||
|
||||
// Validation
|
||||
if (!import.meta.env.VITE_SUPABASE_URL) {
|
||||
console.error("Missing VITE_SUPABASE_URL environment variable");
|
||||
}
|
||||
```
|
||||
|
||||
### Create .env.example
|
||||
|
||||
```bash
|
||||
# Supabase Configuration
|
||||
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=your-anon-key-here
|
||||
|
||||
# API Keys
|
||||
GEMINI_API_KEY=your-gemini-key
|
||||
OPENROUTER_API_KEY=your-openrouter-key
|
||||
|
||||
# Important: Copy this to .env and replace with real values
|
||||
# Never commit .env to version control!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-Exposure Actions
|
||||
|
||||
If credentials are exposed:
|
||||
|
||||
1. **Rotate Immediately** - Generate new credentials
|
||||
2. **Revoke Old Credentials** - Disable compromised keys
|
||||
3. **Audit Usage** - Check for unauthorized access
|
||||
4. **Monitor** - Set up alerts for unusual activity
|
||||
5. **Update Code** - Deploy with new credentials
|
||||
6. **Notify** - If public exposure, notify security team
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||
- [AWS Credentials Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||
- [GitHub Secret Scanning Patterns](https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning)
|
||||
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