-
Notifications
You must be signed in to change notification settings - Fork 39
/
ARTHIR.ps1
1341 lines (1169 loc) · 53.2 KB
/
ARTHIR.ps1
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
<#
___ ______ _____ _ _ ___________
/ _ \ | ___ \_ _| | | |_ _| ___ \
/ /_\ \| |_/ / | | | |_| | | | | |_/ /
| _ || / | | | _ | | | | /
| | | || |\ \ | | | | | |_| |_| |\ \
\_| |_/\_| \_| \_/ \_| |_/\___/\_| \_|
.VERSION
Version 1.0 - Mar 2019
This version was forked and updated by Malware Archaeology to run more than just powerShell scripts
https://github.com/MalwareArchaeology/ARTHIR
http://www.ARTHIR.com
Shout out to Olaf Hartong for his assistance in updates to this script.
.SYNOPSIS
ARTHIR is a Powershell based incident response framework for Windows
environments. This is a forked and improved version of the KANSA project.
.DESCRIPTION
ARTHIR is a modular, PowerShell based incident response framework for
Windows environments that have Windows Remote Management enabled.
ARTHIR looks for a modules.conf file in the .\Modules directory. If one
is found, it controls which modules execute and in what order. If no
modules.conf is found, all modules will be executed in the order that
ls reads them.
After parsing modules.conf or the -ModulesPath parameter argument,
ARTHIR will execute each module on each target (remote host) and
write the output to a folder named for each module in a time stamped
output path. Each target will have its data written to separate files.
For example, the Get-PS_Version_Logging_Details.ps1 module data will be written
to:
* Output_timestamp\Get-PS_Version_Logging_Details\Hostname\hostname-Get-PS_Version_Logging_Details.txt.
All modules should create and then return reports.
ARTHIR.ps1 like Kansa was written to avoid the need for CredSSP, therefore
"second-hops" should be avoided. For more details on this see:
* http://trustedsignal.blogspot.com/2014/04/kansa-modular-live-response-tool-for.html
The script assumes you will have administrator level privileges on
target hosts, though such privileges may not be required by all
modules.
If you run this script without the -TargetList argument, Remote Server
Administration Tools (RSAT), is required to query Active Directory.
RSAT is available from Microsoft's Download Center for Windows 7 and 8.
You can search for RSAT at:
* http://www.microsoft.com/en-us/download/default.aspx
.PARAMETER ModulePath
An optional parameter, default value is .\Modules\, that specifies the
path to the collector modules or a specific module. Spaces in the path
are not supported, however, ModulePath may point directly to a specific
module and if that module takes a parameter, you should have a space
between the path to the script and its first argument, put the whole
thing in quotes. See example.
.PARAMETER TargetList
An optional parameter, the name of a file containing a list of servers
to collect data from. If these hosts are outside the current forest,
fully qualified domain names are required. In general, it is advised to
use FQDNs.
PARAMETER Target
An optional parameter, the name of a single system to collect data from.
.PARAMETER TargetCount
An optional parameter that specifies the maximum number of targets.
In the absence of the TargetList and / or Target arguments, ARTHIR will
use Remote System Administration Tools (a separate installed package)
to query Active Directory and will build a list of hosts to target
automatically.
.PARAMETER Credential
An optional credential that the script will use for execution. Use the
$Credential = Get-Credential convention to populate a suitable variable.
.PARAMETER Pushbin
An optional flag that causes ARTHIR to push required binaries to the
ADMIN$ shares of targets. Modules that require third-party binaries,
must include the "BINDEP <binary>" directive.
For example, the Get-LOG-MD_Autoruns.ps1 collector has a dependency on
IMF Security LOG-MD. The Get-LOG-MD-Autoruns.ps1 collector
contains a special line called a "directive" that instructs ARTHIR.ps1
to copy the LOG-MD.exe binary to remote systems when called with the
-Pushbin flag. ARTHIR will push the binary, assuming the user placed
any required binaries into the \Modules\bin\ path.
Only LOG-MD Free Edition is distributed with ARTHIR, all other tools/utilities/binaries
the user must download and place into the \Modules\bin\ path and adjust the Bindep
directive to reference the proper binary in the appropriate module.
Directives should be placed in module's .SYNOPSIS sections under .NOTES
* Directives must appear on a line by themselves
* Directives must start the line
# ARTHIR - Directives must be in all CAPITAL letters
For example, the directive for Get-LOG-MD-Autoruns.ps1 as of this writing is
* BINDEP .\Modules\bin\LOG-MD.exe
If your required binaries are already present on each target and in the
path where the modules expect them to be, you can omit the -Pushbin
flag and save the step of copying binaries.
.PARAMETER Rmbin
An optional switch for removing binaries that may have been pushed to
remote hosts via -Pushbin either on this run, or during a previous run.
.PARAMETER Ascii
An optional switch that tells ARTHIR you want all text output (i.e. txt,
csv and tsv) and errors written as Ascii. Unicode is the default.
.PARAMETER UpdatePath
An optional switch that adds Analysis script paths to the user's path
and then exits. ARTHIR will automatically add Analysis script paths to
the user's path when run normally, this switch is for convenience when
coming back to the data for analysis.
.PARAMETER ListModules
An optional switch that lists the available modules. Useful for
constructing a modules.conf file. ARTHIR exits after listing.
You'll likely want to sort the according to the order of volatility.
.PARAMETER ListAnalysis
An optional switch that lists the available analysis scripts. Useful
for constructing an analysis.conf file. ARTHIR exits after listing. If
you use this switch to build an analysis.conf file, you'll likely want
to edit the list so you're only running the analysis scripts you want
to run.
.PARAMETER Analysis
An optional switch that causes ARTHIR to run automated analysis based on
the contents of the Analysis\Analysis.conf file.
.PARAMETER Transcribe
An optional flag that causes Start-Transcript to run at the start
of the script, writing to $OutputPath\yyyyMMddhhmmss.log
.PARAMETER Quiet
An optional flag that overrides ARTHIR's default of running with -Verbose.
.PARAMETER UseSSL
An optional flag for use in environments that have authentication
certificates deployed. If this flag is used and certificates are
deployed, connections will be made over HTTPS and will be encrypted.
Without this flag traffic passes in the clear. Note authentication is
done via Kerberos regardless of whether or not SSL is used. Alternate
authentication methods are available via the -Authentication parameter
argument.
.PARAMETER Port
An optional parameter if WinRM is listening on a non-standard port.
.PARAMETER Authentication
An optional parameter specifying what authentication method should be
used. The default is Kerberos, but that won't work for authenticating
against local administrator accounts or for scenarios with non-domain
joined systems.
Valid options: Basic, CredSSP, Default, Digest, Kerberos, Negotiate,
NegotiateWithImplicitCredential.
Whereever possible, you should use Kerberos, some of these options are
considered dangerous, so be careful and read up on the different
methods before using an alternate.
.PARAMETER JSONDepth
An optional parameter specifying how many levels of contained objects
are included in the JSON representation. Default is 10.
.PARAMETER Rmdownload
An optional switch for removing files that have been downloaded by the
module. The files were most likely temporary (output) files as a result
of the binary that ran.
.INPUTS
None, you cannot pipe objects to this cmdlet
.OUTPUTS
The reports generated by your module and any errors and transcript files which are .TXT.
.NOTES
In the absence of a configuration file, specifying which modules to run,
this script will run each module across all hosts.
Each module should return objects.
Because modules should only COLLECT data from remote hosts, their
filenames must begin with "Get-". Examples:
* Get-PrefetchListing.ps1
* Get-Netstat.ps1
Any module not beginning with "Get-" will be ignored.
Note: this read-only aspect is unenforced, therefore, ARTHIR can be used
to make changes to remote hosts. As a result, it can be used to
facilitate remediation or configuration changes.
The script can take a list of targets, read from a text file, via the
-TargetList <file> argument. You may also supply the -TargetCount
argument to limit how many hosts will be targeted. To target a single
host, use the -Target <hostname> argument.
In the absence of the -TargetList or -Target arguments, ARTHIR.ps1 will
query Acitve Directory for a complete list of hosts and will attempt to
target all of them. Warning, this could take a while... Test it first.
.EXAMPLE
* ARTHIR.ps1
In the above example the user has specified no arguments, which will
cause ARTHIR to run modules per the .\Modules\Modules.conf file against
a list of hosts that it is able to query from Active Directory. Errors
and all output will be written to a timestamped output directory. If
.\Modules\Modules.conf is not found, all ps1 scripts starting with Get-
under the .\Modules\ directory (recursively) will be run.
* Not recommended unless you test and know how long and what you want to run
.EXAMPLE
* ARTHIR.ps1 -TargetList hosts.txt -Credential $Credential -Transcribe
In this example the user has specified a list of hosts to target, a
user credential under which to execute. The -Transcribe flag is also
supplied, causing all script output to be written to a transcript. By
default, the script will also output verbose runstate information.
.EXAMPLE
* ARTHIR.ps1 -ModulePath ".\Modules\Info\Get-PS_Version_Logging_Details.ps1" -Target Pluto
In this example -ModulePath refers to a specific module that is run
against a single target.
.EXAMPLE
* ARTHIR.ps1 -TargetList hostlist -Analysis
Runs collection according to the configuration in Modules\Modules.conf.
Following collection, runs analysis scripts per Analysis\Analysis.conf.
.EXAMPLE
* ARTHIR.ps1 -ListModules
Returns a list of all the modules found under the default modules path.
.EXAMPLE
* ARTHIR.ps1 -ListAnalysis
Returns a list of all analysis scripts found under the Analysis path.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$False,Position=0)]
[String]$ModulePath="Modules\",
[Parameter(Mandatory=$False,Position=1)]
[String]$TargetList=$Null,
[Parameter(Mandatory=$False,Position=2)]
[String]$Target=$Null,
[Parameter(Mandatory=$False,Position=3)]
[int]$TargetCount=0,
[Parameter(Mandatory=$False,Position=4)]
[System.Management.Automation.PSCredential]$Credential=$Null,
[Parameter(Mandatory=$False,Position=5)]
[ValidateSet("CSV","JSON","TSV","XML")]
[String]$OutputFormat="CSV",
[Parameter(Mandatory=$False,Position=6)]
[Switch]$Pushbin,
[Parameter(Mandatory=$False,Position=7)]
[Switch]$Rmbin,
[Parameter(Mandatory=$False,Position=8)]
[Int]$ThrottleLimit=0,
[Parameter(Mandatory=$False,Position=9)]
[ValidateSet("Ascii","BigEndianUnicode","Byte","Default","Oem","String","Unicode","Unknown","UTF32","UTF7","UTF8")]
[String]$Encoding="Unicode",
[Parameter(Mandatory=$False,Position=10)]
[Switch]$UpdatePath,
[Parameter(Mandatory=$False,Position=11)]
[Switch]$ListModules,
[Parameter(Mandatory=$False,Position=12)]
[Switch]$ListAnalysis,
[Parameter(Mandatory=$False,Position=13)]
[Switch]$Analysis,
[Parameter(Mandatory=$False,Position=14)]
[Switch]$Transcribe,
[Parameter(Mandatory=$False,Position=15)]
[Switch]$Quiet=$False,
[Parameter(Mandatory=$False,Position=16)]
[Switch]$UseSSL,
[Parameter(Mandatory=$False,Position=17)]
[ValidateRange(0,65535)]
[uint16]$Port=5985,
[Parameter(Mandatory=$False,Position=18)]
[ValidateSet("Basic","CredSSP","Default","Digest","Kerberos","Negotiate","NegotiateWithImplicitCredential")]
[String]$Authentication="Kerberos",
[Parameter(Mandatory=$false,Position=19)]
[int32]$JSONDepth="10",
[Parameter(Mandatory=$false,Position=20)]
[Switch]$Rmdownload,
[Parameter(Mandatory=$false,Position=21)]
[Switch]$Timeout,
[Parameter(Mandatory=$false,Position=22)]
[String]$Modconf=".\Modules\Modules.conf",
[Parameter(Mandatory=$false,Position=23)]
[String]$OutputPath = $(Get-Location | Select-Object -ExpandProperty Path) + "\Output_$([String] (Get-Date -Format yyyyMMddHHmmss))\"
)
# ARTHIR - Opening with a Try so the Finally block at the bottom will always call
# ARTHIR - the Exit-Script function and clean up things as needed.
Try {
# ARTHIR - Long paths prevent data from being written, this is used to test their length
# ARTHIR - Per http://msdn.microsoft.com/en-us/library/aa365247.aspx#maxpath, maximum
# ARTHIR - path length should be 260 characters. We set it to 241 here to account for
# ARTHIR - max computername length of 15 characters, it's part of the path, plus a
# ARTHIR - hyphen separator and a dot-three extension.
# ARTHIR - extension -- 260 - 19 = 241.
Set-Variable -Name MAXPATH -Value 241 -Option Constant
# ARTHIR - Since ARTHIR provides so much useful information through Write-Vebose, we
# ARTHIR - want it to run with that flag enabled by default. This behavior can be
# ARTHIR - overridden by passing the -Quiet flag. This is scoped only to this context,
# ARTHIR - so we don't need to reset it when we're done.
if(!$Quiet) {
$VerbosePreference = "Continue"
}
function FuncTemplate {
<#
.SYNOPSIS
ARTHIR - Default function template, copy when making new function
#>
Param(
[Parameter(Mandatory=$False,Position=0)]
[String]$ParamTemplate=$Null
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
# ARTHIR - Non-terminating errors can be checked via
if ($Error) {
# ARTHIR - Write the $Error to the $Errorlog
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
Try {
<# ARTHIR -
Try/Catch blocks are for terminating errors. See some of the
functions below for examples of how non-terminating errors can
be caught and handled.
#>
} Catch [Exception] {
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
<# ARTHIR - End FuncTemplate #>
function Exit-Script {
<#
.SYNOPSIS
ARTHIR - Exit the script somewhat gracefully, closing any open transcript.
#>
Set-Location $StartingPath
if ($Transcribe) {
[void] (Stop-Transcript)
}
if ($Error) {
"Exit-Script function was passed an error, this may be a duplicate that wasn't previously cleared, or ARTHIR.ps1 has crashed." | Add-Content -Encoding $Encoding $ErrorLog
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
if ($Error) {
Write-Output "Script completed with warnings or errors. See ${ErrorLog} for details."
}
if (!(Get-ChildItem -Force $OutputPath)) {
# ARTHIR - $OutputPath is empty, delete it
# ARTHIR - "Output path was created, but ARTHIR finished with no hits, no runs and no errors. Deleting the folder."
[void] (Remove-Item $OutputPath -Force)
}
Exit
}
function Get-Modules {
<#
.SYNOPSIS
Looks for modules.conf in the $Modulepath, default is Modules. If found,
returns an ordered hashtable of script files and their arguments, if any.
If no modules.conf is found, returns an ordered hashtable of all modules
found in $Modulepath, but no arguments will be present so scripts will
run with default params. A module is a .ps1 script starting with Get-.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[String]$ModulePath
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
# ARTHIR - ToDo: There should probably be some error handling in this function.
Write-Debug "`$ModulePath is ${ModulePath}."
# ARTHIR - User may have passed a full path to a specific module, possibly with an argument
$ModuleScript = ($ModulePath -split " ")[0]
$ModuleArgs = @($ModulePath -split [regex]::escape($ModuleScript))[1].Trim()
$Modules = $FoundModules = @()
# ARTHIR - Need to maintain the order for "order of volatility"
$ModuleHash = New-Object System.Collections.Specialized.OrderedDictionary
if (!(ls $ModuleScript | Select-Object -ExpandProperty PSIsContainer)) {
# ARTHIR - User may have provided full path to a .ps1 module, which is how you run a single module explicitly
$ModuleHash.Add((ls $ModuleScript), $ModuleArgs)
if (Test-Path($ModuleScript)) {
$Module = ls $ModuleScript | Select-Object -ExpandProperty BaseName
Write-Verbose "Running module: `n$Module $ModuleArgs"
Return $ModuleHash
}
}
if (Test-Path($Modconf)) {
Write-Verbose "Found ${Modconf}."
# ARTHIR - ignore blank and commented lines, trim misc. white space
Get-Content $Modconf | Foreach-Object { $_.Trim() } | Where-Object { $_ -gt 0 -and (!($_.StartsWith("#"))) } | Foreach-Object { $Module = $_
# ARTHIR - verify listed modules exist
$ModuleScript = ($Module -split " ")[0]
$ModuleArgs = ($Module -split [regex]::escape($ModuleScript))[1].Trim()
$Modpath = $ModulePath + "\" + $ModuleScript
if (!(Test-Path($Modpath))) {
"WARNING: Could not find module specified in ${Modconf}: $ModuleScript. Skipping." | Add-Content -Encoding $Encoding $ErrorLog
} else {
# ARTHIR - module found add it and its arguments to the $ModuleHash
$ModuleHash.Add((ls $ModPath), $Moduleargs)
# ARTHIR - $FoundModules += ls $ModPath # ARTHIR - deprecated code, remove after testing
}
}
# ARTHIR - $Modules = $FoundModules # ARTHIR - deprecated, remove after testing
} else {
# ARTHIR - we had no modules.conf
ls -r "${ModulePath}\Get-*.ps1" | Foreach-Object { $Module = $_
$ModuleHash.Add($Module, $null)
}
}
Write-Verbose "Running modules:`n$(($ModuleHash.Keys | Select-Object -ExpandProperty BaseName) -join "`n")"
$ModuleHash
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
function Load-AD {
# ARTHIR - no targets provided so we'll query AD to build it, need to load the AD module
Write-Debug "Entering $($MyInvocation.MyCommand)"
if (Get-Module -ListAvailable | Where-Object { $_.Name -match "ActiveDirectory" }) {
$Error.Clear()
Import-Module ActiveDirectory
if ($Error) {
"ERROR: Could not load the required Active Directory module. Please install the Remote Server Administration Tool for AD. Quitting." | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
Exit
}
} else {
"ERROR: Could not load the required Active Directory module. Please install the Remote Server Administration Tool for AD. Quitting." | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
Exit
}
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
function Get-Forest {
# ARTHIR - what forest are we in?
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
$Forest = (Get-ADForest).Name
if ($Forest) {
Write-Verbose "Forest is ${forest}."
$Forest
} elseif ($Error) {
# ARTHIR - Write the $Error to the $Errorlog
$Error | Add-Content -Encoding $Encoding $ErrorLog
"ERROR: Get-Forest could not find current forest. Quitting." | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
Exit
}
}
function Get-Targets {
Param(
[Parameter(Mandatory=$False,Position=0)]
[String]$TargetList=$Null,
[Parameter(Mandatory=$False,Position=1)]
[int]$TargetCount=0
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
$Targets = $False
if ($TargetList) {
# ARTHIR - user provided a list of targets
if ($TargetCount -eq 0) {
$Targets = Get-Content $TargetList | Foreach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
} else {
$Targets = Get-Content $TargetList | Foreach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 } | Select-Object -First $TargetCount
}
} else {
# ARTHIR - no target list provided, we'll query AD for it
Write-Verbose "`$TargetCount is ${TargetCount}."
if ($TargetCount -eq 0 -or $TargetCount -eq $Null) {
$Targets = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name
} else {
$Targets = Get-ADComputer -Filter * -ResultSetSize $TargetCount | Select-Object -ExpandProperty Name
}
# ARTHIR - Iterate through targets, cleaning up AD Replication errors
# ARTHIR - In some AD environments, when there are duplicate object names, AD will add the objectGUID to the Name
# ARTHIR - displayed in the format of "hostname\0ACNF:ObjectGUID". If you expand the property Name, you get 2 lines
# ARTHIR - returned, the hostname, and then CNF:ObjectGUID. This code will look for hosts with more than one line and return
# ARTHIR - the first line which is assumed to be the host name.
foreach ($item in $Targets) {
$numlines = $item | Measure-Object -Line
if ($numlines.Lines -gt 1) {
$lines = $item.Split("`n")
$i = [array]::IndexOf($targets, $item)
$targets[$i] = $lines[0]
}
}
$TargetList = "hosts.txt"
Set-Content -Path $TargetList -Value $Targets -Encoding $Encoding
}
if ($Targets) {
Write-Verbose "`$Targets are ${Targets}."
return $Targets
} else {
Write-Verbose "Get-Targets function found no targets. Checking for errors."
}
if ($Error) { # ARTHIR - if we make it here, something went wrong
$Error | Add-Content -Encoding $Encoding $ErrorLog
"ERROR: Get-Targets function could not get a list of targets. Quitting."
$Error.Clear()
Exit
}
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
function Get-LegalFileName {
<#
.SYNOPSIS
Returns argument with illegal filename characters removed.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[Array]$Argument
)
Write-Debug "Entering ($MyInvocation.MyCommand)"
$Argument = $Arguments -join ""
$Argument -replace [regex]::Escape("\") -replace [regex]::Escape("/") -replace [regex]::Escape(":") `
-replace [regex]::Escape("*") -replace [regex]::Escape("?") -replace "`"" -replace [regex]::Escape("<") `
-replace [regex]::Escape(">") -replace [regex]::Escape("|") -replace " "
}
function Get-Directives {
<#
.SYNOPSIS
Returns a hashtable of directives found in the script
Directives are used for two things:
1) The BINDEP directive tells ARTHIR that a module depends on some
binary and what the name of the binary is. If ARTHIR is called with
-PushBin, the script will look in Modules\bin\ for the binary and
attempt to copy it to targets. Specify multiple BINDEPs by
separating each path with a semi-colon (;).
2) The DATADIR directive tells ARTHIR what the output path is for
the given module's data so that if it is called with the -Analysis
flag, the analysis scripts can find the data.
TK Some collector output paths are dynamically generated based on
arguments, so this breaks for analysis. Solve.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[String]$Module,
[Parameter(Mandatory=$False,Position=1)]
[Switch]$AnalysisPath
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
if ($AnalysisPath) {
$Module = ".\Analysis\" + $Module
}
if (Test-Path($Module)) {
$DirectiveHash = @{}
Get-Content $Module | Select-String -CaseSensitive -Pattern "BINDEP|DATADIR|DOWNLOAD" | Foreach-Object { $Directive = $_
if ( $Directive -match "(^BINDEP|^# ARTHIR - BINDEP) (.*)" ) {
$DirectiveHash.Add("BINDEP", $($matches[2]))
}
if ( $Directive -match "(^DATADIR|^# ARTHIR - DATADIR) (.*)" ) {
$DirectiveHash.Add("DATADIR", $($matches[2]))
}
if ( $Directive -match "(^DOWNLOAD|^# ARTHIR - DOWNLOAD) (.*)" ) {
$DirectiveHash.Add("DOWNLOAD", $($matches[2]))
}
}
$DirectiveHash
} else {
"WARNING: Get-Directives was passed invalid module $Module." | Add-Content -Encoding $Encoding $ErrorLog
}
}
function Get-TargetData {
<#
.SYNOPSIS
Runs each module against each target. Writes out the returned data to host where ARTHIR is run from.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[Array]$Targets,
[Parameter(Mandatory=$True,Position=1)]
[System.Collections.Specialized.OrderedDictionary]$Modules,
[Parameter(Mandatory=$False,Position=2)]
[System.Management.Automation.PSCredential]$Credential=$False,
[Parameter(Mandatory=$False,Position=3)]
[Int]$ThrottleLimit
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
# ARTHIR - Create our sessions with targets
if ($Credential) {
if ($UseSSL) {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -UseSSL -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile) -Credential $Credential
} else {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile) -Credential $Credential
}
} else {
if ($UseSSL) {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -UseSSL -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile)
} else {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile)
}
}
# ARTHIR - Check for and log errors
if ($Error) {
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
$Modules.Keys | Foreach-Object { $Module = $_
$ModuleName = $Module | Select-Object -ExpandProperty BaseName
$Arguments = @()
$Arguments += $($Modules.Get_Item($Module)) -split ","
if ($Arguments) {
$ArgFileName = Get-LegalFileName $Arguments
} else { $ArgFileName = "" }
# ARTHIR - Get our directives both old and new style
$DirectivesHash = @{}
$DirectivesHash = Get-Directives $Module
if ($Pushbin) {
$bindeps = [string]$DirectivesHash.Get_Item("BINDEP") -split ';'
foreach($bindep in $bindeps) {
if ($bindep) {
# ARTHIR - Send-File only supports a single destination at a time, so we have to loop this.
foreach ($PSSession in $PSSessions)
{
$RemoteWindir = Invoke-Command -Session $PSSession -ScriptBlock { Get-ChildItem -Force env: | Where-Object { $_.Name -match "windir" } | Select-Object -ExpandProperty value }
$null = Send-File -Path (ls $bindep).FullName -Destination $RemoteWindir -Session $PSSession
}
}
}
}
# ARTHIR - run the module on the targets
if ($Arguments) {
Write-Debug "Invoke-Command -Session $PSSessions -FilePath $Module -ArgumentList `"$Arguments`" -AsJob -ThrottleLimit $ThrottleLimit"
$Job = Invoke-Command -Session $PSSessions -FilePath $Module -ArgumentList $Arguments -AsJob -ThrottleLimit $ThrottleLimit
Write-Verbose "Waiting for $ModuleName $Arguments to complete."
} else {
Write-Debug "Invoke-Command -Session $PSSessions -FilePath $Module -AsJob -ThrottleLimit $ThrottleLimit"
$Job = Invoke-Command -Session $PSSessions -FilePath $Module -AsJob -ThrottleLimit $ThrottleLimit
Write-Verbose "Waiting for $ModuleName to complete."
}
# ARTHIR - Wait-Job does return data to stdout, add $suppress = to start of next line, if needed
if ($Timeout) {
Wait-Job $Job -Timeout $Timeout
} else {
Wait-Job $Job
}
# ARTHIR - set up our output location
$GetlessMod = $($ModuleName -replace "Get-")
# ARTHIR - Long paths prevent output from being written, so we truncate $ArgFileName to accommodate
# ARTHIR - We're estimating the output path because at this point, we don't know what the hostname
# ARTHIR - is and it is part of the path. Hostnames are 15 characters max, so we assume worst case
$EstOutPathLength = $OutputPath.Length + ($GetlessMod.Length * 2) + ($ArgFileName.Length * 2)
if ($EstOutPathLength -gt $MAXPATH) {
# ARTHIR - Get the path length without the arguments, then we can determine how long $ArgFileName can be
$PathDiff = [int] $EstOutPathLength - ($OutputPath.Length + ($GetlessMod.Length * 2) -gt 0)
$MaxArgLength = $PathDiff - $MAXPATH
if ($MaxArgLength -gt 0 -and $MaxArgLength -lt $ArgFileName.Length) {
$OrigArgFileName = $ArgFileName
$ArgFileName = $ArgFileName.Substring(0, $MaxArgLength)
"WARNING: ${GetlessMod}'s output path contains the arguments that were passed to it. Those arguments were truncated from $OrigArgFileName to $ArgFileName to accommodate Window's MAXPATH limit of 260 characters." | Add-Content -Encoding $Encoding $ErrorLog
}
}
# if (!(Test-Path $OutputPath$GetlessMod$ArgFileName)) {
# [void] (New-Item -Path $OutputPath -name ($GetlessMod + $ArgFileName) -ItemType Directory)
# }
$Job.ChildJobs | Foreach-Object { $ChildJob = $_
$Recpt = Receive-Job $ChildJob
# ARTHIR - Log errors from child jobs, including module and host that failed.
if($Error) {
$ModuleName + " reports error on " + $ChildJob.Location + ": `"" + $Error + "`"" | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
# ARTHIR - Depricated for direct output to a file that is then returned
# }
# ARTHIR - Now that we know our hostname, let's double check our path length, if it's too long, we'll write an error
# ARTHIR - Max path is 260 characters, if we're over 256, we can't accomodate an extension
# $Outfile = $OutputPath + $GetlessMod + $ArgFileName + "\" + $ChildJob.Location + "-" + $GetlessMod + $ArgFileName
# if ($Outfile.length -gt 256) {
# "ERROR: ${GetlessMod}'s output path length exceeds 260 character limit. Can't write the output to disk for $($ChildJob.Location)." | Add-Content -Encoding $Encoding $ErrorLog
# Return
# }
# ARTHIR - save the data of the raw output that is no longer used This section is not useful !!!!!!!!!!!!!!!!!!!!!!!!!!!
# switch -Wildcard ($OutputFormat) {
# "*csv" {
# $Outfile = $Outfile + ".csv"
# $Recpt | Export-Csv -NoTypeInformation -Encoding $Encoding $Outfile
# }
# "*json" {
# $Outfile = $Outfile + ".json"
# $Recpt | ConvertTo-Json -Depth $JSONDepth | Set-Content -Encoding $Encoding $Outfile
# }
# "*tsv" {
# $Outfile = $Outfile + ".tsv"
# # ARTHIR - LogParser can't handle quoted tab separated values, so we'll strip the quotes.
# $Recpt | ConvertTo-Csv -NoTypeInformation -Delimiter "`t" | ForEach-Object { $_ -replace "`"" } | Set-Content -Encoding $Encoding $Outfile
# }
# "*xml" {
# $Outfile = $Outfile + ".xml"
# $Recpt | Export-Clixml $Outfile -Encoding $Encoding
# }
# <# ARTHIR - Following output formats are no longer supported in ARTHIR
# "*bin" {
# $Outfile = $Outfile + ".bin"
# $Recpt | Set-Content -Encoding Byte $Outfile
# }
# "*zip" {
# # ARTHIR - Compression should be done in the collector
# # ARTHIR - Default collector template has a function
# # ARTHIR - for compressing data as an example
# $Outfile = $Outfile + ".zip"
# $Recpt | Set-Content -Encoding Byte $Outfile
# }
# #>
# default {
# $Outfile = $Outfile + ".csv"
# $Recpt | Export-Csv -NoTypeInformation -Encoding $Encoding $Outfile
# }
}
}
# ARTHIR - Output.
Remove-Job $Job
if ($rmbin) {
if ($bindeps) {
foreach ($bindep in $bindeps) {
$RemoteBinDep = "$RemoteWinDir\$(split-path -path $bindep -leaf)"
Invoke-Command -Session $PSSession -ScriptBlock { Remove-Item -force -path $using:RemoteBinDep}
}
}
}
$files = [string]$DirectivesHash.Get_Item("DOWNLOAD") -split ';'
foreach($file in $files) {
if ($file) {
# ARTHIR - Send-File only supports a single destination at a time, so we have to loop this.
foreach ($PSSession in $PSSessions)
{
$dir = Split-Path -Path $file
$dirLength = $dir.length
$RemoteFiles = Invoke-Command -Session $PSSession -ScriptBlock {Get-ChildItem $using:file -rec | Where-Object { ! $_.PSIsContainer }}
foreach ($RemoteFile in $RemoteFiles)
{
$FilePath = $OutputPath+$ModuleName+"\"+$PSSession.ComputerName+"\"+([string]$RemoteFile.FullName).Substring($dirLength+1)
Write-Host $FilePath
$destdir = (Split-Path -Path $FilePath)
if (!(Test-Path (Split-Path -Path $FilePath))) {$null = New-Item -ItemType Directory -Force -Path $destdir}
if ($PSVersionTable.PSVersion.major -lt 5) {
Invoke-Command -Session $PSSession -ScriptBlock {Get-Content -ReadCount 1000 -Path $Using:RemoteFile} | Set-Content -Path $FilePath
} else {
# ARTHIR - Copy the file that is created by the module.
$null = Copy-Item -FromSession $PSSession -Path $RemoteFile.FullName -Destination $destdir
}
}
}
}
}
if ($rmdownload) {
$rmdownloads = [string]$DirectivesHash.Get_Item("DOWNLOAD") -split ';'
foreach ($dir in $rmdownloads) {
if ($dir) {
# Write-Host $dir
Invoke-Command -Session $PSSession -ScriptBlock { Get-ChildItem $using:dir -rec | Remove-Item}
}
}
}
}
Remove-PSSession $PSSessions
if ($Error) {
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
$Runtime2 = ([String] (Get-Date -Format g))
Write-Verbose "Start Time - $Runtime2"
function Push-Bindep {
<#
.SYNOPSIS
Attempts to copy required binaries to targets.
If a module depends on an external binary, the binary should be copied
to .\Modules\bin\ and the module should reference the binary in the
.NOTES section of the .SYNOPSIS as follows:
BINDEP .\Modules\bin\LOG-MD.exe
!! This directive is case-sensitve and must start the line !!
Some Modules may require multiple binary files, say an executable and
required dlls. See the .\Modules\Disk\Get-FlsBodyFile.ps1 as an
example. The BINDEP line in that module references
.\Modules\bin\fls.zip. ARTHIR will copy that zip file to the targets,
but the module itself handles the unzipping of the fls.zip file.
BINDEP must include the path to the binary, relative to ARTHIR.ps1's
path.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[Array]$Targets,
[Parameter(Mandatory=$True,Position=1)]
[String]$Module,
[Parameter(Mandatory=$True,Position=2)]
[String]$Bindep,
[Parameter(Mandatory=$False,Position=3)]
[System.Management.Automation.PSCredential]$Credential
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
Write-Verbose "${Module} has dependency on ${Bindep}."
if (-not (Test-Path("$Bindep"))) {
Write-Verbose "${Bindep} not found in ${ModulePath}bin, skipping."
"WARNING: ${Bindep} not found in ${ModulePath}\bin, skipping." | Add-Content -Encoding $Encoding $ErrorLog
Continue
}
Write-Verbose "Attempting to copy ${Bindep} to targets..."
$Targets | Foreach-Object { $Target = $_
Try {
if ($Credential) {
[void] (New-PSDrive -PSProvider FileSystem -Name "ARTHIRDrive" -Root "\\$Target\ADMIN$" -Credential $Credential)
Copy-Item "$Bindep" "ARTHIRDrive:"
[void] (Remove-PSDrive -Name "ARTHIRDrive")
} else {
[void] (New-PSDrive -PSProvider FileSystem -Name "ARTHIRDrive" -Root "\\$Target\ADMIN$")
Copy-Item "$Bindep" "ARTHIRDrive:"
[void] (Remove-PSDrive -Name "ARTHIRDrive")
}
} Catch [Exception] {
"Caught: $_" | Add-Content -Encoding $Encoding $ErrorLog
}
if ($Error) {
"WARNING: Failed to copy ${Bindep} to ${Target}." | Add-Content -Encoding $Encoding $ErrorLog
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
}
Write-Debug "Exiting $($MyInvocation.MyCommand)"
}
function Send-File
{
<#
.SYNOPSIS
This function sends a file (or folder of files recursively) to a destination WinRm session. This function was originally
built by Lee Holmes (http://poshcode.org/2216) but has been modified to recursively send folders of files as well
as to support UNC paths.
.PARAMETER Path
The local or UNC folder path that you'd like to copy to the session. This also support multiple paths in a comma-delimited format.
If this is a UNC path, it will be copied locally to accomodate copying. If it's a folder, it will recursively copy
all files and folders to the destination.
.PARAMETER Destination
The local path on the remote computer where you'd like to copy the folder or file. If the folder does not exist on the remote
computer it will be created.
.PARAMETER Session
The remote session. Create with New-PSSession.
.EXAMPLE
$session = New-PSSession -ComputerName MYSERVER
Send-File -Path C:\test.txt -Destination C:\ -Session $session
This example will copy the file C:\test.txt to be C:\test.txt on the computer MYSERVER
.INPUTS
None. This function does not accept pipeline input.
.OUTPUTS
System.IO.FileInfo
#>
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string[]]$Path,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Destination,
[Parameter(Mandatory)]
[System.Management.Automation.Runspaces.PSSession]$Session
)
process
{
foreach ($p in $Path)
{
try
{
if ($p.StartsWith('\\'))
{
Write-Verbose -Message "[$($p)] is a UNC path. Copying locally first"
Copy-Item -Path $p -Destination ([environment]::GetEnvironmentVariable('TEMP', 'Machine'))
$p = "$([environment]::GetEnvironmentVariable('TEMP', 'Machine'))\$($p | Split-Path -Leaf)"
}
if (Test-Path -Path $p -PathType Container)
{
Write-Log -Source $MyInvocation.MyCommand -Message "[$($p)] is a folder. Sending all files"
$files = Get-ChildItem -Force -Path $p -File -Recurse
$sendFileParamColl = @()
foreach ($file in $Files)
{
$sendParams = @{
'Session' = $Session
'Path' = $file.FullName
}
if ($file.DirectoryName -ne $p) ## ARTHIR - It's a subdirectory
{
$subdirpath = $file.DirectoryName.Replace("$p\", '')
$sendParams.Destination = "$Destination\$subDirPath"
}
else
{
$sendParams.Destination = $Destination
}
$sendFileParamColl += $sendParams
}
foreach ($paramBlock in $sendFileParamColl)
{
Send-File @paramBlock
}
}
else
{
Write-Verbose -Message "Starting WinRM copy of [$($p)] to [$($Destination)]"
# ARTHIR - Get the source file, and then get its contents
$sourceBytes = [System.IO.File]::ReadAllBytes($p);
$streamChunks = @();
# ARTHIR - Now break it into chunks to stream.
$streamSize = 1MB;
for ($position = 0; $position -lt $sourceBytes.Length; $position += $streamSize)