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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +39 -0
- data/README.md +109 -0
- data/lib/vtk/cli.rb +3 -0
- data/lib/vtk/commands/scan/README.md +102 -0
- data/lib/vtk/commands/scan/credentials.rb +59 -0
- data/lib/vtk/commands/scan/machine.rb +60 -0
- data/lib/vtk/commands/scan/repo.rb +77 -0
- data/lib/vtk/commands/scan.rb +75 -0
- data/lib/vtk/commands/socks/setup.rb +4 -4
- data/lib/vtk/version.rb +1 -1
- data/scripts/credential-audit.ps1 +620 -0
- data/scripts/credential-audit.sh +535 -0
- data/scripts/shai-hulud-machine-check.ps1 +625 -0
- data/scripts/shai-hulud-machine-check.sh +531 -0
- data/scripts/shai-hulud-repo-check.ps1 +615 -0
- data/scripts/shai-hulud-repo-check.sh +849 -0
- metadata +14 -6
|
@@ -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
|