vtk 1.1.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/CHANGELOG.md +10 -0
- data/README.md +75 -0
- data/lib/vtk/commands/scan/README.md +102 -0
- data/lib/vtk/commands/scan/credentials.rb +59 -0
- data/lib/vtk/commands/scan/repo.rb +77 -0
- data/lib/vtk/commands/scan.rb +41 -3
- 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-repo-check.ps1 +615 -0
- data/scripts/shai-hulud-repo-check.sh +849 -0
- metadata +10 -2
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Credential Audit Script for Windows
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Audits which credentials are present on this machine and provides rotation
|
|
7
|
+
instructions for each. Run this after a suspected or confirmed security incident.
|
|
8
|
+
|
|
9
|
+
WHAT THIS CHECKS:
|
|
10
|
+
|
|
11
|
+
NPM:
|
|
12
|
+
- %USERPROFILE%\.npmrc (auth tokens)
|
|
13
|
+
- $env:NPM_TOKEN, $env:NPM_CONFIG_TOKEN environment variables
|
|
14
|
+
|
|
15
|
+
AWS:
|
|
16
|
+
- %USERPROFILE%\.aws\credentials, .aws\config
|
|
17
|
+
- $env:AWS_ACCESS_KEY_ID, $env:AWS_SECRET_ACCESS_KEY
|
|
18
|
+
|
|
19
|
+
GCP:
|
|
20
|
+
- %APPDATA%\gcloud\application_default_credentials.json
|
|
21
|
+
- $env:GOOGLE_APPLICATION_CREDENTIALS
|
|
22
|
+
|
|
23
|
+
Azure:
|
|
24
|
+
- %USERPROFILE%\.azure\ directory
|
|
25
|
+
- $env:AZURE_CLIENT_SECRET, $env:AZURE_TENANT_ID
|
|
26
|
+
|
|
27
|
+
GitHub:
|
|
28
|
+
- %APPDATA%\GitHub CLI\hosts.yml (GitHub CLI)
|
|
29
|
+
- %USERPROFILE%\.git-credentials
|
|
30
|
+
- $env:GITHUB_TOKEN, $env:GH_TOKEN
|
|
31
|
+
|
|
32
|
+
Other:
|
|
33
|
+
- SSH keys (%USERPROFILE%\.ssh\)
|
|
34
|
+
- Docker config (%USERPROFILE%\.docker\config.json)
|
|
35
|
+
- Kubernetes config (%USERPROFILE%\.kube\config)
|
|
36
|
+
- Sensitive environment variables
|
|
37
|
+
|
|
38
|
+
EXIT CODES:
|
|
39
|
+
0 - No credentials found
|
|
40
|
+
1 - Credentials found (rotation recommended)
|
|
41
|
+
|
|
42
|
+
.PARAMETER Verbose
|
|
43
|
+
Show all checks including clean ones
|
|
44
|
+
|
|
45
|
+
.PARAMETER Json
|
|
46
|
+
Output results as JSON
|
|
47
|
+
|
|
48
|
+
.EXAMPLE
|
|
49
|
+
.\credential-audit.ps1
|
|
50
|
+
Standard output
|
|
51
|
+
|
|
52
|
+
.EXAMPLE
|
|
53
|
+
.\credential-audit.ps1 -Verbose
|
|
54
|
+
Show all checks even clean ones
|
|
55
|
+
|
|
56
|
+
.EXAMPLE
|
|
57
|
+
.\credential-audit.ps1 -Json
|
|
58
|
+
JSON output format
|
|
59
|
+
|
|
60
|
+
.NOTES
|
|
61
|
+
Author: Eric Boehs / EERT (with Claude Code)
|
|
62
|
+
Version: 1.0.0
|
|
63
|
+
Date: December 2025
|
|
64
|
+
Requires: PowerShell 5.1+
|
|
65
|
+
|
|
66
|
+
References:
|
|
67
|
+
- EERT Playbooks: https://department-of-veterans-affairs.github.io/eert/
|
|
68
|
+
#>
|
|
69
|
+
|
|
70
|
+
[CmdletBinding()]
|
|
71
|
+
param(
|
|
72
|
+
[switch]$Json,
|
|
73
|
+
[switch]$Help
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Handle help
|
|
77
|
+
if ($Help) {
|
|
78
|
+
Get-Help $MyInvocation.MyCommand.Path -Detailed
|
|
79
|
+
exit 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Results tracking
|
|
83
|
+
$script:RotationInstructions = [System.Collections.ArrayList]::new()
|
|
84
|
+
$script:TotalFound = 0
|
|
85
|
+
|
|
86
|
+
# Color support - SupportsVirtualTerminal doesn't exist in PS 5.1, so check safely
|
|
87
|
+
$UseColors = -not $Json -and (($Host.UI.psobject.Properties.Name -contains 'SupportsVirtualTerminal') -and $Host.UI.SupportsVirtualTerminal)
|
|
88
|
+
|
|
89
|
+
function Write-Log {
|
|
90
|
+
param([string]$Message)
|
|
91
|
+
if (-not $Json) {
|
|
92
|
+
Write-Host $Message
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Write-LogVerbose {
|
|
97
|
+
param([string]$Message)
|
|
98
|
+
if ($VerbosePreference -eq 'Continue' -and -not $Json) {
|
|
99
|
+
Write-Host $Message
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Write-Found {
|
|
104
|
+
param([string]$Message)
|
|
105
|
+
if ($UseColors) {
|
|
106
|
+
Write-Host " [FOUND] " -ForegroundColor Red -NoNewline
|
|
107
|
+
Write-Host $Message
|
|
108
|
+
} else {
|
|
109
|
+
Write-Host " [FOUND] $Message"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Write-Clean {
|
|
114
|
+
param([string]$Message)
|
|
115
|
+
if ($VerbosePreference -eq 'Continue' -and -not $Json) {
|
|
116
|
+
if ($UseColors) {
|
|
117
|
+
Write-Host " [CLEAN] " -ForegroundColor Green -NoNewline
|
|
118
|
+
Write-Host $Message
|
|
119
|
+
} else {
|
|
120
|
+
Write-Host " [CLEAN] $Message"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Write-Skip {
|
|
126
|
+
param([string]$Message)
|
|
127
|
+
if ($VerbosePreference -eq 'Continue' -and -not $Json) {
|
|
128
|
+
if ($UseColors) {
|
|
129
|
+
Write-Host " [SKIP] " -ForegroundColor DarkGray -NoNewline
|
|
130
|
+
Write-Host $Message
|
|
131
|
+
} else {
|
|
132
|
+
Write-Host " [SKIP] $Message"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Log-Found {
|
|
138
|
+
param(
|
|
139
|
+
[string]$Service,
|
|
140
|
+
[string]$Location,
|
|
141
|
+
[string]$Instruction
|
|
142
|
+
)
|
|
143
|
+
[void]$script:RotationInstructions.Add([PSCustomObject]@{
|
|
144
|
+
Service = $Service
|
|
145
|
+
Location = $Location
|
|
146
|
+
Instruction = $Instruction
|
|
147
|
+
})
|
|
148
|
+
$script:TotalFound++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Header
|
|
152
|
+
Write-Log ""
|
|
153
|
+
Write-Log "Credential Audit"
|
|
154
|
+
Write-Log "Checking for credentials that may need rotation..."
|
|
155
|
+
Write-Log ""
|
|
156
|
+
|
|
157
|
+
###########################################
|
|
158
|
+
# NPM CREDENTIALS
|
|
159
|
+
###########################################
|
|
160
|
+
|
|
161
|
+
Write-Log "NPM"
|
|
162
|
+
$npmFound = 0
|
|
163
|
+
|
|
164
|
+
# Check ~/.npmrc
|
|
165
|
+
$npmrcPath = Join-Path $env:USERPROFILE ".npmrc"
|
|
166
|
+
if (Test-Path $npmrcPath -PathType Leaf) {
|
|
167
|
+
$content = Get-Content $npmrcPath -Raw -ErrorAction SilentlyContinue
|
|
168
|
+
if ($content -match "//.*:_authToken=|_auth=|authToken") {
|
|
169
|
+
Write-Found "~\.npmrc contains auth tokens"
|
|
170
|
+
Log-Found "NPM" "~\.npmrc" "npm token revoke <token> && npm login"
|
|
171
|
+
$npmFound++
|
|
172
|
+
} else {
|
|
173
|
+
Write-Clean "~\.npmrc exists but no tokens found"
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
Write-Skip "~\.npmrc not found"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Check NPM environment variables
|
|
180
|
+
if ($env:NPM_TOKEN) {
|
|
181
|
+
Write-Found "`$env:NPM_TOKEN is set"
|
|
182
|
+
Log-Found "NPM" "`$env:NPM_TOKEN" "Revoke token in npm account settings, regenerate and update env"
|
|
183
|
+
$npmFound++
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if ($env:NPM_CONFIG_TOKEN) {
|
|
187
|
+
Write-Found "`$env:NPM_CONFIG_TOKEN is set"
|
|
188
|
+
Log-Found "NPM" "`$env:NPM_CONFIG_TOKEN" "Revoke token in npm account settings, regenerate and update env"
|
|
189
|
+
$npmFound++
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ($npmFound -eq 0) {
|
|
193
|
+
Write-Log " None found"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Write-Log ""
|
|
197
|
+
|
|
198
|
+
###########################################
|
|
199
|
+
# AWS CREDENTIALS
|
|
200
|
+
###########################################
|
|
201
|
+
|
|
202
|
+
Write-Log "AWS"
|
|
203
|
+
$awsFound = 0
|
|
204
|
+
|
|
205
|
+
# Check ~/.aws/credentials
|
|
206
|
+
$awsCredsPath = Join-Path $env:USERPROFILE ".aws\credentials"
|
|
207
|
+
if (Test-Path $awsCredsPath -PathType Leaf) {
|
|
208
|
+
Write-Found "~\.aws\credentials"
|
|
209
|
+
Log-Found "AWS" "~\.aws\credentials" "aws iam delete-access-key && aws iam create-access-key"
|
|
210
|
+
$awsFound++
|
|
211
|
+
} else {
|
|
212
|
+
Write-Skip "~\.aws\credentials not found"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Check ~/.aws/config
|
|
216
|
+
$awsConfigPath = Join-Path $env:USERPROFILE ".aws\config"
|
|
217
|
+
if (Test-Path $awsConfigPath -PathType Leaf) {
|
|
218
|
+
$content = Get-Content $awsConfigPath -Raw -ErrorAction SilentlyContinue
|
|
219
|
+
if ($content -match "aws_access_key_id|aws_secret_access_key") {
|
|
220
|
+
Write-Found "~\.aws\config contains access keys"
|
|
221
|
+
Log-Found "AWS" "~\.aws\config" "Remove keys from config, use aws configure"
|
|
222
|
+
$awsFound++
|
|
223
|
+
} else {
|
|
224
|
+
Write-Clean "~\.aws\config exists (no embedded keys)"
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
Write-Skip "~\.aws\config not found"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Check AWS environment variables
|
|
231
|
+
if ($env:AWS_ACCESS_KEY_ID) {
|
|
232
|
+
Write-Found "`$env:AWS_ACCESS_KEY_ID is set"
|
|
233
|
+
Log-Found "AWS" "`$env:AWS_ACCESS_KEY_ID" "Rotate key in IAM console, update env"
|
|
234
|
+
$awsFound++
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if ($env:AWS_SECRET_ACCESS_KEY) {
|
|
238
|
+
Write-Found "`$env:AWS_SECRET_ACCESS_KEY is set"
|
|
239
|
+
Log-Found "AWS" "`$env:AWS_SECRET_ACCESS_KEY" "Rotate key in IAM console, update env"
|
|
240
|
+
$awsFound++
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ($env:AWS_SESSION_TOKEN) {
|
|
244
|
+
if ($UseColors) {
|
|
245
|
+
Write-Host " [FOUND] " -ForegroundColor Yellow -NoNewline
|
|
246
|
+
Write-Host "`$env:AWS_SESSION_TOKEN is set (temporary)"
|
|
247
|
+
} else {
|
|
248
|
+
Write-Host " [FOUND] `$env:AWS_SESSION_TOKEN is set (temporary)"
|
|
249
|
+
}
|
|
250
|
+
Log-Found "AWS" "`$env:AWS_SESSION_TOKEN" "Wait for expiration or re-authenticate with aws sso login"
|
|
251
|
+
$awsFound++
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if ($awsFound -eq 0) {
|
|
255
|
+
Write-Log " None found"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Write-Log ""
|
|
259
|
+
|
|
260
|
+
###########################################
|
|
261
|
+
# GCP CREDENTIALS
|
|
262
|
+
###########################################
|
|
263
|
+
|
|
264
|
+
Write-Log "GCP"
|
|
265
|
+
$gcpFound = 0
|
|
266
|
+
|
|
267
|
+
# Check Application Default Credentials
|
|
268
|
+
$adcPath = Join-Path $env:APPDATA "gcloud\application_default_credentials.json"
|
|
269
|
+
if (Test-Path $adcPath -PathType Leaf) {
|
|
270
|
+
Write-Found "Application Default Credentials"
|
|
271
|
+
Log-Found "GCP" $adcPath "gcloud auth application-default revoke && gcloud auth application-default login"
|
|
272
|
+
$gcpFound++
|
|
273
|
+
} else {
|
|
274
|
+
Write-Skip "ADC not found"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Check gcloud credentials.db
|
|
278
|
+
$gcloudCredsDb = Join-Path $env:APPDATA "gcloud\credentials.db"
|
|
279
|
+
if (Test-Path $gcloudCredsDb -PathType Leaf) {
|
|
280
|
+
Write-Found "gcloud credentials.db"
|
|
281
|
+
Log-Found "GCP" "~\AppData\Roaming\gcloud\credentials.db" "gcloud auth revoke --all && gcloud auth login"
|
|
282
|
+
$gcpFound++
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Check GOOGLE_APPLICATION_CREDENTIALS
|
|
286
|
+
if ($env:GOOGLE_APPLICATION_CREDENTIALS) {
|
|
287
|
+
if (Test-Path $env:GOOGLE_APPLICATION_CREDENTIALS -PathType Leaf) {
|
|
288
|
+
Write-Found "`$env:GOOGLE_APPLICATION_CREDENTIALS points to: $($env:GOOGLE_APPLICATION_CREDENTIALS)"
|
|
289
|
+
Log-Found "GCP" "`$env:GOOGLE_APPLICATION_CREDENTIALS" "Rotate service account key in GCP Console"
|
|
290
|
+
$gcpFound++
|
|
291
|
+
} else {
|
|
292
|
+
Write-Skip "`$env:GOOGLE_APPLICATION_CREDENTIALS set but file doesn't exist"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if ($gcpFound -eq 0) {
|
|
297
|
+
Write-Log " None found"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
Write-Log ""
|
|
301
|
+
|
|
302
|
+
###########################################
|
|
303
|
+
# AZURE CREDENTIALS
|
|
304
|
+
###########################################
|
|
305
|
+
|
|
306
|
+
Write-Log "Azure"
|
|
307
|
+
$azureFound = 0
|
|
308
|
+
|
|
309
|
+
# Check ~/.azure directory
|
|
310
|
+
$azureDir = Join-Path $env:USERPROFILE ".azure"
|
|
311
|
+
if (Test-Path $azureDir -PathType Container) {
|
|
312
|
+
$accessTokens = Join-Path $azureDir "accessTokens.json"
|
|
313
|
+
$azureProfile = Join-Path $azureDir "azureProfile.json"
|
|
314
|
+
if ((Test-Path $accessTokens -PathType Leaf) -or (Test-Path $azureProfile -PathType Leaf)) {
|
|
315
|
+
Write-Found "~\.azure\ contains auth tokens"
|
|
316
|
+
Log-Found "Azure" "~\.azure\" "az logout && az login"
|
|
317
|
+
$azureFound++
|
|
318
|
+
} else {
|
|
319
|
+
Write-Clean "~\.azure\ exists but no tokens found"
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
Write-Skip "~\.azure\ not found"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Check Azure environment variables
|
|
326
|
+
if ($env:AZURE_CLIENT_SECRET) {
|
|
327
|
+
Write-Found "`$env:AZURE_CLIENT_SECRET is set"
|
|
328
|
+
Log-Found "Azure" "`$env:AZURE_CLIENT_SECRET" "Rotate client secret in Azure AD app registration"
|
|
329
|
+
$azureFound++
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if ($env:AZURE_CLIENT_ID -and $env:AZURE_TENANT_ID) {
|
|
333
|
+
if ($UseColors) {
|
|
334
|
+
Write-Host " [INFO] " -ForegroundColor Yellow -NoNewline
|
|
335
|
+
Write-Host "Azure service principal env vars configured"
|
|
336
|
+
} else {
|
|
337
|
+
Write-Host " [INFO] Azure service principal env vars configured"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if ($azureFound -eq 0) {
|
|
342
|
+
Write-Log " None found"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
Write-Log ""
|
|
346
|
+
|
|
347
|
+
###########################################
|
|
348
|
+
# GITHUB CREDENTIALS
|
|
349
|
+
###########################################
|
|
350
|
+
|
|
351
|
+
Write-Log "GitHub"
|
|
352
|
+
$githubFound = 0
|
|
353
|
+
|
|
354
|
+
# Check GitHub CLI
|
|
355
|
+
$ghHostsPath = Join-Path $env:APPDATA "GitHub CLI\hosts.yml"
|
|
356
|
+
if (Test-Path $ghHostsPath -PathType Leaf) {
|
|
357
|
+
Write-Found "GitHub CLI authenticated (~\AppData\Roaming\GitHub CLI\hosts.yml)"
|
|
358
|
+
Log-Found "GitHub" "~\AppData\Roaming\GitHub CLI\hosts.yml" "gh auth logout && gh auth login"
|
|
359
|
+
$githubFound++
|
|
360
|
+
} else {
|
|
361
|
+
Write-Skip "GitHub CLI not authenticated"
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Check GITHUB_TOKEN / GH_TOKEN
|
|
365
|
+
if ($env:GITHUB_TOKEN) {
|
|
366
|
+
Write-Found "`$env:GITHUB_TOKEN is set"
|
|
367
|
+
Log-Found "GitHub" "`$env:GITHUB_TOKEN" "Revoke token at github.com/settings/tokens, regenerate"
|
|
368
|
+
$githubFound++
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if ($env:GH_TOKEN) {
|
|
372
|
+
Write-Found "`$env:GH_TOKEN is set"
|
|
373
|
+
Log-Found "GitHub" "`$env:GH_TOKEN" "Revoke token at github.com/settings/tokens, regenerate"
|
|
374
|
+
$githubFound++
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Check .gitconfig for credential store
|
|
378
|
+
$gitconfigPath = Join-Path $env:USERPROFILE ".gitconfig"
|
|
379
|
+
if (Test-Path $gitconfigPath -PathType Leaf) {
|
|
380
|
+
$content = Get-Content $gitconfigPath -Raw -ErrorAction SilentlyContinue
|
|
381
|
+
if ($content -match "helper.*store|credential.*=.*https") {
|
|
382
|
+
if ($UseColors) {
|
|
383
|
+
Write-Host " [WARN] " -ForegroundColor Yellow -NoNewline
|
|
384
|
+
Write-Host "~\.gitconfig uses credential store"
|
|
385
|
+
} else {
|
|
386
|
+
Write-Host " [WARN] ~\.gitconfig uses credential store"
|
|
387
|
+
}
|
|
388
|
+
Log-Found "GitHub" "~\.gitconfig" "git config --global --unset credential.helper (if using store)"
|
|
389
|
+
$githubFound++
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Check .git-credentials
|
|
394
|
+
$gitCredsPath = Join-Path $env:USERPROFILE ".git-credentials"
|
|
395
|
+
if (Test-Path $gitCredsPath -PathType Leaf) {
|
|
396
|
+
Write-Found "~\.git-credentials (plaintext credentials)"
|
|
397
|
+
Log-Found "GitHub" "~\.git-credentials" "Remove-Item ~\.git-credentials && regenerate PATs"
|
|
398
|
+
$githubFound++
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if ($githubFound -eq 0) {
|
|
402
|
+
Write-Log " None found"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Write-Log ""
|
|
406
|
+
|
|
407
|
+
###########################################
|
|
408
|
+
# SSH KEYS
|
|
409
|
+
###########################################
|
|
410
|
+
|
|
411
|
+
Write-Log "SSH"
|
|
412
|
+
$sshFound = 0
|
|
413
|
+
|
|
414
|
+
$sshDir = Join-Path $env:USERPROFILE ".ssh"
|
|
415
|
+
if (Test-Path $sshDir -PathType Container) {
|
|
416
|
+
# Count private keys (files without .pub extension that aren't config/known_hosts)
|
|
417
|
+
$privateKeys = Get-ChildItem $sshDir -File -ErrorAction SilentlyContinue |
|
|
418
|
+
Where-Object { $_.Name -notlike "*.pub" -and $_.Name -notlike "known_hosts*" -and $_.Name -ne "config" -and $_.Name -ne "authorized_keys" }
|
|
419
|
+
|
|
420
|
+
if ($privateKeys) {
|
|
421
|
+
Write-Found "$($privateKeys.Count) SSH private key(s) in ~\.ssh\"
|
|
422
|
+
Log-Found "SSH" "~\.ssh\" "ssh-keygen -t ed25519, update public keys on all services"
|
|
423
|
+
$sshFound++
|
|
424
|
+
|
|
425
|
+
if ($VerbosePreference -eq 'Continue') {
|
|
426
|
+
foreach ($key in $privateKeys) {
|
|
427
|
+
Write-Log " - $($key.Name)"
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
Write-Skip "No SSH private keys found"
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
Write-Skip "~\.ssh\ not found"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if ($sshFound -eq 0) {
|
|
438
|
+
Write-Log " None found"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Write-Log ""
|
|
442
|
+
|
|
443
|
+
###########################################
|
|
444
|
+
# DOCKER CREDENTIALS
|
|
445
|
+
###########################################
|
|
446
|
+
|
|
447
|
+
Write-Log "Docker"
|
|
448
|
+
$dockerFound = 0
|
|
449
|
+
|
|
450
|
+
$dockerConfigPath = Join-Path $env:USERPROFILE ".docker\config.json"
|
|
451
|
+
if (Test-Path $dockerConfigPath -PathType Leaf) {
|
|
452
|
+
$content = Get-Content $dockerConfigPath -Raw -ErrorAction SilentlyContinue
|
|
453
|
+
if ($content -match '"auth"') {
|
|
454
|
+
Write-Found "~\.docker\config.json contains auth"
|
|
455
|
+
Log-Found "Docker" "~\.docker\config.json" "docker logout && docker login"
|
|
456
|
+
$dockerFound++
|
|
457
|
+
} else {
|
|
458
|
+
Write-Clean "~\.docker\config.json exists (no auth)"
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
Write-Skip "~\.docker\config.json not found"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if ($dockerFound -eq 0) {
|
|
465
|
+
Write-Log " None found"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
Write-Log ""
|
|
469
|
+
|
|
470
|
+
###########################################
|
|
471
|
+
# KUBERNETES CREDENTIALS
|
|
472
|
+
###########################################
|
|
473
|
+
|
|
474
|
+
Write-Log "Kubernetes"
|
|
475
|
+
$k8sFound = 0
|
|
476
|
+
|
|
477
|
+
$kubeConfigPath = Join-Path $env:USERPROFILE ".kube\config"
|
|
478
|
+
if (Test-Path $kubeConfigPath -PathType Leaf) {
|
|
479
|
+
Write-Found "~\.kube\config"
|
|
480
|
+
Log-Found "Kubernetes" "~\.kube\config" "Rotate cluster credentials, re-run az aks get-credentials or equivalent"
|
|
481
|
+
$k8sFound++
|
|
482
|
+
} else {
|
|
483
|
+
Write-Skip "~\.kube\config not found"
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if ($k8sFound -eq 0) {
|
|
487
|
+
Write-Log " None found"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
Write-Log ""
|
|
491
|
+
|
|
492
|
+
###########################################
|
|
493
|
+
# SENSITIVE ENVIRONMENT VARIABLES
|
|
494
|
+
###########################################
|
|
495
|
+
|
|
496
|
+
Write-Log "Environment Variables"
|
|
497
|
+
|
|
498
|
+
# Get all env vars and filter for sensitive ones (excluding already checked)
|
|
499
|
+
$excludedVars = @("NPM_TOKEN", "NPM_CONFIG_TOKEN", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY",
|
|
500
|
+
"AWS_SESSION_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "GOOGLE_APPLICATION_CREDENTIALS",
|
|
501
|
+
"AZURE_CLIENT_SECRET")
|
|
502
|
+
|
|
503
|
+
$sensitiveVars = [Environment]::GetEnvironmentVariables().GetEnumerator() |
|
|
504
|
+
Where-Object { $_.Key -match "token|secret|password|credential|api.?key|auth" } |
|
|
505
|
+
Where-Object { $_.Key -notin $excludedVars }
|
|
506
|
+
|
|
507
|
+
$sensitiveCount = ($sensitiveVars | Measure-Object).Count
|
|
508
|
+
|
|
509
|
+
if ($sensitiveCount -gt 0) {
|
|
510
|
+
if ($UseColors) {
|
|
511
|
+
Write-Host " [FOUND] " -ForegroundColor Yellow -NoNewline
|
|
512
|
+
Write-Host "$sensitiveCount additional sensitive env var(s)"
|
|
513
|
+
} else {
|
|
514
|
+
Write-Host " [FOUND] $sensitiveCount additional sensitive env var(s)"
|
|
515
|
+
}
|
|
516
|
+
Log-Found "Environment" "shell environment" "Check PowerShell profile for secrets"
|
|
517
|
+
|
|
518
|
+
if ($VerbosePreference -eq 'Continue') {
|
|
519
|
+
foreach ($var in $sensitiveVars) {
|
|
520
|
+
Write-Log " - `$$($var.Key)"
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
Write-Log " None found"
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
Write-Log ""
|
|
528
|
+
|
|
529
|
+
###########################################
|
|
530
|
+
# SUMMARY
|
|
531
|
+
###########################################
|
|
532
|
+
|
|
533
|
+
Write-Log "========================================"
|
|
534
|
+
|
|
535
|
+
if ($script:TotalFound -gt 0) {
|
|
536
|
+
if ($UseColors) {
|
|
537
|
+
Write-Host " CREDENTIALS FOUND: $($script:TotalFound)" -ForegroundColor Red
|
|
538
|
+
Write-Host "========================================" -ForegroundColor Red
|
|
539
|
+
} else {
|
|
540
|
+
Write-Log " CREDENTIALS FOUND: $($script:TotalFound)"
|
|
541
|
+
Write-Log "========================================"
|
|
542
|
+
}
|
|
543
|
+
Write-Log ""
|
|
544
|
+
Write-Log "Rotation Instructions:"
|
|
545
|
+
Write-Log ""
|
|
546
|
+
|
|
547
|
+
# Group by service
|
|
548
|
+
$grouped = $script:RotationInstructions | Group-Object -Property Service
|
|
549
|
+
foreach ($group in $grouped) {
|
|
550
|
+
if ($UseColors) {
|
|
551
|
+
Write-Host "$($group.Name):" -ForegroundColor Cyan
|
|
552
|
+
} else {
|
|
553
|
+
Write-Log "$($group.Name):"
|
|
554
|
+
}
|
|
555
|
+
foreach ($item in $group.Group) {
|
|
556
|
+
Write-Log " $($item.Location)"
|
|
557
|
+
Write-Log " $($item.Instruction)"
|
|
558
|
+
Write-Log ""
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if ($UseColors) {
|
|
563
|
+
Write-Host "IMPORTANT: " -ForegroundColor Yellow -NoNewline
|
|
564
|
+
Write-Host "If your machine was compromised, assume ALL of these"
|
|
565
|
+
} else {
|
|
566
|
+
Write-Log "IMPORTANT: If your machine was compromised, assume ALL of these"
|
|
567
|
+
}
|
|
568
|
+
Write-Log "credentials were exfiltrated. Rotate them immediately."
|
|
569
|
+
Write-Log ""
|
|
570
|
+
Write-Log "Note: This list is not exhaustive. You may need to rotate other"
|
|
571
|
+
Write-Log "credentials not detected by this scan (e.g., database passwords,"
|
|
572
|
+
Write-Log "API keys in config files, or service-specific tokens)."
|
|
573
|
+
Write-Log ""
|
|
574
|
+
Write-Log "See: https://department-of-veterans-affairs.github.io/eert/"
|
|
575
|
+
|
|
576
|
+
$exitCode = 1
|
|
577
|
+
} else {
|
|
578
|
+
if ($UseColors) {
|
|
579
|
+
Write-Host " NO CREDENTIALS FOUND" -ForegroundColor Green
|
|
580
|
+
Write-Host "========================================" -ForegroundColor Green
|
|
581
|
+
} else {
|
|
582
|
+
Write-Log " NO CREDENTIALS FOUND"
|
|
583
|
+
Write-Log "========================================"
|
|
584
|
+
}
|
|
585
|
+
Write-Log ""
|
|
586
|
+
Write-Log "No credential files or environment variables were detected."
|
|
587
|
+
Write-Log "This machine has minimal credential exposure risk."
|
|
588
|
+
|
|
589
|
+
$exitCode = 0
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
Write-Log ""
|
|
593
|
+
|
|
594
|
+
###########################################
|
|
595
|
+
# JSON OUTPUT
|
|
596
|
+
###########################################
|
|
597
|
+
|
|
598
|
+
if ($Json) {
|
|
599
|
+
$status = if ($script:TotalFound -gt 0) { "CREDENTIALS_FOUND" } else { "CLEAN" }
|
|
600
|
+
|
|
601
|
+
$credentials = @()
|
|
602
|
+
foreach ($item in $script:RotationInstructions) {
|
|
603
|
+
$credentials += [PSCustomObject]@{
|
|
604
|
+
service = $item.Service
|
|
605
|
+
location = $item.Location
|
|
606
|
+
rotation = $item.Instruction
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
$output = [ordered]@{
|
|
611
|
+
status = $status
|
|
612
|
+
credentials_found = $script:TotalFound
|
|
613
|
+
credentials = $credentials
|
|
614
|
+
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
$output | ConvertTo-Json -Depth 3
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
exit $exitCode
|