|
# Shai-Hulud Repository Scanner
|
|
# Scans repositories and files for indicators of Shai-Hulud compromise
|
|
# Includes detection for "Shai-Hulud: The Second Coming" (fake Bun runtime attack)
|
|
# Usage: .\shai-hulud.ps1 <directory_to_scan> [-Paranoid] [-SaveLog <file>] [-Parallelism <N>] [-Quiet] [-Verbose] [-Json]
|
|
#
|
|
# For active malware detection (running processes), use: .\shai-hulud-active.ps1
|
|
# Requires: PowerShell 7+ (PowerShell Core)
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true, Position=0)]
|
|
[string]$ScanDir,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$Paranoid,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$SaveLog,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int]$Parallelism = 4,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateSet("Select-String", "sls")]
|
|
[string]$GrepTool = "Select-String",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$Quiet,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$Json
|
|
)
|
|
|
|
# Check PowerShell version
|
|
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
|
Write-Error "ERROR: Shai-Hulud Detector requires PowerShell 7 or newer."
|
|
Write-Error "You appear to be running: $($PSVersionTable.PSVersion)"
|
|
exit 1
|
|
}
|
|
|
|
# Set error handling
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
# Script directory for locating companion files
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
|
|
# Global temp directory for file-based storage
|
|
$TempDir = ""
|
|
|
|
# Global variables for risk tracking
|
|
$script:high_risk = 0
|
|
$script:medium_risk = 0
|
|
|
|
# Global variables for output modes
|
|
$script:OutputMode = "normal" # normal, quiet, verbose, json
|
|
$script:JsonOutput = @{
|
|
timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"
|
|
scan_directory = ""
|
|
findings = @{
|
|
high_risk = @()
|
|
medium_risk = @()
|
|
}
|
|
summary = @{
|
|
high_risk_count = 0
|
|
medium_risk_count = 0
|
|
total_issues = 0
|
|
exit_code = 0
|
|
}
|
|
credential_inventory = @()
|
|
}
|
|
|
|
# Set output mode
|
|
# Note: $VerbosePreference is automatically available from [CmdletBinding()]
|
|
if ($Json) {
|
|
$script:OutputMode = "json"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
} elseif ($Quiet) {
|
|
$script:OutputMode = "quiet"
|
|
} elseif ($VerbosePreference -eq "Continue") {
|
|
$script:OutputMode = "verbose"
|
|
}
|
|
|
|
# Color output functions
|
|
function Write-Status {
|
|
param(
|
|
[string]$Color,
|
|
[string]$Message,
|
|
[switch]$AlwaysShow = $false
|
|
)
|
|
|
|
# Skip output in quiet mode unless AlwaysShow is set
|
|
if ($script:OutputMode -eq "quiet" -and -not $AlwaysShow) {
|
|
return
|
|
}
|
|
|
|
# Skip non-critical output in quiet mode
|
|
if ($script:OutputMode -eq "quiet" -and $Color -in @("BLUE", "ORANGE", "NC")) {
|
|
return
|
|
}
|
|
|
|
# In JSON mode, collect data instead of outputting
|
|
if ($script:OutputMode -eq "json") {
|
|
# JSON output is handled separately
|
|
return
|
|
}
|
|
|
|
$colorMap = @{
|
|
"RED" = "Red"
|
|
"YELLOW" = "Yellow"
|
|
"GREEN" = "Green"
|
|
"BLUE" = "Cyan"
|
|
"ORANGE" = "DarkYellow"
|
|
"NC" = "White"
|
|
}
|
|
$psColor = $colorMap[$Color]
|
|
if (-not $psColor) { $psColor = "White" }
|
|
Write-Host $Message -ForegroundColor $psColor
|
|
}
|
|
|
|
# Known malicious file hashes
|
|
$MALICIOUS_HASHLIST = @(
|
|
"de0e25a3e6c1e1e5998b306b7141b3dc4c0088da9d7bb47c1c00c91e6e4f85d6",
|
|
"81d2a004a1bca6ef87a1caf7d0e0b355ad1764238e40ff6d1b1cb77ad4f595c3",
|
|
"83a650ce44b2a9854802a7fb4c202877815274c129af49e6c2d1d5d5d55c501e",
|
|
"4b2399646573bb737c4969563303d8ee2e9ddbd1b271f1ca9e35ea78062538db",
|
|
"dc67467a39b70d1cd4c1f7f7a459b35058163592f4a9e8fb4dffcbba98ef210c",
|
|
"46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09",
|
|
"b74caeaa75e077c99f7d44f46daaf9796a3be43ecf24f2a1fd381844669da777",
|
|
"86532ed94c5804e1ca32fa67257e1bb9de628e3e48a1f56e67042dc055effb5b",
|
|
"aba1fcbd15c6ba6d9b96e34cec287660fff4a31632bf76f2a766c499f55ca1ee"
|
|
)
|
|
|
|
# Set parallelism based on CPU count
|
|
if ($PSVersionTable.PSVersion.Major -ge 7) {
|
|
$PARALLELISM = [Math]::Min($Parallelism, [Environment]::ProcessorCount)
|
|
} else {
|
|
$PARALLELISM = [Math]::Min($Parallelism, (Get-WmiObject Win32_ComputerSystem).NumberOfLogicalProcessors)
|
|
}
|
|
|
|
# Timing variables
|
|
$script:SCAN_START_TIME = Get-Date
|
|
|
|
function Get-ElapsedTime {
|
|
$elapsed = (Get-Date) - $script:SCAN_START_TIME
|
|
$seconds = [Math]::Floor($elapsed.TotalSeconds)
|
|
$milliseconds = $elapsed.Milliseconds
|
|
return "${seconds}.$($milliseconds.ToString('000'))s"
|
|
}
|
|
|
|
function Write-StageComplete {
|
|
param([string]$StageName)
|
|
$elapsed = Get-ElapsedTime
|
|
$msg = " $StageName completed [$elapsed]"
|
|
# Only show stage completion in verbose or normal mode, not in quiet mode
|
|
if ($script:OutputMode -ne "quiet" -and $script:OutputMode -ne "json") {
|
|
Write-Status "BLUE" $msg
|
|
}
|
|
}
|
|
|
|
# Compromised packages and namespaces
|
|
$COMPROMISED_PACKAGES_MAP = @{}
|
|
$COMPROMISED_NAMESPACES_MAP = @{}
|
|
|
|
$COMPROMISED_NAMESPACES = @(
|
|
"@crowdstrike",
|
|
"@art-ws",
|
|
"@ngx",
|
|
"@ctrl",
|
|
"@nativescript-community",
|
|
"@ahmedhfarag",
|
|
"@operato",
|
|
"@teselagen",
|
|
"@things-factory",
|
|
"@hestjs",
|
|
"@nstudio",
|
|
"@basic-ui-components-stc",
|
|
"@nexe",
|
|
"@thangved",
|
|
"@tnf-dev",
|
|
"@ui-ux-gang",
|
|
"@yoobic"
|
|
)
|
|
|
|
foreach ($ns in $COMPROMISED_NAMESPACES) {
|
|
$COMPROMISED_NAMESPACES_MAP[$ns] = $true
|
|
}
|
|
|
|
function Load-CompromisedPackages {
|
|
$packagesFile = Join-Path $ScriptDir "infected-packages.txt"
|
|
$count = 0
|
|
|
|
if (Test-Path $packagesFile) {
|
|
Get-Content $packagesFile | Where-Object {
|
|
$_ -notmatch '^\s*#' -and $_ -match '^[a-zA-Z@][^:]+:[0-9]+\.[0-9]+\.[0-9]+'
|
|
} | ForEach-Object {
|
|
$pkg = $_.Trim()
|
|
$COMPROMISED_PACKAGES_MAP[$pkg] = $true
|
|
$count++
|
|
}
|
|
$loadMsg = "Loaded $count compromised packages from $packagesFile (O(1) lookup enabled)"
|
|
Write-Status "BLUE" $loadMsg
|
|
} else {
|
|
$warnMsg = "Warning: $packagesFile not found, using embedded package list"
|
|
Write-Status "YELLOW" $warnMsg
|
|
$fallbackPackages = @(
|
|
"@ctrl/tinycolor:4.1.0",
|
|
"@ctrl/tinycolor:4.1.1",
|
|
"@ctrl/tinycolor:4.1.2",
|
|
"@ctrl/deluge:1.2.0",
|
|
"angulartics2:14.1.2",
|
|
"koa2-swagger-ui:5.11.1",
|
|
"koa2-swagger-ui:5.11.2"
|
|
)
|
|
foreach ($pkg in $fallbackPackages) {
|
|
$COMPROMISED_PACKAGES_MAP[$pkg] = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-CompromisedPackage {
|
|
param([string]$PackageVersion)
|
|
return $COMPROMISED_PACKAGES_MAP.ContainsKey($PackageVersion)
|
|
}
|
|
|
|
function Test-CompromisedNamespace {
|
|
param([string]$Namespace)
|
|
return $COMPROMISED_NAMESPACES_MAP.ContainsKey($Namespace)
|
|
}
|
|
|
|
function New-TempDir {
|
|
$tempBase = $env:TEMP
|
|
if (-not $tempBase) { $tempBase = $env:TMP }
|
|
if (-not $tempBase) { $tempBase = "C:\Temp" }
|
|
|
|
$tempName = "shai-hulud-detect-$(Get-Random)"
|
|
$script:TempDir = Join-Path $tempBase $tempName
|
|
|
|
try {
|
|
New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
|
|
|
|
# Create findings files
|
|
@(
|
|
"workflow_files.txt", "malicious_hashes.txt", "compromised_found.txt",
|
|
"suspicious_found.txt", "suspicious_content.txt", "crypto_patterns.txt",
|
|
"git_branches.txt", "postinstall_hooks.txt", "trufflehog_activity.txt",
|
|
"shai_hulud_repos.txt", "namespace_warnings.txt", "low_risk_findings.txt",
|
|
"integrity_issues.txt", "typosquatting_warnings.txt", "network_exfiltration_warnings.txt",
|
|
"lockfile_safe_versions.txt", "bun_setup_files.txt", "bun_environment_files.txt",
|
|
"bun_environment_files_found.txt", "new_workflow_files.txt", "github_sha1hulud_runners.txt",
|
|
"preinstall_bun_patterns.txt", "second_coming_repos.txt", "actions_secrets_files.txt",
|
|
"discussion_workflows.txt", "github_runners.txt", "destructive_patterns.txt", "trufflehog_patterns.txt"
|
|
) | ForEach-Object {
|
|
New-Item -ItemType File -Path (Join-Path $script:TempDir $_) -Force | Out-Null
|
|
}
|
|
} catch {
|
|
Write-Error "Error: Cannot create temporary directory"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
function Remove-TempDir {
|
|
if ($script:TempDir -and (Test-Path $script:TempDir)) {
|
|
Remove-Item -Path $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# Register cleanup on exit
|
|
Register-EngineEvent PowerShell.Exiting -Action { Remove-TempDir } | Out-Null
|
|
|
|
function Get-CachedFileHash {
|
|
param([string]$FilePath)
|
|
|
|
$fileSize = (Get-Item $FilePath).Length
|
|
$fileMtime = (Get-Item $FilePath).LastWriteTime.Ticks
|
|
$cacheKeyString = "$FilePath`:$fileSize`:$fileMtime"
|
|
# Create a simple hash from the string for cache key
|
|
$cacheKeyBytes = [System.Text.Encoding]::UTF8.GetBytes($cacheKeyString)
|
|
$cacheKeyHash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($cacheKeyBytes)
|
|
$cacheKey = [System.BitConverter]::ToString($cacheKeyHash) -replace '-', ''
|
|
$hashCacheFile = Join-Path $script:TempDir "hcache_$cacheKey"
|
|
|
|
if (Test-Path $hashCacheFile) {
|
|
return Get-Content $hashCacheFile -Raw
|
|
}
|
|
|
|
$fileHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash
|
|
|
|
if ($fileHash) {
|
|
Set-Content -Path $hashCacheFile -Value $fileHash
|
|
return $fileHash
|
|
}
|
|
}
|
|
|
|
# Function: Transform-PnpmYaml
|
|
# Purpose: Convert pnpm-lock.yaml to pseudo-package-lock.json format for parsing
|
|
# Args: $1 = packages_file (path to pnpm-lock.yaml)
|
|
# Returns: Outputs JSON to stdout with packages structure compatible with package-lock parser
|
|
function Transform-PnpmYaml {
|
|
param([string]$PackagesFile)
|
|
|
|
$output = @()
|
|
$output += "{"
|
|
$output += " `"packages`": {"
|
|
|
|
$lines = Get-Content $PackagesFile
|
|
$depth = 0
|
|
$path = @{}
|
|
|
|
foreach ($line in $lines) {
|
|
# Find indentation
|
|
$sep = $line -replace '[^ ].*', ''
|
|
$currentDepth = $sep.Length
|
|
|
|
# Remove surrounding whitespace
|
|
$line = $line.Trim()
|
|
|
|
# Remove comments
|
|
if ($line -match '#') {
|
|
$line = $line -replace '#.*', ''
|
|
$line = $line.Trim()
|
|
}
|
|
|
|
# Skip empty lines and comments
|
|
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) {
|
|
continue
|
|
}
|
|
|
|
# Split into key/val
|
|
if ($line -match '^([^:]+):(.*)$') {
|
|
$key = $matches[1].Trim()
|
|
$val = $matches[2].Trim()
|
|
|
|
# Save current path
|
|
$path[$currentDepth] = $key
|
|
|
|
# Interested in packages.*
|
|
if ($currentDepth -eq 0 -and $key -ne 'packages') {
|
|
continue
|
|
}
|
|
if ($currentDepth -ne 2) {
|
|
continue
|
|
}
|
|
|
|
# Remove quotes from key
|
|
$key = $key -replace "^'", '' -replace "'$", ''
|
|
$key = $key -replace '^"', '' -replace '"$', ''
|
|
|
|
# Split into name/version
|
|
if ($key -match '^(.+)@(.+)$') {
|
|
$name = $matches[1].Trim()
|
|
$version = $matches[2].Trim()
|
|
|
|
$output += " `"$name`": {"
|
|
$output += " `"version`": `"$version`""
|
|
$output += " },"
|
|
}
|
|
}
|
|
}
|
|
|
|
$output += " }"
|
|
$output += "}"
|
|
return $output -join "`n"
|
|
}
|
|
|
|
# Fast pattern matching helpers
|
|
function Find-FilesWithPattern {
|
|
param(
|
|
[string[]]$Files,
|
|
[string]$Pattern,
|
|
[switch]$CaseInsensitive,
|
|
[switch]$FixedString
|
|
)
|
|
|
|
$results = @()
|
|
foreach ($file in $Files) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
try {
|
|
if ($FixedString) {
|
|
$content = Get-Content $file -Raw -ErrorAction SilentlyContinue
|
|
if ($CaseInsensitive) {
|
|
if ($content -match [regex]::Escape($Pattern)) { $results += $file }
|
|
} else {
|
|
if ($content -cmatch [regex]::Escape($Pattern)) { $results += $file }
|
|
}
|
|
} else {
|
|
$match = Select-String -Path $file -Pattern $Pattern -CaseSensitive:(-not $CaseInsensitive) -ErrorAction SilentlyContinue
|
|
if ($match) { $results += $file }
|
|
}
|
|
} catch {
|
|
# Skip files that can't be read
|
|
continue
|
|
}
|
|
}
|
|
return $results
|
|
}
|
|
|
|
function Test-FileContainsPattern {
|
|
param(
|
|
[string]$FilePath,
|
|
[string]$Pattern
|
|
)
|
|
try {
|
|
$match = Select-String -Path $FilePath -Pattern $Pattern -Quiet -ErrorAction SilentlyContinue
|
|
return $match
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Collect-AllFiles {
|
|
param([string]$ScanDir)
|
|
|
|
if (-not (Test-Path $script:TempDir)) {
|
|
New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
|
|
}
|
|
|
|
# Collect all relevant files
|
|
$extensions = @("*.js", "*.ts", "*.json", "*.mjs", "*.yml", "*.yaml", "*.py", "*.sh", "*.bat", "*.ps1", "*.cmd")
|
|
$allFiles = @()
|
|
|
|
foreach ($ext in $extensions) {
|
|
$files = Get-ChildItem -Path $ScanDir -Filter $ext -Recurse -File -ErrorAction SilentlyContinue
|
|
$allFiles += $files.FullName
|
|
}
|
|
|
|
# Specific files
|
|
$specificFiles = @(
|
|
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
"shai-hulud-workflow.yml", "setup_bun.js", "bun_environment.js",
|
|
"actionsSecrets.json", "*trufflehog*", "formatter_*.yml"
|
|
)
|
|
|
|
foreach ($pattern in $specificFiles) {
|
|
$files = Get-ChildItem -Path $ScanDir -Filter $pattern -Recurse -File -ErrorAction SilentlyContinue
|
|
$allFiles += $files.FullName
|
|
}
|
|
|
|
$allFiles = $allFiles | Sort-Object -Unique
|
|
$allFiles | Set-Content (Join-Path $script:TempDir "all_files_raw.txt")
|
|
|
|
# Categorize files
|
|
$allFiles | Where-Object { $_ -match "package\.json$" } | Set-Content (Join-Path $script:TempDir "package_files.txt")
|
|
$allFiles | Where-Object { $_ -match "\.(js|ts|json|mjs)$" } | Set-Content (Join-Path $script:TempDir "code_files.txt")
|
|
$allFiles | Where-Object { $_ -match "\.(yml|yaml)$" } | Set-Content (Join-Path $script:TempDir "yaml_files.txt")
|
|
$allFiles | Where-Object { $_ -match "\.(py|sh|bat|ps1|cmd)$" } | Set-Content (Join-Path $script:TempDir "script_files.txt")
|
|
$allFiles | Where-Object { $_ -match "(package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$" } | Set-Content (Join-Path $script:TempDir "lockfiles.txt")
|
|
$allFiles | Where-Object { $_ -match "shai-hulud-workflow\.yml$" } | Set-Content (Join-Path $script:TempDir "workflow_files_found.txt")
|
|
$allFiles | Where-Object { $_ -match "setup_bun\.js$" } | Set-Content (Join-Path $script:TempDir "setup_bun_files.txt")
|
|
$allFiles | Where-Object { $_ -match "bun_environment\.js$" } | Set-Content (Join-Path $script:TempDir "bun_environment_files.txt")
|
|
$allFiles | Where-Object { $_ -match "actionsSecrets\.json$" } | Set-Content (Join-Path $script:TempDir "actions_secrets_found.txt")
|
|
$allFiles | Where-Object { $_ -match "trufflehog" } | Set-Content (Join-Path $script:TempDir "trufflehog_files.txt")
|
|
$allFiles | Where-Object { $_ -match "formatter_.*\.yml$" } | Set-Content (Join-Path $script:TempDir "formatter_workflows.txt")
|
|
$allFiles | Where-Object { $_ -match "/\.github/workflows/.*\.ya?ml$" } | Set-Content (Join-Path $script:TempDir "github_workflows.txt")
|
|
|
|
# Git repositories
|
|
Get-ChildItem -Path $ScanDir -Filter ".git" -Recurse -Directory -ErrorAction SilentlyContinue |
|
|
ForEach-Object { $_.Parent.FullName } |
|
|
Sort-Object -Unique |
|
|
Set-Content (Join-Path $script:TempDir "git_repos.txt")
|
|
|
|
# Suspicious directories
|
|
Get-ChildItem -Path $ScanDir -Directory -Recurse -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -match "\.dev-env|shai.*hulud" } |
|
|
ForEach-Object { $_.FullName } |
|
|
Set-Content (Join-Path $script:TempDir "suspicious_dirs.txt")
|
|
}
|
|
|
|
function Test-WorkflowFiles {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for malicious workflow files..."
|
|
|
|
$workflowFiles = Get-Content (Join-Path $script:TempDir "workflow_files_found.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $workflowFiles) {
|
|
if (Test-Path $file) {
|
|
Add-Content -Path (Join-Path $script:TempDir "workflow_files.txt") -Value $file
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-BunAttackFiles {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for November 2025 Bun attack files..."
|
|
|
|
$setupBunHashes = @("a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a")
|
|
$bunEnvironmentHashes = @(
|
|
"62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0",
|
|
"f099c5d9ec417d4445a0328ac0ada9cde79fc37410914103ae9c609cbc0ee068",
|
|
"cbb9bc5a8496243e02f3cc080efbe3e4a1430ba0671f2e43a202bf45b05479cd"
|
|
)
|
|
|
|
$setupBunFiles = Get-Content (Join-Path $script:TempDir "setup_bun_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $setupBunFiles) {
|
|
if (Test-Path $file) {
|
|
Add-Content -Path (Join-Path $script:TempDir "bun_setup_files.txt") -Value $file
|
|
|
|
$fileHash = Get-CachedFileHash $file
|
|
if ($fileHash) {
|
|
foreach ($knownHash in $setupBunHashes) {
|
|
if ($fileHash -eq $knownHash) {
|
|
Add-Content -Path (Join-Path $script:TempDir "malicious_hashes.txt") -Value "$file`:SHA256=$fileHash (CONFIRMED MALICIOUS - Koi.ai IOC)"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$bunEnvFiles = Get-Content (Join-Path $script:TempDir "bun_environment_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $bunEnvFiles) {
|
|
if (Test-Path $file) {
|
|
# Write to a different file to avoid overwriting the input list
|
|
Add-Content -Path (Join-Path $script:TempDir "bun_environment_files_found.txt") -Value $file
|
|
|
|
$fileHash = Get-CachedFileHash $file
|
|
if ($fileHash) {
|
|
foreach ($knownHash in $bunEnvironmentHashes) {
|
|
if ($fileHash -eq $knownHash) {
|
|
Add-Content -Path (Join-Path $script:TempDir "malicious_hashes.txt") -Value "$file`:SHA256=$fileHash (CONFIRMED MALICIOUS - Koi.ai IOC)"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-NewWorkflowPatterns {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for new workflow patterns..."
|
|
|
|
$formatterWorkflows = Get-Content (Join-Path $script:TempDir "formatter_workflows.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $formatterWorkflows) {
|
|
if ((Test-Path $file) -and ($file -match "/\.github/workflows/")) {
|
|
Add-Content -Path (Join-Path $script:TempDir "new_workflow_files.txt") -Value $file
|
|
}
|
|
}
|
|
|
|
$actionsSecretsFiles = Get-Content (Join-Path $script:TempDir "actions_secrets_found.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $actionsSecretsFiles) {
|
|
if (Test-Path $file) {
|
|
Add-Content -Path (Join-Path $script:TempDir "actions_secrets_files.txt") -Value $file
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-DiscussionWorkflows {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for malicious discussion workflows..."
|
|
|
|
$workflowFiles = Get-Content (Join-Path $script:TempDir "github_workflows.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $workflowFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
$content = Get-Content $file -Raw -ErrorAction SilentlyContinue
|
|
if ($content -match "on:.*discussion|on:\s*discussion") {
|
|
Add-Content -Path (Join-Path $script:TempDir "discussion_workflows.txt") -Value "$file`:Discussion trigger detected"
|
|
}
|
|
|
|
if ($content -match "runs-on:.*self-hosted" -and $content -match '\$\{\{ github\.event\..*\.body \}\}') {
|
|
Add-Content -Path (Join-Path $script:TempDir "discussion_workflows.txt") -Value "$file`:Self-hosted runner with dynamic payload execution"
|
|
}
|
|
|
|
$fileName = Split-Path $file -Leaf
|
|
if ($fileName -eq "discussion.yaml" -or $fileName -eq "discussion.yml") {
|
|
Add-Content -Path (Join-Path $script:TempDir "discussion_workflows.txt") -Value "$file`:Suspicious discussion workflow filename"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-GitHubRunners {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for malicious GitHub Actions runners..."
|
|
|
|
$suspiciousDirs = Get-Content (Join-Path $script:TempDir "suspicious_dirs.txt") -ErrorAction SilentlyContinue
|
|
$allDirs = @()
|
|
|
|
# Find runner directories
|
|
$runnerPatterns = @(".dev-env", "actions-runner", ".runner", "_work")
|
|
foreach ($pattern in $runnerPatterns) {
|
|
$dirs = Get-ChildItem -Path $ScanDir -Filter $pattern -Recurse -Directory -ErrorAction SilentlyContinue
|
|
$allDirs += $dirs.FullName
|
|
}
|
|
|
|
$allDirs = $allDirs | Sort-Object -Unique
|
|
|
|
foreach ($dir in $allDirs) {
|
|
if (-not (Test-Path $dir)) { continue }
|
|
|
|
$hasConfig = (Test-Path (Join-Path $dir ".runner")) -or
|
|
(Test-Path (Join-Path $dir ".credentials")) -or
|
|
(Test-Path (Join-Path $dir "config.sh"))
|
|
|
|
$hasBinary = (Test-Path (Join-Path $dir "Runner.Worker")) -or
|
|
(Test-Path (Join-Path $dir "run.sh")) -or
|
|
(Test-Path (Join-Path $dir "run.cmd"))
|
|
|
|
if ($hasConfig) {
|
|
Add-Content -Path (Join-Path $script:TempDir "github_runners.txt") -Value "$dir`:Runner configuration files found"
|
|
}
|
|
|
|
if ($hasBinary) {
|
|
Add-Content -Path (Join-Path $script:TempDir "github_runners.txt") -Value "$dir`:Runner executable files found"
|
|
}
|
|
|
|
$dirName = Split-Path $dir -Leaf
|
|
if ($dirName -eq ".dev-env") {
|
|
Add-Content -Path (Join-Path $script:TempDir "github_runners.txt") -Value "$dir`:Suspicious .dev-env directory (matches Koi.ai report)"
|
|
}
|
|
}
|
|
|
|
# Check home directory
|
|
if (Test-Path "$env:USERPROFILE\.dev-env") {
|
|
Add-Content -Path (Join-Path $script:TempDir "github_runners.txt") -Value "$env:USERPROFILE\.dev-env`:Malicious runner directory in home folder (Koi.ai IOC)"
|
|
}
|
|
}
|
|
|
|
function Test-DestructivePatterns {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for destructive payload patterns..."
|
|
|
|
$scriptFiles = Get-Content (Join-Path $script:TempDir "script_files.txt") -ErrorAction SilentlyContinue
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
$allScriptFiles = ($scriptFiles + $codeFiles) | Sort-Object -Unique
|
|
|
|
$basicDestructiveRegex = 'rm -rf\s+(\$HOME|~[^a-zA-Z0-9_/]|/home/)|del /s /q\s+(%USERPROFILE%|\$HOME)|Remove-Item -Recurse\s+(\$HOME|~[^a-zA-Z0-9_/])|find\s+(\$HOME|~[^a-zA-Z0-9_/]|/home/).*-exec rm|find\s+(\$HOME|~[^a-zA-Z0-9_/]|/home/).*-delete'
|
|
$shaiHuludWiperRegex = 'Bun\.spawnSync.{1,50}(cmd\.exe|bash).{1,100}(del /F|shred|cipher /W)|shred.{1,30}-[nuvz].{1,50}(\$HOME|~/)|cipher\s*/W:.{0,30}USERPROFILE|del\s*/F\s*/Q\s*/S.{1,30}USERPROFILE|find.{1,30}\$HOME.{1,50}shred|rd\s*/S\s*/Q.{1,30}USERPROFILE'
|
|
$shellConditionalRegex = 'if.*credential.*(fail|error).*rm|if.*token.*not.*found.*(delete|rm)|if.*github.*auth.*fail.*rm|catch.*rm -rf|error.*delete.*home'
|
|
|
|
$jsPyFiles = $allScriptFiles | Where-Object { $_ -match "\.(js|py)$" }
|
|
$shellFiles = $allScriptFiles | Where-Object { $_ -match "\.(sh|bat|ps1|cmd)$" }
|
|
|
|
# Basic destructive patterns
|
|
$matches = Find-FilesWithPattern -Files $allScriptFiles -Pattern $basicDestructiveRegex -CaseInsensitive
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "destructive_patterns.txt") -Value "$file`:Basic destructive pattern detected"
|
|
}
|
|
|
|
# Shai-Hulud wiper patterns
|
|
$matches = Find-FilesWithPattern -Files $jsPyFiles -Pattern $shaiHuludWiperRegex -CaseInsensitive
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "destructive_patterns.txt") -Value "$file`:Shai-Hulud wiper pattern detected (JS/Python context)"
|
|
}
|
|
|
|
# Shell conditional patterns
|
|
$matches = Find-FilesWithPattern -Files $shellFiles -Pattern $shellConditionalRegex -CaseInsensitive
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "destructive_patterns.txt") -Value "$file`:Conditional destruction pattern detected (Shell script context)"
|
|
}
|
|
}
|
|
|
|
function Test-PreinstallBunPatterns {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for fake Bun preinstall patterns..."
|
|
|
|
$packageFiles = Get-Content (Join-Path $script:TempDir "package_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $packageFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
$content = Get-Content $file -Raw -ErrorAction SilentlyContinue
|
|
if ($content -match '"preinstall"\s*:\s*"node setup_bun\.js"') {
|
|
Add-Content -Path (Join-Path $script:TempDir "preinstall_bun_patterns.txt") -Value $file
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-GitHubActionsRunner {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for SHA1HULUD GitHub Actions runners..."
|
|
|
|
$yamlFiles = Get-Content (Join-Path $script:TempDir "yaml_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $yamlFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
if (Test-FileContainsPattern -FilePath $file -Pattern "SHA1HULUD") {
|
|
Add-Content -Path (Join-Path $script:TempDir "github_sha1hulud_runners.txt") -Value $file
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-SecondComingRepos {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for 'Second Coming' repository descriptions..."
|
|
|
|
$gitRepos = Get-Content (Join-Path $script:TempDir "git_repos.txt") -ErrorAction SilentlyContinue
|
|
foreach ($repoDir in $gitRepos) {
|
|
if (-not (Test-Path (Join-Path $repoDir ".git"))) { continue }
|
|
|
|
try {
|
|
Push-Location $repoDir
|
|
$description = git config --get --local repository.description 2>$null
|
|
if ($description -match "Sha1-Hulud: The Second Coming") {
|
|
Add-Content -Path (Join-Path $script:TempDir "second_coming_repos.txt") -Value $repoDir
|
|
}
|
|
} catch {
|
|
# Skip repos where git command fails
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
}
|
|
|
|
function Collect-CredentialInventory {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Collecting credential inventory from findings..."
|
|
|
|
$inventory = @()
|
|
|
|
# Common credential types that might be exposed
|
|
$credentialTypes = @{
|
|
"GITHUB_TOKEN" = @{
|
|
pattern = "GITHUB_TOKEN|github_pat_|ghp_|gho_|ghu_|ghs_|ghr_"
|
|
description = "GitHub Personal Access Token or GitHub Actions Token"
|
|
rotation_url = "https://github.com/settings/tokens"
|
|
severity = "HIGH"
|
|
}
|
|
"NPM_TOKEN" = @{
|
|
pattern = "NPM_TOKEN|npm_[a-zA-Z0-9]{36}"
|
|
description = "npm authentication token"
|
|
rotation_url = "https://www.npmjs.com/settings/[username]/tokens"
|
|
severity = "HIGH"
|
|
}
|
|
"AWS_ACCESS_KEY" = @{
|
|
pattern = "AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|AKIA[0-9A-Z]{16}"
|
|
description = "AWS Access Key ID or Secret Access Key"
|
|
rotation_url = "https://console.aws.amazon.com/iam/home#/security_credentials"
|
|
severity = "CRITICAL"
|
|
}
|
|
"SLACK_TOKEN" = @{
|
|
pattern = "SLACK_TOKEN|SLACK_WEBHOOK|xox[baprs]-"
|
|
description = "Slack API token or webhook URL"
|
|
rotation_url = "https://api.slack.com/apps"
|
|
severity = "HIGH"
|
|
}
|
|
"EXPO_TOKEN" = @{
|
|
pattern = "EXPO_TOKEN"
|
|
description = "Expo authentication token"
|
|
rotation_url = "https://expo.dev/accounts/[username]/settings/access-tokens"
|
|
severity = "MEDIUM"
|
|
}
|
|
"CODECOV_TOKEN" = @{
|
|
pattern = "CODECOV_TOKEN"
|
|
description = "Codecov authentication token"
|
|
rotation_url = "https://codecov.io/settings"
|
|
severity = "MEDIUM"
|
|
}
|
|
"WEBFLOW_TOKEN" = @{
|
|
pattern = "WEBFLOW_TOKEN"
|
|
description = "Webflow API token"
|
|
rotation_url = "https://webflow.com/dashboard/account/api"
|
|
severity = "MEDIUM"
|
|
}
|
|
}
|
|
|
|
# Check trufflehog activity files for credential references
|
|
if (Test-Path (Join-Path $script:TempDir "trufflehog_activity.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "trufflehog_activity.txt") -ErrorAction SilentlyContinue
|
|
foreach ($entry in $entries) {
|
|
foreach ($credType in $credentialTypes.Keys) {
|
|
if ($entry -match $credentialTypes[$credType].pattern) {
|
|
$parts = $entry -split ":", 3
|
|
$file = $parts[0]
|
|
$inventory += [PSCustomObject]@{
|
|
type = $credType
|
|
description = $credentialTypes[$credType].description
|
|
found_in = $file
|
|
severity = $credentialTypes[$credType].severity
|
|
rotation_url = $credentialTypes[$credType].rotation_url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check crypto theft patterns
|
|
if (Test-Path (Join-Path $script:TempDir "crypto_patterns.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "crypto_patterns.txt") -ErrorAction SilentlyContinue
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match "AWS_ACCESS_KEY|AWS_SECRET") {
|
|
$parts = $entry -split ":"
|
|
$file = $parts[0]
|
|
$inventory += [PSCustomObject]@{
|
|
type = "AWS_ACCESS_KEY"
|
|
description = $credentialTypes["AWS_ACCESS_KEY"].description
|
|
found_in = $file
|
|
severity = "CRITICAL"
|
|
rotation_url = $credentialTypes["AWS_ACCESS_KEY"].rotation_url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check compromised packages - these may have exposed credentials
|
|
if (Test-Path (Join-Path $script:TempDir "compromised_found.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "compromised_found.txt") -ErrorAction SilentlyContinue
|
|
foreach ($entry in $entries) {
|
|
$parts = $entry -split ":"
|
|
$package = $parts[1]
|
|
$inventory += [PSCustomObject]@{
|
|
type = "COMPROMISED_PACKAGE"
|
|
description = "Credentials may have been exposed via compromised package: $package"
|
|
found_in = $parts[0]
|
|
severity = "HIGH"
|
|
rotation_url = "https://www.npmjs.com/package/$($package -replace '@', '')"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Store inventory
|
|
$script:JsonOutput.credential_inventory = $inventory | Select-Object -Unique type, description, found_in, severity, rotation_url
|
|
}
|
|
|
|
function Test-FileHashes {
|
|
param([string]$ScanDir)
|
|
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
$totalFiles = ($codeFiles | Measure-Object).Count
|
|
|
|
Write-Status "BLUE" " Filtering files for hash checking..."
|
|
|
|
# Priority files
|
|
$priorityFiles = @()
|
|
$priorityFiles += $codeFiles | Where-Object { $_ -match "(setup_bun\.js|bun_environment\.js|actionsSecrets\.json|trufflehog)" }
|
|
$priorityFiles += $codeFiles | Where-Object { $_ -notmatch "/node_modules/" }
|
|
$priorityFiles = $priorityFiles | Sort-Object -Unique
|
|
|
|
$filesCount = ($priorityFiles | Measure-Object).Count
|
|
$checkMsg = " Checking $filesCount priority files for known malicious content (filtered from $totalFiles total)..."
|
|
Write-Status "BLUE" $checkMsg
|
|
|
|
Write-Status "BLUE" " Computing hashes in parallel..."
|
|
|
|
# Compute hashes - use parallel processing for PowerShell 7+, sequential for 5.1
|
|
$fileHashes = @{}
|
|
if ($PSVersionTable.PSVersion.Major -ge 7) {
|
|
$priorityFiles | ForEach-Object -Parallel {
|
|
$file = $_
|
|
if (Test-Path $file) {
|
|
try {
|
|
$hash = (Get-FileHash -Path $file -Algorithm SHA256).Hash
|
|
[PSCustomObject]@{ File = $file; Hash = $hash }
|
|
} catch {
|
|
$null
|
|
}
|
|
}
|
|
} -ThrottleLimit $PARALLELISM | ForEach-Object {
|
|
if ($_.Hash) {
|
|
$fileHashes[$_.File] = $_.Hash
|
|
}
|
|
}
|
|
} else {
|
|
# PowerShell 5.1 - sequential processing
|
|
foreach ($file in $priorityFiles) {
|
|
if (Test-Path $file) {
|
|
try {
|
|
$hash = (Get-FileHash -Path $file -Algorithm SHA256).Hash
|
|
if ($hash) {
|
|
$fileHashes[$file] = $hash
|
|
}
|
|
} catch {
|
|
# Skip files that can't be hashed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Status "BLUE" " Checking against known malicious hashes..."
|
|
foreach ($file in $fileHashes.Keys) {
|
|
$hash = $fileHashes[$file]
|
|
if ($MALICIOUS_HASHLIST -contains $hash) {
|
|
Add-Content -Path (Join-Path $script:TempDir "malicious_hashes.txt") -Value "$file`:$hash"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-Packages {
|
|
param([string]$ScanDir)
|
|
|
|
$packageFiles = Get-Content (Join-Path $script:TempDir "package_files.txt") -ErrorAction SilentlyContinue
|
|
$filesCount = ($packageFiles | Measure-Object).Count
|
|
|
|
Write-Status "BLUE" " Checking $filesCount package.json files for compromised packages..."
|
|
|
|
Write-Status "BLUE" " Extracting dependencies from all package.json files..."
|
|
|
|
$allDeps = @()
|
|
foreach ($file in $packageFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
try {
|
|
$packageJson = Get-Content $file -Raw | ConvertFrom-Json
|
|
|
|
$deps = @{}
|
|
if ($packageJson.dependencies) {
|
|
foreach ($key in $packageJson.dependencies.PSObject.Properties.Name) {
|
|
$deps[$key] = $packageJson.dependencies.$key
|
|
}
|
|
}
|
|
if ($packageJson.devDependencies) {
|
|
foreach ($key in $packageJson.devDependencies.PSObject.Properties.Name) {
|
|
$deps[$key] = $packageJson.devDependencies.$key
|
|
}
|
|
}
|
|
|
|
foreach ($pkgName in $deps.Keys) {
|
|
$version = $deps[$pkgName]
|
|
# Try exact match first (with version prefix)
|
|
$pkgVersionFull = "$pkgName`:$version"
|
|
# Also try without prefix for matching
|
|
$versionClean = $version -replace '^[\^~]', ''
|
|
$pkgVersionClean = "$pkgName`:$versionClean"
|
|
|
|
# Store both versions for matching
|
|
$allDeps += [PSCustomObject]@{
|
|
File = $file
|
|
Package = $pkgVersionClean
|
|
PackageFull = $pkgVersionFull
|
|
PackageName = $pkgName
|
|
Version = $versionClean
|
|
}
|
|
}
|
|
} catch {
|
|
# Skip invalid JSON
|
|
continue
|
|
}
|
|
}
|
|
|
|
Write-Status "BLUE" " Checking dependencies against compromised list..."
|
|
$depCount = ($allDeps | Measure-Object).Count
|
|
Write-Status "BLUE" " Found $depCount total dependencies to check"
|
|
|
|
# Check for compromised packages - exact match on package:version
|
|
foreach ($dep in $allDeps) {
|
|
if (Test-CompromisedPackage $dep.Package) {
|
|
Add-Content -Path (Join-Path $script:TempDir "compromised_found.txt") -Value "$($dep.File):$($dep.Package -replace ':', '@')"
|
|
}
|
|
}
|
|
|
|
# Check for compromised namespaces
|
|
Write-Status "BLUE" " Checking for compromised namespaces..."
|
|
foreach ($namespace in $COMPROMISED_NAMESPACES) {
|
|
$matchingDeps = $allDeps | Where-Object { $_.Package -like "$namespace/*" }
|
|
foreach ($dep in $matchingDeps) {
|
|
Add-Content -Path (Join-Path $script:TempDir "namespace_warnings.txt") -Value "$($dep.File):Contains packages from compromised namespace: $namespace"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-PostinstallHooks {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for suspicious postinstall hooks..."
|
|
|
|
$packageFiles = Get-Content (Join-Path $script:TempDir "package_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $packageFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
try {
|
|
$packageJson = Get-Content $file -Raw | ConvertFrom-Json
|
|
if ($packageJson.scripts -and $packageJson.scripts.postinstall) {
|
|
$postinstallCmd = $packageJson.scripts.postinstall
|
|
if ($postinstallCmd -match "curl|wget|node -e|eval") {
|
|
Add-Content -Path (Join-Path $script:TempDir "postinstall_hooks.txt") -Value "$file`:Suspicious postinstall: $postinstallCmd"
|
|
}
|
|
}
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-Content {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for suspicious content patterns..."
|
|
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
$yamlFiles = Get-Content (Join-Path $script:TempDir "yaml_files.txt") -ErrorAction SilentlyContinue
|
|
$allFiles = ($codeFiles + $yamlFiles) | Sort-Object -Unique
|
|
|
|
$webhookMatches = Find-FilesWithPattern -Files $allFiles -Pattern "webhook\.site" -FixedString
|
|
foreach ($file in $webhookMatches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "suspicious_content.txt") -Value "$file`:webhook.site reference"
|
|
}
|
|
|
|
$endpointMatches = Find-FilesWithPattern -Files $allFiles -Pattern "bb8ca5f6-4175-45d2-b042-fc9ebb8170b7" -FixedString
|
|
foreach ($file in $endpointMatches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "suspicious_content.txt") -Value "$file`:malicious webhook endpoint"
|
|
}
|
|
}
|
|
|
|
function Test-CryptoTheftPatterns {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for cryptocurrency theft patterns..."
|
|
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
|
|
# Known crypto theft function names
|
|
$cryptoFunctions = "checkethereumw|runmask|newdlocal|_0x19ca67"
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern $cryptoFunctions
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:Known crypto theft function names detected"
|
|
}
|
|
|
|
# Known attacker wallets
|
|
$walletAddresses = "0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976|1H13VnQJKtT4HjD5ZFKaaiZEetMbG7nDHx|TB9emsCq6fQw6wRk4HBxxNnU6Hwt1DnV67"
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern $walletAddresses
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:Known attacker wallet address detected - HIGH RISK"
|
|
}
|
|
|
|
# npmjs.help phishing domain
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern "npmjs\.help" -FixedString
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:Phishing domain npmjs.help detected"
|
|
}
|
|
|
|
# XMLHttpRequest hijacking
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern "XMLHttpRequest\.prototype\.send" -FixedString
|
|
foreach ($file in $matches) {
|
|
$isFramework = $file -match "/react-native/Libraries/Network/|/next/dist/compiled/"
|
|
$hasCrypto = Test-FileContainsPattern -FilePath $file -Pattern "0x[a-fA-F0-9]{40}|checkethereumw|runmask|webhook\.site|npmjs\.help"
|
|
|
|
if ($hasCrypto) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:XMLHttpRequest prototype modification with crypto patterns detected - HIGH RISK"
|
|
} elseif ($isFramework) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:XMLHttpRequest prototype modification detected in framework code - LOW RISK"
|
|
} else {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:XMLHttpRequest prototype modification detected - MEDIUM RISK"
|
|
}
|
|
}
|
|
|
|
# JavaScript obfuscation
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern "javascript-obfuscator" -FixedString
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:JavaScript obfuscation detected"
|
|
}
|
|
|
|
# Ethereum wallet address patterns
|
|
$matches = Find-FilesWithPattern -Files $codeFiles -Pattern "0x[a-fA-F0-9]{40}"
|
|
foreach ($file in $matches) {
|
|
if (Test-FileContainsPattern -FilePath $file -Pattern "ethereum|wallet|address|crypto") {
|
|
Add-Content -Path (Join-Path $script:TempDir "crypto_patterns.txt") -Value "$file`:Ethereum wallet address patterns detected"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-GitBranches {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for suspicious git branches..."
|
|
|
|
$gitRepos = Get-Content (Join-Path $script:TempDir "git_repos.txt") -ErrorAction SilentlyContinue
|
|
foreach ($repoDir in $gitRepos) {
|
|
$refsDir = Join-Path $repoDir ".git\refs\heads"
|
|
if (-not (Test-Path $refsDir)) { continue }
|
|
|
|
$branches = Get-ChildItem -Path $refsDir -File -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -match "shai.*hulud" }
|
|
|
|
foreach ($branch in $branches) {
|
|
$branchName = $branch.Name
|
|
$commitHash = Get-Content $branch.FullName -Raw -ErrorAction SilentlyContinue
|
|
$shortHash = if ($commitHash) { $commitHash.Substring(0, [Math]::Min(8, $commitHash.Length)) } else { "unknown" }
|
|
Add-Content -Path (Join-Path $script:TempDir "git_branches.txt") -Value "$repoDir`:Branch '$branchName' (commit: ${shortHash}...)"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-TrufflehogActivity {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for Trufflehog activity and secret scanning..."
|
|
|
|
$trufflehogFiles = Get-Content (Join-Path $script:TempDir "trufflehog_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $trufflehogFiles) {
|
|
if (Test-Path $file) {
|
|
Add-Content -Path (Join-Path $script:TempDir "trufflehog_activity.txt") -Value "$file`:HIGH:Trufflehog binary found"
|
|
}
|
|
}
|
|
|
|
$scriptFiles = Get-Content (Join-Path $script:TempDir "script_files.txt") -ErrorAction SilentlyContinue
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
$scanFiles = ($scriptFiles + $codeFiles) | Sort-Object -Unique
|
|
|
|
# Dynamic TruffleHog download patterns
|
|
$downloadPattern = "curl.*trufflehog|wget.*trufflehog|bunExecutable.*trufflehog|download.*trufflehog"
|
|
$matches = Find-FilesWithPattern -Files $scanFiles -Pattern $downloadPattern -CaseInsensitive
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "trufflehog_activity.txt") -Value "$file`:HIGH:November 2025 pattern - Dynamic TruffleHog download via curl/wget/Bun"
|
|
}
|
|
|
|
# Credential harvesting patterns
|
|
$credPattern = "TruffleHog.*scan.*credential|trufflehog.*env|trufflehog.*AWS|trufflehog.*NPM_TOKEN"
|
|
$matches = Find-FilesWithPattern -Files $scanFiles -Pattern $credPattern -CaseInsensitive
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "trufflehog_activity.txt") -Value "$file`:HIGH:TruffleHog credential scanning pattern detected"
|
|
}
|
|
|
|
# Credential patterns with exfiltration
|
|
$exfilPattern = '(AWS_ACCESS_KEY|GITHUB_TOKEN|NPM_TOKEN).*(webhook\.site|curl|https\.request)'
|
|
$matches = Find-FilesWithPattern -Files $scanFiles -Pattern $exfilPattern
|
|
$matches = $matches | Where-Object { $_ -notmatch "/node_modules/|\.d\.ts$" }
|
|
foreach ($file in $matches) {
|
|
Add-Content -Path (Join-Path $script:TempDir "trufflehog_activity.txt") -Value "$file`:HIGH:Credential patterns with potential exfiltration"
|
|
}
|
|
|
|
# Medium priority: Trufflehog references
|
|
$refPattern = "trufflehog|TruffleHog"
|
|
$matches = Find-FilesWithPattern -Files $scanFiles -Pattern $refPattern -CaseInsensitive
|
|
$matches = $matches | Where-Object { $_ -notmatch "/node_modules/|\.md$|/docs/|\.d\.ts$" }
|
|
foreach ($file in $matches) {
|
|
if (-not (Test-FileContainsPattern -FilePath (Join-Path $script:TempDir "trufflehog_activity.txt") -Pattern "^$([regex]::Escape($file)):")) {
|
|
Add-Content -Path (Join-Path $script:TempDir "trufflehog_activity.txt") -Value "$file`:MEDIUM:Contains trufflehog references in source code"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-ShaiHuludRepos {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for Shai-Hulud repositories and migration patterns..."
|
|
|
|
$gitRepos = Get-Content (Join-Path $script:TempDir "git_repos.txt") -ErrorAction SilentlyContinue
|
|
foreach ($repoDir in $gitRepos) {
|
|
$repoName = Split-Path $repoDir -Leaf
|
|
if ($repoName -match "shai.*hulud") {
|
|
Add-Content -Path (Join-Path $script:TempDir "shai_hulud_repos.txt") -Value "$repoDir`:Repository name contains 'Shai-Hulud'"
|
|
}
|
|
|
|
if ($repoName -match "-migration") {
|
|
Add-Content -Path (Join-Path $script:TempDir "shai_hulud_repos.txt") -Value "$repoDir`:Repository name contains migration pattern"
|
|
}
|
|
|
|
$gitConfig = Join-Path $repoDir ".git\config"
|
|
if (Test-Path $gitConfig) {
|
|
$configContent = Get-Content $gitConfig -Raw -ErrorAction SilentlyContinue
|
|
if ($configContent -match "shai.*hulud") {
|
|
Add-Content -Path (Join-Path $script:TempDir "shai_hulud_repos.txt") -Value "$repoDir`:Git remote contains 'Shai-Hulud'"
|
|
}
|
|
}
|
|
|
|
$dataJson = Join-Path $repoDir "data.json"
|
|
if (Test-Path $dataJson) {
|
|
$content = Get-Content $dataJson -TotalCount 5 -ErrorAction SilentlyContinue
|
|
if ($content -match "eyJ" -and $content -match "==") {
|
|
Add-Content -Path (Join-Path $script:TempDir "shai_hulud_repos.txt") -Value "$repoDir`:Contains suspicious data.json (possible base64-encoded credentials)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-PackageIntegrity {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking package lock files for integrity issues..."
|
|
|
|
$lockfiles = Get-Content (Join-Path $script:TempDir "lockfiles.txt") -ErrorAction SilentlyContinue
|
|
foreach ($lockfile in $lockfiles) {
|
|
if (-not (Test-Path $lockfile)) { continue }
|
|
|
|
$orgFile = $lockfile
|
|
$tempLockfile = $null
|
|
|
|
try {
|
|
# Transform pnpm-lock.yaml into pseudo-package-lock.json
|
|
if ((Split-Path $orgFile -Leaf) -eq "pnpm-lock.yaml") {
|
|
$tempLockfile = Join-Path $script:TempDir "pnpm_temp_$(Get-Random).json"
|
|
$transformed = Transform-PnpmYaml $orgFile
|
|
Set-Content -Path $tempLockfile -Value $transformed
|
|
$lockfile = $tempLockfile
|
|
}
|
|
|
|
if ($lockfile -match "package-lock\.json$|pnpm.*\.json$") {
|
|
$lockContent = Get-Content $lockfile -Raw | ConvertFrom-Json
|
|
|
|
# Check packages section (newer format)
|
|
if ($lockContent.packages) {
|
|
foreach ($pkgPath in $lockContent.packages.PSObject.Properties.Name) {
|
|
$pkg = $lockContent.packages.$pkgPath
|
|
if ($pkg.version) {
|
|
$pkgName = $pkgPath -replace "^node_modules/", ""
|
|
$pkgVersion = "$pkgName`:$($pkg.version)"
|
|
if (Test-CompromisedPackage $pkgVersion) {
|
|
Add-Content -Path (Join-Path $script:TempDir "integrity_issues.txt") -Value "$orgFile`:Compromised package in lockfile: $pkgVersion"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check dependencies section (older format)
|
|
if ($lockContent.dependencies) {
|
|
foreach ($pkgName in $lockContent.dependencies.PSObject.Properties.Name) {
|
|
$pkg = $lockContent.dependencies.$pkgName
|
|
if ($pkg.version) {
|
|
$pkgVersion = "$pkgName`:$($pkg.version)"
|
|
if (Test-CompromisedPackage $pkgVersion) {
|
|
Add-Content -Path (Join-Path $script:TempDir "integrity_issues.txt") -Value "$orgFile`:Compromised package in lockfile: $pkgVersion"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} elseif ($orgFile -match "yarn\.lock$") {
|
|
$lockContent = Get-Content $orgFile -Raw
|
|
# Simple yarn.lock parsing
|
|
$lines = $lockContent -split "`n"
|
|
foreach ($line in $lines) {
|
|
# Parse yarn.lock format: "package-name@version":
|
|
if ($line -match '^"(.+)@(.+)"') {
|
|
$pkgName = $Matches[1]
|
|
$pkgVersion = $Matches[2]
|
|
$pkgVersionFull = "$pkgName`:$pkgVersion"
|
|
if (Test-CompromisedPackage $pkgVersionFull) {
|
|
Add-Content -Path (Join-Path $script:TempDir "integrity_issues.txt") -Value "$orgFile`:Compromised package in lockfile: $pkgVersionFull"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check for @ctrl packages (potential worm activity)
|
|
$content = Get-Content $orgFile -Raw -ErrorAction SilentlyContinue
|
|
if ($content -match "@ctrl") {
|
|
Add-Content -Path (Join-Path $script:TempDir "integrity_issues.txt") -Value "$orgFile`:Lockfile contains @ctrl packages (potential worm activity)"
|
|
}
|
|
} catch {
|
|
# Skip invalid lockfiles
|
|
continue
|
|
} finally {
|
|
# Cleanup temp lockfile for pnpm
|
|
if ($tempLockfile -and (Test-Path $tempLockfile)) {
|
|
Remove-Item $tempLockfile -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-Typosquatting {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for typosquatting patterns..."
|
|
|
|
$packageFiles = Get-Content (Join-Path $script:TempDir "package_files.txt") -ErrorAction SilentlyContinue
|
|
$warnedPackages = @{}
|
|
|
|
foreach ($file in $packageFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
|
|
try {
|
|
$packageJson = Get-Content $file -Raw | ConvertFrom-Json
|
|
$deps = @{}
|
|
if ($packageJson.dependencies) {
|
|
foreach ($key in $packageJson.dependencies.PSObject.Properties.Name) {
|
|
$deps[$key] = $packageJson.dependencies.$key
|
|
}
|
|
}
|
|
if ($packageJson.devDependencies) {
|
|
foreach ($key in $packageJson.devDependencies.PSObject.Properties.Name) {
|
|
$deps[$key] = $packageJson.devDependencies.$key
|
|
}
|
|
}
|
|
|
|
foreach ($pkgName in $deps.Keys) {
|
|
$key = "$file`:$pkgName"
|
|
if ($warnedPackages.ContainsKey($key)) { continue }
|
|
|
|
# Check for non-ASCII characters
|
|
$hasUnicode = $pkgName -notmatch '^[a-zA-Z0-9@/._-]*$'
|
|
if ($hasUnicode) {
|
|
Add-Content -Path (Join-Path $script:TempDir "typosquatting_warnings.txt") -Value "$file`:Potential Unicode/homoglyph characters in package: $pkgName"
|
|
$warnedPackages[$key] = $true
|
|
}
|
|
}
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-NetworkExfiltration {
|
|
param([string]$ScanDir)
|
|
|
|
Write-Status "BLUE" " Checking for network exfiltration patterns..."
|
|
|
|
$suspiciousDomains = @(
|
|
"pastebin.com", "hastebin.com", "ix.io", "0x0.st", "transfer.sh",
|
|
"file.io", "anonfiles.com", "mega.nz", "dropbox.com/s/",
|
|
"discord.com/api/webhooks", "telegram.org", "t.me",
|
|
"ngrok.io", "localtunnel.me", "serveo.net",
|
|
"requestbin.com", "webhook.site", "beeceptor.com",
|
|
"pipedream.com", "zapier.com/hooks"
|
|
)
|
|
|
|
$codeFiles = Get-Content (Join-Path $script:TempDir "code_files.txt") -ErrorAction SilentlyContinue
|
|
foreach ($file in $codeFiles) {
|
|
if (-not (Test-Path $file)) { continue }
|
|
if ($file -match "/vendor/|/node_modules/") { continue }
|
|
|
|
$content = Get-Content $file -Raw -ErrorAction SilentlyContinue
|
|
if (-not $content) { continue }
|
|
|
|
# Check for hardcoded IPs
|
|
if ($content -match '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b') {
|
|
$ips = [regex]::Matches($content, '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b') |
|
|
Select-Object -First 3 -ExpandProperty Value
|
|
$ipsStr = $ips -join ' '
|
|
if ($ipsStr -notmatch "127\.0\.0\.1|0\.0\.0\.0") {
|
|
Add-Content -Path (Join-Path $script:TempDir "network_exfiltration_warnings.txt") -Value "$file`:Hardcoded IP addresses found: $ipsStr"
|
|
}
|
|
}
|
|
|
|
# Check for suspicious domains
|
|
if ($file -notmatch "package-lock\.json|yarn\.lock|/vendor/|/node_modules/") {
|
|
foreach ($domain in $suspiciousDomains) {
|
|
$domainPattern = [regex]::Escape($domain)
|
|
# Check for URL pattern or domain in content
|
|
$urlPattern = "https?://.*$domainPattern"
|
|
# Simple check: domain appears in content (not just in comments)
|
|
$hasDomain = $content -match $domainPattern
|
|
if ($content -match $urlPattern -or $hasDomain) {
|
|
Add-Content -Path (Join-Path $script:TempDir "network_exfiltration_warnings.txt") -Value "$file`:Suspicious domain found: $domain"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Write-LogFile {
|
|
param([string]$LogFile, [bool]$ParanoidMode)
|
|
|
|
$reportLines = @()
|
|
|
|
# Header
|
|
$reportLines += ""
|
|
$reportLines += "=============================================="
|
|
if ($ParanoidMode) {
|
|
$reportLines += " SHAI-HULUD + PARANOID SECURITY REPORT"
|
|
} else {
|
|
$reportLines += " SHAI-HULUD DETECTION REPORT"
|
|
}
|
|
$reportLines += "=============================================="
|
|
$reportLines += ""
|
|
$reportLines += "Report generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
$reportLines += ""
|
|
|
|
$highRiskCount = 0
|
|
$mediumRiskCount = 0
|
|
|
|
# Report malicious workflow files
|
|
if (Test-Path (Join-Path $script:TempDir "workflow_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "workflow_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: Malicious workflow files detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report malicious file hashes
|
|
if (Test-Path (Join-Path $script:TempDir "malicious_hashes.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "malicious_hashes.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Files with known malicious hashes:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " Hash: $($parts[1])"
|
|
}
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report November 2025 Bun attack files
|
|
if (Test-Path (Join-Path $script:TempDir "bun_setup_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "bun_setup_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: November 2025 Bun attack setup files detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "bun_environment_files_found.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "bun_environment_files_found.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: November 2025 Bun environment payload detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "new_workflow_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "new_workflow_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: November 2025 malicious workflow files detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "actions_secrets_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "actions_secrets_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: Actions secrets exfiltration files detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report discussion workflows
|
|
if (Test-Path (Join-Path $script:TempDir "discussion_workflows.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "discussion_workflows.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Malicious discussion-triggered workflows detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " Reason: $($parts[1])"
|
|
}
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report GitHub runners
|
|
if (Test-Path (Join-Path $script:TempDir "github_runners.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "github_runners.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Malicious GitHub Actions runners detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " Reason: $($parts[1])"
|
|
}
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report destructive patterns
|
|
if (Test-Path (Join-Path $script:TempDir "destructive_patterns.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "destructive_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "CRITICAL: Destructive payload patterns detected:"
|
|
$reportLines += " WARNING: These patterns can cause permanent data loss!"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " Pattern: $($parts[1])"
|
|
}
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += " IMMEDIATE ACTION REQUIRED: Quarantine these files and review for data destruction capabilities"
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report preinstall Bun patterns
|
|
if (Test-Path (Join-Path $script:TempDir "preinstall_bun_patterns.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "preinstall_bun_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: Fake Bun preinstall patterns detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report SHA1HULUD runners
|
|
if (Test-Path (Join-Path $script:TempDir "github_sha1hulud_runners.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "github_sha1hulud_runners.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
$reportLines += "HIGH RISK: SHA1HULUD GitHub Actions runners detected:"
|
|
foreach ($file in $files) {
|
|
$reportLines += " - $file"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report second coming repos
|
|
if (Test-Path (Join-Path $script:TempDir "second_coming_repos.txt")) {
|
|
$repos = Get-Content (Join-Path $script:TempDir "second_coming_repos.txt") -ErrorAction SilentlyContinue
|
|
if ($repos) {
|
|
$reportLines += "HIGH RISK: 'Shai-Hulud: The Second Coming' repositories detected:"
|
|
foreach ($repo in $repos) {
|
|
$reportLines += " - $repo"
|
|
$reportLines += " Repository description: Sha1-Hulud: The Second Coming."
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report compromised packages
|
|
if (Test-Path (Join-Path $script:TempDir "compromised_found.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "compromised_found.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Compromised package versions detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Package: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These specific package versions are known to be compromised."
|
|
$reportLines += "You should immediately update or remove these packages."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report postinstall hooks
|
|
if (Test-Path (Join-Path $script:TempDir "postinstall_hooks.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "postinstall_hooks.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Suspicious postinstall hooks detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Hook: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += "NOTE: Postinstall hooks can execute arbitrary code during package installation."
|
|
$reportLines += "Review these hooks carefully for malicious behavior."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report Trufflehog activity
|
|
if (Test-Path (Join-Path $script:TempDir "trufflehog_activity.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "trufflehog_activity.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$highTrufflehog = @()
|
|
$mediumTrufflehog = @()
|
|
|
|
foreach ($entry in $entries) {
|
|
$parts = $entry -split ":", 3
|
|
if ($parts.Length -ge 2) {
|
|
if ($parts[1] -eq "HIGH") {
|
|
$highTrufflehog += $entry
|
|
} elseif ($parts[1] -eq "MEDIUM") {
|
|
$mediumTrufflehog += $entry
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($highTrufflehog) {
|
|
$reportLines += "HIGH RISK: Trufflehog/secret scanning activity detected:"
|
|
foreach ($entry in $highTrufflehog) {
|
|
$parts = $entry -split ":", 3
|
|
$reportLines += " - Activity: $($parts[2])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These patterns indicate likely malicious credential harvesting."
|
|
$reportLines += "Immediate investigation and remediation required."
|
|
$reportLines += ""
|
|
}
|
|
|
|
if ($mediumTrufflehog) {
|
|
$reportLines += "MEDIUM RISK: Potentially suspicious secret scanning patterns:"
|
|
foreach ($entry in $mediumTrufflehog) {
|
|
$parts = $entry -split ":", 3
|
|
$reportLines += " - Pattern: $($parts[2])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$mediumRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These may be legitimate security tools or framework code."
|
|
$reportLines += "Manual review recommended to determine if they are malicious."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
}
|
|
|
|
# Report crypto patterns
|
|
if (Test-Path (Join-Path $script:TempDir "crypto_patterns.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "crypto_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$highCrypto = @()
|
|
$mediumCrypto = @()
|
|
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match "HIGH RISK|Known attacker wallet") {
|
|
$highCrypto += $entry
|
|
} elseif ($entry -notmatch "LOW RISK") {
|
|
$mediumCrypto += $entry
|
|
}
|
|
}
|
|
|
|
if ($highCrypto) {
|
|
$reportLines += "HIGH RISK: Cryptocurrency theft patterns detected:"
|
|
foreach ($entry in $highCrypto) {
|
|
$reportLines += " - $entry"
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These patterns strongly indicate crypto theft malware from the September 8 attack."
|
|
$reportLines += "Immediate investigation and remediation required."
|
|
$reportLines += ""
|
|
}
|
|
|
|
if ($mediumCrypto) {
|
|
$reportLines += "MEDIUM RISK: Potential cryptocurrency manipulation patterns:"
|
|
foreach ($entry in $mediumCrypto) {
|
|
$reportLines += " - $entry"
|
|
$mediumRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These may be legitimate crypto tools or framework code."
|
|
$reportLines += "Manual review recommended to determine if they are malicious."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
}
|
|
|
|
# Report Shai-Hulud repos
|
|
if (Test-Path (Join-Path $script:TempDir "shai_hulud_repos.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "shai_hulud_repos.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "HIGH RISK: Shai-Hulud repositories detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Repository: $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " $($parts[1])"
|
|
}
|
|
$highRiskCount++
|
|
}
|
|
$reportLines += "NOTE: 'Shai-Hulud' repositories are created by the malware for exfiltration."
|
|
$reportLines += "These should be deleted immediately after investigation."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report git branches
|
|
if (Test-Path (Join-Path $script:TempDir "git_branches.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "git_branches.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "MEDIUM RISK: Suspicious git branches:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Repository: $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
$reportLines += " $($parts[1])"
|
|
}
|
|
$mediumRiskCount++
|
|
}
|
|
$reportLines += "NOTE: 'shai-hulud' branches may indicate compromise."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report suspicious content
|
|
if (Test-Path (Join-Path $script:TempDir "suspicious_content.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "suspicious_content.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "MEDIUM RISK: Suspicious content patterns:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Pattern: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$mediumRiskCount++
|
|
}
|
|
$reportLines += "NOTE: Manual review required to determine if these are malicious."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report integrity issues
|
|
if (Test-Path (Join-Path $script:TempDir "integrity_issues.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "integrity_issues.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "MEDIUM RISK: Package integrity issues detected:"
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Issue: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$mediumRiskCount++
|
|
}
|
|
$reportLines += "NOTE: These issues may indicate tampering with package dependencies."
|
|
$reportLines += "Verify package versions and regenerate lockfiles if necessary."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report typosquatting (paranoid mode only)
|
|
if ($ParanoidMode -and (Test-Path (Join-Path $script:TempDir "typosquatting_warnings.txt"))) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "typosquatting_warnings.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "MEDIUM RISK (PARANOID): Potential typosquatting/homoglyph attacks detected:"
|
|
$count = 0
|
|
foreach ($entry in $entries) {
|
|
if ($count -lt 5) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Warning: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$mediumRiskCount++
|
|
$count++
|
|
}
|
|
}
|
|
if (($entries | Measure-Object).Count -gt 5) {
|
|
$reportLines += " - ... and $(($entries | Measure-Object).Count - 5) more typosquatting warnings (truncated for brevity)"
|
|
}
|
|
$reportLines += "NOTE: These packages may be impersonating legitimate packages."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Report network exfiltration (paranoid mode only)
|
|
if ($ParanoidMode -and (Test-Path (Join-Path $script:TempDir "network_exfiltration_warnings.txt"))) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "network_exfiltration_warnings.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$reportLines += "MEDIUM RISK (PARANOID): Network exfiltration patterns detected:"
|
|
$count = 0
|
|
foreach ($entry in $entries) {
|
|
if ($count -lt 5) {
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
$reportLines += " - Warning: $($parts[1])"
|
|
$reportLines += " Found in: $($parts[0])"
|
|
$mediumRiskCount++
|
|
$count++
|
|
}
|
|
}
|
|
if (($entries | Measure-Object).Count -gt 5) {
|
|
$reportLines += " - ... and $(($entries | Measure-Object).Count - 5) more network warnings (truncated for brevity)"
|
|
}
|
|
$reportLines += "NOTE: These patterns may indicate data exfiltration or communication with C2 servers."
|
|
$reportLines += ""
|
|
}
|
|
}
|
|
|
|
# Summary
|
|
$reportLines += "=============================================="
|
|
$totalIssues = $highRiskCount + $mediumRiskCount
|
|
if ($totalIssues -eq 0) {
|
|
$reportLines += "No indicators of Shai-Hulud compromise detected."
|
|
$reportLines += "Your system appears clean from this specific attack."
|
|
} else {
|
|
$reportLines += " SUMMARY:"
|
|
$reportLines += " High Risk Issues: $highRiskCount"
|
|
$reportLines += " Medium Risk Issues: $mediumRiskCount"
|
|
$reportLines += " Total Critical Issues: $totalIssues"
|
|
$reportLines += ""
|
|
$reportLines += "IMPORTANT:"
|
|
$reportLines += " - High risk issues likely indicate actual compromise"
|
|
$reportLines += " - Medium risk issues require manual investigation"
|
|
if ($ParanoidMode) {
|
|
$reportLines += " - Issues marked (PARANOID) are general security checks, not Shai-Hulud specific"
|
|
}
|
|
$reportLines += " - Consider running additional security scans"
|
|
}
|
|
$reportLines += "=============================================="
|
|
|
|
# Write all lines to file
|
|
$reportLines | Set-Content $LogFile
|
|
|
|
Write-Status "GREEN" "Comprehensive report saved to: $LogFile"
|
|
}
|
|
|
|
function Write-Report {
|
|
param([bool]$ParanoidMode)
|
|
|
|
Write-Host ""
|
|
Write-Status "BLUE" "=============================================="
|
|
if ($ParanoidMode) {
|
|
Write-Status "BLUE" " SHAI-HULUD + PARANOID SECURITY REPORT"
|
|
} else {
|
|
Write-Status "BLUE" " SHAI-HULUD DETECTION REPORT"
|
|
}
|
|
Write-Status "BLUE" "=============================================="
|
|
Write-Host ""
|
|
|
|
$script:high_risk = 0
|
|
$script:medium_risk = 0
|
|
|
|
# Report malicious workflow files
|
|
if (Test-Path (Join-Path $script:TempDir "workflow_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "workflow_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: Malicious workflow files detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report malicious file hashes
|
|
if (Test-Path (Join-Path $script:TempDir "malicious_hashes.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "malicious_hashes.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Files with known malicious hashes:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " Hash: $($parts[1])"
|
|
}
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report November 2025 Bun attack files
|
|
if (Test-Path (Join-Path $script:TempDir "bun_setup_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "bun_setup_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: November 2025 Bun attack setup files detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "bun_environment_files_found.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "bun_environment_files_found.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: November 2025 Bun environment payload detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "new_workflow_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "new_workflow_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: November 2025 malicious workflow files detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
if (Test-Path (Join-Path $script:TempDir "actions_secrets_files.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "actions_secrets_files.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: Actions secrets exfiltration files detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report discussion workflows
|
|
if (Test-Path (Join-Path $script:TempDir "discussion_workflows.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "discussion_workflows.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Malicious discussion-triggered workflows detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " Reason: $($parts[1])"
|
|
}
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report GitHub runners
|
|
if (Test-Path (Join-Path $script:TempDir "github_runners.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "github_runners.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Malicious GitHub Actions runners detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " Reason: $($parts[1])"
|
|
}
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report destructive patterns
|
|
if (Test-Path (Join-Path $script:TempDir "destructive_patterns.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "destructive_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "CRITICAL: Destructive payload patterns detected:"
|
|
Write-Status "RED" " WARNING: These patterns can cause permanent data loss!"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " Pattern: $($parts[1])"
|
|
}
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "RED" " IMMEDIATE ACTION REQUIRED: Quarantine these files and review for data destruction capabilities"
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report preinstall Bun patterns
|
|
if (Test-Path (Join-Path $script:TempDir "preinstall_bun_patterns.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "preinstall_bun_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: Fake Bun preinstall patterns detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report SHA1HULUD runners
|
|
if (Test-Path (Join-Path $script:TempDir "github_sha1hulud_runners.txt")) {
|
|
$files = Get-Content (Join-Path $script:TempDir "github_sha1hulud_runners.txt") -ErrorAction SilentlyContinue
|
|
if ($files) {
|
|
Write-Status "RED" "HIGH RISK: SHA1HULUD GitHub Actions runners detected:"
|
|
foreach ($file in $files) {
|
|
Write-Host " - $file"
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report second coming repos
|
|
if (Test-Path (Join-Path $script:TempDir "second_coming_repos.txt")) {
|
|
$repos = Get-Content (Join-Path $script:TempDir "second_coming_repos.txt") -ErrorAction SilentlyContinue
|
|
if ($repos) {
|
|
Write-Status "RED" "HIGH RISK: 'Shai-Hulud: The Second Coming' repositories detected:"
|
|
foreach ($repo in $repos) {
|
|
Write-Host " - $repo"
|
|
Write-Host " Repository description: Sha1-Hulud: The Second Coming."
|
|
$script:high_risk++
|
|
}
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report compromised packages
|
|
if (Test-Path (Join-Path $script:TempDir "compromised_found.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "compromised_found.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Compromised package versions detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Package: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These specific package versions are known to be compromised."
|
|
Write-Status "YELLOW" "You should immediately update or remove these packages."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report postinstall hooks
|
|
if (Test-Path (Join-Path $script:TempDir "postinstall_hooks.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "postinstall_hooks.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Suspicious postinstall hooks detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Hook: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: Postinstall hooks can execute arbitrary code during package installation."
|
|
Write-Status "YELLOW" "Review these hooks carefully for malicious behavior."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report Trufflehog activity
|
|
if (Test-Path (Join-Path $script:TempDir "trufflehog_activity.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "trufflehog_activity.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$highTrufflehog = @()
|
|
$mediumTrufflehog = @()
|
|
|
|
foreach ($entry in $entries) {
|
|
$parts = $entry -split ":", 3
|
|
if ($parts.Length -ge 2) {
|
|
if ($parts[1] -eq "HIGH") {
|
|
$highTrufflehog += $entry
|
|
} elseif ($parts[1] -eq "MEDIUM") {
|
|
$mediumTrufflehog += $entry
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($highTrufflehog) {
|
|
Write-Status "RED" "HIGH RISK: Trufflehog/secret scanning activity detected:"
|
|
foreach ($entry in $highTrufflehog) {
|
|
$parts = $entry -split ":", 3
|
|
Write-Host " - Activity: $($parts[2])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "RED" "NOTE: These patterns indicate likely malicious credential harvesting."
|
|
Write-Status "RED" "Immediate investigation and remediation required."
|
|
Write-Host ""
|
|
}
|
|
|
|
if ($mediumTrufflehog) {
|
|
Write-Status "YELLOW" "MEDIUM RISK: Potentially suspicious secret scanning patterns:"
|
|
foreach ($entry in $mediumTrufflehog) {
|
|
$parts = $entry -split ":", 3
|
|
Write-Host " - Pattern: $($parts[2])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:medium_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These may be legitimate security tools or framework code."
|
|
Write-Status "YELLOW" "Manual review recommended to determine if they are malicious."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
}
|
|
|
|
# Report crypto patterns
|
|
if (Test-Path (Join-Path $script:TempDir "crypto_patterns.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "crypto_patterns.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
$highCrypto = @()
|
|
$mediumCrypto = @()
|
|
|
|
foreach ($entry in $entries) {
|
|
if ($entry -match "HIGH RISK|Known attacker wallet") {
|
|
$highCrypto += $entry
|
|
} elseif ($entry -notmatch "LOW RISK") {
|
|
$mediumCrypto += $entry
|
|
}
|
|
}
|
|
|
|
if ($highCrypto) {
|
|
Write-Status "RED" "HIGH RISK: Cryptocurrency theft patterns detected:"
|
|
foreach ($entry in $highCrypto) {
|
|
Write-Host " - $entry"
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "RED" "NOTE: These patterns strongly indicate crypto theft malware from the September 8 attack."
|
|
Write-Status "RED" "Immediate investigation and remediation required."
|
|
Write-Host ""
|
|
}
|
|
|
|
if ($mediumCrypto) {
|
|
Write-Status "YELLOW" "MEDIUM RISK: Potential cryptocurrency manipulation patterns:"
|
|
foreach ($entry in $mediumCrypto) {
|
|
Write-Host " - $entry"
|
|
$script:medium_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These may be legitimate crypto tools or framework code."
|
|
Write-Status "YELLOW" "Manual review recommended to determine if they are malicious."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
}
|
|
|
|
# Report Shai-Hulud repos
|
|
if (Test-Path (Join-Path $script:TempDir "shai_hulud_repos.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "shai_hulud_repos.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "RED" "HIGH RISK: Shai-Hulud repositories detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Repository: $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " $($parts[1])"
|
|
}
|
|
$script:high_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: 'Shai-Hulud' repositories are created by the malware for exfiltration."
|
|
Write-Status "YELLOW" "These should be deleted immediately after investigation."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report git branches
|
|
if (Test-Path (Join-Path $script:TempDir "git_branches.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "git_branches.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "YELLOW" "MEDIUM RISK: Suspicious git branches:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Repository: $($parts[0])"
|
|
if ($parts.Length -gt 1) {
|
|
Write-Host " $($parts[1])"
|
|
}
|
|
$script:medium_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: 'shai-hulud' branches may indicate compromise."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report suspicious content
|
|
if (Test-Path (Join-Path $script:TempDir "suspicious_content.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "suspicious_content.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "YELLOW" "MEDIUM RISK: Suspicious content patterns:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Pattern: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:medium_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: Manual review required to determine if these are malicious."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report integrity issues
|
|
if (Test-Path (Join-Path $script:TempDir "integrity_issues.txt")) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "integrity_issues.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "YELLOW" "MEDIUM RISK: Package integrity issues detected:"
|
|
foreach ($entry in $entries) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Issue: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:medium_risk++
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These issues may indicate tampering with package dependencies."
|
|
Write-Status "YELLOW" "Verify package versions and regenerate lockfiles if necessary."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report typosquatting (paranoid mode only)
|
|
if ($ParanoidMode -and (Test-Path (Join-Path $script:TempDir "typosquatting_warnings.txt"))) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "typosquatting_warnings.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "YELLOW" "MEDIUM RISK (PARANOID): Potential typosquatting/homoglyph attacks detected:"
|
|
$count = 0
|
|
foreach ($entry in $entries) {
|
|
if ($count -lt 5) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Warning: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:medium_risk++
|
|
$count++
|
|
}
|
|
}
|
|
if (($entries | Measure-Object).Count -gt 5) {
|
|
Write-Host " - ... and $(($entries | Measure-Object).Count - 5) more typosquatting warnings (truncated for brevity)"
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These packages may be impersonating legitimate packages."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report network exfiltration (paranoid mode only)
|
|
if ($ParanoidMode -and (Test-Path (Join-Path $script:TempDir "network_exfiltration_warnings.txt"))) {
|
|
$entries = Get-Content (Join-Path $script:TempDir "network_exfiltration_warnings.txt") -ErrorAction SilentlyContinue
|
|
if ($entries) {
|
|
Write-Status "YELLOW" "MEDIUM RISK (PARANOID): Network exfiltration patterns detected:"
|
|
$count = 0
|
|
foreach ($entry in $entries) {
|
|
if ($count -lt 5) {
|
|
# Split on colon, handling Windows paths (C:\path\to\file:description)
|
|
# For Windows paths, the drive colon is at position 1, so we split on the last colon
|
|
if ($entry -match '^[A-Z]:\\.*:') {
|
|
# Windows path - split on last colon (after drive letter)
|
|
$lastColon = $entry.LastIndexOf(':')
|
|
$parts = @($entry.Substring(0, $lastColon), $entry.Substring($lastColon + 1))
|
|
} else {
|
|
# Unix path or simple format - split on first colon
|
|
$parts = $entry -split ":", 2
|
|
}
|
|
Write-Host " - Warning: $($parts[1])"
|
|
Write-Host " Found in: $($parts[0])"
|
|
$script:medium_risk++
|
|
$count++
|
|
}
|
|
}
|
|
if (($entries | Measure-Object).Count -gt 5) {
|
|
Write-Host " - ... and $(($entries | Measure-Object).Count - 5) more network warnings (truncated for brevity)"
|
|
}
|
|
Write-Status "YELLOW" "NOTE: These patterns may indicate data exfiltration or communication with C2 servers."
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Report credential inventory (always show, even in quiet mode)
|
|
if ($script:JsonOutput.credential_inventory.Count -gt 0) {
|
|
if ($script:OutputMode -ne "json") {
|
|
Write-Host ""
|
|
Write-Status "ORANGE" "==============================================" -AlwaysShow
|
|
Write-Status "ORANGE" "CREDENTIAL INVENTORY - ACTION REQUIRED" -AlwaysShow
|
|
Write-Status "ORANGE" "==============================================" -AlwaysShow
|
|
Write-Host ""
|
|
Write-Status "RED" "⚠️ The following credentials may have been exposed:" -AlwaysShow
|
|
Write-Host ""
|
|
|
|
$grouped = $script:JsonOutput.credential_inventory | Group-Object type
|
|
foreach ($group in $grouped) {
|
|
$first = $group.Group[0]
|
|
Write-Status "RED" "🔑 $($first.type): $($first.description)"
|
|
Write-Host " Severity: $($first.severity)"
|
|
Write-Host " Rotation URL: $($first.rotation_url)"
|
|
Write-Host " Found in:"
|
|
foreach ($item in $group.Group) {
|
|
Write-Host " - $($item.found_in)"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
Write-Status "YELLOW" "⚠️ IMMEDIATE ACTION REQUIRED:" -AlwaysShow
|
|
Write-Status "YELLOW" " 1. Rotate all listed credentials immediately" -AlwaysShow
|
|
Write-Status "YELLOW" " 2. Review access logs for unauthorized usage" -AlwaysShow
|
|
Write-Status "YELLOW" " 3. Enable MFA/2FA where available" -AlwaysShow
|
|
Write-Status "YELLOW" " 4. Review and revoke any suspicious access" -AlwaysShow
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# Summary (always show, even in quiet mode)
|
|
if ($script:OutputMode -ne "json") {
|
|
Write-Status "BLUE" "==============================================" -AlwaysShow
|
|
$totalIssues = $script:high_risk + $script:medium_risk
|
|
if ($totalIssues -eq 0) {
|
|
Write-Status "GREEN" "No indicators of Shai-Hulud compromise detected." -AlwaysShow
|
|
Write-Status "GREEN" "Your system appears clean from this specific attack." -AlwaysShow
|
|
} else {
|
|
Write-Status "RED" " SUMMARY:" -AlwaysShow
|
|
Write-Status "RED" " High Risk Issues: $script:high_risk" -AlwaysShow
|
|
Write-Status "YELLOW" " Medium Risk Issues: $script:medium_risk" -AlwaysShow
|
|
Write-Status "BLUE" " Total Critical Issues: $totalIssues" -AlwaysShow
|
|
Write-Host ""
|
|
if ($script:OutputMode -eq "verbose" -or $script:OutputMode -eq "normal") {
|
|
Write-Status "YELLOW" "IMPORTANT:"
|
|
Write-Status "YELLOW" " - High risk issues likely indicate actual compromise"
|
|
Write-Status "YELLOW" " - Medium risk issues require manual investigation"
|
|
if ($ParanoidMode) {
|
|
Write-Status "YELLOW" " - Issues marked (PARANOID) are general security checks, not Shai-Hulud specific"
|
|
}
|
|
Write-Status "YELLOW" " - Consider running additional security scans"
|
|
}
|
|
}
|
|
Write-Status "BLUE" "==============================================" -AlwaysShow
|
|
}
|
|
}
|
|
|
|
function Write-JsonOutput {
|
|
# Collect all findings into JSON structure
|
|
$json = @{
|
|
timestamp = $script:JsonOutput.timestamp
|
|
scan_directory = $script:JsonOutput.scan_directory
|
|
findings = @{
|
|
high_risk = @()
|
|
medium_risk = @()
|
|
}
|
|
summary = @{
|
|
high_risk_count = $script:high_risk
|
|
medium_risk_count = $script:medium_risk
|
|
total_issues = $script:high_risk + $script:medium_risk
|
|
exit_code = if ($script:high_risk -gt 0) { 1 } elseif ($script:medium_risk -gt 0) { 2 } else { 0 }
|
|
}
|
|
credential_inventory = $script:JsonOutput.credential_inventory
|
|
}
|
|
|
|
# Collect high risk findings
|
|
$highRiskFiles = @(
|
|
(Join-Path $script:TempDir "workflow_files.txt"),
|
|
(Join-Path $script:TempDir "malicious_hashes.txt"),
|
|
(Join-Path $script:TempDir "compromised_found.txt"),
|
|
(Join-Path $script:TempDir "postinstall_hooks.txt"),
|
|
(Join-Path $script:TempDir "bun_setup_files.txt"),
|
|
(Join-Path $script:TempDir "bun_environment_files_found.txt"),
|
|
(Join-Path $script:TempDir "actions_secrets_files.txt"),
|
|
(Join-Path $script:TempDir "new_workflow_files.txt"),
|
|
(Join-Path $script:TempDir "github_sha1hulud_runners.txt"),
|
|
(Join-Path $script:TempDir "preinstall_bun_patterns.txt"),
|
|
(Join-Path $script:TempDir "second_coming_repos.txt"),
|
|
(Join-Path $script:TempDir "shai_hulud_repos.txt"),
|
|
(Join-Path $script:TempDir "github_runners.txt"),
|
|
(Join-Path $script:TempDir "destructive_patterns.txt")
|
|
)
|
|
|
|
foreach ($file in $highRiskFiles) {
|
|
if (Test-Path $file) {
|
|
$entries = Get-Content $file -ErrorAction SilentlyContinue
|
|
foreach ($entry in $entries) {
|
|
$json.findings.high_risk += $entry
|
|
}
|
|
}
|
|
}
|
|
|
|
# Collect medium risk findings
|
|
$mediumRiskFiles = @(
|
|
(Join-Path $script:TempDir "suspicious_content.txt"),
|
|
(Join-Path $script:TempDir "git_branches.txt"),
|
|
(Join-Path $script:TempDir "namespace_warnings.txt"),
|
|
(Join-Path $script:TempDir "integrity_issues.txt"),
|
|
(Join-Path $script:TempDir "typosquatting_warnings.txt"),
|
|
(Join-Path $script:TempDir "network_exfiltration_warnings.txt")
|
|
)
|
|
|
|
foreach ($file in $mediumRiskFiles) {
|
|
if (Test-Path $file) {
|
|
$entries = Get-Content $file -ErrorAction SilentlyContinue
|
|
foreach ($entry in $entries) {
|
|
$json.findings.medium_risk += $entry
|
|
}
|
|
}
|
|
}
|
|
|
|
# Output JSON
|
|
$json | ConvertTo-Json -Depth 10 | Write-Output
|
|
}
|
|
|
|
# Main function
|
|
function Main {
|
|
# Load compromised packages
|
|
Load-CompromisedPackages
|
|
|
|
# Create temporary directory
|
|
New-TempDir
|
|
|
|
# Convert scan directory to absolute path
|
|
if (-not (Test-Path $ScanDir)) {
|
|
if ($script:OutputMode -eq "json") {
|
|
Write-Output (@{error = "Directory does not exist: $ScanDir"; exit_code = 1} | ConvertTo-Json)
|
|
} else {
|
|
Write-Error "Error: Directory does not exist: $ScanDir"
|
|
}
|
|
exit 1
|
|
}
|
|
|
|
$ScanDir = Resolve-Path $ScanDir
|
|
$script:JsonOutput.scan_directory = $ScanDir
|
|
|
|
if ($script:OutputMode -ne "quiet" -and $script:OutputMode -ne "json") {
|
|
Write-Status "GREEN" "Starting Shai-Hulud detection scan..."
|
|
if ($Paranoid) {
|
|
Write-Status "BLUE" "Scanning directory: $ScanDir (with paranoid mode enabled)"
|
|
} else {
|
|
Write-Status "BLUE" "Scanning directory: $ScanDir"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
# Stage 1: Collect files
|
|
Write-Status "ORANGE" "[Stage 1/6] Collecting file inventory for analysis"
|
|
Collect-AllFiles $ScanDir
|
|
$totalFiles = (Get-Content (Join-Path $script:TempDir "all_files_raw.txt") -ErrorAction SilentlyContinue | Measure-Object).Count
|
|
Write-StageComplete "File collection: $totalFiles files found"
|
|
|
|
# Stage 2: Core detection
|
|
Write-Status "ORANGE" '[Stage 2/6] Core detection (workflows, hashes, packages, hooks)'
|
|
Test-WorkflowFiles $ScanDir
|
|
Test-FileHashes $ScanDir
|
|
Test-Packages $ScanDir
|
|
Test-PostinstallHooks $ScanDir
|
|
Write-StageComplete "Core detection"
|
|
|
|
# Stage 3: Content analysis
|
|
Write-Status "ORANGE" '[Stage 3/6] Content analysis (patterns, crypto, trufflehog, git)'
|
|
Test-Content $ScanDir
|
|
Test-CryptoTheftPatterns $ScanDir
|
|
Test-TrufflehogActivity $ScanDir
|
|
Test-GitBranches $ScanDir
|
|
Write-StageComplete "Content analysis"
|
|
|
|
# Stage 4: Repository analysis
|
|
Write-Status "ORANGE" '[Stage 4/6] Repository analysis (repos, integrity, bun, workflows)'
|
|
Test-ShaiHuludRepos $ScanDir
|
|
Test-PackageIntegrity $ScanDir
|
|
Test-BunAttackFiles $ScanDir
|
|
Test-NewWorkflowPatterns $ScanDir
|
|
Write-StageComplete "Repository analysis"
|
|
|
|
# Stage 5: Advanced detection
|
|
Write-Status "ORANGE" '[Stage 5/6] Advanced detection (discussions, runners, destructive)'
|
|
Test-DiscussionWorkflows $ScanDir
|
|
Test-GitHubRunners $ScanDir
|
|
Test-DestructivePatterns $ScanDir
|
|
Test-PreinstallBunPatterns $ScanDir
|
|
Write-StageComplete "Advanced detection"
|
|
|
|
# Stage 6: Final checks
|
|
Write-Status "ORANGE" "[Stage 6/6] Final checks (actions runner, second coming repos)"
|
|
Test-GitHubActionsRunner $ScanDir
|
|
Test-SecondComingRepos $ScanDir
|
|
Write-StageComplete "Final checks"
|
|
|
|
# Paranoid mode checks
|
|
if ($Paranoid) {
|
|
Write-Status "BLUE" "[Paranoid] Running extra security checks"
|
|
Test-Typosquatting $ScanDir
|
|
Test-NetworkExfiltration $ScanDir
|
|
Write-StageComplete "Paranoid mode checks"
|
|
}
|
|
|
|
# Collect credential inventory
|
|
Collect-CredentialInventory $ScanDir
|
|
|
|
# Generate report (skip in JSON mode)
|
|
if ($script:OutputMode -ne "json") {
|
|
Write-Status "BLUE" 'Generating report'
|
|
Write-Report $Paranoid
|
|
}
|
|
|
|
# Write log file if requested
|
|
if ($SaveLog) {
|
|
Write-LogFile $SaveLog $Paranoid
|
|
}
|
|
|
|
Write-StageComplete 'Total scan time'
|
|
|
|
# Output JSON if requested
|
|
if ($script:OutputMode -eq "json") {
|
|
Write-JsonOutput
|
|
}
|
|
|
|
# Return appropriate exit code
|
|
if ($script:high_risk -gt 0) {
|
|
exit 1
|
|
} elseif ($script:medium_risk -gt 0) {
|
|
exit 2
|
|
} else {
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
# Run main function
|
|
try {
|
|
Main
|
|
} finally {
|
|
Remove-TempDir
|
|
}
|
|
|