keyhog 0.5.73

GPU-accelerated secret scanner for code, Git history, cloud, containers, browser assets, and live credential verification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Windows installs should clear Mark-of-the-Web after staging keyhog.exe.

fn ps_function<'a>(script: &'a str, name: &str) -> &'a str {
    let marker = format!("function {name}");
    let start = script
        .find(&marker)
        .unwrap_or_else(|| panic!("install.ps1 missing {marker}"));
    let tail = &script[start..];
    let end = tail.find("\nfunction ").unwrap_or(tail.len());
    &tail[..end]
}

fn assert_in_order(haystack: &str, needles: &[&str]) {
    let mut offset = 0;
    for needle in needles {
        let rest = &haystack[offset..];
        let found = rest
            .find(needle)
            .unwrap_or_else(|| panic!("missing `{needle}` after byte {offset}"));
        offset += found + needle.len();
    }
}

#[test]
fn powershell_installer_unblocks_staged_binary() {
    let script = include_str!("../../../install.ps1");
    assert!(
        script.contains("function Clear-MarkOfTheWeb"),
        "install.ps1 must define a single Mark-of-the-Web cleanup helper"
    );
    assert!(
        script.contains("Get-Command Unblock-File"),
        "cleanup helper must use PowerShell's Unblock-File when available"
    );
    assert!(
        script.contains("Clear-MarkOfTheWeb -Path $dest"),
        "Stage-Install must unblock the final keyhog.exe path after Move-Item"
    );
}

#[test]
fn powershell_installer_explains_smartscreen_if_unblock_fails() {
    let script = include_str!("../../../install.ps1");
    assert!(
        script.contains("SmartScreen prompts"),
        "install.ps1 must explain what to do if Windows still shows SmartScreen"
    );
    assert!(
        script.contains("verify the SHA256 above"),
        "SmartScreen guidance must tie the operator back to the checksum proof"
    );
}

#[test]
fn powershell_upgrade_path_backs_up_before_overwrite() {
    let script = include_str!("../../../install.ps1");
    let stage_install = ps_function(script, "Stage-Install");

    assert_in_order(
        stage_install,
        &[
            "$Script:InstallBackup = $null",
            "if (Test-Path $dest)",
            "Copy-Item -Force $dest $backup",
            "$Script:InstallBackup = $backup",
            "Move-Item -Force $tmp $dest",
        ],
    );
    assert!(
        stage_install
            .contains("Refusing to overwrite it - your current install is left untouched.")
            && stage_install.contains("Remove-Item -Force $tmp -ErrorAction SilentlyContinue"),
        "backup failure must abort before touching the existing keyhog.exe"
    );
}

#[test]
fn powershell_finalize_restores_or_removes_after_failed_health_check() {
    let script = include_str!("../../../install.ps1");
    let finalize_install = ps_function(script, "Finalize-Install");
    let restore_install = ps_function(script, "Restore-PreviousInstallOrRemove");

    assert_in_order(
        finalize_install,
        &[
            "if (-not (Invoke-AutorouteCalibration -BinPath $BinPath))",
            "Restore-PreviousInstallOrRemove -BinPath $BinPath -RemovedNote \"Removed the uncalibrated binary; no working keyhog was overwritten.\"",
            "return $false",
            "if ($Script:InstallBackup) { Remove-Item -Force $Script:InstallBackup",
            "return $true",
            "Restore-PreviousInstallOrRemove -BinPath $BinPath -RemovedNote \"Removed the non-runnable download; no working keyhog was overwritten.\"",
        ],
    );
    assert_in_order(
        restore_install,
        &[
            "if ($Script:InstallBackup -and (Test-Path $Script:InstallBackup))",
            "Move-Item -Force $Script:InstallBackup $BinPath",
            "Rolled back to your previous working keyhog",
            "} else {",
            "Remove-Item -Force $BinPath -ErrorAction SilentlyContinue",
            "Warn $RemovedNote",
        ],
    );
}

#[test]
fn powershell_calibration_cleanup_runs_from_finally() {
    let script = include_str!("../../../install.ps1");
    let calibration = ps_function(script, "Invoke-AutorouteCalibration");

    assert_in_order(
        calibration,
        &[
            "$tmpDir = Join-Path",
            "New-Item -ItemType Directory -Force -Path $tmpDir",
            "$dockerImagesToRemove = @()",
            "$webJobsToStop = @()",
            "} finally {",
            "foreach ($job in $webJobsToStop)",
            "Stop-Job -Job $job -ErrorAction SilentlyContinue",
            "Remove-Job -Job $job -Force -ErrorAction SilentlyContinue",
            "foreach ($image in $dockerImagesToRemove)",
            "& $dockerPath image rm -f $image *> $null",
            "} finally {",
            "Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue",
        ],
    );
    assert!(
        !calibration.contains("KEYHOG_AUTOROUTE_CALIBRATE")
            && !calibration.contains("KEYHOG_BATCH_PIPELINE")
            && !calibration.contains("KEYHOG_GPU_AUTOROUTE"),
        "PowerShell calibration must use explicit scan flags, not ambient env state that must be restored"
    );
}

