|
#!/bin/bash |
|
|
|
# Shai-Hulud "Second Coming" Checker |
|
# Analyzes package.json and lockfiles to detect vulnerable packages |
|
|
|
set -e |
|
|
|
AFFECTED_URL="https://raw.githubusercontent.com/tenable/shai-hulud-second-coming-affected-packages/refs/heads/main/list.json" |
|
DATADOG_IOC_URL="https://raw.githubusercontent.com/DataDog/indicators-of-compromise/refs/heads/main/shai-hulud-2.0/consolidated_iocs.csv" |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
NC='\033[0m' # No Color |
|
|
|
echo "π Downloading Tenable Shai-Hulud 'Second Coming' list..." |
|
|
|
# Download vulnerable packages list |
|
if ! VULN_LIST=$(curl -sS "$AFFECTED_URL"); then |
|
echo "β Error: Unable to download vulnerable packages list" |
|
exit 1 |
|
fi |
|
|
|
# Check that jq is installed |
|
if ! command -v jq &> /dev/null; then |
|
echo "β Error: 'jq' must be installed to run this script" |
|
echo " Installation: brew install jq (macOS) or apt-get install jq (Linux)" |
|
exit 1 |
|
fi |
|
|
|
PACKAGES_COUNT=$(echo "$VULN_LIST" | jq 'length') |
|
echo "β‘οΈ $PACKAGES_COUNT packages present in Tenable list.json" |
|
|
|
echo "" |
|
echo "π Downloading Datadog Indicators of Compromise list..." |
|
|
|
# Download Datadog IOC CSV |
|
if ! DATADOG_CSV=$(curl -sS "$DATADOG_IOC_URL"); then |
|
echo "β οΈ Warning: Unable to download Datadog IOC list (continuing with Tenable list only)" |
|
DATADOG_CSV="" |
|
else |
|
DATADOG_PACKAGES_COUNT=$(echo "$DATADOG_CSV" | tail -n +2 | wc -l | tr -d ' ') |
|
echo "β‘οΈ $DATADOG_PACKAGES_COUNT packages present in Datadog consolidated_iocs.csv" |
|
fi |
|
echo "" |
|
|
|
FOUND_VULNERABLE=0 |
|
VULNERABLE_PACKAGES=() |
|
|
|
# Function to extract package name from a lockfile line |
|
extract_package_name() { |
|
local line="$1" |
|
# For yarn.lock and npm (format: "package@version" or "@scope/package@version") |
|
if [[ "$line" =~ ^\"?(@?[^@\"]+)@.*\"?:?$ ]]; then |
|
echo "${BASH_REMATCH[1]}" |
|
fi |
|
} |
|
|
|
# Function to check if a package+version is vulnerable |
|
check_vulnerability() { |
|
local package_name="$1" |
|
local version="$2" |
|
local source="$3" |
|
local found_in="" |
|
|
|
# Check in Tenable list |
|
local vuln_versions=$(echo "$VULN_LIST" | jq -r --arg pkg "$package_name" '.[$pkg].vuln_vers // [] | .[]') |
|
|
|
if [ -n "$vuln_versions" ]; then |
|
while IFS= read -r vuln_ver; do |
|
if [ "$version" = "$vuln_ver" ]; then |
|
found_in="Tenable" |
|
break |
|
fi |
|
done <<< "$vuln_versions" |
|
fi |
|
|
|
# Check in Datadog CSV if not already found and CSV is available |
|
if [ -z "$found_in" ] && [ -n "$DATADOG_CSV" ]; then |
|
# CSV format: package_name,package_versions,sources |
|
# Search for exact package name and version match |
|
if echo "$DATADOG_CSV" | grep -q "^${package_name},${version},"; then |
|
found_in="Datadog" |
|
fi |
|
fi |
|
|
|
if [ -n "$found_in" ]; then |
|
echo -e "${RED}β οΈ [$source] $package_name@$version (vulnerable - source: $found_in)${NC}" |
|
FOUND_VULNERABLE=1 |
|
VULNERABLE_PACKAGES+=("$source|$package_name@$version") |
|
return 0 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
# Function to analyze a package-lock.json file |
|
analyze_package_lock() { |
|
local lockfile="$1" |
|
local lockdir=$(dirname "$lockfile") |
|
|
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${GREEN}π Folder: $lockdir${NC}" |
|
echo -e "${GREEN}π File: $lockfile${NC}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
echo "π¦ Analyzing $lockfile..." |
|
|
|
PACKAGES=$(jq -r ' |
|
((.dependencies // {}) | to_entries[] | "\(.key)@\(.value.version)"), |
|
((.packages // {}) | to_entries[] | |
|
select(.key | startswith("node_modules/")) | |
|
"\(.key | split("node_modules/")[-1])@\(.value.version)") |
|
' "$lockfile" 2>/dev/null | sort -u) |
|
|
|
if [ -n "$PACKAGES" ]; then |
|
while IFS= read -r pkg_line; do |
|
if [[ "$pkg_line" =~ ^(.+)@(.+)$ ]]; then |
|
pkg_name="${BASH_REMATCH[1]}" |
|
pkg_version="${BASH_REMATCH[2]}" |
|
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true |
|
fi |
|
done <<< "$PACKAGES" |
|
fi |
|
echo "" |
|
} |
|
|
|
# Function to analyze a yarn.lock file |
|
analyze_yarn_lock() { |
|
local lockfile="$1" |
|
local lockdir=$(dirname "$lockfile") |
|
|
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${GREEN}π Folder: $lockdir${NC}" |
|
echo -e "${GREEN}π File: $lockfile${NC}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
echo "π¦ Analyzing $lockfile..." |
|
|
|
current_package="" |
|
while IFS= read -r line; do |
|
if [[ "$line" =~ ^[^\s].*:$ ]] && [[ ! "$line" =~ ^[[:space:]] ]]; then |
|
pkg_line=$(echo "$line" | sed 's/:$//' | tr -d '"') |
|
if [[ "$pkg_line" =~ ^(@[^@]+)@.*$ ]]; then |
|
current_package="${BASH_REMATCH[1]}" |
|
elif [[ "$pkg_line" =~ ^([^@]+)@.*$ ]]; then |
|
current_package="${BASH_REMATCH[1]}" |
|
fi |
|
elif [[ "$line" =~ ^[[:space:]]+version[[:space:]]+\"?([^\"]+)\"?$ ]] && [ -n "$current_package" ]; then |
|
pkg_version="${BASH_REMATCH[1]}" |
|
check_vulnerability "$current_package" "$pkg_version" "$lockfile" || true |
|
current_package="" |
|
fi |
|
done < "$lockfile" |
|
echo "" |
|
} |
|
|
|
# Function to analyze a pnpm-lock.yaml file |
|
analyze_pnpm_lock() { |
|
local lockfile="$1" |
|
local lockdir=$(dirname "$lockfile") |
|
|
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${GREEN}π Folder: $lockdir${NC}" |
|
echo -e "${GREEN}π File: $lockfile${NC}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
echo "π¦ Analyzing $lockfile..." |
|
|
|
if command -v yq &> /dev/null; then |
|
PACKAGES=$(yq eval '.packages | keys | .[]' "$lockfile" 2>/dev/null | grep '^/' | sed 's|^/||') |
|
|
|
while IFS= read -r pkg_line; do |
|
if [[ "$pkg_line" =~ ^(@?[^@]+)@(.+)$ ]]; then |
|
pkg_name="${BASH_REMATCH[1]}" |
|
pkg_version="${BASH_REMATCH[2]}" |
|
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true |
|
fi |
|
done <<< "$PACKAGES" |
|
else |
|
# Fallback: parsing basique avec grep - avoid pipe to preserve array |
|
PACKAGES=$(grep -E "^\s+/" "$lockfile" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/:$//' | sed 's|^/||') |
|
|
|
while IFS= read -r pkg_line; do |
|
if [[ "$pkg_line" =~ ^(@?[^@]+)@(.+)$ ]]; then |
|
pkg_name="${BASH_REMATCH[1]}" |
|
pkg_version="${BASH_REMATCH[2]}" |
|
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true |
|
fi |
|
done <<< "$PACKAGES" |
|
fi |
|
echo "" |
|
} |
|
|
|
# Function to analyze a bun.lock file (JSON format) |
|
analyze_bun_lock() { |
|
local lockfile="$1" |
|
local lockdir=$(dirname "$lockfile") |
|
|
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${GREEN}π Folder: $lockdir${NC}" |
|
echo -e "${GREEN}π File: $lockfile${NC}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
echo "π¦ Analyzing $lockfile..." |
|
|
|
# Bun lockfiles are JSON format but may have trailing commas |
|
# Use grep to extract package@version strings directly (more robust than jq for malformed JSON) |
|
PACKAGES=$(grep -E '^\s*"@?[^"]+@[^"]+",?$' "$lockfile" | sed 's/[",]//g' | sed 's/^[[:space:]]*//') |
|
|
|
if [ -n "$PACKAGES" ]; then |
|
while IFS= read -r pkg_line; do |
|
if [[ "$pkg_line" =~ ^(.+)@(.+)$ ]]; then |
|
pkg_name="${BASH_REMATCH[1]}" |
|
pkg_version="${BASH_REMATCH[2]}" |
|
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true |
|
fi |
|
done <<< "$PACKAGES" |
|
fi |
|
echo "" |
|
} |
|
|
|
# Search for all lockfiles in the project |
|
echo "π Searching for all lockfiles in the project (respecting .gitignore)..." |
|
|
|
TEMP_LOCKFILES=$(find . \( -name "package-lock.json" -o -name "npm-shrinkwrap.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" -o -name "bun.lock" \) -type f ! -path "*/node_modules/*" ! -path "*/.yarn/*" ! -path "*/.git/*") |
|
|
|
# Filter using git check-ignore if in a git repo |
|
if git rev-parse --git-dir > /dev/null 2>&1; then |
|
LOCKFILES="" |
|
while IFS= read -r file; do |
|
if ! git check-ignore -q "$file" 2>/dev/null; then |
|
if [ -z "$LOCKFILES" ]; then |
|
LOCKFILES="$file" |
|
else |
|
LOCKFILES="$LOCKFILES |
|
$file" |
|
fi |
|
fi |
|
done <<< "$TEMP_LOCKFILES" |
|
else |
|
LOCKFILES="$TEMP_LOCKFILES" |
|
fi |
|
|
|
if [ -z "$LOCKFILES" ]; then |
|
echo "β No lockfiles found" |
|
else |
|
LOCKFILE_COUNT=$(echo "$LOCKFILES" | wc -l | tr -d ' ') |
|
echo "β
$LOCKFILE_COUNT lockfile(s) found" |
|
echo "" |
|
|
|
# Display list of all found lockfiles |
|
echo "π List of found lockfiles:" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
while IFS= read -r lockfile; do |
|
lockdir=$(dirname "$lockfile") |
|
lockname=$(basename "$lockfile") |
|
echo " π $lockdir/" |
|
echo " ββ $lockname" |
|
done <<< "$LOCKFILES" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
|
|
# Analyze each lockfile |
|
echo "π Starting lockfiles analysis..." |
|
echo "" |
|
|
|
while IFS= read -r lockfile; do |
|
lockname=$(basename "$lockfile") |
|
|
|
case "$lockname" in |
|
"package-lock.json"|"npm-shrinkwrap.json") |
|
analyze_package_lock "$lockfile" |
|
;; |
|
"yarn.lock") |
|
analyze_yarn_lock "$lockfile" |
|
;; |
|
"pnpm-lock.yaml") |
|
analyze_pnpm_lock "$lockfile" |
|
;; |
|
"bun.lock") |
|
analyze_bun_lock "$lockfile" |
|
;; |
|
esac |
|
done <<< "$LOCKFILES" |
|
fi |
|
|
|
echo "" |
|
|
|
# Search for all package.json files in the project |
|
echo "π Searching for all package.json files in the project (respecting .gitignore)..." |
|
|
|
# Find all package.json and filter those in .gitignore |
|
TEMP_FILES=$(find . -name "package.json" -type f ! -path "*/node_modules/*" ! -path "*/.yarn/*" ! -path "*/.git/*") |
|
|
|
# Filter using git check-ignore if in a git repo |
|
if git rev-parse --git-dir > /dev/null 2>&1; then |
|
PACKAGE_JSON_FILES="" |
|
while IFS= read -r file; do |
|
if ! git check-ignore -q "$file" 2>/dev/null; then |
|
if [ -z "$PACKAGE_JSON_FILES" ]; then |
|
PACKAGE_JSON_FILES="$file" |
|
else |
|
PACKAGE_JSON_FILES="$PACKAGE_JSON_FILES |
|
$file" |
|
fi |
|
fi |
|
done <<< "$TEMP_FILES" |
|
else |
|
# If not in a git repo, use the complete list |
|
PACKAGE_JSON_FILES="$TEMP_FILES" |
|
fi |
|
|
|
if [ -z "$PACKAGE_JSON_FILES" ]; then |
|
echo "β No package.json files found" |
|
else |
|
PACKAGE_COUNT=$(echo "$PACKAGE_JSON_FILES" | wc -l | tr -d ' ') |
|
echo "β
$PACKAGE_COUNT package.json file(s) found" |
|
echo "" |
|
|
|
# Display list of all found package.json files |
|
echo "π List of found package.json files:" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
while IFS= read -r package_file; do |
|
package_dir=$(dirname "$package_file") |
|
echo " π $package_dir/" |
|
echo " ββ package.json" |
|
done <<< "$PACKAGE_JSON_FILES" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
|
|
# Analyze each found package.json |
|
echo "π Starting dependencies analysis..." |
|
echo "" |
|
|
|
while IFS= read -r package_file; do |
|
# Determine the package.json folder |
|
package_dir=$(dirname "$package_file") |
|
|
|
# Display folder being analyzed |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${GREEN}π Folder being analyzed: $package_dir${NC}" |
|
echo -e "${GREEN}π File: $package_file${NC}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "" |
|
|
|
echo "π¦ Analyzing declared dependencies in $package_file..." |
|
|
|
# Extract all dependencies |
|
ALL_DEPS=$(jq -r ' |
|
[ |
|
(.dependencies // {} | keys[]), |
|
(.devDependencies // {} | keys[]), |
|
(.optionalDependencies // {} | keys[]), |
|
(.peerDependencies // {} | keys[]) |
|
] | unique | .[] |
|
' "$package_file" 2>/dev/null) |
|
|
|
if [ -n "$ALL_DEPS" ]; then |
|
while IFS= read -r pkg_name; do |
|
found_source="" |
|
vuln_versions="" |
|
|
|
# Check if this package is in Tenable's list |
|
if echo "$VULN_LIST" | jq -e --arg pkg "$pkg_name" '.[$pkg]' > /dev/null 2>&1; then |
|
vuln_versions=$(echo "$VULN_LIST" | jq -r --arg pkg "$pkg_name" '.[$pkg].vuln_vers | join(", ")') |
|
found_source="Tenable" |
|
fi |
|
|
|
# Check if this package is in Datadog's list |
|
if [ -n "$DATADOG_CSV" ]; then |
|
datadog_match=$(echo "$DATADOG_CSV" | grep "^${pkg_name}," | head -1) |
|
if [ -n "$datadog_match" ]; then |
|
datadog_version=$(echo "$datadog_match" | cut -d',' -f2) |
|
if [ -z "$found_source" ]; then |
|
found_source="Datadog" |
|
vuln_versions="$datadog_version" |
|
else |
|
found_source="Tenable+Datadog" |
|
vuln_versions="$vuln_versions (Datadog: $datadog_version)" |
|
fi |
|
fi |
|
fi |
|
|
|
if [ -n "$found_source" ]; then |
|
echo -e "${YELLOW}β οΈ [$package_file] $pkg_name is in the list - source: $found_source (vuln. versions: $vuln_versions)${NC}" |
|
echo " β Check your lockfile for the exact installed version" |
|
FOUND_VULNERABLE=1 |
|
fi |
|
done <<< "$ALL_DEPS" |
|
else |
|
echo " βΉοΈ No dependencies found in this package.json" |
|
fi |
|
echo "" |
|
done <<< "$PACKAGE_JSON_FILES" |
|
fi |
|
|
|
echo "" |
|
echo "=============================" |
|
if [ $FOUND_VULNERABLE -eq 0 ]; then |
|
echo -e "${GREEN}β
No vulnerable packages detected${NC}" |
|
else |
|
echo -e "${RED}β οΈ WARNING: Vulnerable packages have been detected${NC}" |
|
echo "" |
|
echo "Vulnerable packages found:" |
|
echo "" |
|
|
|
# Display vulnerabilities grouped by file (simple approach without associative arrays) |
|
current_file="" |
|
for vuln in "${VULNERABLE_PACKAGES[@]}"; do |
|
IFS='|' read -r file pkg <<< "$vuln" |
|
if [ "$file" != "$current_file" ]; then |
|
if [ -n "$current_file" ]; then |
|
echo "" |
|
fi |
|
echo -e "${RED}π File: $file${NC}" |
|
current_file="$file" |
|
fi |
|
echo -e "${RED} ββ $pkg${NC}" |
|
done |
|
echo "" |
|
|
|
echo -e "${YELLOW}Recommendations:${NC}" |
|
echo " - Update to versions not listed in Tenable's database" |
|
echo " - Also check your CI/CD pipeline and generated artifacts" |
|
fi |
|
echo "=============================" |
|
echo "" |
|
|
|
exit $FOUND_VULNERABLE |