diff --git a/.checks/pre-push b/.checks/pre-push index 34ff968..537d5d9 100755 --- a/.checks/pre-push +++ b/.checks/pre-push @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Pre-push hook for comprehensive repository checks +# Advanced Pre-Push Security and Quality Check Script # Fail fast on any error set -euo pipefail @@ -12,12 +12,9 @@ readonly NC='\033[0m' # Paths readonly REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -readonly CONTENT_DIR="${REPO_ROOT}/content" -readonly ZENSICAL_CFG_PATH="${REPO_ROOT}/zensical.toml" -readonly SITE_DIR="${REPO_ROOT}/deploy" -readonly LOG_DIR="${REPO_ROOT}/logs" +readonly LOG_DIR="${REPO_ROOT}/.checks/logs" -# Logging and error handling +# Logging functions log_error() { echo -e "${RED}[ERROR] $*${NC}" >&2 } @@ -30,54 +27,74 @@ log_warning() { echo -e "${YELLOW}[WARNING] $*${NC}" } -# Cleanup function -cleanup() { - if [[ -n "${SERVER_PID:-}" ]] && ps -p "$SERVER_PID" >/dev/null 2>&1; then - kill "$SERVER_PID" >/dev/null 2>&1 || true - for _ in {1..30}; do - ps -p "$SERVER_PID" >/dev/null 2>&1 || break - sleep 0.1 - done - fi +# 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 } -trap cleanup EXIT INT TERM -# Initialize log directory +# 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 +# Vulnerability Scanning with Trivy run_trivy_scan() { if ! command -v trivy &>/dev/null; then log_warning "Trivy not installed. Skipping vulnerability scan." return 0 - fi + } - log_info "Running Trivy scan..." - trivy fs . \ + 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 failed. See $LOG_DIR/trivy.log" + log_error "Trivy scan failed. See $LOG_DIR/trivy.log" return 1 } } -# Secrets Detection +# 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 - fi + } - log_info "Running TruffleHog (verified only)..." + log_info "Running TruffleHog secrets scan..." local tmpf tmpf="$(mktemp)" - trufflehog filesystem . --json >"$tmpf" 2>"$LOG_DIR/trufflehog.log" || true + # 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 ' ')" @@ -92,32 +109,48 @@ run_trufflehog_scan() { rm -f "$tmpf" } -# Additional security and lint checks -run_additional_checks() { - # Git Secrets - if command -v git-secrets &>/dev/null; then - 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" +# 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 } - fi +} - # Python Security (Bandit) - if command -v bandit &>/dev/null; then - log_info "Running Bandit Python security scan..." - local py_files - py_files="$(find . -type f -name "*.py")" - if [[ -n "$py_files" ]]; then - # shellcheck disable=SC2086 - 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 - } - fi - fi - - # Dependabot-like checks +# 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 || { @@ -126,23 +159,64 @@ run_additional_checks() { } fi - # Stylelint - if command -v stylelint &>/dev/null; then - log_info "Running Stylelint..." - stylelint "**/*.{css,scss}" --formatter json >"$LOG_DIR/stylelint.json" 2>"$LOG_DIR/stylelint.log" || { - log_error "Stylelint issues found. See $LOG_DIR/stylelint.log" + # 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 } -# Main execution +# 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 - run_trivy_scan - run_trufflehog_scan - run_additional_checks + 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!" }