-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAmcacheParser-Wrapper.hta
More file actions
2320 lines (2269 loc) · 148 KB
/
Copy pathAmcacheParser-Wrapper.hta
File metadata and controls
2320 lines (2269 loc) · 148 KB
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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html>
<head>
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>AmcacheParser Wrapper</title>
<HTA:APPLICATION
ID="amcacheHta"
APPLICATIONNAME="AmcacheParser Wrapper"
BORDER="thick"
CAPTION="yes"
SCROLL="auto"
SINGLEINSTANCE="yes"
SHOWINTASKBAR="yes"
MAXIMIZEBUTTON="yes"
MINIMIZEBUTTON="yes"
INNERBORDER="no"
CONTEXTMENU="yes"
SELECTION="yes" />
<!--
AmcacheParser CSV contract, VERIFIED 2026-07-03 against AmcacheParser 1.5.1.0 on a real case hive (new format):
One run produces EIGHT CSVs, all sharing one timestamp prefix <ts>_Amcache_*.csv:
UnassociatedFileEntries / AssociatedFileEntries (only with -i) — identical header, the primary dataset:
ApplicationName,ProgramId,FileKeyLastWriteTimestamp,SHA1,IsOsComponent,FullPath,Name,FileExtension,
LinkDate,ProductName,Size,Version,ProductVersion,LongPathHash,BinaryType,IsPeFile,BinFileVersion,
BinProductVersion,Usn,Language,Description
ProgramEntries:
ProgramId,KeyLastWriteTimestamp,Name,Version,Publisher,InstallDateArpLastModified,InstallDate,InstallDateMsi,
OSVersionAtInstallTime,InstallDateFromLinkFile,BundleManifestPath,HiddenArp,InboxModernApp,Language,ManifestPath,
MsiPackageCode,MsiProductCode,PackageFullName,ProgramInstanceId,RegistryKeyPath,RootDirPath,Type,Source,
StoreAppType,UninstallString,Manufacturer
DriveBinaries:
KeyName,KeyLastWriteTimestamp,DriverTimeStamp,DriverLastWriteTime,DriverName,DriverInBox,DriverIsKernelMode,
DriverSigned,DriverCheckSum,DriverCompany,DriverId,DriverPackageStrongName,DriverType,DriverVersion,ImageSize,
Inf,Product,ProductVersion,Service,WdfVersion
ShortCuts: KeyName,LnkName,KeyLastWriteTimestamp
DevicePnps / DeviceContainers / DriverPackages: generic-grid only.
- All timestamps UTC "yyyy-MM-dd HH:mm:ss". SHA1 covers only the first 30 MB of the file.
- FileKeyLastWriteTimestamp ~= when the binary was first seen/scanned; Amcache presence != execution.
- AmcacheParser is -f single-hive only (no -d). The LIVE hive (C:\Windows\appcompat\Programs\Amcache.hve)
is locked; parse a collected copy, or snapshot it with: esentutl.exe /y <src> /vss /d <dst> (elevated).
- Transaction logs (.LOG1/.LOG2) beside the hive are replayed automatically IN MEMORY — the source hive is
not modified (verified). Keep the LOGs next to the hive; --nl skips replay if the LOGs are corrupt.
- EZ tools need a real console (net4 builds crash at Console.WindowWidth without one) → always run via cmd window.
-->
<style>
html{margin:0;height:100%;background:#0d1117}
body{margin:0;height:100%;background:#0d1117;color:#e6edf3;font-family:Segoe UI,Tahoma,sans-serif;font-size:16px}
#content{zoom:0.9;width:111.111%;box-sizing:border-box} /* zoom<1 shrinks a 100%-wide block below viewport width — widen by 1/zoom in CSS so it fills the window from the first paint (applyZoom keeps it in sync for A-/A+) */
a{color:#58a6ff}
.mono{font-family:Consolas,"Courier New",monospace}
.muted{color:#8b949e}
.err{color:#f85149}
.ok{color:#3fb950}
header{padding:8px 14px;background:#161b22;border-bottom:1px solid #30363d}
header h1{display:inline-block;font-size:15px;margin:0;font-weight:600;vertical-align:middle}
header h1 small{color:#8b949e;font-weight:400}
.pill{display:inline-block;font-size:11px;color:#8b949e;border:1px solid #30363d;border-radius:20px;padding:2px 10px;margin-left:8px;vertical-align:middle}
.pill.ok{color:#7ee787;border-color:#2ea043} /* green: current / present */
.pill.bad{color:#ff8b3d;border-color:#bd561d} /* orange: out of date */
.wrap{padding:12px 14px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:12px;margin-bottom:12px}
.card h3{margin:0 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:#8b949e}
label.fld{display:block;margin:6px 0 2px;font-size:11px;color:#8b949e}
input.txt,select,textarea{background:#1c2330;border:1px solid #30363d;color:#e6edf3;border-radius:6px;padding:5px 8px;font-size:13px;font-family:Consolas,monospace}
input.wide{width:62%}
.btn{background:#1c2330;border:1px solid #30363d;color:#e6edf3;border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;margin:2px}
.btn:hover{border-color:#58a6ff}
.btn.primary{background:#1f6feb;border-color:#1f6feb}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff}
.row{white-space:nowrap}
.stat{display:inline-block;background:#1c2330;border:1px solid #30363d;border-radius:8px;padding:6px 12px;margin:0 6px 6px 0;min-width:70px;vertical-align:top}
.stat .n{font-size:17px;font-weight:700;display:block}
.stat .l{font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px}
.cards3{width:100%}
.col3{display:inline-block;width:32%;vertical-align:top;margin-right:1%}
.brow{white-space:nowrap;margin:3px 0}
.blab{display:inline-block;width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;font-family:Consolas,monospace;font-size:11px}
.blab.lnk{cursor:pointer;color:#58a6ff}
.blab.lnk:hover{text-decoration:underline}
.btrack{display:inline-block;width:32%;height:12px;background:#1c2330;border-radius:3px;overflow:hidden;vertical-align:middle}
.bfill{display:block;height:100%;background:#1f6feb}
.bval{display:inline-block;width:18%;text-align:right;font-size:11px;color:#8b949e;vertical-align:middle}
table{width:100%;border-collapse:collapse;font-size:12px}
th,td{text-align:left;padding:4px 7px;border-bottom:1px solid #30363d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
th{background:#1c2330;cursor:pointer;color:#c9d1d9;position:relative;z-index:3;box-shadow:0 1px 0 #30363d}
th:hover{color:#58a6ff}
.cgrip{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;z-index:6}
.cgrip:hover{background:#58a6ff;filter:alpha(opacity=45);opacity:.45}
td{max-width:360px}
tr.sus td{background:#2a1416}
tr.sus td.first{border-left:3px solid #ff6b6b}
tr.sel td{background:#1c2f4a}
.scroll{max-height:52vh;overflow:auto;border:1px solid #30363d;border-radius:8px;width:100%;box-sizing:border-box}
.tag{display:inline-block;font-size:10px;padding:1px 6px;border-radius:10px;font-weight:600;margin-right:3px}
.tag.sus{background:#3a1416;color:#ff6b6b}
.tag.usr{background:#3a2d10;color:#d29922}
.tag.lol{background:#10263f;color:#58a6ff}
.tag.rare{background:#241a3a;color:#b083f0}
.tag.ioc{background:#3a1416;color:#ff6b6b;border:1px solid #ff6b6b}
.tag.gen{background:#21262d;color:#8b949e}
.dl{font-family:Consolas,"Courier New",monospace;font-size:11px;max-height:210px;overflow:auto;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:6px;margin:4px 0;white-space:nowrap;width:100%;box-sizing:border-box}
.dl div{overflow:hidden;text-overflow:ellipsis}
.hl-user{color:#d29922}
.hl-ioc{color:#ff6b6b;font-weight:700}
#log{white-space:pre-wrap;font-family:Consolas,monospace;font-size:11px;color:#8b949e;max-height:140px;overflow:auto;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px;display:none;margin-top:8px;width:100%;box-sizing:border-box}
.hide{display:none}
.lnk{cursor:pointer;color:#58a6ff}
.lnk:hover{text-decoration:underline}
.sep{display:inline-block;width:1px;height:18px;background:#30363d;margin:0 6px;vertical-align:middle}
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.66);z-index:1000;overflow:auto}
.modal .mcard{max-width:780px;margin:36px auto;background:#161b22;border:1px solid #30363d;border-radius:10px;padding:18px 22px}
.modal h2{margin:0 0 6px;font-size:18px;font-weight:600}
.modal h3{margin:12px 0 4px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b949e}
.modal ul{margin:4px 0;padding-left:20px}.modal li{margin:3px 0;font-size:13px}
.modal p{font-size:13px;margin:6px 0}
.modal code{background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-family:Consolas,monospace;font-size:12px}
.modal .x{float:right;cursor:pointer;color:#8b949e;font-size:22px;line-height:1}.modal .x:hover{color:#e6edf3}
.modal .gh{font-family:Consolas,monospace;font-size:12px}
.maxwarn{display:inline-block;color:#e3b341;font-weight:600;cursor:help;border:1px solid #8a7a2a;background:#2a2410;border-radius:10px;padding:1px 8px;font-size:11px;margin-left:6px;vertical-align:middle}
</style>
</head>
<body>
<header>
<h1>AmcacheParser Wrapper <small>— DFIR Amcache triage</small></h1>
<span class="pill" id="apppill" title="This app's version">app: checking…</span>
<span class="pill" id="verpill" title="AmcacheParser.exe version">AmcacheParser: checking…</span>
<span class="pill" id="srcpill">no data loaded</span>
<span style="float:right">
<button class="btn" id="instrBtn" onclick="openInstructions()" title="Download the latest HTML field guide from GitHub and open it in your browser">Instructions</button>
<button class="btn" id="helpBtn" onclick="openHelp()" title="Help, about & GitHub link">Help</button>
<button class="btn primary hide" id="selfUpd" onclick="doSelfUpdate()" title="A newer version is available on GitHub">Update</button>
<button class="btn" id="fdown" title="Smaller text">A−</button>
<button class="btn" id="fup" title="Larger text">A+</button>
</span>
</header>
<div class="wrap" id="content">
<!-- CONTROL PANEL -->
<div class="card">
<h3>AmcacheParser control panel</h3>
<div class="muted" id="exeline" style="font-size:11px;margin-bottom:6px"></div>
<label class="fld">Amcache hive to process — an Amcache.hve, or a collection folder to search recursively (keep .LOG1/.LOG2 beside the hive for transaction-log replay)
<label style="font-size:12px" title="-i: also export file entries that are associated with a program entry (the AssociatedFileEntries CSV). Without it only unassociated entries are written. Leave on — the analysis view uses both.">
<input type="checkbox" id="optInc" checked> include associated entries</label>
<label style="font-size:12px" title="--nl: do NOT replay the Amcache.hve.LOG1/.LOG2 transaction logs. Replay is in-memory and evidence-safe; only skip it when replay fails on corrupt/mismatched LOG files.">
<input type="checkbox" id="optNl"> skip log replay</label>
</label>
<input class="txt wide" id="pfPath" value="">
<input type="file" id="pfPick" style="display:none">
<button class="btn" id="browseBtn">Browse file…</button>
<button class="btn" id="browseDirBtn" title="Pick a collection folder — it is searched recursively for Amcache.hve (KAPE / Velociraptor trees work as-is).">Browse folder…</button>
<button class="btn" id="liveBtn" title="Copy this machine's LIVE Amcache hive (locked while Windows runs) plus its transaction logs with esentutl /vss into a LiveSnapshot folder next to this app, then set the copy as the input. Requires an ELEVATED run.">Snapshot live Amcache</button>
<div style="margin-top:6px" id="hiveRow" class="hide">
<label class="fld">Multiple Amcache hives found — pick one</label>
<select id="hiveSel" style="min-width:70%"></select>
<button class="btn primary" id="useHiveBtn">Use selected → process</button>
</div>
<label class="fld" title="Required before processing. Guessed from the input path (Collection-<host>-… segments, or this machine for live paths) — overwrite it if the guess is wrong.">Target hostname (required) — names the output folder _Processed\<host>\AmcacheParser next to this app</label>
<input class="txt" id="hostName" value="" style="width:260px" placeholder="e.g. WORKSTATION-01">
<label class="fld">CSV output directory</label>
<input class="txt wide" id="outDir" value="">
<button class="btn" id="openDirBtn">Open folder</button>
<div style="margin-top:10px">
<button class="btn" id="verBtn">Check version</button>
<button class="btn" id="updBtn">Update / download AmcacheParser</button>
<span class="sep"></span>
<button class="btn primary" id="procBtn">Process → analyze</button>
<button class="btn" id="loadBtn">Load existing CSV…</button>
<input type="file" id="csvPick" style="display:none">
<span class="sep"></span>
<button class="btn" id="logToggle">Show log</button>
<span class="muted" id="status" style="margin-left:8px"></span>
</div>
<div style="margin-top:8px" id="producedRow" class="hide">
<label class="fld">Produced CSVs — pick one to view</label>
<select id="producedSel" style="min-width:70%"></select>
<button class="btn" id="loadSelBtn">Load selected</button>
</div>
<div id="log"></div>
</div>
<!-- RESULTS -->
<div id="results" class="hide">
<div class="card">
<h3 id="ovTitle">Overview</h3>
<div id="statbar"></div>
</div>
<div class="card cards3 hide" id="pfCards">
<div class="col3"><h3>Newest first-seen (FileKeyLastWrite)</h3><div id="topNew"></div></div>
<div class="col3"><h3>Top publisher / product</h3><div id="topPubs"></div></div>
<div class="col3"><h3>Suspicious entries</h3><div id="topSus"></div></div>
</div>
<div class="card">
<h3 id="tableTitle">Records</h3>
<div class="row hide" style="margin-bottom:8px" id="viewToggle">
<button class="btn on" id="vFiles" title="One row per Amcache file entry (Unassociated + Associated merged)">File entries</button>
<button class="btn" id="vProgs" title="One row per installed-program entry (ProgramEntries)">Programs</button>
<button class="btn" id="vDrv" title="One row per driver binary (DriveBinaries) — unsigned kernel-mode drivers sort first">Drivers</button>
</div>
<div class="row hide" style="margin-bottom:8px" id="pfCats">
<button class="btn on" data-cat="all">All</button>
<button class="btn" data-cat="sus">Suspicious</button>
<button class="btn" data-cat="userpath">User-path</button>
<button class="btn" data-cat="lolbin">LOLBIN</button>
<button class="btn" data-cat="recent">Recent</button>
<button class="btn" data-cat="nometa">No-metadata PE</button>
<button class="btn" data-cat="unsigned">Unsigned drivers</button>
<button class="btn" data-cat="ioc">IOC hits</button>
</div>
<div class="row" style="margin-bottom:8px">
<input class="txt" id="q" placeholder="search name, path, hash, product, publisher…" style="width:280px">
<span id="userWrap" class="hide"><span class="sep"></span><label class="muted" style="font-size:12px">user:</label>
<select id="userSel" title="Filter File-entry and Program rows to those whose path sits in one user's profile (…\Users\<name>\…). NOTE: Amcache records presence, not the invoking account — this isolates binaries that ran FROM a user's profile, not what that user ran. Rows on system paths group as (no user path); the Drivers view has no user dimension."><option value="">All users</option></select></span>
<span id="srcWrap" class="hide"><span class="sep"></span><label class="muted" style="font-size:12px">source:</label>
<select id="srcSel" title="Filter by source file."><option value="">all source files</option></select></span>
<span id="pfDateCtl" class="hide">
<span class="sep"></span>
<input class="txt mono" id="dfrom" placeholder="from YYYY-MM-DD" style="width:140px" title="Filter by first-seen / installed / driver-write date, per view (UTC)">
<span class="muted">→</span>
<input class="txt mono" id="dto" placeholder="to YYYY-MM-DD" style="width:140px" title="Filter by first-seen / installed / driver-write date, per view (UTC)">
</span>
<span class="sep"></span>
<button class="btn" id="iocBtn">IOC / keywords…</button>
<button class="btn" id="clearBtn">clear</button>
<button class="btn" id="exportBtn">Export view → CSV</button>
<button class="btn" id="copyBtn">Copy for case notes</button>
<span class="muted" id="count" style="margin-left:8px"></span>
</div>
<div id="iocWrap" class="hide" style="margin-bottom:8px">
<label class="fld">IOC / keyword list — comma or newline separated. 40-hex tokens are treated as SHA1 hashes (exact match against each entry's SHA1); everything else is a case-insensitive substring matched against name, path, product, publisher, program and ProgramId. Matches score +3. NB: Amcache SHA1 covers only the first 30 MB of a file.</label>
<textarea id="iocBox" rows="3" style="width:70%;vertical-align:top"></textarea>
<button class="btn" id="iocApply">Apply</button>
<button class="btn" id="iocLoad">Load file…</button>
<input type="file" id="iocPick" style="display:none">
<span class="muted" id="iocInfo" style="font-size:11px"></span>
</div>
<div class="scroll" id="tblScroll"><table id="tbl"></table></div>
<div class="muted" id="pfFoot" style="font-size:11px;margin-top:6px">All timestamps UTC. Suspicious rows are shaded (file entries: score ≥ 3 — combinations like user-path + LOLBIN, masquerade, IOC; programs ≥ 2; unsigned kernel drivers flag directly). <b>Amcache presence ≠ execution</b> — entries are written by the compatibility scanner and can exist for binaries that never ran (and recent activity may be missing if the scanner hasn't run); corroborate with prefetch / SRUM. Click a row for full detail incl. copyable SHA1; click a User cell (or the user: dropdown) to isolate binaries that ran from one profile's path — this is a path attribute, not the invoking account (Amcache records neither). Max 6000 rows shown; export writes the full filtered set.</div>
</div>
<div class="card hide" id="detailCard">
<h3>Record detail <span class="lnk" id="detCopy" style="float:right;margin-left:12px">copy record</span><span class="lnk" id="detClose" style="float:right">close ×</span></h3>
<div id="detBody"></div>
</div>
</div>
</div>
<script language="JScript">
"use strict";
var APP_VERSION = "1.9.2";
var KNOWN_LATEST_AMCP = "2026.5.0"; // EZ site CalVer as of 2026-07-11 (schemesComparable() guards the SemVer-banner mismatch)
var AMCP_ZIP_URL = "https://download.ericzimmermanstools.com/net9/AmcacheParser.zip"; // official EZ mirror (.NET 9 build)
var AMCP_PAGE = "https://ericzimmerman.github.io/"; // for live latest-version lookup
var LIVE_HIVE = "C:\\Windows\\appcompat\\Programs\\Amcache.hve"; // locked while Windows runs — snapshot before parsing
var EXE = "";
var EXE_DIR = "";
var LOCAL_VER = "";
var LATEST_VER = "";
var WEB_OK = false; // could we reach the Zimmerman Tools site to check latest?
var QUERY = "";
var MAXROWS = 6000;
/* Row-limit disclosure (family-wide wave): a capped table that merely says "(showing N)" reads
as complete to anyone who does not know the cap exists. One consistent amber chip wherever
the render cap bites, with the remedy in the tooltip. The cap keeps mshta responsive; the
DATA is intact - exports always write the FULL filtered set. */
function maxRowsWarn(shown, total, extra){
if(!(total>shown)) return "";
return ' <span class="maxwarn" title="'+esc("Drawing the first "+nf(shown)+" of "+nf(total)+" matching rows - the render cap keeps the app responsive; it does NOT mean the rest were discarded. Narrow the view (search box, filters, case date window) to bring what you need inside the cap, or use Export view -> CSV: exports always write the FULL filtered set."+(extra?(" "+extra):""))+'">⚠ Warning: Maximum Rows Exceeded</span>';
}
function copyCapNote(arr){ var total=(arr||[]).length;
return total>500 ? (" - first 500 of "+nf(total)+" (clipboard cap); Export view -> CSV writes the full set") : "";
}
var LASTOUTDIR = "";
var qTimer = null;
var MODE = "pf"; // "pf" = rich Amcache analysis, "generic" = raw table
var ROWS = []; // Amcache file-entry records (Unassociated + Associated merged)
var CURCAT = "all";
var DFROM = 0, DTO = 0;
var VIEW = "files"; // "files" | "progs" | "drv"
var FSORT = {k:3, d:-1}; // files view: default First seen desc
var PSORT = {k:4, d:-1}; // programs view: default Installed desc
var DSORT = {k:0, d:1}; // drivers view: default Tags (score) — unsigned KMD first
var IOC_HASHES = []; // lowercased 40-hex SHA1 IOCs (exact match)
var IOC_TERMS = []; // lowercased keyword IOCs (substring match)
var DETREC = null; // record currently shown in the detail pane
var DETKIND = "file"; // which view the detail record belongs to: file | prog | drv
var GHEAD = null, GROWS = [], GSRC = "";
var GSORT = {k:-1, d:1}; // generic grid: unsorted until a header is clicked
var GSRCIDX = -1; // generic mode: index of the SourceFile(name) column
var SRCFILTER = ""; // selected source-file filter (generic mode)
var USERFILTER = ""; // selected user filter (pf mode; userDisp value from a path — content, not provenance; "" = all)
var BUSY = false; // a AmcacheParser run is in progress
var PRODUCED = []; // CSVs produced by the last run
var AUTO_RUN = false; // launched by the Finder with /auto (bulk) — reuse existing output silently
var FORCE_REPROCESS = false; // /force on the CLI — always re-run even if prior output exists (item 8)
/* suspicion knobs (spec §7) */
var USERPATHS = ["\\users\\","\\appdata\\","\\temp\\","\\downloads\\","\\programdata\\","\\perflogs\\","\\windows\\temp\\","\\$recycle.bin\\","\\public\\"];
var LOLBINS = {powershell:1,powershell_ise:1,pwsh:1,cmd:1,wscript:1,cscript:1,mshta:1,rundll32:1,regsvr32:1,certutil:1,certreq:1,
bitsadmin:1,msiexec:1,curl:1,wget:1,node:1,java:1,javaw:1,python:1,pythonw:1,php:1,autoit3:1,nc:1,ncat:1,psexec:1,psexesvc:1,
wmic:1,schtasks:1,reg:1,sc:1};
var OSNAMES = {svchost:1,lsass:1,csrss:1,services:1,winlogon:1,explorer:1,taskhostw:1,dllhost:1,conhost:1,smss:1,wininit:1};
var RECENT_DAYS = 14;
/* ---------- tiny helpers ---------- */
function $(id){ return document.getElementById(id); }
function esc(s){ if(s==null) return ""; s=String(s);
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); }
function endsWith(s,suf){ if(!s) return false; s=String(s); return s.length>=suf.length && s.substring(s.length-suf.length)===suf; }
function contains(s,sub){ return String(s).indexOf(sub)>=0; }
function nf(n){ n=String(n); var dot=n.indexOf("."); var dec=""; if(dot>=0){ dec=n.substring(dot); n=n.substring(0,dot); }
var r="",c=0; for(var k=n.length-1;k>=0;k--){ r=n.charAt(k)+r; c++; if(c%3===0&&k>0) r=","+r; } return r+dec; }
function setStatus(msg,cls){ var el=$("status"); el.innerHTML=esc(msg); el.className=cls||"muted"; }
function appendLog(t){ var el=$("log"); if(el.getAttribute("data-ph")==="1"){ el.innerHTML=""; el.setAttribute("data-ph","0"); }
el.style.display="block"; $("logToggle").innerHTML="Hide log"; el.innerHTML+=esc(t)+"\n"; el.scrollTop=el.scrollHeight; }
function trimStr(s){ return String(s).replace(/^\s+|\s+$/g,""); }
function baseName(p){ if(!p) return ""; p=String(p); var i=Math.max(p.lastIndexOf("\\"),p.lastIndexOf("/")); return i>=0?p.substring(i+1):p; }
function showEl(id,vis){ var e=$(id); if(!e) return; var c=e.className.replace(/\s*\bhide\b/g,"");
e.className = vis ? c : (c+" hide"); }
function msOf(s){ if(!s) return 0; var m=String(s).match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/);
if(!m) return 0; return Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6]); }
function dayOf(s){ if(!s) return 0; var m=String(s).match(/(\d{4})-(\d{2})-(\d{2})/); if(!m) return 0; return Number(m[1]+m[2]+m[3]); }
/* ---------- file IO via ActiveX (UTF-8 safe, with network-zone fallback) ----------
ADODB.Stream is the proper UTF-8 reader, but when the .hta runs from a NETWORK location
(mapped drive / UNC share) mshta assigns it the intranet/internet zone and ADODB refuses
every LoadFromFile/SaveToFile with "Safety settings on this computer prohibit accessing a
data source on another domain" — regardless of where the target file lives. Seen in the
field 2026-07-02 (app run from Z:\ on an analysis VM). FSO is not zone-checked, so we fall
back to ANSI file IO: CSV structure is unaffected (all-ASCII), only rare non-ASCII chars
(accented usernames in paths) display mojibake'd. Best fidelity = run locally. */
var FILE_IO="adodb"; // flips to "fso" permanently on the first zone-policy block
function zoneNote(){
appendLog("NOTE: this app is running from a network location, so Windows zone policy blocks the UTF-8 file reader (ADODB.Stream). Falling back to ANSI file IO — everything works, but non-ASCII characters (accented usernames in paths) may display incorrectly. For full fidelity copy this folder to a LOCAL path (e.g. C:\\Tools\\AmcacheParser) and run it from there.");
}
function readUtf8(path){
if(FILE_IO==="adodb"){
try{
// ReadText(-1) on a large file is quadratic in MSHTML (30s for 16MB); read in 64KB chunks (~0.1s).
var st=new ActiveXObject("ADODB.Stream");
st.Type=2; st.Charset="utf-8"; st.Open(); st.LoadFromFile(path);
var parts=[]; while(!st.EOS){ parts.push(st.ReadText(65536)); } st.Close();
return parts.join("");
}catch(e){
if(!/safety settings|data source/i.test(e.message||"")) throw e; // real IO errors propagate
FILE_IO="fso"; zoneNote();
}
}
var fso=new ActiveXObject("Scripting.FileSystemObject");
var f=fso.OpenTextFile(path,1,false,0), out=f.ReadAll(); f.Close();
// a UTF-8 BOM read as ANSI shows up as three cp1252 chars — strip it
if(out.length>=3 && out.charCodeAt(0)===239 && out.charCodeAt(1)===187 && out.charCodeAt(2)===191) out=out.substring(3);
return out;
}
function writeUtf8(path,text){
if(FILE_IO==="adodb"){
try{
var st=new ActiveXObject("ADODB.Stream");
st.Type=2; st.Charset="utf-8"; st.Open(); st.WriteText(text); st.SaveToFile(path,2); st.Close();
return;
}catch(e){
if(!/safety settings|data source/i.test(e.message||"")) throw e;
FILE_IO="fso"; zoneNote();
}
}
var fso=new ActiveXObject("Scripting.FileSystemObject");
var f=fso.CreateTextFile(path,true,false); f.Write(text); f.Close();
}
function uid(){ return new Date().getTime()+""+Math.floor(Math.random()*100000); }
/* cmd expands %N / %VAR% inside .bat lines even within quotes — Velociraptor trees contain literal % (C%3A) */
function batEsc(s){ return String(s).replace(/%/g,"%%"); }
/* Run a batch script, wait, return {rc, log}. show=true → visible console window. */
function runBat(lines, show){
var fso=new ActiveXObject("Scripting.FileSystemObject");
var sh=new ActiveXObject("WScript.Shell");
var tmp=fso.GetSpecialFolder(2)+"";
var stamp=uid();
var bat=tmp+"\\amc_"+stamp+".bat";
var logp=tmp+"\\amc_"+stamp+".log";
var f=fso.CreateTextFile(bat,true);
f.WriteLine("@echo off");
for(var i=0;i<lines.length;i++){ f.WriteLine(lines[i].replace(/__LOG__/g,logp)); }
f.Close();
var rc=sh.Run('cmd /c ""'+bat+'""', show?1:0, true);
var txt="";
try{ if(fso.FileExists(logp)) txt=readUtf8(logp); }catch(e){ txt="(could not read log: "+e.message+")"; }
try{ fso.DeleteFile(bat); }catch(e2){}
try{ if(fso.FileExists(logp)) fso.DeleteFile(logp); }catch(e3){}
return { rc:rc, log:txt };
}
/* Run a batch NON-blocking; writes a .done marker when finished. Caller polls with pollDone().
This keeps the HTA script responsive (no MSHTML long-running-script watchdog). */
function runBatAsync(lines, show){
var fso=new ActiveXObject("Scripting.FileSystemObject");
var sh=new ActiveXObject("WScript.Shell");
var tmp=fso.GetSpecialFolder(2)+"";
var stamp=uid();
var bat=tmp+"\\amc_"+stamp+".bat";
var logp=tmp+"\\amc_"+stamp+".log";
var donep=tmp+"\\amc_"+stamp+".done";
var f=fso.CreateTextFile(bat,true);
f.WriteLine("@echo off");
for(var i=0;i<lines.length;i++){ f.WriteLine(lines[i].replace(/__LOG__/g,logp)); }
f.WriteLine('echo done> "'+donep+'"'); // marker written after everything above completes
f.Close();
sh.Run('cmd /c ""'+bat+'""', show?1:0, false); // false = do NOT block the script
return { bat:bat, log:logp, done:donep, stamp:stamp, elapsed:0, graceSecs:10 };
}
function procAliveByTag(tag){
try{
var w=GetObject("winmgmts:\\\\.\\root\\cimv2");
var items=w.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE CommandLine LIKE '%amc_"+tag+"%'");
return items.Count>0;
}catch(e){ return true; } // WMI unavailable → assume alive, so we never end a run early on error
}
function pollDone(ctx){
var fso=new ActiveXObject("Scripting.FileSystemObject");
var grace=ctx.graceSecs||10;
var finished=fso.FileExists(ctx.done);
if(!finished && ctx.elapsed>=grace && ctx.stamp && !procAliveByTag(ctx.stamp)) finished=true; // worker gone but .done sentinel lost -> still complete (recovers finished runs)
if(finished){
var log=""; try{ if(fso.FileExists(ctx.log)) log=readUtf8(ctx.log); }catch(e){ log="(could not read log: "+e.message+")"; }
try{ fso.DeleteFile(ctx.bat); }catch(e1){}
try{ fso.DeleteFile(ctx.done); }catch(e2){}
try{ if(fso.FileExists(ctx.log)) fso.DeleteFile(ctx.log); }catch(e3){}
ctx.onDone(log);
return;
}
ctx.elapsed+=1;
if(ctx.elapsed>1800){ // 30-min safety cap
try{ fso.DeleteFile(ctx.bat); }catch(eT1){} // tidy the temp triplet even when we stop watching (F12)
try{ if(fso.FileExists(ctx.done)) fso.DeleteFile(ctx.done); }catch(eT2){}
try{ if(fso.FileExists(ctx.log)) fso.DeleteFile(ctx.log); }catch(eT3){}
ctx.onTimeout&&ctx.onTimeout(); return; }
if(ctx.onTick) ctx.onTick(ctx.elapsed);
window.setTimeout(function(){ pollDone(ctx); }, 1000);
}
/* ---------- source-file filter (generic mode) ---------- */
function distinctVals(arr){ var seen={}, out=[]; for(var i=0;i<arr.length;i++){ var v=arr[i]; if(v!=null && v!=="" && !seen[v]){ seen[v]=1; out.push(v); } } return out; }
function shortPathN(p,n){ p=String(p); var parts=p.split("\\"); if(parts.length<=n) return p; return "…\\"+parts.slice(parts.length-n).join("\\"); }
function shortPath(p){ return shortPathN(p,4); }
function populateSourceFilter(vals){
var sel=$("srcSel"); sel.innerHTML="";
var o0=document.createElement("option"); o0.value=""; o0.text="all source files ("+vals.length+")"; sel.add(o0);
vals=vals.slice(0).sort();
for(var i=0;i<vals.length;i++){ var o=document.createElement("option"); o.value=vals[i]; o.text=shortPath(vals[i]); sel.add(o); }
SRCFILTER=""; sel.value="";
showEl("srcWrap", vals.length>1);
}
/* CONTENT-derived user (not provenance): the \Users\<name>\ segment of an executed-file / install path.
Amcache is a single system hive with no SourceFile and no invoking-user field, so this answers
"which binaries ran from user X's profile", not "what did user X run". Blank => system path. */
function userFromPath(p){ if(!p) return ""; var m=String(p).match(/[\\\/]users[\\\/]([^\\\/]+)/i); return m?m[1]:""; }
function populateUserFilter(vals){
var sel=$("userSel"); if(!sel) return;
var prev=USERFILTER, found=false;
sel.innerHTML="";
var o0=document.createElement("option"); o0.value=""; o0.text="All users ("+vals.length+")"; sel.add(o0);
vals=vals.slice(0).sort();
for(var i=0;i<vals.length;i++){ var o=document.createElement("option"); o.value=vals[i]; o.text=vals[i]; sel.add(o);
if(vals[i]===prev) found=true; }
if(prev&&found){ sel.value=prev; } else { USERFILTER=""; sel.value=""; }
showEl("userWrap", vals.length>1);
}
/* ---------- tooling: locate / version / download ---------- */
function fileExists(p){ try{ return new ActiveXObject("Scripting.FileSystemObject").FileExists(p); }catch(e){ return false; } }
function folderExists(p){ try{ return new ActiveXObject("Scripting.FileSystemObject").FolderExists(p); }catch(e){ return false; } }
function parentDir(p){ var i=String(p).lastIndexOf("\\"); return i>=0?p.substring(0,i):p; }
function ensureDir(p){ try{ var fso=new ActiveXObject("Scripting.FileSystemObject");
if(!p||fso.FolderExists(p)) return fso.FolderExists(p);
var parent=parentDir(p); if(parent&&parent!==p&&!fso.FolderExists(parent)) ensureDir(parent);
fso.CreateFolder(p); return true; }catch(e){ return false; } }
function htaFolder(){
try{ var cl=amcacheHta.commandLine+""; var m=cl.match(/"([^"]+\.hta)"/i); if(!m) m=cl.match(/([A-Za-z]:\\[^"]+\.hta)/i);
if(m) return parentDir(m[1]); }catch(e){}
try{ var p=decodeURIComponent(location.pathname+"").replace(/^\//,"").replace(/\//g,"\\"); return parentDir(p); }catch(e2){}
return "";
}
function resolveExe(){
var fso=new ActiveXObject("Scripting.FileSystemObject");
var dir=htaFolder(), cands=[];
if(dir) cands.push(dir+"\\AmcacheParser.exe");
cands.push("C:\\ZimmermanTools\\net6\\AmcacheParser.exe"); // prefer net6/net9 builds; the root net4 exe is console-fragile
cands.push("C:\\ZimmermanTools\\AmcacheParser.exe");
for(var i=0;i<cands.length;i++){ if(fso.FileExists(cands[i])){ EXE=cands[i]; EXE_DIR=parentDir(cands[i]); return true; } }
EXE=""; EXE_DIR=dir||""; return false;
}
function checkDeps(){
// framework-dependent builds ship AmcacheParser.dll + runtimeconfig next to the exe; self-contained net4 has neither
var fso=new ActiveXObject("Scripting.FileSystemObject"); var miss=[];
if(!EXE_DIR) return ["(install folder unknown)"];
if(fso.FileExists(EXE_DIR+"\\AmcacheParser.runtimeconfig.json") && !fso.FileExists(EXE_DIR+"\\AmcacheParser.dll")) miss.push("AmcacheParser.dll");
return miss;
}
function verTuple(v){ var p=String(v).split("."); var a=[]; for(var i=0;i<p.length;i++) a.push(parseInt(p[i],10)||0); return a; }
function verCmp(a,b){ a=verTuple(a); b=verTuple(b); var n=Math.max(a.length,b.length);
for(var i=0;i<n;i++){ var x=a[i]||0,y=b[i]||0; if(x!==y) return x<y?-1:1; } return 0; }
/* EZ Tools moved to CalVer (year-based major, e.g. 2026.5.0) but the shipped exe --version banners still
report the old SemVer (e.g. 1.5.1.0). Numbers from the two schemes are NOT comparable — verCmp'ing them
yields a permanent bogus "update available". Only compare when both look like the same scheme. */
function schemesComparable(a,b){ var am=parseInt(String(a).split(".")[0],10)||0, bm=parseInt(String(b).split(".")[0],10)||0;
return (am>=2000)===(bm>=2000); }
/* latest-version lookup — ASYNC (does not block launch; the site fetch used to freeze startup 2–4 s offline) */
function fetchLatestAsync(done){
LATEST_VER=KNOWN_LATEST_AMCP; WEB_OK=false;
try{
var x=new ActiveXObject("MSXML2.XMLHTTP.6.0");
x.open("GET", AMCP_PAGE, true);
x.onreadystatechange=function(){ if(x.readyState!==4) return;
try{ if(x.status===200){ WEB_OK=true; var t=x.responseText+"";
var m=t.match(/AmcacheParser[\s\S]{0,800}?(\d+\.\d+\.\d+)/i); if(m) LATEST_VER=m[1]; } }catch(e){}
if(done) done();
};
x.send();
}catch(e){ WEB_OK=false; if(done) done(); }
}
/* exe pill: green = present/current, orange = a comparable newer version exists, grey = not found (downloaded on demand) */
function updateVerPill(){
var p=$("verpill"); if(!p) return;
if(!LOCAL_VER){ p.className="pill"; p.innerHTML="AmcacheParser: not found";
p.title="AmcacheParser.exe is not next to this app or in C:\\ZimmermanTools — it is downloaded on demand when you Process."; return; }
var outdated = WEB_OK && LATEST_VER && schemesComparable(LOCAL_VER,LATEST_VER) && verCmp(LOCAL_VER,LATEST_VER)<0;
if(outdated){ p.className="pill bad"; p.innerHTML="AmcacheParser v"+esc(LOCAL_VER)+" · update v"+esc(LATEST_VER);
p.title="A newer AmcacheParser (v"+LATEST_VER+") is on the Zimmerman Tools site — use 'Update / download AmcacheParser'."; return; }
p.className="pill ok"; p.innerHTML="AmcacheParser v"+esc(LOCAL_VER);
p.title = !WEB_OK ? ("Present and working ("+EXE+"). Couldn't reach the Zimmerman Tools site to check for a newer version.")
: (LATEST_VER && !schemesComparable(LOCAL_VER,LATEST_VER)
? ("Present ("+EXE+"). Site lists v"+LATEST_VER+" (CalVer) vs the exe banner v"+LOCAL_VER+" (SemVer) — different schemes, not comparable, so no bogus update is forced.")
: ("Up to date ("+EXE+")."));
}
function evaluateTooling(promptIfNeeded){
var dir=htaFolder();
var localComplete = dir ? fileExists(dir+"\\AmcacheParser.exe") : false; // exe next to the .hta (self-contained)
var found=resolveExe();
if(EXE) $("exeline").innerHTML="Tool: "+esc(EXE)+" · run with DOTNET_ROLL_FORWARD=Major (covers .NET 9-target builds)";
else $("exeline").innerHTML='<span class="err">AmcacheParser.exe not found next to this app ('+esc(dir||"app folder")+') or in C:\\ZimmermanTools</span>';
var miss=found?checkDeps():[];
function afterVer(){
if(miss.length) setStatus("AmcacheParser support files missing next to the exe: "+miss.join(", "),"err"); // re-assert over the version status
fetchLatestAsync(function(){ // …then check the site for the latest WITHOUT blocking launch (F10)
updateVerPill();
if(promptIfNeeded) maybePromptTooling(found, localComplete, dir);
});
}
if(found) checkVersion(afterVer); // async exe banner run — a cold .NET start no longer blocks launch (1.9.0)
else { LOCAL_VER=""; updateVerPill(); afterVer(); }
}
function maybePromptTooling(found, localComplete, dir){
if(localComplete){
// exe already next to the .hta → only prompt when the web says a genuinely-newer (same-scheme) version exists (F1: never on CalVer/SemVer mismatch)
if(WEB_OK && LOCAL_VER && schemesComparable(LOCAL_VER,LATEST_VER) && verCmp(LOCAL_VER,LATEST_VER)<0){
if(confirm("A newer AmcacheParser is available.\n\nNext to this app: v"+LOCAL_VER+"\nLatest online: v"+LATEST_VER+"\n\nDownload the update next to the app now?")) downloadLatest();
}
} else {
if(WEB_OK){
var msg = (found ? ("AmcacheParser is not set up next to this app (currently using "+EXE+").\n\n") : "AmcacheParser.exe was not found.\n\n")
+ "Download the latest (v"+LATEST_VER+") to a self-contained copy next to the app at:\n"+(dir||"?")+"\n\nProceed?";
if(confirm(msg)) downloadLatest();
} else if(found){
setStatus("Using local AmcacheParser ("+EXE+") — no internet to reach Zimmerman Tools, so can't verify/download the latest.","muted");
} else {
setStatus("AmcacheParser.exe not found next to this .hta and no internet to download it. Place AmcacheParser.exe (net6/net9 build) next to this .hta.","err");
}
}
}
/* stage a temp .ps1 (stop-on-error + TLS 1.2 preamble prepended), run it hidden via runBat, return the
captured output. Blocking, like the runBat it wraps. Staging or launch failures throw to the caller;
PowerShell-level failures land in the returned log (callers check their OK sentinel). Shared by the
AmcacheParser download, the field-guide fetch and self-update — a .ps1 file avoids -Command quoting. */
function runPsScript(lines){
var fso=new ActiveXObject("Scripting.FileSystemObject");
var ps1=fso.GetSpecialFolder(2)+"\\amc_ps_"+uid()+".ps1";
var L=["$ErrorActionPreference='Stop'","[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12"].concat(lines);
var f=fso.CreateTextFile(ps1,true); f.Write(L.join("\r\n")); f.Close();
try{
var r=runBat(['powershell -NoProfile -ExecutionPolicy Bypass -File "'+ps1+'" > "__LOG__" 2>&1']);
}finally{
try{ fso.DeleteFile(ps1); }catch(e0){}
}
return r.log;
}
function downloadLatest(silent, done){ // silent: skip the confirms (unattended /auto); done(ok): fired after install
var target=htaFolder()||EXE_DIR; // always install next to the .hta (self-contained)
if(!target){ setStatus("Cannot determine the app folder to install into","err"); if(done) done(false); return; }
if(!silent && !WEB_OK && !confirm("The Zimmerman Tools site did not respond earlier (no internet?).\n\nTry downloading AmcacheParser from\n"+AMCP_ZIP_URL+"\ninto\n"+target+" anyway?")) return;
if(!silent && WEB_OK && !confirm("Download latest AmcacheParser (v"+(LATEST_VER||KNOWN_LATEST_AMCP)+") from:\n"+AMCP_ZIP_URL+"\n\nInstall next to the app into:\n"+target+"\n\nProceed?")) return;
setStatus("Downloading & installing AmcacheParser… (give it a moment)","muted");
var tq=String(target).replace(/'/g,"''");
try{
var log=runPsScript([
"$u='"+AMCP_ZIP_URL+"'",
"$z=Join-Path $env:TEMP 'AmcacheParser_dl.zip'",
"$x=Join-Path $env:TEMP ('AmcacheParser_x_'+[guid]::NewGuid().ToString('N'))",
"Invoke-WebRequest -Uri $u -OutFile $z",
"Expand-Archive -Force -Path $z -DestinationPath $x",
"$exe=Get-ChildItem -Path $x -Recurse -Filter AmcacheParser.exe | Select-Object -First 1",
"if(-not $exe){throw 'AmcacheParser.exe not found in downloaded archive'}",
"$src=Split-Path $exe.FullName",
"New-Item -ItemType Directory -Force -Path '"+tq+"' | Out-Null",
"Copy-Item -Path (Join-Path $src '*') -Destination '"+tq+"' -Recurse -Force",
"Remove-Item $z,$x -Recurse -Force -ErrorAction SilentlyContinue",
"Write-Output 'INSTALL_OK'"]);
appendLog("--- AmcacheParser download / install ---\n"+log);
if((log+"").indexOf("INSTALL_OK")>=0){ setStatus("AmcacheParser installed to "+target,"ok"); evaluateTooling(false); if(done) done(true); }
else { setStatus("Download/install failed — see log","err"); if(done) done(false); }
}catch(e){ setStatus("Download failed: "+e.message,"err"); if(done) done(false); }
}
/* /auto launch: if the tool exe is missing, fetch it first and run in the install callback.
The Finder's Process / Process-all fires wrappers unattended, so a missing exe must self-resolve
rather than just erroring. Exe already present, or genuinely offline, falls straight through to the
normal run (processPf emits the existing clear "not found" message when offline). */
function autoProcess(){
if(EXE && fileExists(EXE)){ processPf(); return; }
if(WEB_OK){
setStatus("AmcacheParser.exe not present - downloading it before this auto run…","muted");
appendLog("/auto: AmcacheParser.exe missing - fetching it before processing");
downloadLatest(true, function(ok){ if(ok) processPf(); });
return;
}
processPf(); // offline: let processPf surface the existing "not found" error
}
/* ---------- actions ---------- */
/* ASYNC: runs the exe hidden and parses its version banner; done() (optional) fires afterwards either
way. Was synchronous via runBat — a cold .NET start blocked the UI 1–3 s at every launch (1.9.0). */
function checkVersion(done){
if(typeof done!=="function") done=null; // belt-and-braces against a stray event object
if(!EXE || !fileExists(EXE)){ LOCAL_VER=""; updateVerPill(); setStatus("AmcacheParser.exe not found — use 'Update / download AmcacheParser'","err"); if(done) done(); return; }
var ctx;
try{
ctx=runBatAsync([
"set DOTNET_ROLL_FORWARD=Major",
'"'+EXE+'" > "__LOG__" 2>&1'
], false);
}catch(e){ LOCAL_VER=""; updateVerPill(); setStatus("Version check failed: "+e.message,"err"); if(done) done(); return; }
ctx.onDone=function(log){
var m=log.match(/AmcacheParser version ([0-9.]+)/i);
if(m){ LOCAL_VER=m[1]; updateVerPill(); setStatus("AmcacheParser v"+LOCAL_VER+" detected","ok"); }
else { LOCAL_VER=""; updateVerPill(); setStatus("Ran AmcacheParser but could not parse version — see log","err"); appendLog(log); }
if(done) done();
};
ctx.onTimeout=function(){ LOCAL_VER=""; updateVerPill(); setStatus("Version check timed out — the exe may be blocked or broken; see 'Check version' / the log.","err"); if(done) done(); };
window.setTimeout(function(){ pollDone(ctx); }, 1000);
}
/* can we actually enumerate this folder? (live C:\Windows\Prefetch is admin-only) */
function canListFolder(p){
try{ var fso=new ActiveXObject("Scripting.FileSystemObject");
var e=new Enumerator(fso.GetFolder(p).Files); e.atEnd(); return true; }catch(err){ return false; }
}
/* recursively find Amcache.hve under a collection root (KAPE / Velociraptor trees; URL-encoded
path segments like C%3A are just literal characters to FSO). Unreadable subfolders are skipped.
Capped — a hit list longer than HIVE_MAX means the wrong root was picked anyway. */
var HIVE_MAX=20, HIVE_DEPTH_MAX=24, HIVE_SCAN_MAX=30000, HIVE_DIRS_SCANNED=0;
function findHives(root){
var fso=new ActiveXObject("Scripting.FileSystemObject"), out=[];
HIVE_DIRS_SCANNED=0;
function walk(fld,depth){
if(out.length>=HIVE_MAX||depth>HIVE_DEPTH_MAX||HIVE_DIRS_SCANNED>=HIVE_SCAN_MAX) return;
HIVE_DIRS_SCANNED++;
try{
var fe=new Enumerator(fld.Files);
for(;!fe.atEnd();fe.moveNext()){ var f=fe.item();
if(String(f.Name).toLowerCase()==="amcache.hve"){ out.push({path:f.Path+"", size:f.Size}); if(out.length>=HIVE_MAX) return; } }
}catch(e1){}
try{
var de=new Enumerator(fld.SubFolders);
for(;!de.atEnd();de.moveNext()){ walk(de.item(),depth+1); if(out.length>=HIVE_MAX) return; }
}catch(e2){}
}
try{ walk(fso.GetFolder(root),0); }catch(e){}
return out;
}
/* snapshot the LIVE (locked) hive + transaction logs via esentutl /vss — needs elevation */
function snapshotLive(){
if(BUSY){ setStatus("A run is already in progress — wait for it to finish.","err"); return; }
var fso=new ActiveXObject("Scripting.FileSystemObject");
var base=htaFolder(); if(!base){ setStatus("Cannot determine the app folder","err"); return; }
var snapRoot=base+"\\LiveSnapshot";
try{ if(!fso.FolderExists(snapRoot)) fso.CreateFolder(snapRoot); }catch(e0){}
var dst=snapRoot+"\\"+nowStamp();
try{ if(!fso.FolderExists(dst)) fso.CreateFolder(dst); }catch(e1){ setStatus("Cannot create "+dst+": "+e1.message,"err"); return; }
var src="C:\\Windows\\appcompat\\Programs";
BUSY=true; $("procBtn").disabled=true;
appendLog("Snapshotting live Amcache via esentutl /y /vss into "+dst+" …");
var ctx=runBatAsync([
"echo Snapshotting the live Amcache hive (esentutl /vss — needs an elevated app)…",
'esentutl.exe /y "'+src+'\\Amcache.hve" /vss /d "'+dst+'\\Amcache.hve" >> "__LOG__" 2>&1',
'esentutl.exe /y "'+src+'\\Amcache.hve.LOG1" /vss /d "'+dst+'\\Amcache.hve.LOG1" >> "__LOG__" 2>&1',
'esentutl.exe /y "'+src+'\\Amcache.hve.LOG2" /vss /d "'+dst+'\\Amcache.hve.LOG2" >> "__LOG__" 2>&1'
], true);
ctx.onTick=function(s){ setStatus("esentutl running… "+s+"s","muted"); };
ctx.onTimeout=function(){ BUSY=false; $("procBtn").disabled=false; setStatus("esentutl still running after 30 min — check the console window.","err"); };
ctx.onDone=function(lg){
BUSY=false; $("procBtn").disabled=false;
appendLog("--- esentutl output ---\n"+lg);
if(fileExists(dst+"\\Amcache.hve")){
$("pfPath").value=dst+"\\Amcache.hve";
hostGuessFrom(LIVE_HIVE); // snapshot of THIS machine's live hive → guess this computer's name
var haveLogs=fileExists(dst+"\\Amcache.hve.LOG1")&&fileExists(dst+"\\Amcache.hve.LOG2");
setStatus("Live hive snapshotted to "+dst+(haveLogs?"":" — transaction logs did NOT copy; parsing may miss the newest entries")+" — click 'Process → analyze'.","ok");
} else {
try{ fso.DeleteFolder(dst); }catch(e2){}
setStatus("Snapshot FAILED — esentutl /vss needs an ELEVATED app. Relaunch as administrator (right-click → Run as administrator), or collect the hive with Velociraptor/KAPE instead. See the log.","err");
}
};
setStatus("Snapshotting live Amcache…","muted");
window.setTimeout(function(){ pollDone(ctx); }, 1000);
}
/* ---------- target hostname + _Processed output convention (family, 2026-07-06) ----------
All processed output lands in <appFolder>\_Processed\<Hostname>\<AppName>\ so the
Artifact-Finder (and the operator) can see per-host state at a glance. The hostname field is
REQUIRED before processing (not for just viewing CSVs). Guess order: Collection-<host>-<date>
segment in the input path → \_Processed\<host>\ in a CLI-passed outDir → live-machine paths
(X:\Windows / X:\Users → this computer's name). User-typed values are never overwritten. */
var APP_NAME="AmcacheParser";
var HOST_TOUCHED=false, OUT_TOUCHED=false;
function sanitHost(s){ return String(s).replace(/[^A-Za-z0-9._\-]/g,"-").replace(/^[\-.]+|[\-.]+$/g,""); }
function guessHost(p){
p=String(p||"");
var m=p.match(/Collection-([A-Za-z0-9][A-Za-z0-9\-_\.]*?)-\d{4}-\d{2}-\d{2}/i); if(m) return m[1];
m=p.match(/[\\\/]_Processed[\\\/]([^\\\/]+)[\\\/]/i); if(m) return m[1];
if(/^[a-z]:\\(windows|users)(\\|$)/i.test(p)){ try{ return new ActiveXObject("WScript.Network").ComputerName+""; }catch(e){} }
return "";
}
function defaultOutDir(){
var h=sanitHost($("hostName").value); if(!h) return "";
var base=htaFolder(); if(!base){ try{ base=new ActiveXObject("Scripting.FileSystemObject").GetSpecialFolder(2)+""; }catch(e){ return ""; } }
return base+"\\_Processed\\"+h+"\\"+APP_NAME;
}
function syncOutDir(){ if(OUT_TOUCHED) return; var d=defaultOutDir(); if(d) $("outDir").value=d; }
function hostGuessFrom(p){
if(!HOST_TOUCHED || !trimStr($("hostName").value)){ var g=guessHost(p); if(g) $("hostName").value=sanitHost(g); }
syncOutDir();
}
function requireHost(){
var h=sanitHost($("hostName").value);
if(!h){ setStatus("Target hostname is required — it names the output folder _Processed\\<host>\\"+APP_NAME+". Type the host this artifact came from, then process again.","err");
try{ $("hostName").focus(); }catch(e){} return ""; }
if(!trimStr($("outDir").value)) syncOutDir();
return h;
}
/* ---------- shared toolkit IOC list (family, 2026-07-06) ----------
IOC.txt next to the .hta (the toolkit folder — same file for every wrapper + the
Artifact-Finder): one term per line, # comments. Auto-merged into the IOC box at launch
(before any /auto run) so one paste covers the whole engagement; terms already present
are not duplicated, and the operator's box stays editable as usual. */
/* merge newline/comma/semicolon-separated IOC terms (# comment lines dropped) into the IOC box,
skipping terms already present (case-insensitive). Returns how many were added; the caller
decides whether to applyIocs(). Shared by the toolkit IOC.txt auto-load and 'Load file…'. */
function mergeIocText(text){
var lines=String(text).split(/\r?\n/), add=[], i, j;
for(i=0;i<lines.length;i++){ var ln=trimStr(lines[i]);
if(!ln || ln.charAt(0)==="#") continue; // whole-line comments — commas inside never leak as terms
var parts=ln.split(/[,;]+/);
for(j=0;j<parts.length;j++){ var s=trimStr(parts[j]); if(s) add.push(s); } }
if(!add.length) return 0;
var cur=$("iocBox").value, curL={}, cp=String(cur).split(/[\r\n,;]+/);
for(i=0;i<cp.length;i++){ var c=trimStr(cp[i]).toLowerCase(); if(c) curL[c]=1; }
var fresh=[]; for(i=0;i<add.length;i++){ if(!curL[add[i].toLowerCase()]) fresh.push(add[i]); }
if(!fresh.length) return 0;
$("iocBox").value=(trimStr(cur)?(cur.replace(/\s+$/,"")+"\r\n"):"")+fresh.join("\r\n");
return fresh.length;
}
function loadToolkitIoc(){
try{
var base=htaFolder(); if(!base) return;
var p=base+"\\IOC.txt";
if(!fileExists(p)) return;
var n=mergeIocText(readUtf8(p));
if(!n) return;
applyIocs();
appendLog("Toolkit IOC list: merged "+n+" term(s) from "+p);
setStatus("Loaded "+n+" IOC term(s) from the toolkit IOC.txt","ok");
}catch(e){}
}
/* ---------- run provenance (family, 2026-07-06) ----------
Appended into <outDir>\runinfo.json after every successful run. The Artifact-Finder reads
these to bind outputs to their exact source artifact (and host) even without its manifest —
standalone wrapper runs show up as processed too. */
function writeRunInfo(outdir, input, freshFiles){
try{
var p=String(outdir).replace(/[\\\/]+$/,"")+"\\runinfo.json";
var arr=[];
try{ if(fileExists(p)){ var old=JSON.parse(readUtf8(p)); if(old && old.length!=null) arr=old; } }catch(e0){}
var d=new Date(); function p2(x){ return (x<10?"0":"")+x; }
var iso=d.getUTCFullYear()+"-"+p2(d.getUTCMonth()+1)+"-"+p2(d.getUTCDate())+"T"+p2(d.getUTCHours())+":"+p2(d.getUTCMinutes())+":"+p2(d.getUTCSeconds())+"Z"; // real UTC (was local, mislabeled)
var names=[]; for(var i=0;i<freshFiles.length && i<50;i++) names.push(freshFiles[i].name||String(freshFiles[i]));
var ent={app:APP_NAME, appVersion:APP_VERSION, host:sanitHost($("hostName").value),
input:String(input||""), finishedUtc:iso, files:names};
if(CASE_FROM) ent.winFrom=CASE_FROM; if(CASE_TO) ent.winTo=CASE_TO; // case window recorded (Finder-observable), never scored
arr.push(ent);
writeUtf8(p, JSON.stringify(arr,null,2));
RUNINFO_LAST={path:p, iso:iso};
}catch(e){}
}
var RUNINFO_LAST=null; // the entry writeRunInfo just appended — patched with a triage summary after the auto-load
function patchRunInfoSummary(summary){
/* second-phase runinfo write (family, 2026-07-07): the scored rows only exist after the
auto-load that follows a run, so the headline triage numbers are patched onto the entry
writeRunInfo appended moments earlier. The Artifact-Finder renders these in its
per-host triage summary. Best-effort — a missing summary just shows as counts-only. */
try{
if(!RUNINFO_LAST || !summary) return;
var arr=JSON.parse(readUtf8(RUNINFO_LAST.path));
if(!arr || arr.length==null) return;
for(var i=arr.length-1;i>=0;i--){ var e=arr[i];
if(e && e.app===APP_NAME && e.finishedUtc===RUNINFO_LAST.iso){ e.summary=summary; break; } }
writeUtf8(RUNINFO_LAST.path, JSON.stringify(arr,null,2));
}catch(e){}
}
function buildRunSummary(){
/* headline triage numbers from the scored file-entry records: entries, wrapper-flagged count,
max score, top-3 "name (score)". "Flagged" uses this app's own suspicion threshold
(rec.sus = score>=3 — Amcache indexes every scanned binary, so the bar is higher than PECmd's). */
try{
if(!ROWS.length) return null;
var flagged=0, maxScore=0, top=[], i;
for(i=0;i<ROWS.length;i++){ var r=ROWS[i];
if(r.score>maxScore) maxScore=r.score;
if(r.sus) flagged++; }
var sorted=ROWS.slice(0);
sorted.sort(function(a,b){ return b.score-a.score; });
for(i=0;i<sorted.length && top.length<3;i++){ if(sorted[i].score>0) top.push(sorted[i].name+" ("+sorted[i].score+")"); }
return {entries:ROWS.length, flagged:flagged, maxScore:maxScore, top:top};
}catch(e){ return null; }
}
/* ---------- reuse already-processed output (item 8, 2026-07-11) ----------
Before re-running AmcacheParser, look for a prior run of THIS exact hive recorded in the output
folder's runinfo.json whose primary CSV still exists. If found: a /auto (Finder bulk) launch loads
it silently; an interactive Process asks first. /force (or the Re-process path) always re-runs. This
mirrors the Finder's own [Open] button, but makes a wrapper launched to *process* skip redundant work. */
function findExistingRun(input, outdir){
try{
var p=String(outdir).replace(/[\\\/]+$/,"")+"\\runinfo.json";
if(!fileExists(p)) return null;
var arr=JSON.parse(readUtf8(p)); if(!arr||arr.length==null) return null;
var inL=String(input).toLowerCase(), best=null, i;
for(i=0;i<arr.length;i++){ var e=arr[i];
if(!e||e.app!==APP_NAME||!e.input) continue;
if(String(e.input).toLowerCase()!==inL) continue;
if(!best||String(e.finishedUtc||"")>String(best.finishedUtc||"")) best=e; }
if(!best||!best.files||!best.files.length) return null;
var dir=parentDir(p), primary="", any="", j;
for(j=0;j<best.files.length;j++){ var fp=dir+"\\"+best.files[j];
if(!fileExists(fp)) continue; any=fp;
if(/_UnassociatedFileEntries\.csv$/i.test(best.files[j])) primary=fp; }
var use=primary||any;
return use?{csv:use, iso:best.finishedUtc||""}:null;
}catch(e){ return null; }
}
function processPf(){
var inp=$("pfPath").value;
if(!requireHost()) return;
var outdir=$("outDir").value;
var fso=new ActiveXObject("Scripting.FileSystemObject");
showEl("hiveRow",false);
if(inp && fso.FolderExists(inp)){
// folder given → search it recursively for Amcache.hve (collection trees, URL-encoded segments and all)
setStatus("Searching "+inp+" for Amcache.hve…","muted");
var hives=findHives(inp);
if(!hives.length){ setStatus("No Amcache.hve found under "+inp+" (searched "+nf(HIVE_DIRS_SCANNED)+" folders"+(HIVE_DIRS_SCANNED>=HIVE_SCAN_MAX?" — search capped, pick a more specific root":"")+"). Collections usually hold it at …\\Windows\\appcompat\\Programs\\Amcache.hve.","err"); return; }
if(hives.length===1){
inp=hives[0].path; $("pfPath").value=inp;
appendLog("Hive discovery: 1 hit under the folder — using "+inp);
} else {
var hsel=$("hiveSel"); hsel.innerHTML="";
for(var hi=0;hi<hives.length;hi++){ var ho=document.createElement("option"); ho.value=hives[hi].path; ho.text=shortPathN(hives[hi].path,6)+" ("+fmtSize(hives[hi].size)+")"; hsel.add(ho); }
showEl("hiveRow",true);
setStatus(hives.length+" Amcache hives found under that folder — pick one and click 'Use selected → process'."+(hives.length>=HIVE_MAX?(" Search capped at "+HIVE_MAX+" — consider a more specific root."):""),"muted");
return;
}
}
if(!inp||!fso.FileExists(inp)){ setStatus("Hive not found: "+inp+" — point at an Amcache.hve or a collection folder (the live hive is locked while Windows runs).","err"); return; }
if(!outdir){ setStatus("Set an output directory","err"); return; }
// item 8: this hive already processed into this output folder? reuse it instead of re-running.
if(!FORCE_REPROCESS){
var prior=findExistingRun(inp, outdir);
if(prior){
var when=prior.iso?(" ("+prior.iso+")"):"";
if(AUTO_RUN){
AUTO_RUN=false;
appendLog("Already processed"+when+" — loading existing results instead of re-running (pass /force to re-run).");
setStatus("Already processed"+when+" — loaded existing results, no reprocessing.","ok");
LASTOUTDIR=outdir; loadCsvFile(prior.csv); return;
}
if(confirm("This hive was already processed into this folder"+when+".\n\nOpen the existing results without re-running AmcacheParser?\n\nOK = open existing results\nCancel = re-process the hive")){
appendLog("Reusing existing results"+when+" for "+inp);
LASTOUTDIR=outdir; loadCsvFile(prior.csv); return;
}
appendLog("Re-processing "+inp+" despite existing results"+when+" (operator chose re-process).");
}
}
if(!EXE||!fileExists(EXE)){
// Process was pressed but the parser has not been downloaded yet — offer to fetch it, then
// continue straight into processing. downloadLatest() shows its own download confirmation and
// is synchronous, re-resolving EXE on success; if the user declines or it fails, EXE stays empty.
setStatus("AmcacheParser.exe not found — downloading it before processing…","muted");
appendLog("Process requested but AmcacheParser.exe is not present — prompting to download it first.");
downloadLatest();
if(!EXE||!fileExists(EXE)){ setStatus("AmcacheParser.exe is still not available — download it (Update / download AmcacheParser), then Process again.","err"); return; }
appendLog("AmcacheParser downloaded on demand — continuing to process "+inp);
}
var extra="";
if($("optInc").checked) extra+=" -i";
if($("optNl").checked) extra+=" --nl";
var before=csvNameSet(outdir);
if(BUSY){ setStatus("An AmcacheParser run is already in progress — wait for it to finish.","err"); return; }
appendLog("Running: \""+EXE+"\" -f \""+inp+"\" --csv \""+outdir+"\""+extra
+"\nA console window shows progress; the app stays responsive and loads results automatically when done.");
BUSY=true; $("procBtn").disabled=true;
var binp=batEsc(inp), bout=batEsc(outdir);
var ctx=runBatAsync([
"set DOTNET_ROLL_FORWARD=Major",
'if not exist "'+bout+'" mkdir "'+bout+'"',
"echo AmcacheParser is processing:",
'echo '+binp,
"echo Output: "+bout,
"echo This window closes automatically when finished.",
"echo.",
'"'+batEsc(EXE)+'" -f "'+binp+'" --csv "'+bout+'"'+extra+' > "__LOG__" 2>&1'
], true);
ctx.onTick=function(s){ setStatus("AmcacheParser running… "+s+"s elapsed (app responsive; results load when done)","muted"); };
ctx.onTimeout=function(){ BUSY=false; $("procBtn").disabled=false; setStatus("AmcacheParser still running after 30 min — stopped watching. Check the output folder / console window.","err"); };
ctx.onDone=function(log){ BUSY=false; $("procBtn").disabled=false; processPfDone(log,outdir,before); };
setStatus("AmcacheParser starting…","muted");
window.setTimeout(function(){ pollDone(ctx); }, 1000);
}
function processPfDone(log,outdir,before){
appendLog("--- AmcacheParser output ---\n"+log);
LASTOUTDIR=outdir;
var all=listCsvFiles(outdir);
var fresh=[]; for(var i=0;i<all.length;i++){ if(!before[all[i].name]) fresh.push(all[i]); }
if(!fresh.length){
// no new output — diagnose why, leniently (load nothing but explain)
// NB "Warning: Administrator privileges not found!" is cosmetic and appears on SUCCESSFUL runs — never alarm on it.
if(/being used by another process|sharing violation/i.test(log))
setStatus("AmcacheParser could not open the hive — it is LOCKED (live system hive). Parse a collected copy, or snapshot it first with: esentutl.exe /y \""+LIVE_HIVE+"\" /vss /d <dest> (elevated console).","err");
else if(/Sequence numbers|transaction log|log file/i.test(log) && /error|fail|exception/i.test(log))
setStatus("AmcacheParser failed during transaction-log replay — the .LOG1/.LOG2 next to the hive may be corrupt or mismatched. Tick 'skip log replay' (--nl) and run again.","err");
else if(/denied|unauthorized/i.test(log))
setStatus("AmcacheParser was denied access to the input file — check NTFS permissions, or relaunch this app elevated.","err");
else
setStatus("AmcacheParser produced no new CSV — see the log (is this really an Amcache.hve?).","err");
populateProduced(all, true); // these are pre-existing files, not this run's output (F9)
return;
}
var errNote = /is in old format/i.test(log) ? " — note: OLD-format (Win8-era) hive; reduced CSV set, generic grid only." : "";
fresh=hostTagCsvs(fresh, $("hostName").value); // stamp the target hostname into each produced CSV filename
writeRunInfo(outdir, $("pfPath").value, fresh);
PRODUCED=fresh;
populateProduced(fresh);
// auto-load the unassociated file entries (the primary dataset); the other 7 CSVs stay in the picker
var main=null;
for(var j=0;j<fresh.length;j++){ if(/_Amcache_UnassociatedFileEntries\.csv$/i.test(fresh[j].name)){ if(!main||fresh[j].name>main.name) main=fresh[j]; } }
if(!main){ main=fresh[0]; for(var k=1;k<fresh.length;k++){ if(fresh[k].size>main.size) main=fresh[k]; } }
try{ $("producedSel").value=main.path; }catch(e){}
setStatus("Produced "+fresh.length+" CSV(s); loaded "+main.name+". Pick the others from 'Produced CSVs'."+errNote,"ok");
loadCsvFile(main.path);
patchRunInfoSummary(buildRunSummary()); // rows are scored now — add the triage headline to the runinfo entry
}
/* ---------- host-tag produced CSVs (1.7.0) ----------
AmcacheParser names its 8 CSVs <ts>_Amcache_<kind>.csv with no host in them. Rename each freshly
produced file to <ts>_<host>_Amcache_<kind>.csv so the target host is visible on disk and in the
Produced-CSVs picker. The host token sits AFTER the numeric timestamp so files still sort newest-
first, and amcSibling()/the *_UnassociatedFileEntries* detection still match. Files already tagged
(or not in AmcacheParser's native form) are left untouched; a rename failure is non-fatal. */
function hostTagCsvs(fresh, host){
var tag=sanitHost(host||""); if(!tag) return fresh;
var fso=new ActiveXObject("Scripting.FileSystemObject"), out=[];
for(var i=0;i<fresh.length;i++){
var f=fresh[i];
var m=String(f.name).match(/^(\d+)_Amcache_([A-Za-z]+\.csv)$/i); // only the native <ts>_Amcache_<kind>.csv form
if(!m){ out.push(f); continue; }
var newName=m[1]+"_"+tag+"_Amcache_"+m[2];
if(newName===f.name){ out.push(f); continue; }
var dir=f.path.substring(0,f.path.lastIndexOf("\\")+1), newPath=dir+newName;
if(fso.FileExists(newPath)){
// collision (same-second re-run, or same AmcacheParser timestamp): don't silently destroy the earlier
// output — suffix a counter so both survive (this is evidence handling). (F7)
var stem=m[1]+"_"+tag+"_Amcache_"+m[2].replace(/\.csv$/i,""), k=2, cand;
do{ cand=dir+stem+"_"+k+".csv"; k++; }while(fso.FileExists(cand) && k<100);