388 lines
10 KiB
Bash
Executable File
388 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# Purpose: Analyze project dependencies for security, versioning, and usage
|
|
# Version: 1.0.0
|
|
# Usage: ./analyze-dependencies.sh [path]
|
|
# Returns: JSON formatted dependency analysis
|
|
# Exit codes: 0=success, 1=error, 2=invalid input
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly PROJECT_DIR="${1:-.}"
|
|
readonly OUTPUT_FORMAT="${2:-json}"
|
|
|
|
# Color codes for output
|
|
readonly RED='\033[0;31m'
|
|
readonly YELLOW='\033[1;33m'
|
|
readonly GREEN='\033[0;32m'
|
|
readonly NC='\033[0m' # No Color
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $*" >&2
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $*" >&2
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
|
}
|
|
|
|
# Validate input
|
|
validate_input() {
|
|
if [[ ! -d "$PROJECT_DIR" ]]; then
|
|
log_error "Directory not found: $PROJECT_DIR"
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
# Detect package manager and dependency files
|
|
detect_package_manager() {
|
|
local pkg_manager=""
|
|
local dep_file=""
|
|
|
|
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
|
pkg_manager="npm"
|
|
dep_file="package.json"
|
|
elif [[ -f "$PROJECT_DIR/requirements.txt" ]]; then
|
|
pkg_manager="pip"
|
|
dep_file="requirements.txt"
|
|
elif [[ -f "$PROJECT_DIR/Pipfile" ]]; then
|
|
pkg_manager="pipenv"
|
|
dep_file="Pipfile"
|
|
elif [[ -f "$PROJECT_DIR/pyproject.toml" ]]; then
|
|
pkg_manager="poetry"
|
|
dep_file="pyproject.toml"
|
|
elif [[ -f "$PROJECT_DIR/Gemfile" ]]; then
|
|
pkg_manager="bundler"
|
|
dep_file="Gemfile"
|
|
elif [[ -f "$PROJECT_DIR/go.mod" ]]; then
|
|
pkg_manager="go"
|
|
dep_file="go.mod"
|
|
elif [[ -f "$PROJECT_DIR/Cargo.toml" ]]; then
|
|
pkg_manager="cargo"
|
|
dep_file="Cargo.toml"
|
|
elif [[ -f "$PROJECT_DIR/composer.json" ]]; then
|
|
pkg_manager="composer"
|
|
dep_file="composer.json"
|
|
else
|
|
log_warn "No recognized dependency file found"
|
|
pkg_manager="unknown"
|
|
dep_file="none"
|
|
fi
|
|
|
|
echo "$pkg_manager|$dep_file"
|
|
}
|
|
|
|
# Count dependencies
|
|
count_dependencies() {
|
|
local pkg_manager="$1"
|
|
local dep_file="$2"
|
|
local direct_count=0
|
|
local dev_count=0
|
|
|
|
case "$pkg_manager" in
|
|
npm)
|
|
if command -v jq &> /dev/null; then
|
|
direct_count=$(jq -r '.dependencies // {} | length' "$PROJECT_DIR/$dep_file" 2>/dev/null || echo 0)
|
|
dev_count=$(jq -r '.devDependencies // {} | length' "$PROJECT_DIR/$dep_file" 2>/dev/null || echo 0)
|
|
else
|
|
direct_count=$(grep -c '"' "$PROJECT_DIR/$dep_file" 2>/dev/null || echo 0)
|
|
fi
|
|
;;
|
|
pip)
|
|
direct_count=$(grep -v '^#' "$PROJECT_DIR/$dep_file" 2>/dev/null | grep -c . || echo 0)
|
|
;;
|
|
go)
|
|
direct_count=$(grep -c 'require' "$PROJECT_DIR/$dep_file" 2>/dev/null || echo 0)
|
|
;;
|
|
*)
|
|
direct_count=0
|
|
;;
|
|
esac
|
|
|
|
echo "$direct_count|$dev_count"
|
|
}
|
|
|
|
# Check for outdated dependencies (simplified - would need package manager specific commands)
|
|
check_outdated() {
|
|
local pkg_manager="$1"
|
|
local outdated_count=0
|
|
|
|
# This is a simplified check - in practice would run actual package manager commands
|
|
case "$pkg_manager" in
|
|
npm)
|
|
if command -v npm &> /dev/null && [[ -f "$PROJECT_DIR/package-lock.json" ]]; then
|
|
log_info "Checking for outdated npm packages..."
|
|
# Would run: npm outdated --json in production
|
|
outdated_count=0 # Placeholder
|
|
fi
|
|
;;
|
|
pip)
|
|
if command -v pip &> /dev/null; then
|
|
log_info "Checking for outdated pip packages..."
|
|
# Would run: pip list --outdated in production
|
|
outdated_count=0 # Placeholder
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "$outdated_count"
|
|
}
|
|
|
|
# Check for security vulnerabilities (simplified)
|
|
check_vulnerabilities() {
|
|
local pkg_manager="$1"
|
|
local vuln_count=0
|
|
local critical=0
|
|
local high=0
|
|
local medium=0
|
|
local low=0
|
|
|
|
# This would integrate with actual security scanners
|
|
case "$pkg_manager" in
|
|
npm)
|
|
if command -v npm &> /dev/null && [[ -f "$PROJECT_DIR/package-lock.json" ]]; then
|
|
log_info "Checking for npm security vulnerabilities..."
|
|
# Would run: npm audit --json in production
|
|
vuln_count=0 # Placeholder
|
|
fi
|
|
;;
|
|
pip)
|
|
if command -v safety &> /dev/null; then
|
|
log_info "Checking for Python security vulnerabilities..."
|
|
# Would run: safety check in production
|
|
vuln_count=0 # Placeholder
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "$critical|$high|$medium|$low"
|
|
}
|
|
|
|
# Analyze dependency tree depth (simplified)
|
|
analyze_tree_depth() {
|
|
local pkg_manager="$1"
|
|
local max_depth=0
|
|
|
|
case "$pkg_manager" in
|
|
npm)
|
|
if [[ -f "$PROJECT_DIR/package-lock.json" ]]; then
|
|
# Simplified depth calculation
|
|
max_depth=3 # Placeholder - would calculate from lockfile
|
|
fi
|
|
;;
|
|
*)
|
|
max_depth=0
|
|
;;
|
|
esac
|
|
|
|
echo "$max_depth"
|
|
}
|
|
|
|
# Find unused dependencies (simplified)
|
|
find_unused() {
|
|
local pkg_manager="$1"
|
|
local unused_count=0
|
|
|
|
# This would require code analysis to see what's actually imported/required
|
|
case "$pkg_manager" in
|
|
npm)
|
|
log_info "Analyzing for unused npm packages..."
|
|
# Would use tools like depcheck in production
|
|
unused_count=0 # Placeholder
|
|
;;
|
|
esac
|
|
|
|
echo "$unused_count"
|
|
}
|
|
|
|
# Check for duplicate dependencies
|
|
check_duplicates() {
|
|
local pkg_manager="$1"
|
|
local duplicate_count=0
|
|
|
|
case "$pkg_manager" in
|
|
npm)
|
|
if [[ -f "$PROJECT_DIR/package-lock.json" ]]; then
|
|
log_info "Checking for duplicate packages..."
|
|
# Would analyze lockfile for version conflicts
|
|
duplicate_count=0 # Placeholder
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "$duplicate_count"
|
|
}
|
|
|
|
# Generate dependency analysis report
|
|
generate_report() {
|
|
local pkg_manager="$1"
|
|
local dep_file="$2"
|
|
local dep_counts="$3"
|
|
local outdated="$4"
|
|
local vulnerabilities="$5"
|
|
local tree_depth="$6"
|
|
local unused="$7"
|
|
local duplicates="$8"
|
|
|
|
IFS='|' read -r direct_deps dev_deps <<< "$dep_counts"
|
|
IFS='|' read -r crit_vulns high_vulns med_vulns low_vulns <<< "$vulnerabilities"
|
|
|
|
local total_deps=$((direct_deps + dev_deps))
|
|
local total_vulns=$((crit_vulns + high_vulns + med_vulns + low_vulns))
|
|
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
cat <<EOF
|
|
{
|
|
"package_manager": "$pkg_manager",
|
|
"dependency_file": "$dep_file",
|
|
"analysis_date": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
|
"dependencies": {
|
|
"total": $total_deps,
|
|
"direct": $direct_deps,
|
|
"development": $dev_deps,
|
|
"outdated": $outdated,
|
|
"unused": $unused,
|
|
"duplicates": $duplicates
|
|
},
|
|
"vulnerabilities": {
|
|
"total": $total_vulns,
|
|
"critical": $crit_vulns,
|
|
"high": $high_vulns,
|
|
"medium": $med_vulns,
|
|
"low": $low_vulns
|
|
},
|
|
"tree_depth": $tree_depth,
|
|
"health_score": $(calculate_health_score "$total_vulns" "$outdated" "$unused" "$duplicates"),
|
|
"recommendations": $(generate_recommendations "$total_vulns" "$outdated" "$unused" "$duplicates")
|
|
}
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
|
|
==============================================
|
|
Dependency Analysis Report
|
|
==============================================
|
|
|
|
Package Manager: $pkg_manager
|
|
Dependency File: $dep_file
|
|
Analysis Date: $(date)
|
|
|
|
Dependencies:
|
|
Total Dependencies: $total_deps
|
|
Direct Dependencies: $direct_deps
|
|
Development Dependencies: $dev_deps
|
|
Outdated: $outdated
|
|
Unused: $unused
|
|
Duplicates: $duplicates
|
|
|
|
Security Vulnerabilities:
|
|
Total: $total_vulns
|
|
Critical: $crit_vulns
|
|
High: $high_vulns
|
|
Medium: $med_vulns
|
|
Low: $low_vulns
|
|
|
|
Dependency Tree:
|
|
Maximum Depth: $tree_depth
|
|
|
|
Health Score: $(calculate_health_score "$total_vulns" "$outdated" "$unused" "$duplicates")/10
|
|
|
|
==============================================
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
# Calculate health score (0-10)
|
|
calculate_health_score() {
|
|
local vulns="$1"
|
|
local outdated="$2"
|
|
local unused="$3"
|
|
local duplicates="$4"
|
|
|
|
local score=10
|
|
|
|
# Deduct points for issues
|
|
score=$((score - vulns)) # -1 per vulnerability
|
|
score=$((score - outdated / 5)) # -1 per 5 outdated packages
|
|
score=$((score - unused / 3)) # -1 per 3 unused packages
|
|
score=$((score - duplicates / 2)) # -1 per 2 duplicates
|
|
|
|
# Ensure score is between 0 and 10
|
|
if (( score < 0 )); then
|
|
score=0
|
|
fi
|
|
|
|
echo "$score"
|
|
}
|
|
|
|
# Generate recommendations
|
|
generate_recommendations() {
|
|
local vulns="$1"
|
|
local outdated="$2"
|
|
local unused="$3"
|
|
local duplicates="$4"
|
|
|
|
local recommendations="["
|
|
|
|
if (( vulns > 0 )); then
|
|
recommendations+='{"priority":"critical","action":"Update packages with security vulnerabilities immediately"},'
|
|
fi
|
|
|
|
if (( outdated > 10 )); then
|
|
recommendations+='{"priority":"high","action":"Review and update outdated dependencies"},'
|
|
fi
|
|
|
|
if (( unused > 5 )); then
|
|
recommendations+='{"priority":"medium","action":"Remove unused dependencies to reduce bundle size"},'
|
|
fi
|
|
|
|
if (( duplicates > 0 )); then
|
|
recommendations+='{"priority":"medium","action":"Resolve duplicate dependencies with version conflicts"},'
|
|
fi
|
|
|
|
# Remove trailing comma if exists
|
|
recommendations="${recommendations%,}"
|
|
recommendations+="]"
|
|
|
|
echo "$recommendations"
|
|
}
|
|
|
|
# Main execution
|
|
main() {
|
|
log_info "Starting dependency analysis..."
|
|
|
|
validate_input
|
|
|
|
# Detect package manager
|
|
IFS='|' read -r pkg_manager dep_file <<< "$(detect_package_manager)"
|
|
|
|
if [[ "$pkg_manager" == "unknown" ]]; then
|
|
log_error "Could not detect package manager"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Detected package manager: $pkg_manager"
|
|
|
|
# Gather metrics
|
|
dep_counts=$(count_dependencies "$pkg_manager" "$dep_file")
|
|
outdated=$(check_outdated "$pkg_manager")
|
|
vulnerabilities=$(check_vulnerabilities "$pkg_manager")
|
|
tree_depth=$(analyze_tree_depth "$pkg_manager")
|
|
unused=$(find_unused "$pkg_manager")
|
|
duplicates=$(check_duplicates "$pkg_manager")
|
|
|
|
# Generate report
|
|
generate_report "$pkg_manager" "$dep_file" "$dep_counts" "$outdated" "$vulnerabilities" "$tree_depth" "$unused" "$duplicates"
|
|
|
|
log_info "Analysis complete"
|
|
exit 0
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|