#[test]
fn powershell_calibration_scan_help_inspection_fails_loud() {
    let script = include_str!("../../../install.ps1");
    let calibration = ps_function(script, "Invoke-AutorouteCalibration");

    assert!(
        !calibration.contains("scan --help 2>$null") && !calibration.contains("catch { '' }"),
        "PowerShell calibration must not hide scan --help failures and guess supported flags"
    );
    assert_in_order(
        calibration,
        &[
            "$scanHelpErr = Join-Path $tmpDir 'scan-help.err'",
            "& $BinPath scan --help 2> $scanHelpErr",
            "$scanHelpExit = $LASTEXITCODE",
            "if ($scanHelpExit -ne 0)",
            "Could not inspect installed keyhog scan --help before autoroute calibration.",
            "scan --help error: $realErr",
            "Installed keyhog scan --help returned no output; refusing to guess calibration flags.",
        ],
    );
}

#[test]
fn powershell_uninstall_delegates_then_removes_installer_integrations() {
    let script = include_str!("../../../install.ps1");
    let uninstall = ps_function(script, "Do-Uninstall");

    assert_in_order(
        uninstall,
        &[
            "Invoke-InstalledBinaryUninstall -BinPath $bin",
            "Remove-Item -Force $bin",
            "Ok \"Removed $bin\"",
            "Remove-WindowsInstallerOwnedIntegrations",
        ],
    );
    assert!(
        !uninstall.contains("Shell profile entries and completions, if any, are left in place."),
        "Windows uninstall must not claim installer-owned PATH/completion artifacts are left behind"
    );
}

#[test]
fn powershell_uninstall_helpers_clean_user_path_and_completion_files() {
    let script = include_str!("../../../install.ps1");
    let binary_uninstall = ps_function(script, "Invoke-InstalledBinaryUninstall");
    let path_cleanup = ps_function(script, "Remove-UserPathEntry");
    let completion_cleanup = ps_function(script, "Remove-InstallerOwnedPowerShellCompletion");
    let integration_cleanup = ps_function(script, "Remove-WindowsInstallerOwnedIntegrations");

    assert!(
        binary_uninstall.contains("& $BinPath uninstall --yes 2> $errFile")
            && binary_uninstall.contains("Test-WizardCommandUnavailable")
            && binary_uninstall.contains("keyhog uninstall --yes failed"),
        "PowerShell uninstall must attempt the binary-owned state cleanup and surface failures"
    );
    assert!(
        path_cleanup.contains("[Environment]::GetEnvironmentVariable(\"Path\", \"User\")")
            && path_cleanup.contains("[Environment]::SetEnvironmentVariable(\"Path\",")
            && path_cleanup.contains("[StringComparison]::OrdinalIgnoreCase"),
        "PowerShell uninstall must remove the installer-owned User PATH entry idempotently"
    );
    assert!(
        completion_cleanup.contains("Documents\\PowerShell\\Completions\\keyhog.ps1")
            && completion_cleanup.contains("Documents\\WindowsPowerShell\\Completions\\keyhog.ps1")
            && completion_cleanup.contains("Remove-Item -Force $path"),
        "PowerShell uninstall must remove known completion files from both PowerShell profile roots"
    );
    assert!(
        integration_cleanup.contains("Remove-UserPathEntry -Path $InstallDir")
            && integration_cleanup.contains("Remove-InstallerOwnedPowerShellCompletion"),
        "PowerShell uninstall integration cleanup must own PATH and completion artifacts"
    );
}

