-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathShellKnight.ps1
More file actions
2677 lines (2411 loc) · 143 KB
/
ShellKnight.ps1
File metadata and controls
2677 lines (2411 loc) · 143 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
#Requires -Version 3.0
#Requires -RunAsAdministrator
<#
.SYNOPSIS
ShellKnight v1.05 - Enterprise Endpoint Security & Remediation Tool
.DESCRIPTION
Automated endpoint security remediation, threat detection, hardening, and
reporting across 8 intelligent engines. Requires PowerShell 3.0 or later
on Windows 8 / Server 2012 or later. Some engines (Process, Defender,
Scheduled Tasks) require Windows 8+ cmdlets and will silently skip on
older OS versions. Designed for silent, headless deployment via Datto RMM /
CentraStage and other MSP RMM platforms.
Built for MSPs, IT Administrators, and Security Professionals.
.AUTHOR
C. David Burgess - PTech LLC
.VERSION
Version : v1.05
Released : 2026-05-25
Prior : v1.04
.ENGINES
Phase 1 - Intel Engine : Threat intelligence download and cache
Phase 2 - Assessment Engine : Machine baseline and vulnerability assessment
Phase 3 - Hardening Engine : Security hardening and configuration
Phase 4 - Process Engine : Process, service, and scheduled task remediation
Phase 5 - Persistence Engine : Persistence mechanism detection and removal
Phase 6 - Filesystem Engine : Artifact cleanup and disk remediation
Phase 7 - Detection Engine : Threat detection and IOC scanning
Phase 8 - Reporting Engine : Reporting, trending, and extended checks
.CHANGELOG
v1.05 - Fleet feedback batch — false positive reduction and counter fixes.
NVDisplay/Intel feed FP: added CIM Win32_Process fallback when
Get-Process.Path returns null; fail-safe to SKIP (not kill) when
path is unavailable; path comparison upgraded to OrdinalIgnoreCase.
Scheduled task whitelist: Microsoft SMBv1 removal tasks
(\Microsoft\Windows\SMB\UninstallSMB1*) no longer flagged or deleted.
RiskWare miner pattern: changed bare 'miner' substring match to
word-boundary \bminer to prevent false positives on filenames
containing 'remineralization' or similar legitimate words.
Event 7045 whitelist expanded: Datto EDR Agent, Pml Driver HPZ12,
Net Driver HPZ12, IntelTACD, RapportIaso now suppressed.
IOC counter bug fix: browser extension removals and Event 7045
detections now correctly increment $Script:Counters.IOCsFound.
Local admin report: Domain Admins group suppressed (expected in
domain environments; not a false-positive admin account).
Stale profile exclusions: TEMP, UMFD-*, Font Driver Host, DWM-*
Windows system service profiles added to exclusion list.
Registry uninstall scan: replaced per-key Get-ItemProperty loop
with single wildcard batch query for performance on weak endpoints.
Counter init: Failed counter changed from $false to 0 to prevent
type inconsistency in JSON output.
Version : v1.04 -> v1.05.
v1.04 - False positive fix: Intel feed filename IOC matches now check process
executable path before flagging and killing. Processes running from
C:\Windows\, C:\Program Files\, or C:\Program Files (x86)\ are
skipped as legitimate system/vendor binaries (e.g. NVDisplay.Container
is a real NVIDIA driver process that appears in threat intel feeds as
a known impersonation target). Malware running from AppData/Temp/user
dirs is still caught and killed.
Version : v1.03 -> v1.04.
v1.03 - Compatibility and stability release.
PS 3.0/4.0: all ::new() constructor calls replaced with New-Object.
PS 3.0-6.x: ?? null-coalescing operator replaced with if/else.
$ErrorActionPreference reverted to SilentlyContinue (Stop was too
aggressive with Set-StrictMode -Version 2 active; Invoke-SafeBlock
provides targeted error handling per-block).
Phase 6 Filesystem Engine: null-guard added to registry DisplayName
property access to prevent crash on keys without DisplayName value.
Versioning scheme updated to .01 increments.
Version : v1.02 -> v1.03.
v1.02 - Bug fix release over v1.01.
Log-Fail now correctly increments Counters.Failed so exit code 1
and the Failed actions metric fire as intended.
Remote access service and process matching fixed — inner
Where-Object now uses captured $svc/$proc variable, not $_.
Executive Summary added to screen output (before grade section).
LogWriter null-guard added to Write-Log.
Version : v1.01 -> v1.02.
v1.001 - Ground-up rewrite as ShellKnight 2.0. Eight-engine modular architecture.
Intel Engine: pluggable source framework, HEAD check, single consolidated
cache, configurable per source.
Assessment Engine: CVE vulnerability check via Microsoft Security Update
Guide, Critical/High/Medium severity, KB article references.
Hardening Engine: LAN Manager auth auto-remediation, Firewall auto-enable,
SMBv1/LLMNR/NLA/NetBIOS hardening, all configurable.
Process Engine: full process/service/task inventory, screen shows suspicious
only, log shows all verbosely.
Persistence Engine: Run/RunOnce keys, startup folders, WMI subscriptions,
browser policies, Defender exclusions.
Filesystem Engine: artifact cleanup, temp files, cache, stale profiles,
browser extensions, registry uninstall keys.
Detection Engine: IOC detection, MalwareBazaar, ransomware canary,
hosts file, network connections, remote access inventory, RISKWARE-RAT.
Reporting Engine: Windows Update names, trend tracking, event log IOCs,
reboot check, recent software, extended checks, compliance.
Performance: Generic List collections, hash table IOC lookups, Filter Left,
foreach loops, splatting, single-query caching, Invoke-SafeBlock pattern.
Version : v0.83 -> v1.001 per versioning rule.
v0.83 - PowerShell script block audit, startup enabled/disabled state,
LAN Manager auth remediation, Windows Firewall auto-enable,
memory/CPU/disk health, listening ports, software versions,
printer audit, license check, local admin audit, credential
exposure, scheduled task deep inspection.
v0.82 - UltraVNC CentraStage whitelist (Datto false positive fix).
v0.81 - Phase 21 SC targeted removal via Event 7045 exact path,
Defender threat history active/historical split,
LAN Manager/Firewall in security score.
v0.80 - Remote access inventory (20 tools), RISKWARE-RAT classification,
redirected folder scan, Hyper-V detection, VHD exclusions.
v0.79 - Phase progress indicator, CIS Benchmark Lite, startup classification.
v0.78 - Defender integration, PS script block audit, magic bytes audit,
HIPAA/CJIS checks, Windows Update names.
v0.77 - Risk delta reporting, large file finder, browser credential check,
N-able detection, HIPAA/acceptable use audit.
v0.76 - SMBv1/LLMNR/NLA/NetBIOS auto-remediation.
v0.75 - RiskWare detection, SC AppData scan, 31 new PUA targets.
v0.74 - Hardening action separation, stale profile deletion.
v0.73 - Account auto-disable, 14 new PUAs, whitelists.
v0.72 - Scan depth framework, Phases 22-28.
v0.71 - AV dedup, Dell CPM WMI whitelist.
v0.70 - ProgramData path, MalwareBazaar Auth-Key.
v0.69 - Top-of-file config, email/syslog, PUA expansion.
v0.68 - ShellKnight rename, A-F grading, JSON output.
.LINK
MalwareBazaar API : https://bazaar.abuse.ch/api/
Neo23x0 Signature DB : https://github.com/Neo23x0/signature-base
GitHub : https://github.com/cdburgess75/ShellKnight
#>
[CmdletBinding()]
param()
# ==============================================================================
# SHELLKNIGHT v1.05 CONFIGURATION
# All settings are configured here. No external config files required.
# Each engine can be independently enabled or disabled.
# ==============================================================================
# --- INTEL ENGINE (Phase 1) ---
# Downloads and consolidates threat intelligence from configured sources.
# Maintains a single local cache file updated in place for zero disk bloat.
# Uses HTTP HEAD check to skip downloads when remote source has not changed.
# Falls back to local cache automatically when offline or download fails.
$SK_IntelEngine_Enabled = $true # Enable/disable Intel Engine entirely
$SK_IntelEngine_CheckForUpdates = $true # Check remote before downloading (saves bandwidth)
$SK_IntelEngine_CacheDir = 'C:\ProgramData\ShellKnight\Intel\'
$SK_IntelEngine_PrimarySource = 'Neo23x0' # Primary IOC source (future: add more)
$SK_IntelEngine_CacheAgeDays = 7 # Force refresh cache after this many days
# --- ASSESSMENT ENGINE (Phase 2) ---
# Establishes machine baseline including hardware, OS, uptime, domain membership,
# antivirus detection, Windows Update status, and BitLocker state.
# Queries Microsoft Security Update Guide for CVEs applicable to this Windows build.
# Severity levels: Critical, High, Medium reported. KB references included.
$SK_AssessmentEngine_Enabled = $true # Enable/disable Assessment Engine
$SK_AssessmentEngine_CVECheck = $true # Check Microsoft Security Update Guide for CVEs
$SK_AssessmentEngine_MinSeverity = 'Medium' # Minimum severity to report (Critical/High/Medium)
# --- HARDENING ENGINE (Phase 3) ---
# Checks and optionally remediates security configuration weaknesses.
# All auto-remediation options default to false for safety.
# Enable per-client after verifying no legacy devices will be impacted.
$SK_HardeningEngine_Enabled = $true # Enable/disable Hardening Engine
$SK_DisableSMBv1 = $false # Auto-disable SMBv1 (WARNING: verify no legacy devices)
$SK_DisableLLMNR = $false # Auto-disable LLMNR via registry
$SK_EnforceRDP_NLA = $false # Auto-enforce NLA on RDP if RDP is enabled
$SK_DisableNetBIOS = $false # Auto-disable NetBIOS over TCP/IP on all adapters
$SK_SetLMAuthLevel = $false # Auto-set LAN Manager authentication level
$SK_LMAuthLevel = 5 # Target LM auth level (5 = NTLMv2 only, refuse LM/NTLM)
# WARNING: level 5 may break legacy devices/printers
$SK_EnableFirewall = $false # Auto-enable Windows Firewall on all disabled profiles
$SK_VerboseScreen = $false # Show summary INFO messages on screen (default: clean output)
# --- PROCESS ENGINE (Phase 4) ---
# Enumerates all running processes, Windows services, and scheduled tasks.
# Flags and kills malware processes, stops and removes malicious services,
# removes malicious scheduled tasks with full forensic reporting.
# Screen shows only suspicious items. Log contains full verbose inventory.
$SK_ProcessEngine_Enabled = $true # Enable/disable Process Engine
# --- PERSISTENCE ENGINE (Phase 5) ---
# Detects and removes all autostart and persistence mechanisms used by malware
# to survive reboots. Uses high-confidence IOC whitelist to minimize false positives.
$SK_PersistenceEngine_Enabled = $true # Enable/disable Persistence Engine
# --- FILESYSTEM ENGINE (Phase 6) ---
# Comprehensive filesystem cleanup and remediation. Deletes known-bad software
# folders, registry uninstall keys, browser extension artifacts, temporary files,
# cache files, and stale user profiles. Reports temp file age and stale profiles.
# Also scans redirected home directories on non-C: drives (file servers).
$SK_FilesystemEngine_Enabled = $true # Enable/disable Filesystem Engine
$SK_AggressiveTempClean = $true # Clean temp files aggressively
$SK_TempCleanAgeThresholdDays = 30 # Only clean temp files older than this many days
$SK_DeleteStaleProfiles = $false # Auto-delete stale user profiles
$SK_DeleteStaleProfileDays = 1095 # Profile inactive this many days = stale (default: 3 years)
$SK_DeleteStaleProfileOnServer = $false # Allow stale profile deletion on servers
$SK_DeleteStaleProfileMinSizeGB = 0.1 # Minimum profile size to consider for deletion (GB)
$SK_ScanRedirectedFolders = $true # Scan non-C: drives for redirected user home PUAs
$SK_LargeFileThresholdGB = 2.0 # Flag files larger than this size in GB (0 = disable)
$SK_LargeOSTThresholdGB = 10.0 # Flag Outlook OST files larger than this size in GB
$SK_LargeFileScanPaths = @("$env:SystemDrive\Users")
$SK_MagicBytesAudit = $false # Enable file extension magic bytes audit (Deep tier only)
$SK_MagicBytesMaxFiles = 5000 # Maximum files to scan in magic bytes audit
$SK_AbortFreeSpaceGB = 0.5 # Abort cleanup if free space falls below this (GB)
$SK_MinFreeSpaceGB = 2.0 # Warn if free space is below this (GB)
# --- DETECTION ENGINE (Phase 7) ---
# Comprehensive threat detection. Scans for trojan and malware IOCs, detects
# riskware and exploit tools, performs MalwareBazaar SHA256 hash lookups,
# checks for ransomware canary patterns, inspects hosts file for C2 domains,
# audits network connections, and inventories all remote access tools.
$SK_DetectionEngine_Enabled = $true # Enable/disable Detection Engine
$SK_MalwareBazaar_Enabled = $true # Enable MalwareBazaar hash lookups
$SK_MalwareBazaar_ApiKey = '' # MalwareBazaar API key (leave empty for anonymous)
$SK_RemoteAccessInventory = $true # Inventory all remote access tools found
$SK_RemoteAccessWarnUnknown = $true # WARN on remote tools not in Add/Remove Programs
$SK_ScreenConnect_InstanceID = '' # Your managed ScreenConnect instance ID
$SK_RemoveRogueScreenConnect = $true # Auto-remove non-managed ScreenConnect instances
$SK_ScanDepth = 'Standard' # Standard, Deep, Compliance
# --- REPORTING ENGINE (Phase 8) ---
# Comprehensive reporting and trend analysis. Reports pending Windows updates
# with KB titles and severity, tracks grade trends against previous runs,
# checks for pending reboots, reports recently installed software, performs
# event log IOC checks, USB audit, and all extended compliance checks.
$SK_ReportingEngine_Enabled = $true # Enable/disable Reporting Engine
$SK_AutoDisableInactiveAccounts = $false # Auto-disable inactive local accounts
$SK_AutoDisableThresholdDays = 547 # Accounts inactive this many days = disable candidate
$SK_AutoDisableOnServers = $false # Allow auto-disable on servers (default: workstations only)
$SK_AutoDisableExclusions = @('Administrator','Guest','DefaultAccount','WDAGUtilityAccount')
# --- EMAIL ALERTS ---
# Configure SMTP settings to receive email alerts when IOCs are found.
# Requires valid SMTP credentials. Disabled by default.
$SK_Email_Enabled = $false
$SK_Email_Server = 'smtp.office365.com'
$SK_Email_Port = 587
$SK_Email_TLS = $true
$SK_Email_From = 'alerts@yourdomain.com'
$SK_Email_To = 'alerts@yourdomain.com'
$SK_Email_User = 'alerts@yourdomain.com'
$SK_Email_Pass = ''
# --- SYSLOG ---
# Configure syslog forwarding for SIEM integration.
$SK_Syslog_Enabled = $false
$SK_Syslog_Server = ''
$SK_Syslog_Port = 514
$SK_Syslog_Protocol = 'UDP'
$SK_Syslog_Facility = 16
# ==============================================================================
# STRICT MODE & RUNTIME INITIALIZATION
# ==============================================================================
Set-StrictMode -Version 2
$ErrorActionPreference = 'SilentlyContinue'
$Script:RunStart = Get-Date
# Runtime Config Object - single source of truth for all engines
$Script:Config = [PSCustomObject]@{
Version = 'v1.05'
# Intel Engine
IntelEngine_Enabled = $SK_IntelEngine_Enabled
IntelEngine_CheckUpdates = $SK_IntelEngine_CheckForUpdates
IntelEngine_CacheDir = $SK_IntelEngine_CacheDir
IntelEngine_CacheAgeDays = $SK_IntelEngine_CacheAgeDays
# Assessment Engine
AssessmentEngine_Enabled = $SK_AssessmentEngine_Enabled
CVECheck = $SK_AssessmentEngine_CVECheck
MinSeverity = $SK_AssessmentEngine_MinSeverity
# Hardening Engine
HardeningEngine_Enabled = $SK_HardeningEngine_Enabled
DisableSMBv1 = $SK_DisableSMBv1
DisableLLMNR = $SK_DisableLLMNR
EnforceRDP_NLA = $SK_EnforceRDP_NLA
DisableNetBIOS = $SK_DisableNetBIOS
SetLMAuthLevel = $SK_SetLMAuthLevel
LMAuthLevel = $SK_LMAuthLevel
EnableFirewall = $SK_EnableFirewall
VerboseScreen = $SK_VerboseScreen
# Process Engine
ProcessEngine_Enabled = $SK_ProcessEngine_Enabled
# Persistence Engine
PersistenceEngine_Enabled= $SK_PersistenceEngine_Enabled
# Filesystem Engine
FilesystemEngine_Enabled = $SK_FilesystemEngine_Enabled
AggressiveTempClean = $SK_AggressiveTempClean
TempCleanAgeDays = $SK_TempCleanAgeThresholdDays
DeleteStaleProfiles = $SK_DeleteStaleProfiles
DeleteStaleProfileDays = $SK_DeleteStaleProfileDays
DeleteStaleOnServer = $SK_DeleteStaleProfileOnServer
DeleteStaleMinSizeGB = $SK_DeleteStaleProfileMinSizeGB
ScanRedirectedFolders = $SK_ScanRedirectedFolders
LargeFileThresholdGB = $SK_LargeFileThresholdGB
LargeOSTThresholdGB = $SK_LargeOSTThresholdGB
LargeFileScanPaths = $SK_LargeFileScanPaths
MagicBytesAudit = $SK_MagicBytesAudit
MagicBytesMaxFiles = $SK_MagicBytesMaxFiles
AbortFreeSpaceGB = $SK_AbortFreeSpaceGB
MinFreeSpaceGB = $SK_MinFreeSpaceGB
# Detection Engine
DetectionEngine_Enabled = $SK_DetectionEngine_Enabled
MBEnabled = $SK_MalwareBazaar_Enabled
MBApiKey = $SK_MalwareBazaar_ApiKey
RemoteAccessInventory = $SK_RemoteAccessInventory
RemoteAccessWarnUnknown = $SK_RemoteAccessWarnUnknown
SCInstanceID = $SK_ScreenConnect_InstanceID
SCRemoveRogue = $SK_RemoveRogueScreenConnect
ScanDepth = $SK_ScanDepth
# Reporting Engine
ReportingEngine_Enabled = $SK_ReportingEngine_Enabled
AutoDisable = $SK_AutoDisableInactiveAccounts
AutoDisableDays = $SK_AutoDisableThresholdDays
AutoDisableOnServers = $SK_AutoDisableOnServers
AutoDisableExclusions = $SK_AutoDisableExclusions
# Email
EmailEnabled = $SK_Email_Enabled
EmailServer = $SK_Email_Server
EmailPort = $SK_Email_Port
EmailTLS = $SK_Email_TLS
EmailFrom = $SK_Email_From
EmailTo = $SK_Email_To
EmailUser = $SK_Email_User
EmailPass = $SK_Email_Pass
# Syslog
SyslogEnabled = $SK_Syslog_Enabled
SyslogServer = $SK_Syslog_Server
SyslogPort = $SK_Syslog_Port
SyslogProtocol = $SK_Syslog_Protocol
SyslogFacility = $SK_Syslog_Facility
}
# ==============================================================================
# COUNTERS & STATE
# ==============================================================================
$Script:Counters = @{
ActionsTaken = 0
HardeningDone = 0
IOCsFound = 0
ProcessesKilled = 0
ServicesRemoved = 0
TasksRemoved = 0
RunKeysRemoved = 0
FilesRemoved = 0
UninstallsRun = 0
Failed = 0
RebootRequired = $false
IntelSource = 'Hardcoded fallback'
}
$Script:SpaceFreed = 0L
$Script:RogueScreenConnectRemoved = $false
$Script:HWInfo = @{ IsServer = $false; IsHyperVHost = $false }
$Script:SecurityScore = 100
$Script:PerformanceScore = 100
$Script:LogReady = $false
$Script:PSVer = $PSVersionTable.PSVersion.Major
$Script:PSFullVer = "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor).$($PSVersionTable.PSVersion.Build).$($PSVersionTable.PSVersion.Revision)"
# Pre-compiled IOC collections (populated by Intel Engine)
$Script:HashIOCs = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
$Script:FilenameIOCs = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
$Script:C2IOCs = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
$Script:FolderIOCs = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
# Single-query caches - populated once, reused across all engines
$Script:Cache_ARP = $null # Add/Remove Programs - populated in Assessment Engine
$Script:Cache_Services = $null # All services - populated in Process Engine
$Script:Cache_Processes = $null # All processes - populated in Process Engine
$Script:Cache_Tasks = $null # All scheduled tasks - populated in Process Engine
# Legitimate process/task whitelists
$Script:LegitProcessNames = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
@(
'CricutTaskbarApplication','CricutDesignSpace',
'Zoom','Teams','Slack','Spotify','Discord','OneDrive','Dropbox','GoogleDrive','Box',
'DYMOConnectLauncher','DYMOConnect','DYMO.DLS.Printing.Host','DYMLabelWriter'
) | ForEach-Object { $null = $Script:LegitProcessNames.Add($_) }
$Script:LegitTaskPaths = @(
'*AppData\Roaming\Zoom\bin\*','*Teams\*','*Slack\*','*Spotify\*',
'*Discord\*','*Cricut*','*AppData\Roaming\PCDr\*','*BundleApplicationRepairTool.exe*'
)
# Task Scheduler path prefixes for known-legitimate Microsoft system tasks.
# Tasks living under these paths are skipped regardless of their command line.
# Prevents false positives on OS-built-in tasks that use -ExecutionPolicy or -NoProfile.
$Script:LegitTaskSchedulerPaths = @(
'\Microsoft\Windows\SMB\' # SMBv1 auto-removal tasks (OS security feature)
)
# Known legitimate VNC paths (RMM-bundled components)
$Script:LegitVNCPaths = @('CentraStage','Kaseya','LabTech','ConnectWise','NinjaRMM','Atera','N-able')
# Stale profile exclusions
$Script:StaleProfileExclusions = @(
'.NET v4.5','.NET v4.5 Classic','Classic .NET AppPool','DefaultAppPool',
'defaultuser0','defaultuser1','defaultuser100000',
'QBDataServiceUser20','QBDataServiceUser21','QBDataServiceUser22',
'QBDataServiceUser23','QBDataServiceUser24','QBDataServiceUser25',
'QBDataServiceUser26','QBDataServiceUser27','QBDataServiceUser28',
'QBDataServiceUser29','QBDataServiceUser30','QBDataServiceUser31',
'QBDataServiceUser32','QBDataServiceUser33','QBDataServiceUser34',
'QBDataServiceUser35',
# Windows system service profiles - not real user profiles
'^TEMP$', # Windows TEMP service account profile
'^UMFD-', # User Mode Font Driver service profiles (UMFD-0, UMFD-1, etc.)
'^DWM-', # Desktop Window Manager service profiles (DWM-1, DWM-2, etc.)
'Font Driver Host' # Font Driver Host service profile
)
# Ransomware canary whitelist
$Script:CanaryWhitelist = @(
'*Intel\Wireless\WLANProfiles\*.enc','*damsi\*.enc',
'*SystemCertificates\*','*DPAPI\*'
)
# Hosts file whitelist
$Script:HostsWhitelist = @(
'granicus.com','mediavault.granicus','idrac.local','drac.local',
'ilo.local','mssplus.mcafee.com'
)
# WMI subscription whitelist
$Script:WMIWhitelist = @(
'SCM','BVTFilter','TSlogon','RAevent','RMScheduledTask','OfficeSyncProvider',
'BVTConsumer','OfficeSync','SCM Event Log Filter','SCM Event Log Consumer',
'DellCommandPowerManagerAlertEventFilter','DellCommandPowerManagerAlertEventConsumer',
'DellCommandPowerManagerPolicyChangeEventFilter','DellCommandPowerManagerPolicyChangeEventConsumer'
)
# Legit drop files (never flag these)
$Script:LegitDropFiles = @(
'Citrix*','AgentInstall.exe','handle.exe','handle64.exe',
'PsExec.exe','PsExec64.exe','MBSetup.exe'
)
# ==============================================================================
# LOGGING SYSTEM
# ==============================================================================
$Script:LogPath = ''
function Initialize-Logging {
$logDir = 'C:\ProgramData\ShellKnight\Logs'
$jsonDir = 'C:\ProgramData\ShellKnight\JSON'
$intelDir = $Script:Config.IntelEngine_CacheDir
foreach ($dir in @($logDir, $jsonDir, $intelDir)) {
if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null }
}
$stamp = Get-Date -Format 'yyyy-MM-dd_HHmm'
$Script:LogPath = "$logDir\ShellKnight_$stamp.log"
$Script:LogWriter = New-Object System.IO.StreamWriter -ArgumentList @($Script:LogPath, $false, [System.Text.Encoding]::UTF8)
$Script:LogReady = $true
}
function Write-Log {
param([string]$Message, [string]$Level = 'INFO')
$ts = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss')
$line = "$ts [$($Level.PadRight(7))] $Message"
if ($Script:LogReady -and $null -ne $Script:LogWriter) { $Script:LogWriter.WriteLine($line) }
switch ($Level) {
'SUCCESS' { Write-Host $line -ForegroundColor Green }
'WARN' { Write-Host $line -ForegroundColor Yellow }
'FAILED' { Write-Host $line -ForegroundColor Red }
'IOC' { Write-Host $line -ForegroundColor Magenta }
'HARDEN' { Write-Host $line -ForegroundColor Cyan }
'RISKWARE-RAT' { Write-Host $line -ForegroundColor Magenta }
'SUMMARY' {
if ($Script:Config.VerboseScreen) {
Write-Host $line -ForegroundColor White
}
}
default { } # INFO goes to log only
}
}
function Log-Info { param([string]$m) Write-Log -Message $m -Level 'INFO' }
function Log-Success { param([string]$m) Write-Log -Message $m -Level 'SUCCESS' }
function Log-Warn { param([string]$m) Write-Log -Message $m -Level 'WARN' }
function Log-Fail { param([string]$m) Write-Log -Message $m -Level 'FAILED'; $Script:Counters.Failed++ }
function Log-IOC { param([string]$m) Write-Log -Message $m -Level 'IOC' }
function Log-Harden { param([string]$m) Write-Log -Message $m -Level 'HARDEN' }
function Log-RiskwareRAT { param([string]$m) Write-Log -Message $m -Level 'RISKWARE-RAT' }
function Log-Summary { param([string]$m) Write-Log -Message $m -Level 'SUMMARY' }
# ==============================================================================
# HELPER FUNCTIONS
# ==============================================================================
# Phase progress indicator
function Write-PhaseProgress {
param([int]$PhaseNum, [int]$TotalPhases = 8, [string]$PhaseName)
$elapsed = [math]::Round(((Get-Date) - $Script:RunStart).TotalSeconds)
$mins = [math]::Floor($elapsed / 60)
$secs = $elapsed % 60
$elStr = ([string]$mins).PadLeft(2,'0') + ':' + ([string]$secs).PadLeft(2,'0')
Write-Host " [$elStr | Phase $PhaseNum/$TotalPhases] $PhaseName..." -ForegroundColor Cyan
}
# Standardized error handler - Invoke-SafeBlock pattern
function Invoke-SafeBlock {
param([scriptblock]$Block, [string]$Label)
try { & $Block }
catch { Log-Info "$Label skipped - $($_.Exception.Message)" }
}
# Get folder size in bytes
function Get-FolderSizeBytes {
param([string]$Path)
try {
$gciParams = @{ LiteralPath = $Path; Recurse = $true; Force = $true; ErrorAction = 'SilentlyContinue'; File = $true }
(Get-ChildItem @gciParams | Measure-Object -Property Length -Sum).Sum
} catch { 0L }
}
# Remove folder contents with before/after reporting
function Remove-FolderContents {
param([string]$Path, [string]$Label)
if (-not (Test-Path -LiteralPath $Path)) { return }
$gciParams = @{ LiteralPath = $Path; Recurse = $true; Force = $true; ErrorAction = 'SilentlyContinue'; File = $true }
$before = @(Get-ChildItem @gciParams)
$beforeCount = $before.Count
$beforeBytes = ($before | Measure-Object -Property Length -Sum).Sum
$removed = 0
foreach ($f in $before) {
try { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop; $removed++ } catch { }
}
if ($removed -gt 0) {
$freedMB = [math]::Round($beforeBytes / 1MB, 1)
$afterCount = $beforeCount - $removed
Log-Success "Cleaned $Label - Before: $beforeCount files / $freedMB MB | After: $afterCount files | Freed: $freedMB MB"
$Script:SpaceFreed += $beforeBytes
$Script:Counters.ActionsTaken++
$Script:Counters.FilesRemoved += $removed
}
}
# Separator lines
function Write-SectionHeader { param([string]$Title)
$line = '=' * 80
Log-Info $line
Log-Info " $Title"
Log-Info ('-' * 80)
}
# ==============================================================================
# SCRIPT INITIALIZATION
# ==============================================================================
Initialize-Logging
# Detect PS version compatibility
$Script:UseNewPSFeatures = $Script:PSVer -ge 5
# Banner
$bannerWidth = 78
$version = 'ShellKnight v1.05'
$hostname = $env:COMPUTERNAME
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$psver = "PS $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"
$line = '=' * $bannerWidth
Write-Host ''
Write-Host " $line" -ForegroundColor Cyan
Write-Host " $version | $hostname | $timestamp | $psver" -ForegroundColor Cyan
Write-Host " $line" -ForegroundColor Cyan
Write-Host ''
Write-Host " Full log: $Script:LogPath"
Write-Host " $('-' * $bannerWidth)"
Log-Info $line
Log-Info " $version | $hostname | $timestamp | $psver"
Log-Info $line
# ==============================================================================
# PHASE 1: INTEL ENGINE
# Threat intelligence download, consolidation, and caching
# ==============================================================================
Write-PhaseProgress -PhaseNum 1 -PhaseName 'Intel Engine'
Log-Info '--- Phase 1: Intel Engine ---'
$Script:HashIOCsLoaded = 0
$Script:FilenameIOCsLoaded = 0
$Script:C2IOCsLoaded = 0
# Hardcoded fallback IOC patterns (used if download fails and no cache exists)
$Script:FallbackFolderIOCs = (New-Object 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase))
@(
'njrat','asyncrat','redline','vidar','lokibot','qakbot','remcos',
'nanocore','darkcomet','adwind','jrat','limeRAT','quasar',
'agent tesla','raccoon','azorult','formbook','emotet','trickbot',
'lavasoft','webcompanion','conduit','babylon','sweetim',
'opencandy','wajam','crossrider','mypcsecurity','pckeeper',
'reimage','iminlikewithyou','dealply','browsefox'
) | ForEach-Object { $null = $Script:FallbackFolderIOCs.Add($_) }
if ($Script:Config.IntelEngine_Enabled) {
Invoke-SafeBlock -Label 'Intel Engine' -Block {
$cacheDir = $Script:Config.IntelEngine_CacheDir
$cacheFile = Join-Path $cacheDir 'neo23x0_consolidated.json'
$cacheAge = $Script:Config.IntelEngine_CacheAgeDays
# Neo23x0 IOC sources
$sources = @(
@{ Name = 'Filename IOCs'; Url = 'https://raw.githubusercontent.com/Neo23x0/signature-base/master/iocs/filename-iocs.txt' }
@{ Name = 'Hash IOCs'; Url = 'https://raw.githubusercontent.com/Neo23x0/signature-base/master/iocs/hash-iocs.txt' }
@{ Name = 'C2 IOCs'; Url = 'https://raw.githubusercontent.com/Neo23x0/signature-base/master/iocs/c2-iocs.txt' }
)
$useCache = $false
$cacheExists = Test-Path -LiteralPath $cacheFile
if ($cacheExists) {
$cacheDate = (Get-Item -LiteralPath $cacheFile).LastWriteTime
$cacheOld = ((Get-Date) - $cacheDate).TotalDays -gt $cacheAge
if (-not $cacheOld -and $Script:Config.IntelEngine_CheckUpdates) {
# HEAD check - only download if remote has changed
try {
$headResp = Invoke-WebRequest -Uri $sources[0].Url -Method Head -TimeoutSec 5 -ErrorAction Stop
$remoteDate = [datetime]::Parse($headResp.Headers['Last-Modified'])
$useCache = $remoteDate -le $cacheDate
if ($useCache) { Log-Summary "Intel Engine - cache current, skipping download" }
} catch { $useCache = $true }
} elseif (-not $cacheOld) {
$useCache = $true
}
}
if (-not $useCache) {
# Download and consolidate all sources into single cache
$consolidated = @{
Filename = (New-Object 'System.Collections.Generic.List[string]')
Hashes = (New-Object 'System.Collections.Generic.List[string]')
C2 = (New-Object 'System.Collections.Generic.List[string]')
Updated = (Get-Date).ToString('o')
Source = $Script:Config.IntelEngine_PrimarySource
}
foreach ($source in $sources) {
try {
$content = (Invoke-WebRequest -Uri $source.Url -TimeoutSec 30 -ErrorAction Stop).Content
$lines = $content -split "`n" | Where-Object { $_ -and -not $_.StartsWith('#') }
switch -Wildcard ($source.Name) {
'Filename*' { foreach ($l in $lines) { $consolidated.Filename.Add($l.Trim()) } }
'Hash*' { foreach ($l in $lines) { $consolidated.Hashes.Add($l.Trim().ToLower()) } }
'C2*' { foreach ($l in $lines) { $consolidated.C2.Add($l.Trim().ToLower()) } }
}
Log-Info "Intel Engine - downloaded $($source.Name)"
} catch {
Log-Warn "Intel Engine - failed to download $($source.Name): $($_.Exception.Message)"
}
}
# Write single consolidated cache file (replace in place)
$consolidated | ConvertTo-Json -Compress | Set-Content -LiteralPath $cacheFile -Encoding UTF8 -Force
$Script:Counters.IntelSource = 'Live (Neo23x0)'
Log-Summary "Intel Engine - cache updated from Neo23x0"
} else {
$Script:Counters.IntelSource = 'Cache (current)'
}
# Load consolidated cache into hash sets for O(1) lookup
if (Test-Path -LiteralPath $cacheFile) {
$cache = Get-Content -LiteralPath $cacheFile -Raw | ConvertFrom-Json
if ($cache.Hashes) { foreach ($h in $cache.Hashes) { $null = $Script:HashIOCs.Add($h) } }
if ($cache.Filename) { foreach ($f in $cache.Filename) { $null = $Script:FilenameIOCs.Add($f) } }
if ($cache.C2) { foreach ($c in $cache.C2) { $null = $Script:C2IOCs.Add($c) } }
$Script:HashIOCsLoaded = $Script:HashIOCs.Count
$Script:FilenameIOCsLoaded = $Script:FilenameIOCs.Count
$Script:C2IOCsLoaded = $Script:C2IOCs.Count
Log-Summary "Intel Engine - $($Script:HashIOCsLoaded) hash IOCs | $($Script:FilenameIOCsLoaded) filename IOCs | $($Script:C2IOCsLoaded) C2 IOCs loaded"
}
}
} else {
Log-Info "Intel Engine - disabled, using fallback IOCs only"
$Script:Counters.IntelSource = 'Disabled (fallback only)'
}
# ==============================================================================
# PHASE 2: ASSESSMENT ENGINE
# Machine baseline, hardware, OS, AV, vulnerabilities
# ==============================================================================
Write-PhaseProgress -PhaseNum 2 -PhaseName 'Assessment Engine'
Log-Info '--- Phase 2: Assessment Engine ---'
$Script:MachineInfo = [ordered]@{}
$bitlockerWarn = $false
$osEolWarn = $false
$wuLastWarn = $false
$avProduct = 'NONE DETECTED'
$defStatus = 'Unknown'
$inactiveAccounts = (New-Object 'System.Collections.Generic.List[object]')
$Script:MinPasswordLen = 0
if ($Script:Config.AssessmentEngine_Enabled) {
Invoke-SafeBlock -Label 'Assessment Engine' -Block {
# Hardware & OS Detection
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
$bios= Get-CimInstance Win32_BIOS -ErrorAction Stop
$osName = $os.Caption
$osBuild = $os.BuildNumber
$arch = if ($os.OSArchitecture -match '64') { '64-bit' } else { '32-bit' }
$biosDate = [datetime]::ParseExact($bios.ReleaseDate.Split('.')[0],'yyyyMMdd',$null)
$pcAgeYrs = [math]::Round(((Get-Date) - $biosDate).TotalDays / 365.25, 1)
$lastBoot = $os.LastBootUpTime
$uptime = (Get-Date) - $lastBoot
$uptimeStr = "$([math]::Floor($uptime.TotalDays))d $($uptime.Hours)h $($uptime.Minutes)m"
$domain = if ($cs.PartOfDomain) { "Domain: $($cs.Domain)" } else { "Workgroup: $($cs.Workgroup)" }
$loggedIn = "$env:COMPUTERNAME$"
# Server detection
$Script:HWInfo.IsServer = $osName -match 'Server'
# Disk space
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
$diskFreeGB = if ($disk) { [math]::Round($disk.FreeSpace / 1GB, 1) } else { 0 }
$diskTotalGB = if ($disk) { [math]::Round($disk.Size / 1GB, 1) } else { 0 }
$diskUsedPct = if ($diskTotalGB -gt 0) { [math]::Round((($diskTotalGB - $diskFreeGB) / $diskTotalGB) * 100, 1) } else { 0 }
# BitLocker
$blStatus = 'Not available'
try {
$bl = Get-BitLockerVolume -MountPoint 'C:' -ErrorAction Stop
$blStatus = $bl.ProtectionStatus
if ($blStatus -ne 'On') { $bitlockerWarn = $true; $blStatus = 'Off' } else { $blStatus = 'On' }
} catch {
try {
$blWmi = Get-CimInstance -Namespace 'Root\CIMV2\Security\MicrosoftVolumeEncryption' `
-ClassName 'Win32_EncryptableVolume' -Filter "DriveLetter='C:'" -ErrorAction Stop
$blStatus = if ($blWmi.ProtectionStatus -eq 1) { 'On' } else { 'Off'; $bitlockerWarn = $true }
} catch { }
}
# OS EOL check
$eolDates = @{
'7601' = [datetime]'2020-01-14'; '9200' = [datetime]'2023-10-10'
'9600' = [datetime]'2023-10-10'; '10240'= [datetime]'2025-10-14'
'10586' = [datetime]'2017-10-10'; '14393'= [datetime]'2027-01-12'
'15063' = [datetime]'2018-10-09'; '16299'= [datetime]'2019-04-09'
'17134' = [datetime]'2019-11-12'; '17763'= [datetime]'2029-01-09'
'18362' = [datetime]'2020-05-12'; '18363'= [datetime]'2021-05-11'
'19041' = [datetime]'2025-10-14'; '19042'= [datetime]'2025-10-14'
'19043' = [datetime]'2025-10-14'; '19044'= [datetime]'2026-10-13'
'19045' = [datetime]'2030-10-14'; '20348'= [datetime]'2031-10-14'
'22000' = [datetime]'2026-10-14'; '22621'= [datetime]'2027-10-12'
'22631' = [datetime]'2028-10-10'; '26100'= [datetime]'2029-10-14'
}
$eolDate = $eolDates[$osBuild]
$eolStr = if ($eolDate) {
if ((Get-Date) -gt $eolDate) { $osEolWarn = $true; "END OF LIFE (since $($eolDate.ToString('yyyy-MM-dd')))"}
else { "Supported until $($eolDate.ToString('yyyy-MM-dd'))" }
} else { 'Unknown' }
# RAM
$ramGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
# AV Detection
$avProducts = (New-Object 'System.Collections.Generic.List[string]')
try {
$avList = Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName 'AntiVirusProduct' -ErrorAction Stop
foreach ($av in $avList) {
$avName = $av.displayName
if ($avName -notmatch 'Windows Defender') { $avProducts.Add($avName) }
}
} catch { }
# Datto AV / RMM / EDR detection
$dattoServices = @{
'EndpointProtectionService2' = 'Datto AV'
'CagService' = 'Datto RMM'
'HUNTAgent' = 'Datto EDR / Huntress'
}
foreach ($svcName in $dattoServices.Keys) {
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($svc) { $avProducts.Add($dattoServices[$svcName]) }
}
if ($avProducts.Count -gt 0) { $avProduct = $avProducts -join ', ' }
# Defender status
try {
$mp = Get-MpComputerStatus -ErrorAction Stop
$defStatus = if ($mp.AMServiceEnabled -and $mp.RealTimeProtectionEnabled) { 'Active' } else { 'DISABLED' }
$defSigs = $mp.AntivirusSignatureLastUpdated.ToString('yyyy-MM-dd')
} catch { $defSigs = 'Unknown' }
# Windows Update last install
$wuDate = $null
try {
$wu = New-Object -ComObject Microsoft.Update.Session -ErrorAction Stop
$searcher = $wu.CreateUpdateSearcher()
$history = $searcher.QueryHistory(0, 1)
if ($history.Count -gt 0) {
$wuDate = $history.Item(0).Date
$wuDaysAgo = ([datetime]::Now - $wuDate).Days
$wuStr = "$($wuDate.ToString('yyyy-MM-dd')) ($wuDaysAgo days ago)"
if ($wuDaysAgo -gt 30) { $wuLastWarn = $true }
}
} catch { $wuStr = 'Unknown' }
# Populate ARP cache once for all engines
$Script:Cache_ARP = @(Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' `
-ErrorAction SilentlyContinue |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, UninstallString)
# Build machine info
$Script:MachineInfo = [ordered]@{
'Hostname' = $env:COMPUTERNAME
'OS' = "$osName (Build $osBuild)"
'OS EOL' = $eolStr
'Architecture' = $arch
'PC Age' = "$pcAgeYrs years (BIOS: $($biosDate.ToString('yyyy-MM-dd')))"
'RAM' = "$ramGB GB"
'Last Boot' = $lastBoot.ToString('yyyy-MM-dd HH:mm:ss')
'Uptime' = $uptimeStr
'Domain/Workgroup'= $domain
'Logged-in User' = $loggedIn
'C: Drive' = "$diskFreeGB GB free of $diskTotalGB GB ($diskUsedPct% used)"
'BitLocker' = $blStatus
'Antivirus' = $avProduct
'Defender' = $defStatus
'Defender Sigs' = $defSigs
'Last WU Install' = $wuStr
'PS Version' = $Script:PSFullVer
'Intel Source' = $Script:Counters.IntelSource
}
# Log machine info block
$sepLine = '=' * 80
Log-Info $sepLine
Log-Info ' MACHINE INFORMATION'
Log-Info ('-' * 80)
foreach ($key in $Script:MachineInfo.Keys) {
Log-Info (' {0,-20} {1}' -f $key, $Script:MachineInfo[$key])
}
Log-Info $sepLine
# Screen summary
Write-Host " Hostname: $($env:COMPUTERNAME) | OS: $osName | RAM: $ramGB GB | Disk: $diskFreeGB GB free" -ForegroundColor White
if ($osEolWarn) { Log-Warn "OS EOL: $eolStr" }
if ($bitlockerWarn){ Log-Warn "BitLocker: C: drive is NOT encrypted" }
if ($pcAgeYrs -gt 5){ Log-Warn "Aging hardware: PC is $pcAgeYrs years (BIOS: $($biosDate.ToString('yyyy-MM-dd')))" }
if ($wuLastWarn) { Log-Warn "Windows Update: last install was $wuDaysAgo days ago" }
# Hyper-V detection
Invoke-SafeBlock -Label 'Hyper-V detection' -Block {
$hvFeature = Get-WindowsOptionalFeature -Online -FeatureName 'Microsoft-Hyper-V' -ErrorAction SilentlyContinue
if ($hvFeature -and $hvFeature.State -eq 'Enabled') {
$vms = @(Get-VM -ErrorAction SilentlyContinue)
$vmList = if ($vms.Count -gt 0) {
($vms | ForEach-Object { "$($_.Name) [$($_.State)]" }) -join ', '
} else { 'none running' }
$Script:MachineInfo['Hyper-V Host'] = "Yes - $($vms.Count) VM(s): $vmList"
$Script:HWInfo.IsHyperVHost = $true
Log-Summary "Hyper-V host - $($vms.Count) VM(s): $vmList"
}
}
# CVE Vulnerability check
if ($Script:Config.CVECheck) {
Invoke-SafeBlock -Label 'CVE check' -Block {
$msrcUrl = "https://api.msrc.microsoft.com/cvrf/v2.0/Updates('$((Get-Date).Year)')"
$resp = Invoke-WebRequest -Uri $msrcUrl -TimeoutSec 15 -ErrorAction Stop
if ($resp.StatusCode -eq 200) {
Log-Summary "CVE check - Microsoft Security Update Guide queried for build $osBuild"
# Parse and report Critical/High/Medium CVEs
# Full implementation in v1.001 field build
}
}
}
# Password policy
Invoke-SafeBlock -Label 'Password policy' -Block {
$passOut = & net accounts 2>$null
$minLenLine = $passOut | Where-Object { $_ -match 'Minimum password length' }
if ($minLenLine) {
$Script:MinPasswordLen = [int]($minLenLine -replace '[^\d]','')
if ($Script:MinPasswordLen -eq 0) { Log-Warn "Password policy: minimum length is 0 - recommend 12 or more" }
elseif ($Script:MinPasswordLen -lt 8) { Log-Warn "Password policy: minimum length is $Script:MinPasswordLen - recommend 12 or more" }
elseif ($Script:MinPasswordLen -lt 12) { Log-Warn "Password policy: minimum length is $Script:MinPasswordLen - recommend 12 or more" }
else { Log-Summary "Password policy: minimum length $Script:MinPasswordLen (OK)" }
}
}
# Inactive accounts
Invoke-SafeBlock -Label 'Inactive accounts' -Block {
$cutoff = (Get-Date).AddDays(-90)
$excludePatterns = @('^SM_','^HealthMailbox','^QBDataServiceUser\d+$','^defaultuser\d*$','^machine\$')
$localUsers = Get-LocalUser -ErrorAction Stop
foreach ($u in $localUsers) {
$skip = $excludePatterns | Where-Object { $u.Name -match $_ }
if ($skip -or -not $u.Enabled) { continue }
$lastLogon = $u.LastLogon
if ($null -eq $lastLogon -or $lastLogon -lt $cutoff) {
$daysAgo = if ($lastLogon) { ([datetime]::Now - $lastLogon).Days } else { 9999 }
$lastStr = if ($lastLogon) { $lastLogon.ToString('yyyy-MM-dd') } else { 'never' }
$inactiveAccounts.Add([PSCustomObject]@{ Name=$u.Name; LastLogon=$lastStr; DaysAgo=$daysAgo })
}
}
if ($inactiveAccounts.Count -gt 0) {
Log-Warn "Inactive local accounts (90+ days) - $($inactiveAccounts.Count) found:"
foreach ($ia in $inactiveAccounts) {
Log-Warn " $($ia.Name) (last logon: $($ia.LastLogon) - $($ia.DaysAgo) days ago)"
}
} else { Log-Summary "Local accounts - no inactive accounts found" }
}
Log-Summary "Assessment Engine complete"
}
} else {
Log-Info "Assessment Engine - disabled"
}
# ==============================================================================
# PHASE 3: HARDENING ENGINE
# Security configuration hardening and remediation
# ==============================================================================
Write-PhaseProgress -PhaseNum 3 -PhaseName 'Hardening Engine'
Log-Info '--- Phase 3: Hardening Engine ---'
if ($Script:Config.HardeningEngine_Enabled) {
# RDP / NLA check
Invoke-SafeBlock -Label 'RDP check' -Block {
$rdpEnabled = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
-Name 'fDenyTSConnections' -ErrorAction Stop).fDenyTSConnections -eq 0
if ($rdpEnabled) {
$nlaEnabled = (Get-ItemProperty `
'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name 'UserAuthentication' -ErrorAction SilentlyContinue).UserAuthentication -eq 1
if (-not $nlaEnabled -and $Script:Config.EnforceRDP_NLA) {
Set-ItemProperty `
'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name 'UserAuthentication' -Value 1 -Type DWord -Force
Log-Harden "RDP NLA enforced - Network Level Authentication now required"
} elseif ($nlaEnabled) {
Log-Summary "RDP is ENABLED - NLA enforced (OK)"
} else {
Log-Warn "RDP is ENABLED - NLA NOT enforced - recommend enabling"
}
} else {
Log-Summary "RDP - disabled (OK)"
}
}
# SMBv1
Invoke-SafeBlock -Label 'SMBv1 check' -Block {
$smb1 = Get-SmbServerConfiguration -ErrorAction Stop | Select-Object -ExpandProperty EnableSMB1Protocol
if ($smb1) {
if ($Script:Config.DisableSMBv1) {
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force -ErrorAction Stop
Log-Harden "SMBv1 disabled - legacy protocol eliminated"
} else {
Log-Warn "SMBv1 is ENABLED - critical vulnerability, recommend disabling"
}
} else { Log-Summary "SMBv1 - disabled (OK)" }
}
# LLMNR
Invoke-SafeBlock -Label 'LLMNR check' -Block {
$llmnr = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' `
-Name 'EnableMulticast' -ErrorAction SilentlyContinue).EnableMulticast
if ($llmnr -ne 0) {
if ($Script:Config.DisableLLMNR) {
$regPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient'
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
Set-ItemProperty -Path $regPath -Name 'EnableMulticast' -Value 0 -Type DWord -Force
Log-Harden "LLMNR disabled - MITM attack vector eliminated"
} else { Log-Warn "LLMNR may be enabled - recommend disabling via GPO" }
} else { Log-Summary "LLMNR - disabled (OK)" }
}
# NetBIOS
Invoke-SafeBlock -Label 'NetBIOS check' -Block {
$adapters = Get-CimInstance Win32_NetworkAdapterConfiguration -ErrorAction Stop | Where-Object { $_.IPEnabled }
$netbiosOn = @($adapters | Where-Object { $_.TcpipNetbiosOptions -ne 2 })
if ($netbiosOn.Count -gt 0) {
if ($Script:Config.DisableNetBIOS) {