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.
@@ -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