#[test]
fn powershell_default_install_resolves_concrete_latest_before_download() {
    let script = include_str!("../../../install.ps1");
    let resolve_tag = ps_function(script, "Resolve-Tag");
    let resolve_tag_from_api = ps_function(script, "Resolve-TagFromApi");
    let resolve_redirect = ps_function(script, "Resolve-TagFromLatestRedirect");
    let resolve_operator_tag = ps_function(script, "Resolve-OperatorReleaseTag");
    let release_label = ps_function(script, "Get-ReleaseTagLabel");
    let show_summary = ps_function(script, "Show-Summary");
    let asset_url = ps_function(script, "Get-ReleaseAssetUrl");
    let download_asset = ps_function(script, "Download-Asset");
    let verify_checksum = ps_function(script, "Verify-Checksum");
    let stage_install = ps_function(script, "Stage-Install");
    let do_install = ps_function(script, "Do-Install");
    let do_repair = ps_function(script, "Do-Repair");
    let do_diagnose = ps_function(script, "Do-Diagnose");
    let github_api = ps_function(script, "Invoke-GitHubApi");

    assert!(
        resolve_tag.contains("$Script:Tag = 'latest'") && !resolve_tag.contains("api.github.com"),
        "Resolve-Tag should only normalize the requested tag; operator paths own concrete latest resolution"
    );
    assert_in_order(
        resolve_operator_tag,
        &[
            "Resolve-Tag",
            "$Script:Tag -eq 'latest'",
            "Resolve-TagFromLatestRedirect",
            "$Script:LatestReleaseAlias = $true",
            "return",
            "checking recent stable releases",
            "Resolve-TagFromApi",
            "$Script:LatestReleaseAlias = $true",
        ],
    );
    assert!(
        resolve_redirect.contains("Invoke-WebRequest")
            && resolve_redirect.contains("-Method Head")
            && resolve_redirect.contains("-MaximumRedirection 0")
            && resolve_redirect.contains("Headers.Location")
            && resolve_redirect.contains("releases/latest/download")
            && resolve_redirect.contains("/releases/download/([^/]+)/"),
        "PowerShell latest resolution must read the first non-API redirect before the GitHub releases API"
    );
    assert!(
        release_label.contains("$($Script:Tag) (latest)")
            && show_summary.contains("Get-ReleaseTagLabel")
            && show_summary.contains("Show-InstalledReleaseRelation"),
        "PowerShell summaries must display the concrete tag while preserving that it came from latest"
    );
    assert!(
        asset_url.contains("releases/latest/download/$Name")
            && asset_url.contains("releases/download/$($Script:Tag)/$Name"),
        "PowerShell release asset URL owner must support latest redirects and pinned tags"
    );
    assert!(
        download_asset.contains("$url = Get-ReleaseAssetUrl -Name $Name")
            && verify_checksum.contains("Get-ReleaseAssetUrl -Name \"$AssetName.sha256\""),
        "asset, signature, and checksum downloads must share the release URL owner"
    );
    assert!(
        !stage_install.contains("Latest release asset redirect did not provide")
            && !stage_install.contains("Resolve-TagFromApi"),
        "Stage-Install must not own a second late latest-resolution route"
    );
    assert_in_order(
        do_install,
        &[
            "Resolve-Asset",
            "Resolve-OperatorReleaseTag",
            "Show-Summary",
        ],
    );
    assert_in_order(do_repair, &["Resolve-Asset", "Resolve-OperatorReleaseTag"]);
    assert_in_order(
        do_diagnose,
        &[
            "Resolve-Asset",
            "Resolve-OperatorReleaseTag",
            "Would install",
        ],
    );
    assert!(
        resolve_tag_from_api.contains("api.github.com/repos/$Repo/releases?per_page=10"),
        "PowerShell API release walk must choose the newest release with assets when the latest redirect cannot prove a tag"
    );
    assert!(
        github_api.contains("$env:GITHUB_TOKEN")
            && github_api.contains("Authorization")
            && github_api.contains("Bearer $env:GITHUB_TOKEN"),
        "PowerShell release-resolution API request must honor optional GITHUB_TOKEN"
    );
}

