diff --git a/.checks/pre-push b/.checks/pre-push index b1d8765..16a6693 100755 --- a/.checks/pre-push +++ b/.checks/pre-push @@ -1,114 +1,140 @@ #!/usr/bin/env bash -# Advanced Pre-Push Security and Quality Check Script - -# Fail fast on any error set -euo pipefail -# Color codes +# Colors readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly NC='\033[0m' -# Paths -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -LOG_DIR="${REPO_ROOT}/.checks/logs" +readonly REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +readonly LOG_DIR="${REPO_ROOT}/.checks/logs" -# Ensure log directory exists -mkdir -p "$LOG_DIR" +log() { echo -e "${GREEN}[INFO] $*${NC}"; } +warn() { echo -e "${YELLOW}[WARN] $*${NC}"; } +err() { echo -e "${RED}[ERROR] $*${NC}" >&2; } -# Logging functions -log_error() { - echo -e "${RED}[ERROR] $*${NC}" >&2 +prepare_logs() { + rm -rf "$LOG_DIR" + mkdir -p "$LOG_DIR" } -log_info() { - echo -e "${GREEN}[INFO] $*${NC}" +# Files that git considers "scannable" (tracked + untracked not ignored) +scannable_files() { + git ls-files --cached --modified --others --exclude-standard } -log_warning() { - echo -e "${YELLOW}[WARNING] $*${NC}" +# Files listed as ignored by git (used to build tool-specific skip args) +ignored_files() { + git ls-files -o -i --exclude-standard } -# Vulnerability Scanning with Trivy -run_trivy_scan() { - # Check if Trivy is installed - if ! command -v trivy &>/dev/null; then - log_warning "Trivy not installed. Skipping vulnerability scan." - return 0 - fi +# Build Trivy skip args from git-ignored paths +trivy_skip_args() { + local path dir + while IFS= read -r path; do + [[ -z "$path" ]] && continue + dir="$(dirname "$path")" + printf -- '--skip-files=%q ' "$path" + printf -- '--skip-dirs=%q ' "$dir" + done < <(ignored_files) +} - # Ensure we have files to scan - local files - mapfile -t files < <(git ls-files) +run_tool() { + local name="$1"; shift + if ! command -v "$name" &>/dev/null; then + warn "$name not installed; skipping." + return 0 + fi + "$@" +} +run_trivy() { + run_tool trivy bash -c ' + files=("$@") if [[ ${#files[@]} -eq 0 ]]; then - log_warning "No files to scan with Trivy" - return 0 - fi - - log_info "Running Trivy vulnerability scan..." - - # Run Trivy scan - if ! trivy fs "${files[@]}" \ - --exit-code 1 \ - --severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN \ - --no-progress \ - >"$LOG_DIR/trivy.log" 2>&1; then - log_error "Trivy scan failed. See $LOG_DIR/trivy.log" - return 1 + echo "no files"; exit 0 fi + # Expand skip args passed via environment variable SKIP_ARGS + eval trivy fs "${files[@]}" $SKIP_ARGS --exit-code 1 --severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN --no-progress + ' _ $(scannable_files) } -# Secrets Detection with Trufflehog -run_trufflehog_scan() { - # Check if Trufflehog and jq are installed - if ! (command -v trufflehog &>/dev/null && command -v jq &>/dev/null); then - log_warning "TruffleHog or jq not installed. Skipping secrets scan." - return 0 - fi - - log_info "Running TruffleHog secrets scan..." - local tmpf +run_trufflehog() { + run_tool trufflehog bash -c ' + files=("$@") tmpf="$(mktemp)" - - # Scan files - if ! trufflehog filesystem . --json >"$tmpf" 2>"$LOG_DIR/trufflehog.log"; then - log_error "Trufflehog scan failed" - rm -f "$tmpf" - return 1 + printf "%s\n" "${files[@]}" | xargs trufflehog filesystem --json >"$tmpf" 2>"'"$LOG_DIR"'/trufflehog.log" || true + if command -v jq &>/dev/null; then + if jq -e '"'"'any(inputs; .verified==true)'"'"' "$tmpf" >/dev/null 2>&1; then + cp "$tmpf" "'"$LOG_DIR"'/trufflehog-findings.json" + echo "verified"; exit 2 + fi fi - - # Check for verified secrets - local verified - verified=$(jq 'select(.verified==true)' "$tmpf" | wc -l | tr -d ' ') - - if [[ "$verified" -gt 0 ]]; then - cp "$tmpf" "$LOG_DIR/trufflehog-findings.json" - log_error "Verified secrets found. See $LOG_DIR/trufflehog-findings.json" - rm -f "$tmpf" - return 1 - fi - rm -f "$tmpf" + ' _ $(git ls-files) + local rc=$?; [[ $rc -eq 2 ]] && { err "TruffleHog found verified secrets. See $LOG_DIR/trufflehog-findings.json"; return 1; } + return 0 +} + +run_git_secrets() { + run_tool git-secrets git-secrets --scan >"$LOG_DIR/git-secrets.log" 2>&1 || { + err "git-secrets detected potential secrets. See $LOG_DIR/git-secrets.log"; return 1; } +} + +run_bandit() { + run_tool bandit bash -c ' + py=("$@") + [[ ${#py[@]} -eq 0 ]] && exit 0 + bandit -r "${py[@]}" -n 5 -lll -f json -o "'"$LOG_DIR"'/bandit.json" 2>"'"$LOG_DIR"'/bandit.log" + ' _ $(git ls-files '*.py') +} + +run_npm_audit() { + if [[ -f package.json ]]; then + run_tool npm npm audit --audit-level=high >"$LOG_DIR/npm-audit.log" 2>&1 || { + err "npm audit found issues. See $LOG_DIR/npm-audit.log"; return 1; } + fi +} + +run_pip_audit() { + if [[ -f requirements.txt ]] || [[ -f pyproject.toml ]]; then + run_tool pip-audit pip-audit -r requirements.txt >"$LOG_DIR/pip-audit.log" 2>&1 || { + err "pip-audit found issues. See $LOG_DIR/pip-audit.log"; return 1; } + fi +} + +run_stylelint() { + run_tool stylelint bash -c ' + files=("$@") + [[ ${#files[@]} -eq 0 ]] && exit 0 + stylelint "${files[@]}" --formatter json > "'"$LOG_DIR"'/stylelint.json" 2>"'"$LOG_DIR"'/stylelint.log" + ' _ $(git ls-files '*.{css,scss,sass,less}') } -# Main execution function main() { - local failed=0 + prepare_logs - # Run each check and track failures - run_trivy_scan || failed=1 - run_trufflehog_scan || failed=1 + # Precompute skip args for trivy (safe quoting) + export SKIP_ARGS + SKIP_ARGS="$(trivy_skip_args || true)" - # Exit with error if any checks failed - if [[ $failed -eq 1 ]]; then - log_error "Some pre-push checks failed. Aborting push." - exit 1 - fi + local failed=0 - log_info "All pre-push checks passed successfully!" + run_trivy || failed=1 + run_trufflehog || failed=1 + run_git_secrets || failed=1 + run_bandit || failed=1 + run_npm_audit || failed=1 + run_pip_audit || failed=1 + run_stylelint || failed=1 + + if [[ $failed -ne 0 ]]; then + err "Pre-push checks failed. Check logs in $LOG_DIR" + exit 1 + fi + + log "All pre-push checks passed." } -# Run the main function main "$@"