vtk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,625 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Shai-Hulud Machine Infection Checker for Windows
4
+
5
+ .DESCRIPTION
6
+ Quick script to check if your machine shows signs of active Shai-Hulud infection.
7
+ This is a FAST check (~5 seconds) for infection indicators only.
8
+
9
+ WHAT THIS CHECKS:
10
+
11
+ CRITICAL (Active Infection):
12
+ - %USERPROFILE%\.dev-env\ persistence folder (contains GitHub self-hosted runner)
13
+ - %USERPROFILE%\.truffler-cache\ malware cache (NOT legit Trufflehog which uses .trufflehog)
14
+ - Running processes: Runner.Listener, SHA1HULUD, suspicious node/bun
15
+ - Malware files: setup_bun.js, bun_environment.js
16
+ - Backdoor workflows: .github\workflows\discussion.yaml
17
+
18
+ HIGH (Exfiltration Likely Occurred):
19
+ - Exfiltration artifacts: cloud.json, truffleSecrets.json, etc.
20
+ - Unexpected %USERPROFILE%\.bun\ installation
21
+ - Unexpected Trufflehog binary
22
+
23
+ INFO (Credentials to Rotate if Infected):
24
+ - Lists credential files the malware targets
25
+ - .npmrc, .aws\, .config\gh\, etc.
26
+
27
+ EXIT CODES:
28
+ 0 - Clean (no infection indicators found)
29
+ 1 - INFECTED (critical indicators found)
30
+ 2 - WARNING (high-risk indicators, needs investigation)
31
+
32
+ .PARAMETER Verbose
33
+ Show detailed output with all checks
34
+
35
+ .PARAMETER Quiet
36
+ Exit code only, no output
37
+
38
+ .PARAMETER Json
39
+ Output results as JSON
40
+
41
+ .PARAMETER ScanDirs
42
+ Additional directories to scan for backdoor workflows (comma-separated)
43
+
44
+ .EXAMPLE
45
+ .\shai-hulud-machine-check.ps1
46
+ Runs with compact output
47
+
48
+ .EXAMPLE
49
+ .\shai-hulud-machine-check.ps1 -Verbose
50
+ Runs with detailed output
51
+
52
+ .EXAMPLE
53
+ .\shai-hulud-machine-check.ps1 -Json
54
+ Outputs JSON format
55
+
56
+ .NOTES
57
+ Author: Eric Boehs / EERT (with Claude Code)
58
+ Version: 1.0.0
59
+ Date: December 2025
60
+ Requires: PowerShell 5.1+
61
+
62
+ References:
63
+ - Wiz.io: https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack
64
+ - Datadog: https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/
65
+ #>
66
+
67
+ [CmdletBinding()]
68
+ param(
69
+ [switch]$Quiet,
70
+ [switch]$Json,
71
+ [string]$ScanDirs,
72
+ [switch]$Help
73
+ )
74
+
75
+ # Handle help
76
+ if ($Help) {
77
+ Get-Help $MyInvocation.MyCommand.Path -Detailed
78
+ exit 0
79
+ }
80
+
81
+ # Results tracking
82
+ $script:CriticalFindings = [System.Collections.ArrayList]::new()
83
+ $script:HighFindings = [System.Collections.ArrayList]::new()
84
+ $script:InfoFindings = [System.Collections.ArrayList]::new()
85
+
86
+ # Default directories to scan for backdoor workflows
87
+ $DefaultScanDirs = @(
88
+ "$env:USERPROFILE\Code",
89
+ "$env:USERPROFILE\Projects",
90
+ "$env:USERPROFILE\src",
91
+ "$env:USERPROFILE\dev",
92
+ "$env:USERPROFILE\workspace",
93
+ "$env:USERPROFILE\repos"
94
+ )
95
+
96
+ # Build final scan dirs list
97
+ $AllScanDirs = [System.Collections.ArrayList]::new()
98
+ foreach ($dir in $DefaultScanDirs) {
99
+ [void]$AllScanDirs.Add($dir)
100
+ }
101
+
102
+ if ($ScanDirs) {
103
+ $extraDirs = $ScanDirs -split ','
104
+ foreach ($dir in $extraDirs) {
105
+ $expanded = $dir.Trim() -replace '^~', $env:USERPROFILE
106
+ [void]$AllScanDirs.Add($expanded)
107
+ }
108
+ }
109
+
110
+ # Color support
111
+ # SupportsVirtualTerminal doesn't exist in PS 5.1, so check safely
112
+ $UseColors = -not $Quiet -and -not $Json -and (($Host.UI.psobject.Properties.Name -contains 'SupportsVirtualTerminal') -and $Host.UI.SupportsVirtualTerminal)
113
+
114
+ function Write-Log {
115
+ param([string]$Message)
116
+ if (-not $Quiet -and -not $Json) {
117
+ Write-Host $Message
118
+ }
119
+ }
120
+
121
+ function Write-LogVerbose {
122
+ param([string]$Message)
123
+ if ($VerbosePreference -eq 'Continue' -and -not $Quiet -and -not $Json) {
124
+ Write-Host $Message
125
+ }
126
+ }
127
+
128
+ function Write-ColorText {
129
+ param(
130
+ [string]$Text,
131
+ [string]$Color = 'White'
132
+ )
133
+ if ($UseColors) {
134
+ Write-Host $Text -ForegroundColor $Color -NoNewline
135
+ } else {
136
+ Write-Host $Text -NoNewline
137
+ }
138
+ }
139
+
140
+ function Log-Critical {
141
+ param([string]$Finding)
142
+ [void]$script:CriticalFindings.Add($Finding)
143
+ }
144
+
145
+ function Log-High {
146
+ param([string]$Finding)
147
+ [void]$script:HighFindings.Add($Finding)
148
+ }
149
+
150
+ function Log-Info {
151
+ param([string]$Finding)
152
+ [void]$script:InfoFindings.Add($Finding)
153
+ }
154
+
155
+ # Verbose header
156
+ Write-LogVerbose ""
157
+ Write-LogVerbose "========================================"
158
+ Write-LogVerbose " Shai-Hulud Machine Infection Check"
159
+ Write-LogVerbose "========================================"
160
+ Write-LogVerbose ""
161
+ Write-LogVerbose "Checking for active infection indicators..."
162
+ Write-LogVerbose ""
163
+
164
+ ###########################################
165
+ # CRITICAL CHECKS
166
+ ###########################################
167
+
168
+ Write-LogVerbose "=== Critical Checks ==="
169
+ Write-LogVerbose ""
170
+
171
+ # 1. Check for persistence folder
172
+ Write-LogVerbose "Checking for persistence folder (~\.dev-env\)..."
173
+ $devEnvPath = Join-Path $env:USERPROFILE ".dev-env"
174
+ if (Test-Path $devEnvPath -PathType Container) {
175
+ Log-Critical "~\.dev-env\ persistence folder found"
176
+ Write-LogVerbose " [CRITICAL] PERSISTENCE FOLDER FOUND: $devEnvPath"
177
+ Write-LogVerbose " This folder contains the malware's self-hosted GitHub runner."
178
+ Write-LogVerbose " Contents:"
179
+ Get-ChildItem $devEnvPath -ErrorAction SilentlyContinue | Select-Object -First 10 | ForEach-Object {
180
+ Write-LogVerbose " $($_.Name)"
181
+ }
182
+ } else {
183
+ Write-LogVerbose " Not found"
184
+ }
185
+
186
+ # 1b. Check for malware Trufflehog cache
187
+ Write-LogVerbose ""
188
+ Write-LogVerbose "Checking for malware Trufflehog cache (~\.truffler-cache\)..."
189
+ $trufflerCachePath = Join-Path $env:USERPROFILE ".truffler-cache"
190
+ if (Test-Path $trufflerCachePath -PathType Container) {
191
+ Log-Critical "~\.truffler-cache\ malware cache found"
192
+ Write-LogVerbose " [CRITICAL] MALWARE CACHE FOUND: $trufflerCachePath"
193
+ Write-LogVerbose " This is a MALWARE-SPECIFIC path (legit Trufflehog uses .trufflehog)."
194
+ Write-LogVerbose " Contents:"
195
+ Get-ChildItem $trufflerCachePath -ErrorAction SilentlyContinue | Select-Object -First 10 | ForEach-Object {
196
+ Write-LogVerbose " $($_.Name)"
197
+ }
198
+ } else {
199
+ Write-LogVerbose " Not found"
200
+ }
201
+
202
+ # 2. Check for malicious processes
203
+ Write-LogVerbose ""
204
+ Write-LogVerbose "Checking for malicious processes..."
205
+
206
+ $maliciousProcessFound = $false
207
+
208
+ # Runner.Listener
209
+ $runnerProcs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like "*Runner.Listener*" -or $_.Path -like "*Runner.Listener*" }
210
+ if ($runnerProcs) {
211
+ Log-Critical "Runner.Listener process running"
212
+ Write-LogVerbose " [CRITICAL] MALICIOUS PROCESS: Runner.Listener is running"
213
+ Write-LogVerbose " PIDs: $($runnerProcs.Id -join ', ')"
214
+ $maliciousProcessFound = $true
215
+ }
216
+
217
+ # SHA1HULUD
218
+ $sha1hulud = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like "*SHA1HULUD*" -or $_.Path -like "*SHA1HULUD*" }
219
+ if ($sha1hulud) {
220
+ Log-Critical "SHA1HULUD process running"
221
+ Write-LogVerbose " [CRITICAL] MALICIOUS PROCESS: SHA1HULUD is running"
222
+ Write-LogVerbose " PIDs: $($sha1hulud.Id -join ', ')"
223
+ $maliciousProcessFound = $true
224
+ }
225
+
226
+ # Processes from .dev-env
227
+ $devEnvProcs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Path -like "*\.dev-env\*" }
228
+ if ($devEnvProcs) {
229
+ Log-Critical "Process running from ~\.dev-env"
230
+ Write-LogVerbose " [CRITICAL] MALICIOUS PROCESS: Process running from .dev-env"
231
+ Write-LogVerbose " PIDs: $($devEnvProcs.Id -join ', ')"
232
+ $maliciousProcessFound = $true
233
+ }
234
+
235
+ # Suspicious bun processes
236
+ $bunPath = Join-Path $env:USERPROFILE ".bun\bin\bun.exe"
237
+ $bunProcs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $bunPath }
238
+ if ($bunProcs) {
239
+ Log-High "bun process running from ~\.bun\bin\bun.exe"
240
+ Write-LogVerbose " [HIGH] SUSPICIOUS PROCESS: bun running from .bun"
241
+ Write-LogVerbose " This could be legitimate if you installed Bun intentionally."
242
+ Write-LogVerbose " PIDs: $($bunProcs.Id -join ', ')"
243
+ }
244
+
245
+ if (-not $maliciousProcessFound) {
246
+ Write-LogVerbose " No malicious processes detected"
247
+ }
248
+
249
+ # 3. Check for malware files
250
+ Write-LogVerbose ""
251
+ Write-LogVerbose "Checking for malware files..."
252
+
253
+ $malwareFiles = @("setup_bun.js", "bun_environment.js")
254
+ $malwareFound = $false
255
+
256
+ foreach ($file in $malwareFiles) {
257
+ $filePath = Join-Path $env:USERPROFILE $file
258
+ if (Test-Path $filePath -PathType Leaf) {
259
+ Log-Critical "Malware file: ~\$file"
260
+ Write-LogVerbose " [CRITICAL] MALWARE FILE FOUND: $filePath"
261
+ $malwareFound = $true
262
+ }
263
+ }
264
+
265
+ # Check common locations
266
+ $searchDirs = @(
267
+ $env:USERPROFILE,
268
+ (Join-Path $env:USERPROFILE "Desktop"),
269
+ (Join-Path $env:USERPROFILE "Downloads"),
270
+ $env:TEMP
271
+ )
272
+
273
+ foreach ($dir in $searchDirs) {
274
+ if (Test-Path $dir -PathType Container) {
275
+ foreach ($file in $malwareFiles) {
276
+ $found = Get-ChildItem -Path $dir -Filter $file -Recurse -Depth 2 -ErrorAction SilentlyContinue | Select-Object -First 5
277
+ foreach ($f in $found) {
278
+ Log-Critical "Malware file: $($f.FullName)"
279
+ Write-LogVerbose " [CRITICAL] MALWARE FILE FOUND: $($f.FullName)"
280
+ $malwareFound = $true
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ if (-not $malwareFound) {
287
+ Write-LogVerbose " No malware files detected"
288
+ }
289
+
290
+ # 4. Check for backdoor workflows
291
+ Write-LogVerbose ""
292
+ Write-LogVerbose "Checking for backdoor workflow files..."
293
+ Write-LogVerbose " Scanning directories:"
294
+
295
+ foreach ($dir in $AllScanDirs) {
296
+ if (Test-Path $dir -PathType Container) {
297
+ Write-LogVerbose " - $dir"
298
+ } else {
299
+ Write-LogVerbose " - $dir (not found)"
300
+ }
301
+ }
302
+
303
+ Write-LogVerbose ""
304
+ $backdoorFound = $false
305
+
306
+ foreach ($baseDir in $AllScanDirs) {
307
+ if (-not (Test-Path $baseDir -PathType Container)) { continue }
308
+
309
+ $workflowFiles = Get-ChildItem -Path $baseDir -Filter "discussion.yaml" -Recurse -Depth 5 -ErrorAction SilentlyContinue |
310
+ Where-Object { $_.DirectoryName -like "*\.github\workflows*" } |
311
+ Select-Object -First 10
312
+
313
+ foreach ($wf in $workflowFiles) {
314
+ $content = Get-Content $wf.FullName -Raw -ErrorAction SilentlyContinue
315
+ if ($content -match "self-hosted" -and $content -match "github\.event\.discussion") {
316
+ Log-Critical "Backdoor workflow: $($wf.FullName)"
317
+ Write-LogVerbose " [CRITICAL] BACKDOOR WORKFLOW FOUND: $($wf.FullName)"
318
+ Write-LogVerbose " This workflow uses self-hosted runner with discussion body injection."
319
+ $backdoorFound = $true
320
+ } else {
321
+ Log-High "Unverified discussion.yaml: $($wf.FullName)"
322
+ Write-LogVerbose " [HIGH] UNVERIFIED WORKFLOW: $($wf.FullName)"
323
+ Write-LogVerbose " Found discussion.yaml - please verify this is legitimate."
324
+ }
325
+ }
326
+ }
327
+
328
+ if (-not $backdoorFound) {
329
+ Write-LogVerbose " No backdoor workflows detected"
330
+ }
331
+
332
+ ###########################################
333
+ # HIGH-RISK CHECKS
334
+ ###########################################
335
+
336
+ Write-LogVerbose ""
337
+ Write-LogVerbose "=== High-Risk Checks ==="
338
+ Write-LogVerbose ""
339
+
340
+ # 5. Check for exfiltration artifacts
341
+ Write-LogVerbose "Checking for exfiltration artifacts..."
342
+ $exfilFiles = @("cloud.json", "truffleSecrets.json", "environment.json", "actionsSecrets.json", "contents.json")
343
+ $exfilFound = $false
344
+
345
+ foreach ($file in $exfilFiles) {
346
+ foreach ($dir in @($env:USERPROFILE, $env:TEMP, (Join-Path $env:USERPROFILE "Desktop"), (Join-Path $env:USERPROFILE "Downloads"))) {
347
+ $filePath = Join-Path $dir $file
348
+ if (Test-Path $filePath -PathType Leaf) {
349
+ Log-High "Exfiltration artifact: $filePath"
350
+ Write-LogVerbose " [HIGH] EXFILTRATION ARTIFACT: $filePath"
351
+ $exfilFound = $true
352
+ }
353
+ }
354
+ }
355
+
356
+ if (-not $exfilFound) {
357
+ Write-LogVerbose " No exfiltration artifacts detected"
358
+ }
359
+
360
+ # 6. Check for unexpected Bun installation
361
+ Write-LogVerbose ""
362
+ Write-LogVerbose "Checking for unexpected Bun installation..."
363
+ $bunDir = Join-Path $env:USERPROFILE ".bun"
364
+ if (Test-Path $bunDir -PathType Container) {
365
+ Log-High "~\.bun\ installation found"
366
+ Write-LogVerbose " [HIGH] BUN INSTALLATION FOUND: $bunDir"
367
+ Write-LogVerbose " If you did NOT install Bun intentionally, this is suspicious."
368
+ $bunExe = Join-Path $bunDir "bin\bun.exe"
369
+ if (Test-Path $bunExe) {
370
+ try {
371
+ $bunVersion = & $bunExe --version 2>$null
372
+ Write-LogVerbose " Bun version: $bunVersion"
373
+ } catch {
374
+ Write-LogVerbose " Bun version: unknown"
375
+ }
376
+ }
377
+ } elseif (Get-Command bun -ErrorAction SilentlyContinue) {
378
+ $bunPath = (Get-Command bun).Source
379
+ Log-High "Bun found: $bunPath"
380
+ Write-LogVerbose " [HIGH] BUN FOUND IN PATH: $bunPath"
381
+ Write-LogVerbose " If you did NOT install Bun intentionally, investigate."
382
+ } else {
383
+ Write-LogVerbose " No unexpected Bun installation"
384
+ }
385
+
386
+ # 7. Check for Trufflehog
387
+ Write-LogVerbose ""
388
+ Write-LogVerbose "Checking for unexpected Trufflehog..."
389
+ if (Get-Command trufflehog -ErrorAction SilentlyContinue) {
390
+ $thPath = (Get-Command trufflehog).Source
391
+ Log-High "Trufflehog found: $thPath"
392
+ Write-LogVerbose " [HIGH] TRUFFLEHOG FOUND: $thPath"
393
+ Write-LogVerbose " The malware uses Trufflehog to scan for secrets."
394
+ Write-LogVerbose " If you did NOT install this intentionally, investigate."
395
+ } else {
396
+ $suspiciousLocations = @(
397
+ (Join-Path $env:USERPROFILE ".local\bin\trufflehog.exe"),
398
+ (Join-Path $env:TEMP "trufflehog.exe")
399
+ )
400
+ $thFound = $false
401
+ foreach ($loc in $suspiciousLocations) {
402
+ if (Test-Path $loc -PathType Leaf) {
403
+ Log-High "Trufflehog in suspicious location: $loc"
404
+ Write-LogVerbose " [HIGH] TRUFFLEHOG FOUND in suspicious location: $loc"
405
+ $thFound = $true
406
+ }
407
+ }
408
+ if (-not $thFound) {
409
+ Write-LogVerbose " No unexpected Trufflehog installation"
410
+ }
411
+ }
412
+
413
+ ###########################################
414
+ # INFORMATIONAL - Credentials at Risk
415
+ ###########################################
416
+
417
+ Write-LogVerbose ""
418
+ Write-LogVerbose "=== Credential Files (Rotate if Infected) ==="
419
+ Write-LogVerbose ""
420
+ Write-LogVerbose "These are files the malware targets for exfiltration."
421
+ Write-LogVerbose "If your machine is infected, ROTATE ALL OF THESE:"
422
+ Write-LogVerbose ""
423
+
424
+ # NPM tokens
425
+ $npmrcPath = Join-Path $env:USERPROFILE ".npmrc"
426
+ if (Test-Path $npmrcPath -PathType Leaf) {
427
+ $content = Get-Content $npmrcPath -Raw -ErrorAction SilentlyContinue
428
+ if ($content -match "authtoken|_auth") {
429
+ Log-Info "~\.npmrc contains auth tokens"
430
+ Write-LogVerbose " [INFO] ~\.npmrc contains auth tokens - ROTATE NPM TOKENS"
431
+ } else {
432
+ Write-LogVerbose " ~\.npmrc exists (no tokens detected)"
433
+ }
434
+ } else {
435
+ Write-LogVerbose " ~\.npmrc not found"
436
+ }
437
+
438
+ # AWS credentials
439
+ $awsDir = Join-Path $env:USERPROFILE ".aws"
440
+ if (Test-Path $awsDir -PathType Container) {
441
+ Log-Info "~\.aws\ exists"
442
+ Write-LogVerbose " [INFO] ~\.aws\ exists - ROTATE AWS ACCESS KEYS"
443
+ } else {
444
+ Write-LogVerbose " ~\.aws\ not found"
445
+ }
446
+
447
+ # GCP credentials
448
+ $gcpAdcPath = Join-Path $env:APPDATA "gcloud\application_default_credentials.json"
449
+ if (Test-Path $gcpAdcPath -PathType Leaf) {
450
+ Log-Info "GCP ADC exists"
451
+ Write-LogVerbose " [INFO] GCP ADC exists - RE-AUTHENTICATE with 'gcloud auth application-default login'"
452
+ } else {
453
+ Write-LogVerbose " GCP ADC not found"
454
+ }
455
+
456
+ # Azure credentials
457
+ $azureDir = Join-Path $env:USERPROFILE ".azure"
458
+ if (Test-Path $azureDir -PathType Container) {
459
+ Log-Info "~\.azure\ exists"
460
+ Write-LogVerbose " [INFO] ~\.azure\ exists - RE-AUTHENTICATE with 'az login'"
461
+ } else {
462
+ Write-LogVerbose " ~\.azure\ not found"
463
+ }
464
+
465
+ # GitHub CLI
466
+ $ghHostsPath = Join-Path $env:APPDATA "GitHub CLI\hosts.yml"
467
+ if (Test-Path $ghHostsPath -PathType Leaf) {
468
+ Log-Info "GitHub CLI authenticated"
469
+ Write-LogVerbose " [INFO] GitHub CLI authenticated - ROTATE TOKEN with 'gh auth logout && gh auth login'"
470
+ } else {
471
+ Write-LogVerbose " GitHub CLI not authenticated"
472
+ }
473
+
474
+ # SSH keys
475
+ $sshDir = Join-Path $env:USERPROFILE ".ssh"
476
+ if (Test-Path $sshDir -PathType Container) {
477
+ $pubKeys = Get-ChildItem -Path $sshDir -Filter "*.pub" -ErrorAction SilentlyContinue
478
+ if ($pubKeys) {
479
+ Log-Info "SSH keys exist"
480
+ Write-LogVerbose " [INFO] SSH keys exist (~\.ssh\) - Consider rotating if infected"
481
+ }
482
+ }
483
+
484
+ # Git credentials
485
+ $gitCredPath = Join-Path $env:USERPROFILE ".git-credentials"
486
+ if (Test-Path $gitCredPath -PathType Leaf) {
487
+ Log-Info "~\.git-credentials exists"
488
+ Write-LogVerbose " [INFO] ~\.git-credentials exists - ROTATE stored credentials"
489
+ }
490
+
491
+ # Environment variables with secrets
492
+ $sensitiveVars = [Environment]::GetEnvironmentVariables() |
493
+ Where-Object { $_.Key -match "token|key|secret|password|credential|auth|api" } |
494
+ Measure-Object
495
+
496
+ if ($sensitiveVars.Count -gt 0) {
497
+ Log-Info "$($sensitiveVars.Count) sensitive env vars"
498
+ Write-LogVerbose " [INFO] $($sensitiveVars.Count) sensitive environment variables detected"
499
+ }
500
+
501
+ ###########################################
502
+ # SUMMARY
503
+ ###########################################
504
+
505
+ Write-LogVerbose ""
506
+ Write-LogVerbose "========================================"
507
+
508
+ $exitCode = 0
509
+
510
+ if ($script:CriticalFindings.Count -gt 0) {
511
+ $exitCode = 1
512
+ if ($VerbosePreference -eq 'Continue') {
513
+ Write-Log ""
514
+ if ($UseColors) {
515
+ Write-Host " STATUS: INFECTED" -ForegroundColor Red
516
+ Write-Host "========================================" -ForegroundColor Red
517
+ } else {
518
+ Write-Log " STATUS: INFECTED"
519
+ Write-Log "========================================"
520
+ }
521
+ Write-Log ""
522
+ Write-Log "CRITICAL FINDINGS ($($script:CriticalFindings.Count)):"
523
+ foreach ($finding in $script:CriticalFindings) {
524
+ Write-Log " - $finding"
525
+ }
526
+ Write-Log ""
527
+ Write-Log "IMMEDIATE ACTIONS:"
528
+ Write-Log " 1. DISCONNECT from network immediately"
529
+ Write-Log " 2. Do NOT run npm/yarn/node commands"
530
+ Write-Log " 3. Follow the cleanup playbook"
531
+ Write-Log " 4. Rotate ALL credentials listed above"
532
+ } else {
533
+ if ($UseColors) {
534
+ Write-Host "Shai-Hulud Check: INFECTED" -ForegroundColor Red
535
+ } else {
536
+ Write-Log "Shai-Hulud Check: INFECTED"
537
+ }
538
+ foreach ($finding in $script:CriticalFindings) {
539
+ Write-Log " - $finding"
540
+ }
541
+ Write-Log ""
542
+ Write-Log "Run with -Verbose for details and remediation steps"
543
+ }
544
+ } elseif ($script:HighFindings.Count -gt 0) {
545
+ $exitCode = 2
546
+ if ($VerbosePreference -eq 'Continue') {
547
+ Write-Log ""
548
+ if ($UseColors) {
549
+ Write-Host " STATUS: WARNING - INVESTIGATE" -ForegroundColor Yellow
550
+ Write-Host "========================================" -ForegroundColor Yellow
551
+ } else {
552
+ Write-Log " STATUS: WARNING - INVESTIGATE"
553
+ Write-Log "========================================"
554
+ }
555
+ Write-Log ""
556
+ Write-Log "HIGH-RISK FINDINGS ($($script:HighFindings.Count)):"
557
+ foreach ($finding in $script:HighFindings) {
558
+ Write-Log " - $finding"
559
+ }
560
+ Write-Log ""
561
+ Write-Log "RECOMMENDED ACTIONS:"
562
+ Write-Log " 1. Verify if Bun/Trufflehog were installed intentionally"
563
+ Write-Log " 2. If not intentional, treat as potentially infected"
564
+ Write-Log " 3. Investigate further before running any package manager commands"
565
+ } else {
566
+ if ($UseColors) {
567
+ Write-Host "Shai-Hulud Check: WARNING" -ForegroundColor Yellow
568
+ } else {
569
+ Write-Log "Shai-Hulud Check: WARNING"
570
+ }
571
+ foreach ($finding in $script:HighFindings) {
572
+ Write-Log " - $finding"
573
+ }
574
+ Write-Log ""
575
+ Write-Log "These may be legitimate if you installed them intentionally."
576
+ Write-Log "Run with -Verbose for details or investigate if unexpected."
577
+ }
578
+ } else {
579
+ if ($VerbosePreference -eq 'Continue') {
580
+ Write-Log ""
581
+ if ($UseColors) {
582
+ Write-Host " STATUS: CLEAN" -ForegroundColor Green
583
+ Write-Host "========================================" -ForegroundColor Green
584
+ } else {
585
+ Write-Log " STATUS: CLEAN"
586
+ Write-Log "========================================"
587
+ }
588
+ Write-Log ""
589
+ Write-Log "No active infection indicators detected."
590
+ Write-Log ""
591
+ Write-Log "Next steps:"
592
+ Write-Log " - Verify your repos don't have compromised packages"
593
+ Write-Log " - Check your lockfiles for known malicious packages"
594
+ } else {
595
+ if ($UseColors) {
596
+ Write-Host "Shai-Hulud Check: CLEAN" -ForegroundColor Green
597
+ } else {
598
+ Write-Log "Shai-Hulud Check: CLEAN"
599
+ }
600
+ }
601
+ }
602
+
603
+ Write-Log ""
604
+
605
+ # JSON output
606
+ if ($Json) {
607
+ $status = if ($script:CriticalFindings.Count -gt 0) { "INFECTED" }
608
+ elseif ($script:HighFindings.Count -gt 0) { "WARNING" }
609
+ else { "CLEAN" }
610
+
611
+ $output = [ordered]@{
612
+ status = $status
613
+ critical_count = $script:CriticalFindings.Count
614
+ high_count = $script:HighFindings.Count
615
+ info_count = $script:InfoFindings.Count
616
+ critical_findings = @($script:CriticalFindings)
617
+ high_findings = @($script:HighFindings)
618
+ info_findings = @($script:InfoFindings)
619
+ timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
620
+ }
621
+
622
+ $output | ConvertTo-Json -Depth 3
623
+ }
624
+
625
+ exit $exitCode