#[test]
fn powershell_installer_downloads_and_seeds_gpu_literal_sidecar() {
    let script = include_str!("../../../install.ps1");
    let sidecar_download = ps_function(script, "Download-VerifiedGpuLiteralSidecar");
    let sidecar_check = ps_function(script, "Test-GpuLiteralSidecarArchive");
    let sidecar_install = ps_function(script, "Install-VerifiedGpuLiteralSidecar");
    let cache_backup = ps_function(script, "Backup-GpuProgramsCacheForInstall");
    let cache_restore = ps_function(script, "Restore-GpuProgramsCacheBackup");
    let stage_install = ps_function(script, "Stage-Install");
    let do_install = ps_function(script, "Do-Install");
    let do_repair = ps_function(script, "Do-Repair");

    assert!(
        sidecar_download.contains("$($Script:Asset).gpu-literals.tar.gz")
            && sidecar_download.contains("$FromFile.gpu-literals.tar.gz")
            && sidecar_download.contains("No local checksum file found beside -FromFile GPU literal sidecar")
            && !sidecar_download.contains("if ($FromFile) { return $true }")
            && sidecar_download.contains("Verify-ReleaseSignature -BinaryPath $sidecarPath -AssetName $sidecarName")
            && sidecar_download
                .contains("Verify-Checksum -BinaryPath $sidecarPath -AssetName $sidecarName")
            && sidecar_download.contains("Test-GpuLiteralSidecarArchive -ArchivePath $sidecarPath")
            && sidecar_download.contains("Refusing to install a release that would recompile shipped detector matchers at runtime."),
        "PowerShell installer must verify the GPU literal sidecar before any cache install"
    );
    assert!(
        sidecar_check.contains("tar.exe")
            && sidecar_check.contains("$tarPath = $tar.Path")
            && !sidecar_check.contains("$tar.Source")
            && sidecar_check.contains("-tzf $ArchivePath")
            && sidecar_check.contains("^[A-Za-z]:")
            && sidecar_check.contains("(^|[\\\\/])\\.\\.[\\s\\.]*([\\\\/]|$)")
            && sidecar_check.contains("-tvzf $ArchivePath")
            && sidecar_check.contains("$global:LASTEXITCODE = 0")
            && sidecar_check.contains("if (-not $? -or $LASTEXITCODE -ne 0)")
            && sidecar_check.contains("$entryKind -eq 'l' -or $entryKind -eq 'h'"),
        "PowerShell sidecar archive validation must reject traversal plus symlink/hardlink tar entries"
    );
    assert!(
        sidecar_install.contains("Get-GpuProgramsCacheDirForInstall")
            && sidecar_install.contains("keyhog-gpu-literals")
            && sidecar_install.contains("Get-ChildItem -Path $extractDir -Filter '*.bin'")
            && sidecar_install.contains("Move-Item -Force -Path $tmpTarget"),
        "PowerShell sidecar install must seed verified .bin artifacts into the runtime program cache"
    );
    assert!(
        cache_backup.contains("Copy-Item -Recurse -Force -Path $programsDir")
            && cache_backup.contains("$Script:GpuProgramsCacheWasMissing = $true")
            && cache_restore.contains("Remove-Item -Recurse -Force $programsDir")
            && cache_restore.contains("Move-Item -Force -Path (Join-Path $Script:GpuProgramsCacheBackupPath 'programs')")
            && cache_restore.contains("Clear-GpuProgramsCacheBackup"),
        "PowerShell installer must be able to roll back GPU literal cache state when final verification fails"
    );
    assert_in_order(
        stage_install,
        &[
            "Verify-ReleaseSignature -BinaryPath $tmp -AssetName $Script:Asset",
            "Verify-Checksum -BinaryPath $tmp -AssetName $Script:Asset",
            "Download-VerifiedGpuLiteralSidecar",
            "Remove-Item -Force $tmp -ErrorAction SilentlyContinue",
            "Clear-GpuLiteralSidecarTemp",
            "exit 1",
            "New-Item -ItemType Directory -Force -Path $InstallDir",
        ],
    );
    assert_in_order(
        do_install,
        &[
            "$bin = Stage-Install",
            "Backup-GpuProgramsCacheForInstall",
            "Install-VerifiedGpuLiteralSidecar",
            "Restore-GpuProgramsCacheBackup",
            "Rollback-StagedInstallAfterSidecarFailure -BinPath $bin",
            "Install failed while seeding shipped GPU literal artifacts.",
            "Finalize-Install -BinPath $bin",
            "Restore-GpuProgramsCacheBackup",
            "Clear-GpuLiteralSidecarTemp",
            "Install failed verification; see above.",
            "Clear-GpuProgramsCacheBackup",
            "Ensure-OnPath",
        ],
    );
    assert_in_order(
        do_repair,
        &[
            "$bin = Stage-Install",
            "Backup-GpuProgramsCacheForInstall",
            "Install-VerifiedGpuLiteralSidecar",
            "Restore-GpuProgramsCacheBackup",
            "Rollback-StagedInstallAfterSidecarFailure -BinPath $bin",
            "Repair failed while seeding shipped GPU literal artifacts.",
            "Finalize-Install -BinPath $bin",
            "Restore-GpuProgramsCacheBackup",
            "Clear-GpuLiteralSidecarTemp",
            "Repair failed; see above.",
            "Clear-GpuProgramsCacheBackup",
            "return",
            "$newBin = Stage-Install",
            "Backup-GpuProgramsCacheForInstall",
            "Install-VerifiedGpuLiteralSidecar",
            "Restore-GpuProgramsCacheBackup",
            "Rollback-StagedInstallAfterSidecarFailure -BinPath $newBin",
            "Repair failed while seeding shipped GPU literal artifacts.",
            "Finalize-Install -BinPath $newBin",
            "Restore-GpuProgramsCacheBackup",
            "Clear-GpuLiteralSidecarTemp",
            "Repair failed; your previous binary was preserved where possible (see above).",
            "Clear-GpuProgramsCacheBackup",
        ],
    );
}