#!/usr/bin/env bash # Advanced Pre-Push Security and Quality Check Script # Fail fast on any error set -euo pipefail # Color codes readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly NC='\033[0m' # Paths readonly REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" readonly LOG_DIR="${REPO_ROOT}/.checks/logs" # Logging functions log_error() { echo -e "${RED}[ERROR] $*${NC}" >&2 } log_info() { echo -e "${GREEN}[INFO] $*${NC}" } log_warning() { echo -e "${YELLOW}[WARNING] $*${NC}" } # Utility function to get scannable files respecting .gitignore get_scannable_files() { # Find files that are tracked by git or not ignored git ls-files --cached --modified --others --exclude-standard } # Gitignore-aware exclusion generator get_gitignore_excludes() { local excludes=() # Use git ls-files to find ignored files while IFS= read -r file; do # Convert file paths to directories for skipping dir=$(dirname "$file") excludes+=("--skip-dirs=$dir" "--skip-files=$file") done < <(git ls-files -o -i --exclude-standard) printf '%s\n' "${excludes[@]}" } # Prepare log directory prepare_log_directory() { rm -rf "$LOG_DIR" mkdir -p "$LOG_DIR" } # Vulnerability Scanning with Trivy run_trivy_scan() { if ! command -v trivy &>/dev/null; then log_warning "Trivy not installed. Skipping vulnerability scan." return 0 } local files mapfile -t files < <(get_scannable_files) if [[ ${#files[@]} -eq 0 ]]; then log_warning "No files to scan with Trivy" return 0 } log_info "Running Trivy vulnerability scan..." # shellcheck disable=SC2046 trivy fs "${files[@]}" \ $(get_gitignore_excludes) \ --exit-code 1 \ --severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN \ --no-progress \ >"$LOG_DIR/trivy.log" 2>&1 || { log_error "Trivy scan failed. See $LOG_DIR/trivy.log" return 1 } } # Secrets Detection with Trufflehog run_trufflehog_scan() { if ! (command -v trufflehog &>/dev/null && command -v jq &>/dev/null); then log_warning "TruffleHog or jq not installed. Skipping secrets scan." return 0 } log_info "Running TruffleHog secrets scan..." local tmpf tmpf="$(mktemp)" # Use git ls-files to respect .gitignore git ls-files | xargs trufflehog filesystem \ --json >"$tmpf" 2>"$LOG_DIR/trufflehog.log" || true 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 Secrets Check run_git_secrets_scan() { if ! command -v git-secrets &>/dev/null; then log_warning "git-secrets not installed. Skipping git-secrets scan." return 0 } log_info "Running git-secrets scan..." git-secrets --scan >"$LOG_DIR/git-secrets.log" 2>&1 || { log_error "git-secrets detected potential secrets. See $LOG_DIR/git-secrets.log" return 1 } } # Python Security Scan with Bandit run_python_security_scan() { if ! command -v bandit &>/dev/null; then log_warning "Bandit not installed. Skipping Python security scan." return 0 } local py_files mapfile -t py_files < <(git ls-files '*.py') if [[ ${#py_files[@]} -eq 0 ]]; then log_warning "No Python files to scan" return 0 } log_info "Running Bandit Python security scan..." # shellcheck disable=SC2046 bandit -r "${py_files[@]}" \ -n 5 -lll \ -f json -o "$LOG_DIR/bandit.json" 2>"$LOG_DIR/bandit.log" || { log_error "Bandit issues found. See $LOG_DIR/bandit.log" return 1 } } # Dependency Vulnerability Check run_dependency_checks() { # npm audit if command -v npm &>/dev/null && [[ -f package.json ]]; then log_info "Running npm audit..." npm audit --audit-level=high >"$LOG_DIR/npm-audit.log" 2>&1 || { log_error "npm audit found issues. See $LOG_DIR/npm-audit.log" return 1 } fi # pip-audit for Python dependencies if command -v pip-audit &>/dev/null && [[ -f requirements.txt ]]; then log_info "Running pip-audit..." pip-audit -r requirements.txt >"$LOG_DIR/pip-audit.log" 2>&1 || { log_error "pip-audit found issues. See $LOG_DIR/pip-audit.log" return 1 } fi } # Stylelint for CSS/SCSS run_stylelint_check() { if ! command -v stylelint &>/dev/null; then log_warning "Stylelint not installed. Skipping stylesheet lint." return 0 } local style_files mapfile -t style_files < <(git ls-files '*.{css,scss,sass}') if [[ ${#style_files[@]} -eq 0 ]]; then log_warning "No stylesheet files to lint" return 0 } log_info "Running Stylelint..." # shellcheck disable=SC2046 stylelint "${style_files[@]}" \ --formatter json >"$LOG_DIR/stylelint.json" 2>"$LOG_DIR/stylelint.log" || { log_error "Stylelint issues found. See $LOG_DIR/stylelint.log" return 1 } } # Main execution function main() { prepare_log_directory local checks=( run_trivy_scan run_trufflehog_scan run_git_secrets_scan run_python_security_scan run_dependency_checks run_stylelint_check ) local failed=0 for check in "${checks[@]}"; do if ! "$check"; then failed=1 fi done if [[ $failed -eq 1 ]]; then log_error "Some pre-push checks failed. Aborting push." exit 1 fi log_info "All pre-push checks passed successfully!" } # Run the main function main "$@"