From 15116d57de3547b2a70866bebbf914bc5744a392 Mon Sep 17 00:00:00 2001 From: virtualian Date: Sun, 7 Dec 2025 18:08:59 +0000 Subject: [PATCH] Convert package-validator.js to Bash, deprecate npm-install-monitor.js (issue #36) - Create scripts/package-validator.sh with pure Bash implementation - Levenshtein distance algorithm for typosquatting detection - Reuses npm-audit-lib.sh functions for email validation and caching - Bash 3 compatible (works on macOS default bash) - Adds --json output flag - Move scripts/package-validator.js to deprecated/ - Move scripts/npm-install-monitor.js to deprecated/ - Update npm-scanner.sh to call package-validator.sh - Update documentation for both scripts - Update CLAUDE.md architecture section Closes #36 --- CLAUDE.md | 7 +- .../npm-install-monitor.js | 0 {scripts => deprecated}/package-validator.js | 0 docs/npm-install-monitor.md | 11 +- docs/package-validator.md | 165 +++-- npm-scanner.sh | 2 +- scripts/package-validator.sh | 596 ++++++++++++++++++ 7 files changed, 712 insertions(+), 69 deletions(-) rename {scripts => deprecated}/npm-install-monitor.js (100%) rename {scripts => deprecated}/package-validator.js (100%) create mode 100755 scripts/package-validator.sh diff --git a/CLAUDE.md b/CLAUDE.md index 1eaa757..86de649 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,8 +21,7 @@ Security toolkit protecting Node.js developers from npm supply chain attacks (Ph - **npm-audit-lib.sh** - Shared library with common functions **Pre-Installation:** -- **package-validator.js** - Pre-installation risk assessment with typosquatting detection -- **npm-install-monitor.js** - Network monitoring during installation +- **package-validator.sh** - Pre-installation risk assessment with typosquatting detection **Response & Automation:** - **credential-rotation-script.sh** - Post-compromise incident response guide @@ -33,6 +32,8 @@ Security toolkit protecting Node.js developers from npm supply chain attacks (Ph ### Deprecated - **deprecated/recursive-audit-script.sh** - Replaced by `npm-scanner.sh scan --project` +- **deprecated/package-validator.js** - Replaced by `package-validator.sh` +- **deprecated/npm-install-monitor.js** - Not integrated, limited value over scan commands ## Constraints @@ -60,7 +61,7 @@ Security toolkit protecting Node.js developers from npm supply chain attacks (Ph - NEVER add package.json, package-lock.json, or node_modules to this project - This toolkit audits npm packages - depending on them creates supply chain risk -- Use only: bash scripts, standalone Node.js files, and system tools (jq, curl, etc.) +- Use only: bash scripts and system tools (jq, curl, etc.) - CI will fail if npm dependency files are detected ## File Structure diff --git a/scripts/npm-install-monitor.js b/deprecated/npm-install-monitor.js similarity index 100% rename from scripts/npm-install-monitor.js rename to deprecated/npm-install-monitor.js diff --git a/scripts/package-validator.js b/deprecated/package-validator.js similarity index 100% rename from scripts/package-validator.js rename to deprecated/package-validator.js diff --git a/docs/npm-install-monitor.md b/docs/npm-install-monitor.md index 155ee41..6dd1f0f 100644 --- a/docs/npm-install-monitor.md +++ b/docs/npm-install-monitor.md @@ -1,7 +1,14 @@ # npm-install-monitor.js -**Type:** Real-Time Installation Monitor -**Language:** Node.js +> **DEPRECATED:** This script has been moved to `deprecated/npm-install-monitor.js`. +> +> **Reason:** Not integrated into the main `npm-scanner.sh` workflow and provides limited value over the `scan` commands. The network monitoring approach requires platform-specific tooling (lsof) and real-time process management that doesn't fit the project's Bash-based architecture. +> +> For pre-installation security checks, use `npm-scanner.sh validate ` instead. + +**Type:** Real-Time Installation Monitor +**Language:** Node.js +**Location:** `deprecated/npm-install-monitor.js` **Dependencies:** child_process, fs, lsof (Unix) --- diff --git a/docs/package-validator.md b/docs/package-validator.md index 1ef1086..9e4feb3 100644 --- a/docs/package-validator.md +++ b/docs/package-validator.md @@ -1,8 +1,8 @@ -# package-validator.js +# package-validator.sh -**Type:** Pre-Installation Security Validator -**Language:** Node.js -**Dependencies:** https (built-in), child_process (built-in) +**Type:** Pre-Installation Security Validator +**Language:** Bash +**Dependencies:** curl, jq, npm-audit-lib.sh --- @@ -15,28 +15,38 @@ Pre-installation risk assessment tool that validates npm packages before install ### 5.1 Risk Scoring System **Risk Weights:** -```javascript -RISK_WEIGHTS = { - MALICIOUS_DOMAIN: 100, // Known malicious infrastructure - HTTP_DEPENDENCY: 50, // Remote dynamic dependencies - NEW_PACKAGE: 20, // < 30 days old - SINGLE_MAINTAINER: 10, // Only one maintainer - LOW_DOWNLOADS: 15, // < 100 weekly downloads - INSTALL_SCRIPTS: 25, // Lifecycle scripts present - RECENT_VERSION: 10 // Latest version < 7 days -} +```bash +RISK_WEIGHTS=( + [MALICIOUS_DOMAIN]=100 # Known malicious infrastructure + [MALICIOUS_IP]=100 # Known malicious IP addresses + [ATTACKER_PATTERN]=100 # Known attacker email/username patterns + [HTTP_DEPENDENCY]=50 # Remote dynamic dependencies + [DISPOSABLE_EMAIL]=40 # Disposable email domain + [SUSPICIOUS_EMAIL_PATTERN]=30 # Suspicious email patterns + [PRIVACY_RELAY_EMAIL]=15 # Privacy relay email (anonymous) + [NEW_PACKAGE]=20 # < 30 days old + [SINGLE_MAINTAINER]=10 # Only one maintainer + [LOW_DOWNLOADS]=15 # < 100 weekly downloads + [INSTALL_SCRIPTS]=25 # Lifecycle scripts present + [RECENT_VERSION]=10 # Latest version < 7 days + [TYPOSQUATTING]=30 # Similar to popular package name + # Positive modifiers (reduce risk) + [TRUSTED_TOP1K]=-10 # Email from top 1k domain + [TRUSTED_TOP10K]=-5 # Email from top 10k domain + [TRUSTED_TOP100K]=-2 # Email from top 100k domain +) ``` **Risk Levels:** -- `CRITICAL` (≥80): Do not install -- `HIGH` (≥50): Extreme caution -- `MEDIUM` (≥25): Proceed with caution -- `LOW` (≥10): Some concerns +- `CRITICAL` (>=80): Do not install +- `HIGH` (>=50): Extreme caution +- `MEDIUM` (>=25): Proceed with caution +- `LOW` (>=10): Some concerns - `SAFE` (<10): Appears safe ### 5.2 Package Age Analysis -**Function:** `checkPackageAge()` +**Function:** `check_package_age()` **Checks:** 1. **Package Creation Date** @@ -46,22 +56,30 @@ RISK_WEIGHTS = { 2. **Latest Version Age** - < 7 days: MEDIUM risk (+10 points) -**Data Source:** `packageData.time.created`, `packageData.time[version]` +**Data Source:** npm registry API `time.created`, `time[version]` ### 5.3 Maintainer Analysis -**Function:** `checkMaintainers()` +**Function:** `check_maintainers()` **Checks:** 1. **Maintainer Count** - 0 maintainers: HIGH risk (+20 points) - 1 maintainer: LOW risk (+10 points) -2. **Email Pattern Analysis** - - Suspicious patterns: `temp|disposable|trash|fake|test\d+` - - Match: HIGH risk (+30 points) +2. **Email Domain Analysis** (via npm-audit-lib.sh) + - Disposable email domains: HIGH risk (+40 points) + - Privacy relay domains: LOW risk (+15 points) + - Trusted domains: Negative points (reduces risk) + +3. **Email Pattern Analysis** + - Suspicious patterns: `temp|disposable|trash|fake|test[0-9]+` + - Match: MEDIUM risk (+30 points) -**Purpose:** Identify potential throwaway/fake accounts +4. **Attacker Pattern Matching** + - Known attacker patterns: CRITICAL risk (+100 points) + +**Purpose:** Identify potential throwaway/fake accounts and known malicious actors **Output Format:** ``` @@ -70,15 +88,13 @@ RISK_WEIGHTS = { Risk points: +10 ``` -Note: The package name is shown in the report header, so findings only include the maintainer information. - ### 5.4 Download Statistics -**Function:** `checkDownloads()` +**Function:** `check_downloads()` **Implementation:** -```javascript -npm view ${packageName} dist.last-week --json +```bash +curl -sL "https://api.npmjs.org/downloads/point/last-week/${package}" ``` **Threshold:** < 100 weekly downloads = MEDIUM risk (+15 points) @@ -87,23 +103,24 @@ npm view ${packageName} dist.last-week --json ### 5.5 Dependency Analysis -**Function:** `checkDependencies()` +**Function:** `check_dependencies()` **Checks:** 1. **HTTP/HTTPS URL Dependencies** - - Detects: `version.match(/^https?:\/\//)` - - Known malicious: CRITICAL (+100 points) + - Known malicious domain: CRITICAL (+100 points) + - Known malicious IP: CRITICAL (+100 points) - Other HTTP URLs: HIGH (+50 points) -2. **Malicious Domains** +2. **Malicious Indicators** (from npm-audit-lib.sh): - `packages.storeartifact.com` - `storeartifact.com` + - `54.173.15.59` **Scope:** dependencies, devDependencies, peerDependencies ### 5.6 Lifecycle Script Detection -**Function:** `checkScripts()` +**Function:** `check_scripts()` **Monitored Scripts:** - `preinstall` @@ -116,32 +133,34 @@ npm view ${packageName} dist.last-week --json ### 5.7 Typosquatting Detection -**Function:** `checkTyposquatting()` +**Function:** `check_typosquatting()` -**Algorithm:** Levenshtein Distance +**Algorithm:** Levenshtein Distance (pure Bash implementation) -**Popular Packages List:** -```javascript -['express', 'react', 'vue', 'angular', 'lodash', 'axios', - 'webpack', 'babel', 'eslint', 'typescript', 'prettier', - 'jest', 'mocha'] +**Popular Packages List:** (from npm-audit-lib.sh TYPOSQUATTING_TARGETS) +```bash +express react vue angular lodash axios webpack +babel eslint typescript prettier jest mocha +chalk moment uuid commander debug request +async bluebird underscore q colors minimist +yargs glob rimraf ``` -**Threshold:** Edit distance ≤ 2 = HIGH risk (+30 points) +**Threshold:** Edit distance <= 2 = HIGH risk (+30 points) **Examples:** -- `expres` (distance: 1) → typosquatting -- `reactt` (distance: 1) → typosquatting +- `expres` (distance: 1) -> typosquatting alert +- `reactt` (distance: 1) -> typosquatting alert ### 5.8 Report Generation -**Function:** `printReport()` +**Function:** `print_report()` **Format:** ``` -═══════════════════════════════════════════════ +=============================================== Package Security Validation Report -═══════════════════════════════════════════════ +=============================================== Package: example-package Risk Score: 45 @@ -150,7 +169,7 @@ Risk Level: MEDIUM Findings: [MEDIUM] Package Age - Package is 25 days old (created Mon Oct 06 2025) + Package is 25 days old (created 2025-10-06) Risk points: +20 [MEDIUM] Install Scripts @@ -158,52 +177,72 @@ Findings: install: node postinstall.js Risk points: +25 -═══════════════════════════════════════════════ +=============================================== Recommendation -═══════════════════════════════════════════════ +=============================================== -⚠️ MEDIUM RISK - Proceed with caution +MEDIUM RISK - Proceed with caution Review the findings and verify the package legitimacy. ``` ## Input Parameters ```bash -Usage: node package-validator.js +Usage: package-validator.sh [--json] Arguments: - package-name # Name of npm package to validate + package-name Name of npm package to validate + --json Output results in JSON format Examples: - node package-validator.js express - node package-validator.js @babel/core + ./npm-scanner.sh validate express + ./npm-scanner.sh validate @babel/core + ./npm-scanner.sh validate lodash --json ``` ## Output Format **Exit Codes:** -- `0`: SAFE or LOW risk +- `0`: SAFE, LOW, or MEDIUM risk - `1`: HIGH or CRITICAL risk, or error **Colors:** - RED: Critical/High severity - YELLOW: Medium severity - BLUE: Low severity -- GREEN: Safe +- GREEN: Safe/Info + +**JSON Output:** +```json +{ + "package": "example-package", + "riskScore": 45, + "riskLevel": "MEDIUM", + "findingsCount": 2, + "findings": [ + { + "severity": "MEDIUM", + "category": "Package Age", + "message": "Package is 25 days old", + "points": 20 + } + ] +} +``` ## Performance Characteristics -- **API Calls:** 1-2 to npm registry +- **API Calls:** 2 to npm registry (package data + downloads) - **Execution Time:** 1-3 seconds -- **Memory Usage:** < 30MB +- **Memory Usage:** Minimal (shell script) ## Use Cases -1. **Pre-Install Check:** `node package-validator.js suspicious-pkg` +1. **Pre-Install Check:** `./npm-scanner.sh validate suspicious-pkg` 2. **CI/CD Gate:** Exit code determines pass/fail 3. **Manual Review:** Before adding new dependencies +4. **JSON Integration:** `./npm-scanner.sh validate pkg --json | jq .riskLevel` --- -[← Back to scheduled-audit-script.sh](scheduled-audit-script.md) | [Next: npm-install-monitor.js →](npm-install-monitor.md) - +[Back to scheduled-audit-script.sh](scheduled-audit-script.md) | [Next: npm-install-monitor.js](npm-install-monitor.md) diff --git a/npm-scanner.sh b/npm-scanner.sh index 37327fb..769c4a3 100755 --- a/npm-scanner.sh +++ b/npm-scanner.sh @@ -281,7 +281,7 @@ cmd_validate() { exit 0 fi warn_data_status || return 1 - node "$SCRIPTS_DIR/package-validator.js" "$@" + "$SCRIPTS_DIR/package-validator.sh" "$@" } cmd_list_iocs() { diff --git a/scripts/package-validator.sh b/scripts/package-validator.sh new file mode 100755 index 0000000..b8fe721 --- /dev/null +++ b/scripts/package-validator.sh @@ -0,0 +1,596 @@ +#!/bin/bash + +# package-validator.sh +# Pre-installation security check for npm packages +# Validates packages before installation to detect potential threats +# +# Usage: package-validator.sh [--json] +# Example: package-validator.sh express + +set -e + +# Get script directory and source shared library +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/npm-audit-lib.sh" + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +# Risk thresholds (matching package-validator.js) +# Using regular variables for Bash 3 compatibility (macOS default) +WEIGHT_MALICIOUS_DOMAIN=100 +WEIGHT_MALICIOUS_IP=100 +WEIGHT_ATTACKER_PATTERN=100 +WEIGHT_HTTP_DEPENDENCY=50 +WEIGHT_DISPOSABLE_EMAIL=40 +WEIGHT_SUSPICIOUS_EMAIL_PATTERN=30 +WEIGHT_PRIVACY_RELAY_EMAIL=15 +WEIGHT_NEW_PACKAGE=20 +WEIGHT_SINGLE_MAINTAINER=10 +WEIGHT_LOW_DOWNLOADS=15 +WEIGHT_INSTALL_SCRIPTS=25 +WEIGHT_RECENT_VERSION=10 +WEIGHT_TYPOSQUATTING=30 +# Positive modifiers (reduce risk) +WEIGHT_TRUSTED_TOP1K=-10 +WEIGHT_TRUSTED_TOP10K=-5 +WEIGHT_TRUSTED_TOP100K=-2 + +# ============================================================================ +# GLOBALS +# ============================================================================ + +PACKAGE_NAME="" +JSON_OUTPUT=false +RISK_SCORE=0 +declare -a FINDINGS=() + +# ============================================================================ +# UTILITY FUNCTIONS +# ============================================================================ + +usage() { + local exit_code="${1:-0}" + echo "Usage: $(basename "$0") [--json]" + echo "" + echo "Pre-installation security validation for npm packages." + echo "" + echo "Arguments:" + echo " package-name Name of the npm package to validate" + echo " --json Output results in JSON format" + echo "" + echo "Examples:" + echo " $(basename "$0") express" + echo " $(basename "$0") lodash --json" + exit "$exit_code" +} + +log_info() { + if [ "$JSON_OUTPUT" = false ]; then + echo -e "${BLUE}$1${NC}" + fi +} + +log_error() { + if [ "$JSON_OUTPUT" = false ]; then + echo -e "${RED}$1${NC}" >&2 + fi +} + +add_finding() { + local severity="$1" + local category="$2" + local message="$3" + local points="$4" + + FINDINGS+=("${severity}|${category}|${message}|${points}") + RISK_SCORE=$((RISK_SCORE + points)) +} + +# ============================================================================ +# LEVENSHTEIN DISTANCE +# ============================================================================ + +# Calculate Levenshtein distance between two strings +# Pure Bash implementation for typosquatting detection +levenshtein_distance() { + local str1="$1" + local str2="$2" + local len1=${#str1} + local len2=${#str2} + + # Edge cases + if [ "$len1" -eq 0 ]; then + echo "$len2" + return + fi + if [ "$len2" -eq 0 ]; then + echo "$len1" + return + fi + + # Initialize matrix using flat array (matrix[i][j] = d[i*cols + j]) + local cols=$((len1 + 1)) + local -a d=() + + # Initialize first row (0, 1, 2, ...) + for ((j = 0; j <= len1; j++)); do + d[j]=$j + done + + # Initialize first column (0, 1, 2, ...) and fill matrix + for ((i = 1; i <= len2; i++)); do + local prev_diag=${d[0]} + d[0]=$i + + for ((j = 1; j <= len1; j++)); do + local old_val=${d[j]} + local char1="${str1:j-1:1}" + local char2="${str2:i-1:1}" + + if [ "$char1" = "$char2" ]; then + d[j]=$prev_diag + else + local sub=$((prev_diag + 1)) + local ins=$((d[j] + 1)) + local del=$((d[j-1] + 1)) + + # min(sub, ins, del) + local min=$sub + [ "$ins" -lt "$min" ] && min=$ins + [ "$del" -lt "$min" ] && min=$del + d[j]=$min + fi + prev_diag=$old_val + done + done + + echo "${d[len1]}" +} + +# ============================================================================ +# PACKAGE CHECKS +# ============================================================================ + +fetch_package_data() { + local pkg="$1" + local url="https://registry.npmjs.org/$(echo "$pkg" | sed 's/@/%40/g')" + local response + + response=$(curl -sL "$url" 2>/dev/null) + + if [ -z "$response" ]; then + log_error "Error: Failed to fetch package data" + exit 1 + fi + + # Check for 404 (package not found) + if echo "$response" | jq -e '.error == "Not found"' >/dev/null 2>&1; then + log_error "Error: Package not found in npm registry" + exit 1 + fi + + echo "$response" +} + +check_package_age() { + local package_data="$1" + + # Get creation date + local created=$(echo "$package_data" | jq -r '.time.created // empty' 2>/dev/null) + if [ -z "$created" ]; then + return + fi + + local age_days=$(lib_get_package_age_days "$created") + local created_formatted=$(echo "$created" | cut -d'T' -f1) + + if [ "$age_days" -lt 30 ]; then + add_finding "HIGH" "Package Age" "Package is only ${age_days} days old (created ${created_formatted})" "$WEIGHT_NEW_PACKAGE" + elif [ "$age_days" -lt 90 ]; then + add_finding "MEDIUM" "Package Age" "Package is ${age_days} days old (created ${created_formatted})" "$((WEIGHT_NEW_PACKAGE / 2))" + fi + + # Check latest version age + local latest_version=$(echo "$package_data" | jq -r '.["dist-tags"].latest // empty' 2>/dev/null) + if [ -n "$latest_version" ]; then + local version_published=$(echo "$package_data" | jq -r --arg v "$latest_version" '.time[$v] // empty' 2>/dev/null) + if [ -n "$version_published" ]; then + local version_age=$(lib_get_package_age_days "$version_published") + if [ "$version_age" -lt 7 ]; then + add_finding "MEDIUM" "Version Age" "Latest version published only ${version_age} days ago" "$WEIGHT_RECENT_VERSION" + fi + fi + fi +} + +check_maintainers() { + local package_data="$1" + + # Get maintainers array + local maintainers_count=$(echo "$package_data" | jq -r '.maintainers | if type == "array" then length else 0 end' 2>/dev/null) + + if [ "$maintainers_count" -eq 0 ]; then + add_finding "HIGH" "Maintainers" "Package has no listed maintainers" "$((WEIGHT_SINGLE_MAINTAINER * 2))" + elif [ "$maintainers_count" -eq 1 ]; then + local maintainer_info=$(echo "$package_data" | jq -r '.maintainers[0] | if type == "object" then "\(.name // "") <\(.email // "")>" else . end' 2>/dev/null) + add_finding "LOW" "Maintainers" "Single maintainer: ${maintainer_info}" "$WEIGHT_SINGLE_MAINTAINER" + fi + + # Check each maintainer email + local emails=$(echo "$package_data" | jq -r '.maintainers[]? | if type == "object" then .email // empty else empty end' 2>/dev/null) + + while IFS= read -r email; do + [ -z "$email" ] && continue + + local domain=$(lib_extract_email_domain "$email") + [ -z "$domain" ] && continue + + # Get cached domain result + local domain_result=$(lib_check_domain "$domain") + + if [ -n "$domain_result" ]; then + # Check for disposable + if [[ "$domain_result" == *"disposable"* ]]; then + add_finding "HIGH" "Disposable Email" "Maintainer uses disposable email domain: ${email}" "$WEIGHT_DISPOSABLE_EMAIL" + fi + + # Check for privacy relay + if [[ "$domain_result" == *"privacy-relay"* ]]; then + add_finding "LOW" "Privacy Relay Email" "Maintainer uses privacy relay: ${email}" "$WEIGHT_PRIVACY_RELAY_EMAIL" + fi + + # Check for trusted domains (positive) + if [[ "$domain_result" == *"trusted:top1k"* ]]; then + add_finding "INFO" "Trusted Domain" "Maintainer email from top 1k domain: ${domain}" "$WEIGHT_TRUSTED_TOP1K" + elif [[ "$domain_result" == *"trusted:top10k"* ]]; then + add_finding "INFO" "Trusted Domain" "Maintainer email from top 10k domain: ${domain}" "$WEIGHT_TRUSTED_TOP10K" + elif [[ "$domain_result" == *"trusted:top100k"* ]]; then + add_finding "INFO" "Trusted Domain" "Maintainer email from top 100k domain: ${domain}" "$WEIGHT_TRUSTED_TOP100K" + fi + fi + + # Check for suspicious patterns in full email + if lib_has_suspicious_email_pattern "$email"; then + add_finding "MEDIUM" "Suspicious Email" "Suspicious email pattern detected: ${email}" "$WEIGHT_SUSPICIOUS_EMAIL_PATTERN" + fi + + # Check for known attacker patterns + if lib_check_attacker_pattern "$email"; then + add_finding "CRITICAL" "Attacker Pattern" "Maintainer email matches known attacker pattern: ${email}" "$WEIGHT_ATTACKER_PATTERN" + fi + done <<< "$emails" + + # Check usernames for attacker patterns + local usernames=$(echo "$package_data" | jq -r '.maintainers[]? | if type == "object" then .name // empty else . end' 2>/dev/null) + + while IFS= read -r username; do + [ -z "$username" ] && continue + if lib_check_attacker_pattern "$username"; then + add_finding "CRITICAL" "Attacker Pattern" "Maintainer username matches known attacker pattern: ${username}" "$WEIGHT_ATTACKER_PATTERN" + fi + done <<< "$usernames" +} + +check_downloads() { + local weekly_downloads + + # Use npm view to get download stats + weekly_downloads=$(npm view "$PACKAGE_NAME" --json 2>/dev/null | jq -r '.dist.unpackedSize // empty' 2>/dev/null) + + # Try API endpoint for download counts + local downloads_response=$(curl -sL "https://api.npmjs.org/downloads/point/last-week/${PACKAGE_NAME}" 2>/dev/null) + weekly_downloads=$(echo "$downloads_response" | jq -r '.downloads // 0' 2>/dev/null) + + if [ -n "$weekly_downloads" ] && [ "$weekly_downloads" -lt 100 ] 2>/dev/null; then + add_finding "MEDIUM" "Downloads" "Low weekly downloads: ${weekly_downloads}" "$WEIGHT_LOW_DOWNLOADS" + fi +} + +check_dependencies() { + local package_data="$1" + + local latest_version=$(echo "$package_data" | jq -r '.["dist-tags"].latest // empty' 2>/dev/null) + if [ -z "$latest_version" ]; then + return + fi + + # Get all dependencies from the latest version + local deps=$(echo "$package_data" | jq -r --arg v "$latest_version" ' + .versions[$v] | + ((.dependencies // {}) + (.devDependencies // {}) + (.peerDependencies // {})) | + to_entries[] | + "\(.key)=\(.value)" + ' 2>/dev/null) + + while IFS='=' read -r name version; do + [ -z "$name" ] && continue + + # Check for HTTP/HTTPS URL dependencies + if [[ "$version" =~ ^https?:// ]]; then + local has_malicious_domain=false + local has_malicious_ip=false + + for domain in "${KNOWN_MALICIOUS_DOMAINS[@]}"; do + if [[ "$version" == *"$domain"* ]]; then + has_malicious_domain=true + break + fi + done + + for ip in "${KNOWN_MALICIOUS_IPS[@]}"; do + if [[ "$version" == *"$ip"* ]]; then + has_malicious_ip=true + break + fi + done + + if [ "$has_malicious_domain" = true ]; then + add_finding "CRITICAL" "Malicious Dependency" "Known malicious domain in dependency: ${name} -> ${version}" "$WEIGHT_MALICIOUS_DOMAIN" + elif [ "$has_malicious_ip" = true ]; then + add_finding "CRITICAL" "Malicious IP" "Known malicious IP in dependency: ${name} -> ${version}" "$WEIGHT_MALICIOUS_IP" + else + add_finding "HIGH" "HTTP Dependency" "External HTTP dependency: ${name} -> ${version}" "$WEIGHT_HTTP_DEPENDENCY" + fi + fi + done <<< "$deps" +} + +check_scripts() { + local package_data="$1" + + local latest_version=$(echo "$package_data" | jq -r '.["dist-tags"].latest // empty' 2>/dev/null) + if [ -z "$latest_version" ]; then + return + fi + + # Get scripts from the latest version + local dangerous_scripts=("preinstall" "install" "postinstall") + local found_scripts="" + + for script_name in "${dangerous_scripts[@]}"; do + local script_content=$(echo "$package_data" | jq -r --arg v "$latest_version" --arg s "$script_name" '.versions[$v].scripts[$s] // empty' 2>/dev/null) + if [ -n "$script_content" ]; then + if [ -n "$found_scripts" ]; then + found_scripts="${found_scripts}\n " + fi + found_scripts="${found_scripts}${script_name}: ${script_content}" + fi + done + + if [ -n "$found_scripts" ]; then + add_finding "MEDIUM" "Install Scripts" "Package has lifecycle scripts that run automatically:\n ${found_scripts}" "$WEIGHT_INSTALL_SCRIPTS" + fi +} + +check_typosquatting() { + for popular in "${TYPOSQUATTING_TARGETS[@]}"; do + # Skip if it's the same package + [ "$PACKAGE_NAME" = "$popular" ] && continue + + local distance=$(levenshtein_distance "$PACKAGE_NAME" "$popular") + + if [ "$distance" -gt 0 ] && [ "$distance" -le 2 ]; then + add_finding "HIGH" "Typosquatting" "Package name is similar to popular package \"${popular}\" (edit distance: ${distance})" "$WEIGHT_TYPOSQUATTING" + fi + done +} + +# ============================================================================ +# RISK ASSESSMENT +# ============================================================================ + +get_risk_level() { + local score="$1" + + if [ "$score" -ge 80 ]; then + echo "CRITICAL" + elif [ "$score" -ge 50 ]; then + echo "HIGH" + elif [ "$score" -ge 25 ]; then + echo "MEDIUM" + elif [ "$score" -ge 10 ]; then + echo "LOW" + else + echo "SAFE" + fi +} + +get_risk_color() { + local level="$1" + + case "$level" in + CRITICAL|HIGH) echo "$RED" ;; + MEDIUM|LOW) echo "$YELLOW" ;; + SAFE) echo "$GREEN" ;; + *) echo "$NC" ;; + esac +} + +# ============================================================================ +# OUTPUT +# ============================================================================ + +print_report() { + local risk_level=$(get_risk_level "$RISK_SCORE") + local risk_color=$(get_risk_color "$risk_level") + + echo "" + echo "===============================================" + echo "Package Security Validation Report" + echo "===============================================" + echo "" + echo "Package: $PACKAGE_NAME" + echo "Risk Score: $RISK_SCORE" + echo -e "Risk Level: ${risk_color}${risk_level}${NC}" + + if [ ${#FINDINGS[@]} -eq 0 ]; then + echo "" + echo -e "${GREEN}No security concerns detected!${NC}" + echo "" + return + fi + + echo "" + echo "Findings:" + echo "" + + # Group and display findings by severity + for severity in CRITICAL HIGH MEDIUM LOW INFO; do + for finding in "${FINDINGS[@]}"; do + IFS='|' read -r f_severity f_category f_message f_points <<< "$finding" + if [ "$f_severity" = "$severity" ]; then + local color + case "$severity" in + CRITICAL|HIGH) color="$RED" ;; + MEDIUM) color="$YELLOW" ;; + LOW) color="$BLUE" ;; + INFO) color="$GREEN" ;; + *) color="$NC" ;; + esac + + echo -e " ${color}[${f_severity}] ${f_category}${NC}" + echo -e " ${f_message}" + if [ "$f_points" -ge 0 ]; then + echo " Risk points: +${f_points}" + else + echo " Risk points: ${f_points}" + fi + echo "" + fi + done + done + + echo "===============================================" + echo "Recommendation" + echo "===============================================" + echo "" + + case "$risk_level" in + CRITICAL) + echo -e "${RED}DO NOT INSTALL THIS PACKAGE${NC}" + echo "This package shows signs of malicious behavior." + ;; + HIGH) + echo -e "${RED}HIGH RISK - Exercise extreme caution${NC}" + echo "Carefully review the package source code before installing." + ;; + MEDIUM) + echo -e "${YELLOW}MEDIUM RISK - Proceed with caution${NC}" + echo "Review the findings and verify the package legitimacy." + ;; + LOW) + echo -e "${YELLOW}LOW RISK - Some concerns noted${NC}" + echo "Package appears mostly safe but review the findings." + ;; + SAFE) + echo -e "${GREEN}Package appears safe to use${NC}" + echo "No significant security concerns detected." + ;; + esac + echo "" +} + +print_json_report() { + local risk_level=$(get_risk_level "$RISK_SCORE") + + # Build findings JSON array + local findings_json="[" + local first=true + for finding in "${FINDINGS[@]}"; do + IFS='|' read -r f_severity f_category f_message f_points <<< "$finding" + if [ "$first" = false ]; then + findings_json+="," + fi + first=false + + # Escape special characters in message + f_message=$(echo "$f_message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g') + + findings_json+=$(cat <