diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md index da8e49ada..1f3433334 100644 --- a/.claude/agents/code-reviewer.md +++ b/.claude/agents/code-reviewer.md @@ -14,7 +14,7 @@ Apply the rules from CLAUDE.md sections listed below. Reference the full section **Error Handling**: catch (e) not catch (error), double-quoted error messages, { cause: e } chaining. -**Backward Compatibility**: FORBIDDEN — actively remove compat shims, don't maintain them. +**Compat shims**: FORBIDDEN — actively remove compat shims, don't maintain them. **Test Style**: Functional tests over source scanning. Never read source files and assert on contents. Verify behavior with real function calls. diff --git a/.claude/agents/refactor-cleaner.md b/.claude/agents/refactor-cleaner.md index 019cf41a0..7f823bd75 100644 --- a/.claude/agents/refactor-cleaner.md +++ b/.claude/agents/refactor-cleaner.md @@ -22,4 +22,4 @@ Apply these rules from CLAUDE.md exactly: - Unreachable code paths - Duplicate logic that should be consolidated - Files >400 LOC that should be split (flag to user, don't split without approval) -- Backward compatibility shims (FORBIDDEN per CLAUDE.md — actively remove) +- Compat shims (FORBIDDEN per CLAUDE.md — actively remove) diff --git a/.git-hooks/commit-msg b/.git-hooks/commit-msg new file mode 100755 index 000000000..92dcc04fa --- /dev/null +++ b/.git-hooks/commit-msg @@ -0,0 +1,73 @@ +#!/bin/bash +# Socket Security Commit-msg Hook +# Additional security layer - validates commit even if pre-commit was bypassed. + +set -e + +# Colors for output. +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +# Allowed public API key (used in socket-lib). +ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api" + +ERRORS=0 + +# Get files in this commit (for security checks). +COMMITTED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || printf "\n") + +# Quick checks for critical issues in committed files. +if [ -n "$COMMITTED_FILES" ]; then + for file in $COMMITTED_FILES; do + if [ -f "$file" ]; then + # Check for Socket API keys (except allowed). + if grep -E 'sktsec_[a-zA-Z0-9_-]+' "$file" 2>/dev/null | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | grep -v '\.example' | grep -q .; then + echo "${RED}✗ SECURITY: Potential API key detected in commit!${NC}" + printf "File: %s\n" "$file" + ERRORS=$((ERRORS + 1)) + fi + + # Check for .env files. + if echo "$file" | grep -qE '^\.env(\.local)?$'; then + echo "${RED}✗ SECURITY: .env file in commit!${NC}" + ERRORS=$((ERRORS + 1)) + fi + fi + done +fi + +# Auto-strip AI attribution from commit message. +COMMIT_MSG_FILE="$1" +if [ -f "$COMMIT_MSG_FILE" ]; then + # Create a temporary file to store the cleaned message. + TEMP_FILE=$(mktemp) + REMOVED_LINES=0 + + # Read the commit message line by line and filter out AI attribution. + while IFS= read -r line || [ -n "$line" ]; do + # Check if this line contains AI attribution patterns. + if echo "$line" | grep -qiE "(Generated with|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|Claude Code|@anthropic|Assistant:|Generated by Claude|Machine generated)"; then + REMOVED_LINES=$((REMOVED_LINES + 1)) + else + # Line doesn't contain AI attribution, keep it. + printf '%s\n' "$line" >> "$TEMP_FILE" + fi + done < "$COMMIT_MSG_FILE" + + # Replace the original commit message with the cleaned version. + if [ $REMOVED_LINES -gt 0 ]; then + mv "$TEMP_FILE" "$COMMIT_MSG_FILE" + echo "${GREEN}✓ Auto-stripped${NC} $REMOVED_LINES AI attribution line(s) from commit message" + else + # No lines were removed, just clean up the temp file. + rm -f "$TEMP_FILE" + fi +fi + +if [ $ERRORS -gt 0 ]; then + echo "${RED}✗ Commit blocked by security validation${NC}" + exit 1 +fi + +exit 0 diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push index 4506dfcf4..92e7ba7f4 100755 --- a/.git-hooks/pre-push +++ b/.git-hooks/pre-push @@ -1,8 +1,17 @@ #!/bin/bash # Socket Security Pre-push Hook # Security enforcement layer for all pushes. -# Validates all commits being pushed for security issues and AI attribution. -# NOTE: Security checks parallel .husky/security-checks.sh — keep in sync. +# Validates commits being pushed for AI attribution and secrets. +# +# Architecture: +# .husky/pre-push (thin wrapper) → .git-hooks/pre-push (this file) +# Husky sets core.hooksPath=.husky/_ which delegates to .husky/pre-push. +# This file contains all the actual logic. +# +# Range logic: +# New branch: remote/.. (only new commits) +# Existing: .. (only new commits) +# We never use release tags — that would re-scan already-merged history. set -e @@ -13,45 +22,16 @@ NC='\033[0m' printf "${GREEN}Running mandatory pre-push validation...${NC}\n" -# Allowed public API key (used in socket-lib). +# Allowed public API key (used in socket-lib test fixtures). ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api" -# Get the remote name and URL. +# Get the remote name and URL from git (passed as arguments to pre-push hooks). remote="$1" url="$2" TOTAL_ERRORS=0 -# ============================================================================ -# PRE-CHECK 1: AgentShield scan on Claude config (blocks push on failure) -# ============================================================================ -if command -v agentshield >/dev/null 2>&1 || [ -x "$(pnpm bin 2>/dev/null)/agentshield" ]; then - AGENTSHIELD="$(command -v agentshield 2>/dev/null || echo "$(pnpm bin)/agentshield")" - if ! "$AGENTSHIELD" scan --quiet < /dev/null 2>/dev/null; then - printf "${RED}✗ AgentShield: security issues found in Claude config${NC}\n" - printf "Run 'pnpm exec agentshield scan' for details\n" - TOTAL_ERRORS=$((TOTAL_ERRORS + 1)) - fi -fi - -# ============================================================================ -# PRE-CHECK 2: zizmor scan on GitHub Actions workflows -# ============================================================================ -ZIZMOR="" -if command -v zizmor >/dev/null 2>&1; then - ZIZMOR="$(command -v zizmor)" -elif [ -x "$HOME/.socket/zizmor/bin/zizmor" ]; then - ZIZMOR="$HOME/.socket/zizmor/bin/zizmor" -fi -if [ -n "$ZIZMOR" ] && [ -d ".github/" ]; then - if ! "$ZIZMOR" .github/ < /dev/null 2>/dev/null; then - printf "${RED}✗ Zizmor: workflow security issues found${NC}\n" - printf "Run 'zizmor .github/' for details\n" - TOTAL_ERRORS=$((TOTAL_ERRORS + 1)) - fi -fi - -# Read stdin for refs being pushed. +# Read stdin for refs being pushed (git provides: local_ref local_sha remote_ref remote_sha). while read local_ref local_sha remote_ref remote_sha; do # Skip tag pushes: tags point to existing commits already validated. if echo "$local_ref" | grep -q '^refs/tags/'; then @@ -59,14 +39,18 @@ while read local_ref local_sha remote_ref remote_sha; do continue fi - # Skip delete pushes. + # Skip delete pushes (local_sha is all zeros when deleting a remote branch). if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then continue fi - # Get the range of commits being pushed. + # ── Compute commit range ────────────────────────────────────────────── + # Goal: only scan commits that are NEW in this push, never re-scan + # commits already on the remote. This prevents false positives from + # old AI-attributed commits that were merged before the hook existed. if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then - # New branch - only check commits not on the default remote branch. + # New branch — compare against the remote's default branch (usually main). + # This ensures we only check commits unique to this branch. default_branch=$(git symbolic-ref "refs/remotes/$remote/HEAD" 2>/dev/null | sed "s@^refs/remotes/$remote/@@") if [ -z "$default_branch" ]; then default_branch="main" @@ -74,15 +58,10 @@ while read local_ref local_sha remote_ref remote_sha; do if git rev-parse "$remote/$default_branch" >/dev/null 2>&1; then range="$remote/$default_branch..$local_sha" else - # No remote default branch, fall back to release tag. - latest_release=$(git tag --list 'v*' --sort=-version:refname --merged "$local_sha" | head -1) - if [ -n "$latest_release" ]; then - range="$latest_release..$local_sha" - else - # No remote branch or tags — skip scan to avoid walking entire history. - printf "${GREEN}✓ Skipping validation (no baseline to compare against)${NC}\n" - continue - fi + # No remote default branch (shallow clone, etc.) — skip to avoid + # walking entire history which would cause false positives. + printf "${GREEN}✓ Skipping validation (no baseline to compare against)${NC}\n" + continue fi else # Existing branch — only check commits not yet on the remote. @@ -97,13 +76,12 @@ while read local_ref local_sha remote_ref remote_sha; do ERRORS=0 - # ============================================================================ - # CHECK 1: Scan commit messages for AI attribution - # ============================================================================ + # ── CHECK 1: AI attribution in commit messages ──────────────────────── + # Strips these at commit time via commit-msg hook, but this catches + # commits made with --no-verify or on other machines. printf "Checking commit messages for AI attribution...\n" - # Check each commit in the range for AI patterns. - while IFS= read -r commit_sha; do + for commit_sha in $(git rev-list "$range"); do full_msg=$(git log -1 --format='%B' "$commit_sha") if echo "$full_msg" | grep -qiE "(Generated with.*(Claude|AI)|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|@anthropic\.com|Assistant:|Generated by Claude|Machine generated)"; then @@ -114,7 +92,7 @@ while read local_ref local_sha remote_ref remote_sha; do printf " - %s\n" "$(git log -1 --oneline "$commit_sha")" ERRORS=$((ERRORS + 1)) fi - done < <(git rev-list "$range") + done if [ $ERRORS -gt 0 ]; then printf "\n" @@ -128,46 +106,41 @@ while read local_ref local_sha remote_ref remote_sha; do printf " git push\n" fi - # ============================================================================ - # CHECK 2: File content security checks - # ============================================================================ + # ── CHECK 2: File content security checks ───────────────────────────── + # Scans files changed in the push range for secrets, keys, and mistakes. printf "Checking files for security issues...\n" - # Get all files changed in these commits. CHANGED_FILES=$(git diff --name-only "$range" 2>/dev/null || echo "") if [ -n "$CHANGED_FILES" ]; then - # Check for sensitive files. + # Check for sensitive files (.env, .DS_Store, log files). if echo "$CHANGED_FILES" | grep -qE '^\.env(\.local)?$'; then printf "${RED}✗ BLOCKED: Attempting to push .env file!${NC}\n" printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '^\.env(\.local)?$')" ERRORS=$((ERRORS + 1)) fi - # Check for .DS_Store. if echo "$CHANGED_FILES" | grep -q '\.DS_Store'; then printf "${RED}✗ BLOCKED: .DS_Store file in push!${NC}\n" printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep '\.DS_Store')" ERRORS=$((ERRORS + 1)) fi - # Check for log files. if echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log' | grep -q .; then printf "${RED}✗ BLOCKED: Log file in push!${NC}\n" printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log')" ERRORS=$((ERRORS + 1)) fi - # Check file contents for secrets. + # Check file contents for secrets and hardcoded paths. while IFS= read -r file; do if [ -f "$file" ] && [ ! -d "$file" ]; then - # Skip test files, example files, and hook scripts. + # Skip test files, example files, and hook scripts themselves. if echo "$file" | grep -qE '\.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/'; then continue fi # Use strings for binary files, grep directly for text files. - # This correctly extracts printable strings from WASM, .lockb, etc. is_binary=false if grep -qI '' "$file" 2>/dev/null; then is_binary=false @@ -176,40 +149,40 @@ while read local_ref local_sha remote_ref remote_sha; do fi if [ "$is_binary" = true ]; then - file_text=$(strings "$file" 2>/dev/null || echo "") + file_text=$(strings "$file" 2>/dev/null) else - file_text=$(cat "$file" 2>/dev/null || echo "") + file_text=$(cat "$file" 2>/dev/null) fi - # Check for hardcoded user paths. + # Hardcoded personal paths (/Users/foo/, /home/foo/, C:\Users\foo\). if echo "$file_text" | grep -qE '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)'; then printf "${RED}✗ BLOCKED: Hardcoded personal path found in: %s${NC}\n" "$file" echo "$file_text" | grep -nE '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' | head -3 ERRORS=$((ERRORS + 1)) fi - # Check for Socket API keys. + # Socket API keys (except allowed public key and test placeholders). if echo "$file_text" | grep -E 'sktsec_[a-zA-Z0-9_-]+' | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'SOCKET_SECURITY_API_KEY=' | grep -v 'fake-token' | grep -v 'test-token' | grep -q .; then printf "${RED}✗ BLOCKED: Real API key detected in: %s${NC}\n" "$file" echo "$file_text" | grep -n 'sktsec_' | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | head -3 ERRORS=$((ERRORS + 1)) fi - # Check for AWS keys. + # AWS keys. if echo "$file_text" | grep -iqE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})'; then printf "${RED}✗ BLOCKED: Potential AWS credentials found in: %s${NC}\n" "$file" echo "$file_text" | grep -niE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' | head -3 ERRORS=$((ERRORS + 1)) fi - # Check for GitHub tokens. + # GitHub tokens. if echo "$file_text" | grep -qE 'gh[ps]_[a-zA-Z0-9]{36}'; then printf "${RED}✗ BLOCKED: Potential GitHub token found in: %s${NC}\n" "$file" echo "$file_text" | grep -nE 'gh[ps]_[a-zA-Z0-9]{36}' | head -3 ERRORS=$((ERRORS + 1)) fi - # Check for private keys. + # Private keys. if echo "$file_text" | grep -qE -- '-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----'; then printf "${RED}✗ BLOCKED: Private key found in: %s${NC}\n" "$file" ERRORS=$((ERRORS + 1)) diff --git a/.husky/commit-msg b/.husky/commit-msg index 92dcc04fa..09dec27aa 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,73 +1,2 @@ -#!/bin/bash -# Socket Security Commit-msg Hook -# Additional security layer - validates commit even if pre-commit was bypassed. - -set -e - -# Colors for output. -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -# Allowed public API key (used in socket-lib). -ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api" - -ERRORS=0 - -# Get files in this commit (for security checks). -COMMITTED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || printf "\n") - -# Quick checks for critical issues in committed files. -if [ -n "$COMMITTED_FILES" ]; then - for file in $COMMITTED_FILES; do - if [ -f "$file" ]; then - # Check for Socket API keys (except allowed). - if grep -E 'sktsec_[a-zA-Z0-9_-]+' "$file" 2>/dev/null | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | grep -v '\.example' | grep -q .; then - echo "${RED}✗ SECURITY: Potential API key detected in commit!${NC}" - printf "File: %s\n" "$file" - ERRORS=$((ERRORS + 1)) - fi - - # Check for .env files. - if echo "$file" | grep -qE '^\.env(\.local)?$'; then - echo "${RED}✗ SECURITY: .env file in commit!${NC}" - ERRORS=$((ERRORS + 1)) - fi - fi - done -fi - -# Auto-strip AI attribution from commit message. -COMMIT_MSG_FILE="$1" -if [ -f "$COMMIT_MSG_FILE" ]; then - # Create a temporary file to store the cleaned message. - TEMP_FILE=$(mktemp) - REMOVED_LINES=0 - - # Read the commit message line by line and filter out AI attribution. - while IFS= read -r line || [ -n "$line" ]; do - # Check if this line contains AI attribution patterns. - if echo "$line" | grep -qiE "(Generated with|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|Claude Code|@anthropic|Assistant:|Generated by Claude|Machine generated)"; then - REMOVED_LINES=$((REMOVED_LINES + 1)) - else - # Line doesn't contain AI attribution, keep it. - printf '%s\n' "$line" >> "$TEMP_FILE" - fi - done < "$COMMIT_MSG_FILE" - - # Replace the original commit message with the cleaned version. - if [ $REMOVED_LINES -gt 0 ]; then - mv "$TEMP_FILE" "$COMMIT_MSG_FILE" - echo "${GREEN}✓ Auto-stripped${NC} $REMOVED_LINES AI attribution line(s) from commit message" - else - # No lines were removed, just clean up the temp file. - rm -f "$TEMP_FILE" - fi -fi - -if [ $ERRORS -gt 0 ]; then - echo "${RED}✗ Commit blocked by security validation${NC}" - exit 1 -fi - -exit 0 +# Run commit message validation and auto-strip AI attribution. +.git-hooks/commit-msg "$1" diff --git a/.husky/pre-push b/.husky/pre-push index ad185b85a..e636e3a64 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,165 +1,2 @@ -#!/bin/bash -# Socket Security Pre-push Hook -# MANDATORY ENFORCEMENT LAYER - Cannot be bypassed with --no-verify. -# Validates all commits being pushed for security issues and AI attribution. - -set -e - -# Colors for output. -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -NC='\033[0;m' - -printf "${GREEN}Running mandatory pre-push validation...${NC}\n" - -# Allowed public API key (used in socket-lib). -ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api" - -# Get the remote name and URL. -remote="$1" -url="$2" - -TOTAL_ERRORS=0 - -# Read stdin for refs being pushed. -while read local_ref local_sha remote_ref remote_sha; do - # Get the range of commits being pushed. - if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then - # New branch: only check commits not already on the remote default branch. - default_branch=$(git symbolic-ref "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s|refs/remotes/${remote}/||") - if [ -z "$default_branch" ]; then - default_branch="main" - fi - # Verify the remote ref exists locally before using it in the range. - if git rev-parse --verify "${remote}/${default_branch}" >/dev/null 2>&1; then - range="${remote}/${default_branch}..$local_sha" - else - # Remote ref missing (shallow clone, --single-branch, etc.), check all commits. - range="$local_sha" - fi - else - # Existing branch - check only new commits being pushed. - range="$remote_sha..$local_sha" - fi - - ERRORS=0 - - # ============================================================================ - # CHECK 1: Scan commit messages for AI attribution - # ============================================================================ - printf "Checking commit messages for AI attribution...\n" - - # Check each commit in the range for AI patterns. - # Use for loop instead of while to avoid subshell (pipe) or bash-only syntax (process substitution). - for commit_sha in $(git rev-list "$range"); do - full_msg=$(git log -1 --format='%B' "$commit_sha") - - if echo "$full_msg" | grep -qiE "(Generated with.*(Claude|AI)|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|@anthropic\.com|Assistant:|Generated by Claude|Machine generated)"; then - if [ $ERRORS -eq 0 ]; then - printf "${RED}✗ BLOCKED: AI attribution found in commit messages!${NC}\n" - printf "Commits with AI attribution:\n" - fi - echo " - $(git log -1 --oneline "$commit_sha")" - ERRORS=$((ERRORS + 1)) - fi - done - - if [ $ERRORS -gt 0 ]; then - printf "\n" - printf "These commits were likely created with --no-verify, bypassing the\n" - printf "commit-msg hook that strips AI attribution.\n" - printf "\n" - printf "To fix:\n" - printf " git rebase -i %s\n" "$remote_sha" - printf " Mark commits as .reword., remove AI attribution, save\n" - printf " git push\n" - fi - - # ============================================================================ - # CHECK 2: File content security checks - # ============================================================================ - printf "Checking files for security issues...\n" - - # Get all files changed in these commits. - CHANGED_FILES=$(git diff --name-only "$range" 2>/dev/null || printf "\n") - - if [ -n "$CHANGED_FILES" ]; then - # Check for sensitive files. - if echo "$CHANGED_FILES" | grep -qE '^\.env(\.local)?$'; then - printf "${RED}✗ BLOCKED: Attempting to push .env file!${NC}\n" - printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '^\.env(\.local)?$')" - ERRORS=$((ERRORS + 1)) - fi - - # Check for .DS_Store. - if echo "$CHANGED_FILES" | grep -q '\.DS_Store'; then - printf "${RED}✗ BLOCKED: .DS_Store file in push!${NC}\n" - printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep '\.DS_Store')" - ERRORS=$((ERRORS + 1)) - fi - - # Check for log files. - if echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log' | grep -q .; then - printf "${RED}✗ BLOCKED: Log file in push!${NC}\n" - printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log')" - ERRORS=$((ERRORS + 1)) - fi - - # Check file contents for secrets. - for file in $CHANGED_FILES; do - if [ -f "$file" ] && [ ! -d "$file" ]; then - # Skip test files, example files, and hook scripts. - if echo "$file" | grep -qE '\.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/'; then - continue - fi - - # Check for hardcoded user paths. - if grep -E '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' "$file" 2>/dev/null | grep -q .; then - printf "${RED}✗ BLOCKED: Hardcoded personal path found in: $file${NC}\n" - grep -n -E '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' "$file" | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for Socket API keys. - if grep -E 'sktsec_[a-zA-Z0-9_-]+' "$file" 2>/dev/null | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'SOCKET_SECURITY_API_KEY=' | grep -v 'fake-token' | grep -v 'test-token' | grep -q .; then - printf "${RED}✗ BLOCKED: Real API key detected in: $file${NC}\n" - grep -n 'sktsec_' "$file" | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for AWS keys. - if grep -iE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' "$file" 2>/dev/null | grep -q .; then - printf "${RED}✗ BLOCKED: Potential AWS credentials found in: $file${NC}\n" - grep -n -iE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' "$file" | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for GitHub tokens. - if grep -E 'gh[ps]_[a-zA-Z0-9]{36}' "$file" 2>/dev/null | grep -q .; then - printf "${RED}✗ BLOCKED: Potential GitHub token found in: $file${NC}\n" - grep -n -E 'gh[ps]_[a-zA-Z0-9]{36}' "$file" | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for private keys. - if grep -E '-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----' "$file" 2>/dev/null | grep -q .; then - printf "${RED}✗ BLOCKED: Private key found in: $file${NC}\n" - ERRORS=$((ERRORS + 1)) - fi - fi - done - fi - - TOTAL_ERRORS=$((TOTAL_ERRORS + ERRORS)) -done - -if [ $TOTAL_ERRORS -gt 0 ]; then - printf "\n" - printf "${RED}✗ Push blocked by mandatory validation!${NC}\n" - printf "Fix the issues above before pushing.\n" - exit 1 -fi - -printf "${GREEN}✓ All mandatory validation passed!${NC}\n" -exit 0 +# Run pre-push security validation. +.git-hooks/pre-push "$@" diff --git a/.husky/security-checks.sh b/.husky/security-checks.sh deleted file mode 100755 index ad4c03e47..000000000 --- a/.husky/security-checks.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash -# Socket Security Checks -# Prevents committing sensitive data and common mistakes. - -set -e - -# Colors for output. -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -NC='\033[0m' - -# Allowed public API key (used in socket-lib and across all Socket repos). -# This is Socket's official public test API key - safe to commit. -# NOTE: This value is intentionally identical across all Socket repos. -ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api" - -echo "${GREEN}Running Socket Security checks...${NC}" - -# Get list of staged files. -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) - -if [ -z "$STAGED_FILES" ]; then - echo "${GREEN}✓ No files to check${NC}" - exit 0 -fi - -ERRORS=0 - -# Check for .DS_Store files. -printf "Checking for .DS_Store files...\n" -if echo "$STAGED_FILES" | grep -q '\.DS_Store'; then - echo "${RED}✗ ERROR: .DS_Store file detected!${NC}" - echo "$STAGED_FILES" | grep '\.DS_Store' - ERRORS=$((ERRORS + 1)) -fi - -# Check for log files. -printf "Checking for log files...\n" -if echo "$STAGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log'; then - echo "${RED}✗ ERROR: Log file detected!${NC}" - echo "$STAGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log' - ERRORS=$((ERRORS + 1)) -fi - -# Check for .env files. -printf "Checking for .env files...\n" -if echo "$STAGED_FILES" | grep -E '^\.env(\.local)?$'; then - echo "${RED}✗ ERROR: .env or .env.local file detected!${NC}" - echo "$STAGED_FILES" | grep -E '^\.env(\.local)?$' - printf "These files should never be committed. Use .env.example instead.\n" - ERRORS=$((ERRORS + 1)) -fi - -# Check for hardcoded user paths (generic detection). -printf "Checking for hardcoded personal paths...\n" -for file in $STAGED_FILES; do - if [ -f "$file" ]; then - # Skip test files and hook scripts. - if echo "$file" | grep -qE '\.(test|spec)\.|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/'; then - continue - fi - - # Check for common user path patterns. - if grep -E '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' "$file" 2>/dev/null | grep -q .; then - echo "${RED}✗ ERROR: Hardcoded personal path found in: $file${NC}" - grep -n -E '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' "$file" | head -3 - printf "Replace with relative paths or environment variables.\n" - ERRORS=$((ERRORS + 1)) - fi - fi -done - -# Check for Socket API keys. -printf "Checking for API keys...\n" -for file in $STAGED_FILES; do - if [ -f "$file" ]; then - if grep -E 'sktsec_[a-zA-Z0-9_-]+' "$file" 2>/dev/null | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'SOCKET_SECURITY_API_KEY=' | grep -v 'fake-token' | grep -v 'test-token' | grep -q .; then - echo "${YELLOW}⚠ WARNING: Potential API key found in: $file${NC}" - grep -n 'sktsec_' "$file" | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | head -3 - printf "If this is a real API key, DO NOT COMMIT IT.\n" - fi - fi -done - -# Check for common secret patterns. -printf "Checking for potential secrets...\n" -for file in $STAGED_FILES; do - if [ -f "$file" ]; then - # Skip test files, example files, and hook scripts. - if echo "$file" | grep -qE '\.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/'; then - continue - fi - - # Check for AWS keys. - if grep -iE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' "$file" 2>/dev/null | grep -q .; then - echo "${RED}✗ ERROR: Potential AWS credentials found in: $file${NC}" - grep -n -iE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' "$file" | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for GitHub tokens. - if grep -E 'gh[ps]_[a-zA-Z0-9]{36}' "$file" 2>/dev/null | grep -q .; then - echo "${RED}✗ ERROR: Potential GitHub token found in: $file${NC}" - grep -n -E 'gh[ps]_[a-zA-Z0-9]{36}' "$file" | head -3 - ERRORS=$((ERRORS + 1)) - fi - - # Check for private keys. - if grep -E '-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----' "$file" 2>/dev/null | grep -q .; then - echo "${RED}✗ ERROR: Private key found in: $file${NC}" - ERRORS=$((ERRORS + 1)) - fi - fi -done - -if [ $ERRORS -gt 0 ]; then - printf "\n" - echo "${RED}✗ Security check failed with $ERRORS error(s).${NC}" - printf "Fix the issues above and try again.\n" - exit 1 -fi - -echo "${GREEN}✓ All security checks passed!${NC}" -exit 0 diff --git a/.npmrc b/.npmrc index 7fa145a3a..9efdcbe6d 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,5 @@ # npm v11+ settings (not pnpm — pnpm v11 only reads auth/registry from .npmrc). +ignore-scripts=true min-release-age=7 trust-policy=no-downgrade trust-policy-exclude[]=undici@6.21.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b02fce93b..2b20b0c42 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,9 @@ +resolutionMode: highest +trustPolicy: no-downgrade +trustPolicyExclude: + - undici@6.21.3 + packages: - packages/* - '!packages/package-builder/build' diff --git a/scripts/fix.mjs b/scripts/fix.mjs index 3b96f0c61..776c67b97 100644 --- a/scripts/fix.mjs +++ b/scripts/fix.mjs @@ -1,6 +1,11 @@ /** - * @fileoverview Monorepo-aware auto-fix script - runs linters with auto-fix enabled. - * Delegates to lint.mjs with --fix flag. + * @fileoverview Monorepo-aware auto-fix script — runs linters, security tools, + * and config auditors with auto-fix enabled. + * + * Steps: + * 1. `pnpm run lint --fix` — oxlint + oxfmt across affected packages + * 2. `zizmor --fix .github/` — fix GitHub Actions workflow issues (if .github/ exists) + * 3. `agentshield scan --fix` — fix Claude config security findings (if .claude/ exists) * * Usage: * node scripts/fix.mjs [options] @@ -13,6 +18,8 @@ * --verbose Show detailed output */ +import { existsSync } from 'node:fs' + import { isQuiet } from '@socketsecurity/lib/argv/flags' import { parseArgs } from '@socketsecurity/lib/argv/parse' import { WIN32 } from '@socketsecurity/lib/constants/platform' @@ -21,6 +28,61 @@ import { spawn } from '@socketsecurity/lib/spawn' import { printHeader } from '@socketsecurity/lib/stdio/header' const logger = getDefaultLogger() + +/** + * Resolve the path to a binary, checking global PATH first then node_modules/.bin. + * Returns undefined if the binary is not found anywhere. + */ +function resolveBin(name) { + // spawn will find it on PATH or in node_modules/.bin via pnpm exec. + // We just need to know if it exists at all. + try { + // Check node_modules/.bin (works in pnpm monorepos). + const localBin = `node_modules/.bin/${name}` + if (existsSync(localBin)) { + return localBin + } + } catch {} + return undefined +} + +/** + * Run a security tool with --fix. Non-blocking: logs warnings on failure + * but does not fail the overall fix run. + * + * @param {object} opts + * @param {string[]} opts.args - Arguments to pass to spawn. + * @param {string} opts.bin - Binary name. + * @param {string} opts.label - Human-readable label for log output. + * @param {boolean} opts.quiet - Suppress progress output. + */ +async function runSecurityFix({ args, bin, label, quiet }) { + if (!quiet) { + logger.stdout.progress(`Running ${label}...`) + } + try { + const result = await spawn(bin, args, { + shell: WIN32, + stdio: quiet ? 'pipe' : 'inherit', + }) + if (!quiet) { + logger.stdout.clearLine() + if (result.code === 0) { + logger.success(`${label} completed`) + } else { + // Non-zero exit is not fatal — the tool may have found unfixable issues. + logger.warn(`${label} exited with code ${result.code}`) + } + } + } catch { + // Tool crashed or is unavailable — skip gracefully. + if (!quiet) { + logger.stdout.clearLine() + logger.warn(`${label} not available, skipping`) + } + } +} + async function main() { const { values } = parseArgs({ options: { @@ -43,7 +105,8 @@ async function main() { logger.log('') } - // Build lint command arguments. + // ── Step 1: Lint fix ───────────────────────────────────────────────── + // Delegates to per-package lint scripts (oxlint --fix, oxfmt --write). const lintArgs = ['run', 'lint', '--fix'] if (values.all) { lintArgs.push('--all') @@ -58,7 +121,6 @@ async function main() { lintArgs.push('--quiet') } - // Run lint with --fix flag. const result = await spawn('pnpm', lintArgs, { shell: WIN32, stdio: quiet ? 'pipe' : 'inherit', @@ -66,14 +128,40 @@ async function main() { if (result.code !== 0) { if (!quiet) { - logger.error('Some fixes could not be applied') + logger.error('Some lint fixes could not be applied') } process.exitCode = 1 - } else { - if (!quiet) { - logger.log('') - logger.success('Auto-fix completed!') - } + } + + // ── Step 2: zizmor ─────────────────────────────────────────────────── + // Fixes GitHub Actions workflow security issues in-place. + // Only runs if .github/ directory exists (some repos don't have workflows). + // Uses --fix=safe (default) — only applies fixes that won't change behavior. + if (existsSync('.github')) { + await runSecurityFix({ + args: ['--fix', '.github/'], + bin: 'zizmor', + label: 'zizmor fix', + quiet, + }) + } + + // ── Step 3: AgentShield ────────────────────────────────────────────── + // Fixes Claude config security findings in-place. + // Only runs if .claude/ directory exists. + // Uses --fix which applies safe auto-fixes to settings.json, CLAUDE.md, etc. + if (existsSync('.claude') && resolveBin('agentshield')) { + await runSecurityFix({ + args: ['exec', 'agentshield', 'scan', '--fix'], + bin: 'pnpm', + label: 'agentshield fix', + quiet, + }) + } + + if (!quiet && !process.exitCode) { + logger.log('') + logger.success('Auto-fix completed!') } } catch (error) { if (!quiet) {