diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b350331..d8defba 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -109,6 +109,19 @@ files: \.(yaml|yml|json)$ require_serial: false +# ============================================================================= +# AI-POWERED VALIDATION HOOKS +# ============================================================================= + +- id: ai_commit_check + name: AI-powered commit validation + entry: ai_commit_check + language: python + pass_filenames: false + stages: [manual] + require_serial: true + additional_dependencies: ['opencode-ai>=0.1.0a36', 'rich>=13.7.0'] + # ============================================================================= # SECURITY SCANNING HOOKS # ============================================================================= diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2703f26 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,397 @@ +# AI Agent Instructions for Pre-Commit Hooks Library + +This file provides context for AI assistants working on this repository. + +## Repository Purpose + +This is a **pre-commit hooks library** for validating and securing code generated by GenAI tools (GitHub Copilot, ChatGPT, etc.). The library provides hooks that detect: +- Hardcoded credentials and secrets +- Security vulnerabilities +- Code quality issues +- Infrastructure misconfigurations + +## Code Architecture + +### Directory Structure +- `hooks/` - Hook implementations (Python scripts and shell scripts) +- `examples/` - Example pre-commit configurations for different project types +- `.pre-commit-hooks.yaml` - Hook registry defining all available hooks +- `README.md` - Main documentation +- `CONFIGURATION_GUIDE.md` - Detailed configuration guide +- `DEPENDENCIES.md` - Installation instructions + +### Hook Types + +**Python Hooks** (`hooks/*.py`): +- Security scanners: `detect_hardcoded_credentials.py`, `detect_hardcoded_urls.py`, `genai_security_check.py` +- Language-specific scanners: `dotnet_security_scan.py`, `ansible_security_scan.py` +- File validators: `check_xml.py`, `check_license.py` +- AI-powered validation: `ai_commit_check.py` (supports both pre-commit and pre-push modes) + +**Shell Script Hooks** (`hooks/*.sh`): +- Language tooling wrappers: `eslint.sh`, `prettier.sh`, `go_fmt.sh` +- Security tools: `semgrep.sh`, `truffhog.sh` +- Infrastructure validation: `terraform_validate.sh`, `cfn_validate.sh` + +## Coding Standards + +### Python Hooks +```python +#!/usr/bin/env python3 +""" +Brief description of what this hook checks. +""" + +import sys +import argparse +from typing import List, Tuple + +def main(filenames: List[str]) -> int: + """Main entry point.""" + # Hook logic here + return 0 # Pass: 0, Fail: 1, Tool missing: 3 + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) +``` + +**Standards**: +- Use type hints for function signatures +- Exit codes: 0 (pass), 1 (fail), 3 (tool missing/error) +- Handle file encoding issues gracefully (UTF-8, ASCII) +- Support inline suppression comments where appropriate +- Provide clear, actionable error messages + +### Shell Script Hooks +```bash +#!/usr/bin/env bash + +# Check if required tool is available +if ! command -v tool_name &> /dev/null; then + echo "Error: tool_name is not installed" >&2 + exit 3 +fi + +# Run the tool +tool_name "$@" +exit $? +``` + +**Standards**: +- Use `#!/usr/bin/env bash` for portability +- Check tool availability before running +- Quote variables to handle paths with spaces +- Exit with appropriate codes + +### Hook Registry (`.pre-commit-hooks.yaml`) +```yaml +- id: hook_name + name: Human-readable name + entry: hook_script_or_command + language: python|script|system + types: [python] # or files: \.py$ + require_serial: false +``` + +## Security Principles + +### For Hook Implementations +1. **No false negatives**: Better to flag false positives than miss real issues +2. **Clear suppression mechanism**: Users should be able to suppress false positives with inline comments +3. **Context awareness**: Be more lenient in test files, strict in production code +4. **Performance**: Hooks should run quickly, even on large codebases + +### For This Repository +1. **No real secrets**: All examples must use obviously fake credentials +2. **Secure defaults**: Default configurations should be secure +3. **Safe examples**: Example code should demonstrate best practices + +### For AI Commit Check Hook +The `ai_commit_check` hook has special security requirements and dual-mode operation: + +**Technical Implementation:** +- **Disabled by default**: Hook uses `stages: [manual]` in `.pre-commit-hooks.yaml` to require explicit opt-in +- **User opt-in required**: Users must add `stages: [pre-commit]` or `stages: [pre-push]` to their config to enable +- **Mode Detection**: Checks `git diff --cached` for staged changes to determine mode +- **Pre-commit mode**: Reviews staged changes using `git diff --cached` +- **Pre-push mode**: Compares current branch to `origin/main` or `origin/master` using `git diff origin/{branch}...HEAD` +- **Security**: Requires `opencode.jsonc` file in repository root with security policies +- **Config enforcement**: Hook checks for `opencode.jsonc` and exits with error if missing +- **Model Restrictions**: Only allows GitHub Copilot and Amazon Bedrock with Claude Sonnet 4.5 +- **Exit behavior**: Blocks commits/pushes with `-COMMIT REJECTED-` prefix in response + +**Supported Providers:** +- **GitHub Copilot** (default): `github-copilot/claude-sonnet-4.5` +- **Amazon Bedrock**: `amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0` + - Required region: `us-east-1` or `us-gov-*` + - Enforced via `OPENCODE_BEDROCK_REGION` environment variable + - Hook exits with error code 3 if region is invalid or not us-east-1/us-gov-* + +**Key Implementation Details:** +- Exit codes: 0 (pass), 1 (fail/rejected), 3 (tool missing/error) +- Denies: `webfetch`, cloud CLIs (`aws`, `az`, `gcloud`), `curl`, `wget`, `terraform` +- Disables all AI providers except: `github-copilot` and `amazon-bedrock` +- Requires `opencode.jsonc` in repository root +- Starts temporary opencode server on available port (default: 61164) +- Creates AI session with commit context and security review prompt +- Sets AWS region environment variables (`AWS_DEFAULT_REGION`, `AWS_REGION`) when using Bedrock + +For user-facing configuration and setup instructions, see CONFIGURATION_GUIDE.md and README.md. + +## Testing Approach + +### Before Committing Hook Changes +1. **Test with multiple file types**: Ensure hook works across languages +2. **Test false positives**: Verify legitimate code isn't flagged incorrectly +3. **Test edge cases**: Empty files, binary files, large files, special characters +4. **Test performance**: Run on large repositories +5. **Cross-platform**: Test on Linux, macOS, and Windows if possible + +### Common Test Cases +```bash +# Test pass case +echo 'const x = 1;' > test.js && python hooks/your_hook.py test.js + +# Test fail case +echo 'password = "secret123"' > test.py && python hooks/your_hook.py test.py + +# Test suppression +echo 'password = "test" # pragma: allowlist secret' > test.py && python hooks/your_hook.py test.py +``` + +## Common Patterns + +### Inline Suppression Support +Hooks should support these suppression comments: +- `# pragma: allowlist secret` - For credentials/secrets (Python) +- `// pragma: allowlist secret` - For credentials/secrets (JavaScript/Java) +- `# genai:ignore` - For GenAI security checks + +### Safe Context Detection +Be more lenient in these contexts: +```python +SAFE_CONTEXTS = { + 'test', 'spec', '__tests__', '.test.', '.spec.', # Test files + 'example', 'demo', 'sample', # Examples + 'docker', 'compose', # Docker configs + 'dev', 'development', 'local', # Dev configs +} +``` + +### Pattern Matching Best Practices +```python +# Good: Specific pattern with context +r'password\s*[:=]\s*["\'][^"\']{3,}["\']' + +# Bad: Too broad, many false positives +r'password.*=.*' + +# Good: Negative lookbehind to exclude compound words +r'(?=0.1.0a36` and `rich>=13.7.0` + ## [1.0.0] - 2024-01-XX ### Added diff --git a/CONFIGURATION_GUIDE.md b/CONFIGURATION_GUIDE.md index f5418f7..f8081f2 100644 --- a/CONFIGURATION_GUIDE.md +++ b/CONFIGURATION_GUIDE.md @@ -6,6 +6,178 @@ This guide helps you configure the pre-commit hooks to minimize false positives Use the `examples/practical-security.yaml` configuration for a balanced approach that reduces false positives. +## AI-Powered Commit Validation + +### ai_commit_check Configuration + +The `ai_commit_check` hook intelligently adapts to two modes: +- **Pre-commit mode**: Reviews staged changes before committing +- **Pre-push mode**: Reviews all branch changes compared to `origin/main` or `origin/master` + +**Problem**: AI validation may be too slow for every commit, or you want comprehensive branch review before pushing + +**Solution**: Choose the stage that fits your workflow + +#### Configuration Options + +**Option 1: Manual pre-commit (recommended for frequent commits)** +```yaml +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: v1.1.7 + hooks: + - id: ai_commit_check + stages: [manual] # Default: disabled, opt-in required +``` + +**Note:** The hook is **disabled by default**. This is the default behavior - users must explicitly opt-in. + +Run with: `pre-commit run --hook-stage manual ai_commit_check` + +**Option 2: Automatic pre-push (recommended for comprehensive review)** +```yaml +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: # Replace with latest release version + hooks: + - id: ai_commit_check + stages: [pre-push] +``` + +**IMPORTANT**: For pre-push mode, install the pre-push hooks: +```bash +pre-commit install --hook-type pre-push +``` + +This runs automatically on `git push` and reviews all changes in your branch. + +#### Required Setup + +**Required: Security Configuration File:** + +The AI commit check hook **requires** an `opencode.jsonc` configuration file in your repository root to enforce security policies. + +**Setup:** + +1. Copy the contents of [`examples/opencode.jsonc`](https://github.com/TriaFed/pre-commit-library/blob/main/examples/opencode.jsonc) from this repository +2. Create a file named `opencode.jsonc` in your repository root +3. Paste the contents into your `opencode.jsonc` file +4. Commit the configuration to your repository + +**What's included:** +- Share disabled +- Auto-updates disabled +- Only GitHub Copilot and Amazon Bedrock allowed +- All other AI providers blocked +- Dangerous operations denied (webfetch, cloud CLIs, curl, wget, terraform) + +**Runtime Output:** + +The hook displays: +``` +✓ Using opencode.jsonc from repository root +``` + +If the file is missing, the hook will exit with an error and instructions on how to set it up. + +#### Environment Variables + +```bash +export OPENCODE_PORT=61164 # Port for opencode server (default: 61164) +export OPENCODE_MODEL=github-copilot/claude-sonnet-4.5 # AI model to use (default) +export OPENCODE_PROVIDER=github-copilot # Provider: github-copilot, amazon-bedrock (default: github-copilot) +export OPENCODE_TIMEOUT=90 # Timeout in seconds (default: 90) +export OPENCODE_BEDROCK_REGION=us-east-1 # Required for amazon-bedrock provider +``` + +#### Permissions and Custom Instructions + +The required `opencode.jsonc` includes safe defaults that deny dangerous operations. + +**To customize permissions**: See [OpenCode Permissions Documentation](https://opencode.ai/docs/permissions/) + +**To add custom AI instructions**: +```bash +# Start opencode and use the /init command +opencode +# Then type: /init +``` + +This creates instruction files with project-specific coding standards that the AI will follow during reviews. + +#### Usage + +| Mode | Command | +|------|---------| +| Manual pre-commit | `pre-commit run --hook-stage manual ai_commit_check` | +| Pre-push (automatic) | `git push` (runs automatically) | +| Pre-push (manual) | `pre-commit run --hook-stage pre-push ai_commit_check` | +| Skip validation | `git commit --no-verify` or `git push --no-verify` | + +#### Amazon Bedrock Provider + +**Problem**: Need to use Amazon Bedrock instead of GitHub Copilot + +**Solution**: Configure Bedrock with required region validation + +```yaml +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: v1.2.0 + hooks: + - id: ai_commit_check + stages: [manual] +``` + +**Set environment variables:** +```bash +export OPENCODE_PROVIDER=amazon-bedrock +export OPENCODE_MODEL=amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0 +export OPENCODE_BEDROCK_REGION=us-east-1 +``` + +**Required AWS Setup:** +```bash +# 1. Configure AWS region (required) +aws configure set region us-east-1 + +# 2. Verify AWS credentials +aws sts get-caller-identity + +# 3. Authenticate with OpenCode +opencode auth login # Select: Amazon Bedrock + +# 4. Test the connection +opencode run "hello world" +``` + +**Allowed Regions**: `us-east-1`, `us-gov-west-1`, `us-gov-east-1` + +**Security Note**: Region validation is enforced for compliance. The hook will exit with an error if an unauthorized region is specified. + +**Example Usage:** +```bash +# Set environment for the session +export OPENCODE_PROVIDER=amazon-bedrock +export OPENCODE_BEDROCK_REGION=us-east-1 + +# Run the AI commit check +pre-commit run --hook-stage manual ai_commit_check +``` + +**Troubleshooting:** + +If you see: +``` +Error: Amazon Bedrock region 'eu-west-1' is not allowed. +``` + +Fix with: +```bash +export OPENCODE_BEDROCK_REGION=us-east-1 +aws configure set region us-east-1 +``` + ## Common False Positive Issues and Solutions ### 1. detect-secrets and hardcoded-credentials diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 5e52efe..972e70f 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -124,6 +124,59 @@ pipx install pre-commit pip install black flake8 isort mypy bandit safety detect_secrets ``` +#### AI-Powered Hooks (Optional) +**Both platforms:** +```bash +# Required for ai_commit_check hook +pip install opencode-ai rich + +# Or install opencode CLI directly +npm install -g @sst/opencode +``` + +**Setup Opencode Authentication:** +```bash +# Authenticate with Opencode +opencode auth login +# Select: github-copilot + +# Configure the AI model +# In the opencode interface, enter: /models +# Select: claude-sonnet-4.5 +``` + +**⚠️ Required Security Configuration:** + +Download the security configuration to your repository root: + +**Setup:** + +1. Copy the contents of [`examples/opencode.jsonc`](https://github.com/TriaFed/pre-commit-library/blob/main/examples/opencode.jsonc) from this repository +2. Create a file named `opencode.jsonc` in your repository root +3. Paste the contents into your `opencode.jsonc` file +4. Commit the configuration to your repository + +**Optional:** Initialize AI instructions for your project by running `opencode` and typing `/init` in the OpenCode window. + +The `opencode.jsonc` file is **required** and must deny dangerous operations: +- `webfetch: "deny"` - Blocks external network requests +- `aws *: "deny"` - Blocks AWS CLI commands +- `az *: "deny"` - Blocks Azure CLI commands +- `gcloud *: "deny"` - Blocks Google Cloud CLI commands +- `terraform *: "deny"` - Blocks Terraform commands +- `curl/wget *: "deny"` - Blocks curl and wget + +Learn more: [OpenCode Permissions Documentation](https://opencode.ai/docs/permissions/) + +**Environment Variables:** +```bash +# Configure AI hook behavior (optional) +export OPENCODE_PORT=61164 # Port for opencode server (default: 61164) +export OPENCODE_MODEL=claude-sonnet-4.5 # AI model to use (default: claude-sonnet-4.5) +export OPENCODE_PROVIDER=github-copilot # AI provider (default: github-copilot) +export OPENCODE_TIMEOUT=90 # Server startup timeout in seconds (default: 90) +``` + #### JavaScript/TypeScript/Node.js **macOS:** diff --git a/README.md b/README.md index 349dba3..0a1fc12 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ pre-commit run --all-files ## 📋 Available Hooks +### 🤖 AI-Powered Hooks + +| Hook ID | Description | Languages | +| ----------------- | --------------------------------------------- | --------- | +| `ai_commit_check` | AI-powered commit validation with Opencode AI | All | + ### 🔒 Security Hooks | Hook ID | Description | Languages | @@ -470,6 +476,271 @@ repos: ## 🎛️ Hook Configuration +### AI-Powered Commit Validation + +The `ai_commit_check` hook uses Opencode AI to review your code changes. It supports two modes: + +- **Pre-commit mode**: Reviews staged changes before committing +- **Pre-push mode**: Reviews all branch changes against `origin/main` or `origin/master` before pushing + +**Pre-commit Configuration:** + +```yaml +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: v1.1.7 + hooks: + - id: ai_commit_check + stages: [manual] # Default: Run manually (opt-in) + # To enable automatic pre-commit: stages: [pre-commit] + # To enable automatic pre-push: stages: [pre-push] +``` + +**Note:** The AI commit check is **disabled by default** (manual stage). You must explicitly opt-in by changing the `stages` configuration. + +**Pre-push Configuration (Recommended for comprehensive review):** + +```yaml +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: # Replace with latest release version + hooks: + - id: ai_commit_check + stages: + - pre-push +``` + +**Setup:** + +```bash +# Install opencode +pip install opencode-ai rich +# or +npm install -g @sst/opencode + +# Authenticate with Opencode +opencode auth login +# Select: github, choose the Github Public option and follow the instructions, login in with your TriaFederal account. + +# Configure the AI model +# In the opencode interface, enter: /models +# Select: claude-sonnet-4.5 + +# Install pre-commit hooks +pre-commit install + +# IMPORTANT: For pre-push mode, also run: +pre-commit install --hook-type pre-push +``` + +**Required: Security Configuration File:** + +This hook **requires** an `opencode.jsonc` configuration file in your repository root to enforce security policies. + +**Setup:** + +1. Copy the contents of [`examples/opencode.jsonc`](https://github.com/TriaFed/pre-commit-library/blob/main/examples/opencode.jsonc) from this repository +2. Create a file named `opencode.jsonc` in your repository root +3. Paste the contents into your `opencode.jsonc` file +4. Commit the configuration to your repository + +The `opencode.jsonc` file enforces: + +- `share: "disabled"` - No sharing of conversations +- `autoupdate: false` - No automatic updates +- `webfetch: "deny"` - Prevents external network requests +- Model restrictions - Only GitHub Copilot and Amazon Bedrock Claude Sonnet 4.5 +- Provider restrictions - All other AI providers blocked +- Cloud CLI blocks - Denies `aws`, `az`, `gcloud`, `terraform`, `curl`, `wget` + +**Runtime Output:** + +When the hook runs, you'll see: + +``` +✓ Using opencode.jsonc from repository root +``` + +If the file is missing, the hook will exit with an error and instructions. + +**Important:** The `opencode.jsonc` file defines security policies and available models, but does NOT hardcode which model to use. Model selection is controlled via environment variables (see below), allowing you to switch between GitHub Copilot and Amazon Bedrock. + +**Environment Variables:** + +```bash +# GitHub Copilot Configuration (Default - no env vars needed) +# The hook defaults to GitHub Copilot if no variables are set + +# Amazon Bedrock Configuration +export OPENCODE_PROVIDER=amazon-bedrock +export OPENCODE_MODEL=amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0 +export OPENCODE_BEDROCK_REGION=us-east-1 # Required: us-east-1 or us-gov-* + +# Server Configuration (optional) +export OPENCODE_PORT=61164 # Default: 61164 +export OPENCODE_TIMEOUT=90 # Default: 90 seconds +``` + +**Pro Tip:** Add Bedrock variables to your `~/.zshrc` or `~/.bashrc` to avoid setting them every time: + +```bash +# Add to ~/.zshrc or ~/.bashrc +export OPENCODE_PROVIDER=amazon-bedrock +export OPENCODE_MODEL=amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0 +export OPENCODE_BEDROCK_REGION=us-east-1 +``` + +**Usage:** + +```bash +# Pre-commit mode (review staged changes) +pre-commit run --hook-stage manual ai_commit_check + +# Pre-push mode (review branch changes) +# This runs automatically on git push if configured with stages: [pre-push] +git push + +# Or run manually +pre-commit run --hook-stage pre-push ai_commit_check + +# Or configure to run automatically on every commit +# (Remove the stages: [manual] line from config) +``` + +The hook automatically detects its mode: + +- **Pre-commit**: If there are staged changes, reviews only those changes +- **Pre-push**: If no staged changes, compares current branch against `origin/main` or `origin/master` + +The hook will: + +- Review code quality and best practices +- Identify potential bugs or security concerns +- Suggest improvements +- **Require explicit approval**: The AI must include "-COMMIT APPROVED-" in its response to allow the commit +- **Block commits/pushes** in these cases: + - Critical issues found (response contains "-COMMIT REJECTED-") + - No response received from AI + - Response doesn't include explicit "-COMMIT APPROVED-" marker +- Provide commands to continue the AI session or auto-fix issues + +**Important:** The hook enforces a strict approval model. The AI must explicitly approve each commit with "-COMMIT APPROVED-" in its response. This ensures the AI has actively reviewed your changes and found no blocking issues, rather than passively accepting by default. + +### Using Amazon Bedrock + +The AI commit check hook supports Amazon Bedrock with Claude Sonnet 4.5. For security and compliance, only specific AWS regions are allowed. + +You need to set the model in your opencode.jsonc. There is an example in examples/opencode.jsonc. You can set it with an env variable or set it directly to bedrock. + +**Allowed Regions:** + +- `us-east-1` (US East - N. Virginia) +- `us-gov-west-1` (AWS GovCloud US-West) +- `us-gov-east-1` (AWS GovCloud US-East) + +**Setup Steps:** + +1. **Configure AWS credentials and region:** + + ```bash + # Set your AWS region + aws configure set region us-east-1 + + # Verify your configuration + aws sts get-caller-identity + ``` + +2. **Set environment variables:** + + ```bash + export OPENCODE_MODEL=amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0 + export OPENCODE_BEDROCK_REGION=us-east-1 + ``` + +3. **Verify the configuration:** + + ```bash + # Test that OpenCode can access Bedrock + opencode run "hello world" --model amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0 + ``` + +4. **Run the AI commit check:** + ```bash + pre-commit run --hook-stage manual ai_commit_check + ``` + +**Troubleshooting:** + +If you get a region error: + +``` +Error: Amazon Bedrock region 'us-west-2' is not allowed. +``` + +Fix by setting the correct region: + +```bash +export OPENCODE_BEDROCK_REGION=us-east-1 +aws configure set region us-east-1 +``` + +**Why only specific regions?** + +For security and compliance reasons, this hook restricts Amazon Bedrock to: + +- `us-east-1` - Standard AWS region with Claude Sonnet 4.5 availability +- `us-gov-*` - AWS GovCloud regions for government compliance + +If you need a different region, please file an issue with your compliance requirements. + +**Custom Instructions (Optional):** + +Provide project-specific context to the AI by creating instruction files: + +1. **Initialize default instructions:** + + ```bash + # Start opencode + opencode + + # Then in the opencode window, type: + /init + ``` + + This creates instruction files (like `CLAUDE.md`) with AI guidelines for your project. + +2. **Or create custom instruction files manually:** + + ```markdown + # AGENTS.md - Project-specific AI review guidelines + + ## Code Standards + + - Use TypeScript strict mode + - Follow React hooks best practices + - All API calls must include error handling + + ## Security Requirements + + - No hardcoded credentials or API keys + - All user input must be validated + - Database queries must use parameterized statements + ``` + +3. **Reference instruction files in `opencode.jsonc`:** + ```jsonc + { + "instruction": ["AGENTS.md", "CLAUDE.md", "docs/CODING_STANDARDS.md"], + } + ``` + +**Example Files:** + +- See [`examples/opencode.jsonc`](examples/opencode.jsonc) for recommended permission settings + +**Troubleshooting:** +If the hook fails with authentication errors, ensure you've completed the `opencode auth login` setup above. + ### Environment Variables You can customize hook behavior using environment variables: @@ -517,8 +788,8 @@ Finds hardcoded passwords, API keys, and tokens: ```javascript // ❌ Will be flagged -const apiKey = 'sk-1234567890abcdef'; -const password = 'mySecretPassword123'; +const apiKey = "sk-1234567890abcdef"; +const password = "mySecretPassword123"; // ✅ Safe alternatives const apiKey = process.env.API_KEY; @@ -526,7 +797,7 @@ const password = process.env.PASSWORD; // ✅ For false positives, use inline comments to suppress persistState(store, { - key: 'AppPreferences', // pragma: allowlist secret + key: "AppPreferences", // pragma: allowlist secret storage: sessionStorage, }); ``` @@ -545,7 +816,7 @@ config = { ```javascript // JavaScript example const settings = { - key: 'PreferenceKey', // pragma: allowlist secret + key: "PreferenceKey", // pragma: allowlist secret }; ``` @@ -733,7 +1004,7 @@ detect-secrets scan --baseline .secrets.baseline --force-use-all-plugins ```javascript // eslint-disable-next-line rule-name -const problematicCode = 'value'; +const problematicCode = "value"; ``` #### Semgrep diff --git a/examples/ai-enhanced-security.yaml b/examples/ai-enhanced-security.yaml new file mode 100644 index 0000000..3313bb2 --- /dev/null +++ b/examples/ai-enhanced-security.yaml @@ -0,0 +1,32 @@ +# AI-Enhanced Security Configuration +# This configuration combines AI-powered validation with essential security hooks +# Suitable for teams using GenAI tools and wanting automated code review + +repos: + - repo: https://github.com/TriaFed/pre-commit-library + rev: v1.1.7 + hooks: + # AI-Powered Validation (runs on manual trigger or CI) + - id: ai_commit_check + stages: [manual] # Run with: pre-commit run --hook-stage manual ai_commit_check + + # Essential Security Hooks + - id: detect_secrets + args: ['--baseline', '.secrets.baseline'] + - id: hardcoded_credentials + - id: genai_security_check + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-yaml + - id: check-json + - id: trailing-whitespace + - id: end-of-file-fixer + +# Usage: +# 1. Regular commits: Security hooks run automatically +# 2. AI validation: Run manually with: +# pre-commit run --hook-stage manual ai_commit_check +# 3. Or configure CI to always run AI validation on pull requests diff --git a/examples/opencode.jsonc b/examples/opencode.jsonc new file mode 100644 index 0000000..f6b766b --- /dev/null +++ b/examples/opencode.jsonc @@ -0,0 +1,87 @@ +{ + "$schema": "https://opencode.ai/config.json", + + // Security: Disable sharing and auto-updates + "share": "disabled", + "autoupdate": false, + + "model": "github-copilot/claude-sonnet-4.5", + "small_model": "github-copilot/claude-sonnet-4.5", + // You can set the model using env vars if you need to alternate between bedrock and github-copilot. + + // "model": "{env:OPENCODE_MODEL}", + // "small_model": "{env:OPENCODE_MODEL}", + + // Set bedrock model + // "model": "amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0", + // "small_model": "amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0", + // Note: Model selection is controlled via OPENCODE_MODEL environment variable + // Disable all providers except approved ones (github-copilot and amazon-bedrock) + "disabled_providers": [ + "openai", + "anthropic", + "gemini", + "azure", + "openrouter", + "ollama", + "lmstudio", + "together", + "fireworks", + "groq", + "deepseek", + "cohere", + "mistral", + "perplexity", + "xai" + ], + + // Provider-specific configuration + "provider": { + "github-copilot": { + "models": { + "github-copilot/claude-sonnet-4.5": { + "options": {} + } + } + }, + "amazon-bedrock": { + "models": { + "amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0": { + "options": {} + } + } + } + }, + + // Strict permissions - deny dangerous operations + "permission": { + "edit": "allow", + "bash": { + "*": "deny", + "git status *": "allow", + "git diff *": "allow", + "git log *": "allow", + "git show *": "allow", + "git rev-parse *": "allow", + "ls *": "allow", + "pwd": "allow", + "cat *": "allow", + "grep *": "allow", + "find *": "allow", + "rg *": "allow", + "npm run test": "ask", + "npm run build": "ask", + "pytest *": "ask", + "aws *": "deny", + "az *": "deny", + "gcloud *": "deny", + "terraform *": "deny", + "curl *": "deny", + "wget *": "deny" + }, + "webfetch": "deny" + }, + + // Project-specific AI instructions + "instructions": ["AGENTS.md"] +} diff --git a/hooks/ai_commit_check.py b/hooks/ai_commit_check.py new file mode 100755 index 0000000..ea46869 --- /dev/null +++ b/hooks/ai_commit_check.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +""" +AI-powered commit validation using Opencode AI. +Reviews staged changes for code quality, security, and best practices. +""" + +from opencode_ai import Opencode +from typing import Optional, Iterable +import subprocess +import sys +import time +import socket +import os +from datetime import datetime +from pathlib import Path + +try: + from rich.console import Console + from rich.markdown import Markdown + from rich.panel import Panel +except ImportError: + Console = None + Markdown = None + Panel = None + +def find_available_port(start_port=61164, max_attempts=10): + for i in range(max_attempts): + port = start_port + i + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('localhost', port)) + sock.close() + return port + except OSError: + continue + return None + +def wait_for_port(port, timeout=30): + start = time.time() + while time.time() - start < timeout: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + sock.connect(('localhost', port)) + sock.close() + return True + except (socket.error, socket.timeout): + time.sleep(0.5) + return False + +def main(): + OPENCODE_PORT = int(os.getenv('OPENCODE_PORT', '61164')) + OPENCODE_MODEL = os.getenv('OPENCODE_MODEL', 'github-copilot/claude-sonnet-4.5') + OPENCODE_PROVIDER = os.getenv('OPENCODE_PROVIDER', 'github-copilot') + OPENCODE_TIMEOUT = int(os.getenv('OPENCODE_TIMEOUT', '90')) + OPENCODE_BEDROCK_REGION = os.getenv('OPENCODE_BEDROCK_REGION', 'us-east-1') + + # Parse model and provider from OPENCODE_MODEL if it contains a slash + # Format: "provider/model-name" -> provider="provider", model="provider/model-name" + # The SDK expects model_id to include the provider prefix + if '/' in OPENCODE_MODEL: + parsed_provider = OPENCODE_MODEL.split('/')[0] + # Use the parsed provider only if OPENCODE_PROVIDER wasn't explicitly set + if os.getenv('OPENCODE_PROVIDER') is None: + OPENCODE_PROVIDER = parsed_provider + + if not (1024 <= OPENCODE_PORT <= 65535): + print(f"Error: Invalid port {OPENCODE_PORT}. Port must be between 1024 and 65535.", file=sys.stderr) + return 3 + + if not (1 <= OPENCODE_TIMEOUT <= 300): + print(f"Error: Invalid timeout {OPENCODE_TIMEOUT}. Timeout must be between 1 and 300 seconds.", file=sys.stderr) + return 3 + + # Validate Bedrock region if using Bedrock provider + if OPENCODE_PROVIDER == 'amazon-bedrock': + if not OPENCODE_BEDROCK_REGION: + print("Error: OPENCODE_BEDROCK_REGION environment variable is required when using amazon-bedrock provider.", file=sys.stderr) + print("", file=sys.stderr) + print("Amazon Bedrock requires explicit region configuration for security and compliance.", file=sys.stderr) + print("", file=sys.stderr) + print("Allowed regions:", file=sys.stderr) + print(" - us-east-1 (US East - N. Virginia)", file=sys.stderr) + print(" - us-gov-west-1 (AWS GovCloud US-West)", file=sys.stderr) + print(" - us-gov-east-1 (AWS GovCloud US-East)", file=sys.stderr) + print("", file=sys.stderr) + print("To use Amazon Bedrock:", file=sys.stderr) + print(" 1. Authenticate to AWS with the correct region:", file=sys.stderr) + print(" aws configure set region us-east-1", file=sys.stderr) + print("", file=sys.stderr) + print(" 2. Set the region environment variable:", file=sys.stderr) + print(" export OPENCODE_BEDROCK_REGION=us-east-1", file=sys.stderr) + print("", file=sys.stderr) + print(" 3. Authenticate with OpenCode:", file=sys.stderr) + print(" opencode auth login", file=sys.stderr) + print(" Select: Amazon Bedrock", file=sys.stderr) + print("", file=sys.stderr) + return 3 + + # Validate region is in allowed list + allowed_regions = ['us-east-1'] + is_govcloud = OPENCODE_BEDROCK_REGION.startswith('us-gov-') + + if not (OPENCODE_BEDROCK_REGION in allowed_regions or is_govcloud): + print(f"Error: Amazon Bedrock region '{OPENCODE_BEDROCK_REGION}' is not allowed.", file=sys.stderr) + print("", file=sys.stderr) + print("For security and compliance reasons, only the following regions are permitted:", file=sys.stderr) + print(" ✓ us-east-1 (US East - N. Virginia)", file=sys.stderr) + print(" ✓ us-gov-* (AWS GovCloud regions)", file=sys.stderr) + print("", file=sys.stderr) + print(f"Current region: {OPENCODE_BEDROCK_REGION}", file=sys.stderr) + print("", file=sys.stderr) + print("To fix:", file=sys.stderr) + print(" export OPENCODE_BEDROCK_REGION=us-east-1", file=sys.stderr) + print("", file=sys.stderr) + return 3 + + serve_process = None + try: + repo_root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip() + + # Check for opencode.jsonc in repo root + config_path = os.path.join(repo_root, 'opencode.jsonc') + if not os.path.exists(config_path): + print("Error: opencode.jsonc not found in repository root.", file=sys.stderr) + print("", file=sys.stderr) + print("The AI commit check requires an opencode.jsonc configuration file", file=sys.stderr) + print("to enforce security policies and restrict AI capabilities.", file=sys.stderr) + print("", file=sys.stderr) + print("To set up:", file=sys.stderr) + print(" 1. Download the security configuration:", file=sys.stderr) + print(" curl -o opencode.jsonc https://raw.githubusercontent.com/TriaFed/pre-commit-library/main/examples/opencode.jsonc", file=sys.stderr) + print("", file=sys.stderr) + print(" 2. Commit the file:", file=sys.stderr) + print(" git add opencode.jsonc && git commit -m 'Add OpenCode security config'", file=sys.stderr) + print("", file=sys.stderr) + print("See CONFIGURATION_GUIDE.md for more details.", file=sys.stderr) + return 3 + + available_port = find_available_port(OPENCODE_PORT) + if not available_port: + print(f"Error: No available ports found starting from port {OPENCODE_PORT}. " + f"Please check if ports {OPENCODE_PORT}-{OPENCODE_PORT+9} are available.", file=sys.stderr) + return 3 + + if available_port != OPENCODE_PORT: + OPENCODE_PORT = available_port + + OPENCODE_BASE_URL = f'http://127.0.0.1:{OPENCODE_PORT}' + os.environ['OPENCODE_BASE_URL'] = OPENCODE_BASE_URL + + # Set AWS region environment variables for Bedrock if needed + if OPENCODE_PROVIDER == 'amazon-bedrock': + os.environ['AWS_DEFAULT_REGION'] = OPENCODE_BEDROCK_REGION + os.environ['AWS_REGION'] = OPENCODE_BEDROCK_REGION + + # Create environment for the opencode server subprocess + server_env = os.environ.copy() + + try: + # Start OpenCode server with explicit model selection + # The --model flag accepts format: provider/model-name + serve_process = subprocess.Popen( + ['opencode', 'serve', '--port', str(OPENCODE_PORT)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=repo_root, # Run in repo root so it can see git diffs + env=server_env + ) + except FileNotFoundError: + print("Error: opencode is not installed.", file=sys.stderr) + print("", file=sys.stderr) + print("To install opencode:", file=sys.stderr) + print(" Visit: https://opencode.ai", file=sys.stderr) + print("", file=sys.stderr) + print("After installation, authenticate with:", file=sys.stderr) + print(" opencode auth login", file=sys.stderr) + print(" Select: GitHub Public", file=sys.stderr) + print("", file=sys.stderr) + return 3 + + if not wait_for_port(OPENCODE_PORT, timeout=OPENCODE_TIMEOUT): + stdout, stderr = serve_process.communicate(timeout=5) + error_msg = stderr.decode() if stderr else "Unknown error" + print(f"Error: Opencode server failed to start within {OPENCODE_TIMEOUT} seconds.", file=sys.stderr) + print(f"Server error: {error_msg}", file=sys.stderr) + print("\nTroubleshooting:", file=sys.stderr) + print(" 1. Install opencode: Visit https://opencode.ai", file=sys.stderr) + print(" 2. Authenticate: opencode auth login (select GitHub Public)", file=sys.stderr) + print(" 3. Increase timeout: export OPENCODE_TIMEOUT=120", file=sys.stderr) + return 3 + + client = Opencode() + + repo_name = os.path.basename(repo_root) + commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], text=True).strip() + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + session_name = f"Commit Review - {repo_name} {commit_hash} - {timestamp}" + + new_session = client.session.create( + extra_body={ + 'name': session_name, + } + ) + + new_session_id = new_session.id + + # Determine review mode: pre-commit (staged changes) or pre-push (branch comparison) + try: + staged_diff = subprocess.check_output( + ['git', 'diff', '--cached', '--name-only'], + text=True, + cwd=repo_root + ).strip() + has_staged_changes = bool(staged_diff) + except subprocess.CalledProcessError: + has_staged_changes = False + + # Determine which mode and construct appropriate prompt + if has_staged_changes: + # Pre-commit mode: review staged changes + review_mode = "pre-commit" + review_target = "the staged changes for this commit" + review_context = """ + **REVIEW MODE:** Pre-commit (staged changes) + + Use `git diff --cached` to see the staged changes that are about to be committed. + """ + else: + # Pre-push mode: compare current branch to origin/main or origin/master + review_mode = "pre-push" + + # Determine the default branch (main or master) + default_branch = None + for branch in ['main', 'master']: + try: + subprocess.check_output( + ['git', 'rev-parse', '--verify', f'origin/{branch}'], + stderr=subprocess.DEVNULL, + text=True, + cwd=repo_root + ) + default_branch = branch + break + except subprocess.CalledProcessError: + continue + + if not default_branch: + print("Error: Could not find origin/main or origin/master branch for comparison.", file=sys.stderr) + print("Please ensure you have a remote tracking branch set up.", file=sys.stderr) + return 3 + + review_target = f"all changes in the current branch compared to origin/{default_branch}" + review_context = f""" + **REVIEW MODE:** Pre-push (branch comparison) + + Use `git diff origin/{default_branch}...HEAD` to see all changes in the current branch + that differ from origin/{default_branch}. Review ALL commits and changes since branching. + """ + + new_session_chat = client.session.chat(id=new_session_id, parts=[ + {'type': 'text', 'text': f""" + Review {review_target}, focusing on these critical issues: + {review_context} + **CRITICAL ISSUES TO CHECK:** + 1. **Unused Variables/Imports/Code**: Variables declared but never used, unused imports, dead code + 2. **Logic Bugs**: Incorrect implementations, missing null checks, wrong conditions, off-by-one errors + 3. **Security Issues**: Input validation missing, potential injection vulnerabilities, hardcoded credentials + 4. **Performance Problems**: Inefficient patterns, potential memory leaks, unnecessary computations + 5. **Type/Syntax Issues**: Incorrect types, missing error handling, type mismatches + 6. **Configuration Issues**: Problems in config files (JSON, YAML, Jenkinsfile, package.json, etc.) + 7. **Build/CI Issues**: Problems with build scripts, pipelines, dependencies + + **REVIEW INSTRUCTIONS:** + - Analyze BOTH the diff changes AND the complete file contents provided + - Focus ONLY on code that has actual problems + - Pay special attention to variables that are declared but never referenced + - Look for imports that aren't used anywhere in the file + - Check for functions or code blocks that serve no purpose + - Review configuration files thoroughly for syntax and logic issues + - Consider how changes in one file might affect other files + - If you find unused variables, clearly state which variables are unused and suggest removing them + + **RESPONSE FORMAT:** + - If you find critical issues that should block the commit, start your response with "-COMMIT REJECTED-" + followed by a clear explanation of the blocking issues with specific file names and line numbers + - If the code has no significant issues that should block the commit, you MUST include "-COMMIT APPROVED-" + in your response, followed by any optional feedback or suggestions + - Always provide specific, actionable feedback + """} + ], model_id=OPENCODE_MODEL, provider_id=OPENCODE_PROVIDER) + + new_session_chat_messages = client.session.messages(id=new_session_id) + + def get_last_assistant_message(messages: Iterable) -> Optional[str]: + last_text: Optional[str] = None + for item in messages: + info = getattr(item, 'info', None) + role = getattr(info, 'role', None) + if role == 'assistant': + parts = getattr(item, 'parts', []) or [] + text_segments = [] + for part in parts: + if getattr(part, 'type', None) == 'text': + text_segments.append((getattr(part, 'text', '') or '').strip()) + combined = '\n'.join(s for s in text_segments if s) + if combined: + last_text = combined + return last_text + + def render_markdown_terminal(message: Optional[str]) -> None: + title = 'AI Commit Review' + if Console and Markdown and Panel: + console = Console() + if not message: + console.print(Panel('_(no assistant message found)_', title=title, border_style='red')) + return + md = Markdown(message) + console.print(Panel(md, title=title, border_style='cyan')) + else: + if not message: + print(f"{title}\n(no assistant message found)") + else: + print(f"{title}\n\n{message}") + + last_msg = get_last_assistant_message(new_session_chat_messages) + + # Check for valid AI response + if not last_msg: + render_markdown_terminal(None) + print("\n❌ ERROR: No response received from AI review. Blocking commit.", file=sys.stderr) + return 1 + + is_rejected = '-COMMIT REJECTED-' in last_msg + is_approved = '-COMMIT APPROVED-' in last_msg + + # Clean up markers for display + display_msg = last_msg + if is_rejected: + display_msg = display_msg.replace('-COMMIT REJECTED-', '').strip() + if is_approved: + display_msg = display_msg.replace('-COMMIT APPROVED-', '').strip() + + render_markdown_terminal(display_msg) + + continue_command = f"opencode --session {new_session_id}" + + # If explicitly rejected, block the commit + if is_rejected: + fix_command = f"opencode run \"resolve these issues\" --session {new_session_id}" + + if Console and Panel: + console = Console() + console.print('\n') + console.print(Panel( + f"[bold red]Critical issues found that block this commit/push.[/bold red]", + border_style='bold red', + padding=(1, 2) + )) + console.print('\n[bold yellow]To automatically resolve these issues:[/bold yellow]') + console.print(f'[bold green]{fix_command}[/bold green]\n') + console.print('[bold yellow]To continue chatting with this session:[/bold yellow]') + console.print(f'[bold cyan]{continue_command}[/bold cyan]') + else: + print('\n' + '='*80) + print('COMMIT REJECTED - Critical issues found') + print('='*80) + print('\nTo automatically resolve these issues:') + print(f"{fix_command}\n") + print('To continue chatting with this session:') + print(f"{continue_command}") + print('='*80) + + return 1 + + # If not explicitly approved, block the commit + elif not is_approved: + if Console and Panel: + console = Console() + console.print('\n') + console.print(Panel( + f"[bold red]AI review did not explicitly approve this commit.[/bold red]", + border_style='bold red', + padding=(1, 2) + )) + console.print('\n[bold yellow]To continue chatting with this session:[/bold yellow]') + console.print(f'[bold cyan]{continue_command}[/bold cyan]') + else: + print('\n' + '='*80) + print('COMMIT REJECTED - AI review did not explicitly approve') + print('='*80) + print('\nTo continue chatting with this session:') + print(f"{continue_command}") + print('='*80) + + return 1 + + # Explicitly approved - allow commit + else: + improve_command = f"opencode run \"apply the suggested improvements\" --session {new_session_id}" + + if Console and Panel: + console = Console() + console.print('\n') + console.print(Panel( + f"[bold green]✓ Commit approved by AI review.[/bold green]", + border_style='green', + padding=(1, 2) + )) + console.print('\n[bold yellow]To apply suggested improvements:[/bold yellow]') + console.print(f'[bold green]{improve_command}[/bold green]\n') + console.print('[bold yellow]To continue chatting with this session:[/bold yellow]') + console.print(f'[bold cyan]{continue_command}[/bold cyan]') + else: + print('\n' + '='*80) + print('Commit approved by AI review') + print('='*80) + print('\nTo apply suggested improvements:') + print(f"{improve_command}\n") + print('To continue chatting with this session:') + print(f"{continue_command}") + print('='*80) + + return 0 + finally: + # Terminate OpenCode server + if serve_process: + serve_process.terminate() + try: + serve_process.wait(timeout=5) + except subprocess.TimeoutExpired: + serve_process.kill() + serve_process.wait() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index e13f61c..231db2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ dependencies = [ "detect-secrets>=1.4.0", "bandit>=1.7.0", "safety>=2.0.0", + "opencode-ai>=0.1.0a36", + "rich>=13.7.0", ] [project.optional-dependencies] @@ -52,6 +54,7 @@ Issues = "https://github.com/TriaFed/pre-commit-library/issues" Documentation = "https://github.com/TriaFed/pre-commit-library#readme" [project.scripts] +ai_commit_check = "hooks.ai_commit_check:main" ansible_security_scan = "hooks.ansible_security_scan:main" check_license = "hooks.check_license:main" check_xml = "hooks.check_xml:main"