-
Notifications
You must be signed in to change notification settings - Fork 266
/
kansa.ps1
2017 lines (1856 loc) · 89.5 KB
/
kansa.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
<#
.SYNOPSIS
Kansa is a Powershell based incident response framework for Windows
environments.
"Improvements" have been made by adding two additional output formats:
GL and SPLUNK. Both allow the user to route data output to a GrayLog
or Splunk instance respectively, while also writing the output to JSON
files in a manner similar to the default JSON output format.
.CONFIGURATION
In order to use the GL and SPLUNK output formats, the user needs to configure
the appropriate OutputFormat parameter for their specific receiving system's
configuration - this is done in logging.conf, which must reside in the same
directory as kansa.ps1. If you want to move logging.conf, edit the appropriate
line(s) in the Get-LoggingConf function (somewhere around line 445).
For GrayLog output, the facility parameter in the message body should be
configured by the user... Unless the user is fine with the pre-defined facility.
.DESCRIPTION
Kansa is a modular, PowerShell based incident response framework for
Windows environments that have Windows Remote Management enabled.
Kansa 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,
Kansa 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-PrefetchListing.ps1 module data will be written
to Output_timestamp\PrefetchListing\Hostname-PrefetchListing.txt.
All modules should return Powershell objects. Kansa converts those
objects into one of several file formats, including csv, json, tsv and
xml. The default output format is csv. Kansa's user may specify
alternate output formats at the command line via the -OutputFormat
parameter argument.
Kansa.ps1 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. These are 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, Kansa 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 Kansa 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-AutorunscDeep.ps1 collector has a dependency on
Sysinternals Autorunsc.exe. The Get-AutorunscDeep.ps1 collector
contains a special line called a "directive" that instructs Kansa.ps1
to copy the Autorunsc.exe binary to remote systems when called with the
-Pushbin flag. The user executing Kansa is responsible for downloading
any required binaries, they are not distributed with Kansa.
Kansa does not ship with third-party binaries. Conventionally,
third-party binaries are placed in the .\Modules\bin\ path. The BINDEP
directive should reference the binary via its path relative to
Kansa.ps1.
Directives should be placed in module's .SYNOPSIS sections under .NOTES
* Directives must appear on a line by themselves
* Directives must start the line
# Directives must be in all capital letters
For example, the directive for Get-AutorunscDeep.ps1 as of this writing is
BINDEP .\Modules\bin\Autorunsc.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 Kansa 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. Kansa 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. Kansa 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. Kansa 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 Kansa 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 Kansa'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 was 10, but has been
changed to 20.
.PARAMETER AutoParse
Although all modules produce standard output (csv, tsv, json), some
output requires additional processing to be truly useful. Examples of
this are autoruns and gpresult output. Added by Joseph Kjar
.PARAMETER ElkAlert
This instructs Kansa to forward module results to an ELK listener.
Logstash was used in testing, but any log ingestion service that can
accept raw json TCP or UDP data fed in on one record per line will work.
The configuration for the listener (IP, port, etc.) is passed into a
separate script file (ElkSender.ps1). NOTE: Standard Elk listeners expect
to receive JSON-formatted strings. Since Kansa is capable of producing
output in various formats, be sure to specify JSON output if you wish to
use this option. ElkAlert requires AutoParse. This variable is an array of
string IP addresses so you can manually load-balance across multiple
logstash servers if you don't have a load-balancer or other stream-processing
service like kafka. The exact IP will be chosen from the supllied array at
random at execution-time.
.PARAMETER JobTimeout
This tells Kansa how long to wait for the current set of jobs to complete
before timing out and proceeding to the next batch (default 10 minutes).
This parameter is greatly affected by changes to ThrottleLimit, as processing
1000 endpoints concurrently versus 200 will result in much longer job times.
If larger jobs are required, consider lengthening the JobTimeout. This
parameter should also be modified if any of the modules in use take a
particularly long time to run (e.g. searching all files on disk).
.PARAMETER ElkPort
This tells the kansa framework what TCP/UDP port to send the results (output)
and errors for each kansa execution. Results will still remain cached
locally on the Kansa server if log collection fails. This variable is an
array of integers so you can manually load-balance across multiple logstash
ingest ports if you don't have a load-balancer or other stream-processing
service like kafka. The exact port will be chosen from the supllied array at
random at execution-time.
.PARAMETER FireForget
Enabling this flag instructs the kansa framwork to enable the Fire&Forget mode
of operation. Traditional Kansa jobs connect to the target execute the module
scripts and remain connected for the duration of execution to collect the
results and bring them back to the kansa "server" where kansa was invoked. In
Fire&Forget mode, the Kansa framework connects to each target only long-
enough to spawn an orgphaned child process that contains the complete module
code and then disconnects moving on to the next target. This improves
deployment speed and allows for much longer-running modules to be executed,
but since the server doesn't stick around to get the results each self-
contained module needs to be "patched" with additional functionality to send
results to a central logging repository upon completion. This extra
functionality is contained in the FFwrapper module. On invocation, with this
flag set, Kansa will wrap the module with the FFwrapper code and pass
parameters specified by FFargs through to the new merged module placing the
resulting dynamically frankensteined module in the FFStagePath folder. Using
the FireForget flag will not work for ANY kansa module at this time since the
output of each module must consist of an array of hashtables. Use the
FFTemplate.ps1 as a model to follow (or any of the modules in the FireForget
module folder.
.PARAMETER FFwrapper
Specifies the path to the Fire&Forget wrapper ps1 file that will be wrapped
around are Fire&Forget standalone kansa module at launch-time to make it
self-contained. It will add functionality to gather results and send them
to a central logging repository such as ELK. The module code will be placed
inside the wrapper where the #<InsertFireForgetModuleContentsHERE> tag is
located
.PARAMETER FFArgs
This is a hashtable of module-specific parameters that can be passed-thru
to a Fire&Forget Module. These variables will replace the tag
#<InsertFireForgetArgumentsHERE> dynamically at launch time. The key names
will be the name of the variables and the values will be assigned
accordingly. Valid FFarg hashtable types include integers, strings, arrays
(of other valid FFarg types), and even other hashtables. The function
recursiveFFArgParser will recursively parse the FFarg variable and write
a variable assignment block at the aforementioned tag at execution time.
Some default values are specified to ensure low-risk defaults for safety
mechanisms in the default FFwrapper.
.PARAMETER FFStagePath
This value specifies the path where Fire&Forget modules that have been
dynamically spliced together with the FFwrapper and FFargs at runtime.
Upon successful merging of module codebases the finished product that will
be pushed to the target and spawned as an orphaned child process is
written to the FFStagePath folder. It is left there even after Kansa
completes its deployment and exits. This allows analysts/developers to
inspect the resulting code for troubleshooting or to review for accuracy
prior to launch. At next execution if the merged module still exists it
will be overwritten.
.PARAMETER SafeWord
In some environments Antivirus or Endpoint security products (EDR/EPP)
may do deep inspection of powershell scripts for malware analysis. The
modules contained in the kansa project contain code snippets that could
trigger these detection/prevention mechanisms and neutering a kansa job
meant to conduct threat-hunting or incident response. This is especially
true of Fire&Forget modules which use many shady-looking techniques to
compress/encode large powershell scripts into small blocks that can be
passed via WMi API calls to spawn an orphaned child process running with
elevated privileges. In order to ensure success, an org may choose to
whitelist processes that use a safeword. For security, this safeword can
be specified dynamically at each kansa invocation so that it lasts only
for the duration of that kansa job and does not represent a permanent
security blindspot that attackers may discover and attempt to exploit.
Analysts may opt to integrate API calls with their AV/EDR/EPP products
to request/submit a temporary safeword or suppression.
.INPUTS
None
You cannot pipe objects to this cmdlet
.OUTPUTS
Various and sundry.
.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, Kansa can be used
to make changes to remote hosts. As a result, it can be used to
facilitate remediation.
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, Kansa.ps1 will
query Acitve Directory for a complete list of hosts and will attempt to
target all of them.
.EXAMPLE
Kansa.ps1
In the above example the user has specified no arguments, which will
cause Kansa 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.
.EXAMPLE
Kansa.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
Kansa.ps1 -ModulePath ".\Modules\Disk\Get-File.ps1 C:\Windows\WindowsUpdate.log" -Target HHWWSQL01
In this example -ModulePath refers to a specific module that takes a
positional parameter (only positional parameters are supported) and the
script is being run against a single target.
.EXAMPLE
Kansa.ps1 -TargetList hostlist -Analysis
Runs collection according to the configuration in Modules\Modules.conf.
Following collection, runs analysis scripts per Analysis\Analysis.conf.
.EXAMPLE
Kansa.ps1 -ListModules
Returns a list of all the modules found under the default modules path.
.EXAMPLE
Kansa.ps1 -ListAnalysis
Returns a list of all analysis scripts found under the Analysis path.
.EXAMPLE
Kansa.ps1 -TargetList hostlist -Pushbin -Rmbin -AutoParse -ElkAlert -Verbose
Runs all modules in Modules.conf against the hosts specified in hostlist. Runs
the autoparse function to clean up output, and sends the formatted JSON output
to an Elk listener.
.EXAMPLE
.\kansa.ps1 -ModulePath .\Modules\FireForget\Get-AccessibilityHijackFF.ps1
-Credential $cred -TargetList .\targets.txt -AutoParse -ElkAlert @("192.168.1.31")
-ElkPort @(1337) -FireForget -FFwrapper .\Modules\FireForget\FFwrapper.ps1
-FFStagePath .\Modules\FFStaging
-FFArgs @{delayedStart = $false; killSwitch = $true; killDelay = 3600; CPUpriority = "Normal"}
-SafeWord "NotTheDroidsURLooking4"
Runs kansa module Get-AccessibilityHijackFF.ps1 in Fire&Forget mode against all systems
listed in the .\targets.txt (one target per line) running using the elevated permissions
specified in the PSCredential $cred object. Kansa will automatically parse the results
and send them to the specified Elk cluster specified by the IP 192.168.1.31 and port 1337.
The module will be wrapped with the FFwrapper.ps1 with module-specific FFargs being transcribed
into the new code and the resulting script will be placed in the .\Modules\FFStaging folder.
Many of these arguments are the default values and do not need to be explicitly specified
they are used here for illustrative purposes only.
#>
[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","GL","SPLUNK")]
[String]$OutputFormat="CSV",
[Parameter(Mandatory=$False,Position=6)]
[Switch]$Pushbin,
[Parameter(Mandatory=$False,Position=7)]
[Switch]$Rmbin,
[Parameter(Mandatory=$False,Position=8)]
[Int]$ThrottleLimit=200,
[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=20,
[Parameter(Mandatory=$false,Position=20)]
[Switch]$AutoParse,
[Parameter(Mandatory=$false,Position=21)]
[String[]]$ElkAlert=@(),
[Parameter(Mandatory=$false,Position=22)]
[int32]$JobTimeout=900,
[Parameter(Mandatory=$false,Position=23)]
[uint16[]]$ElkPort=@(1337),
[Parameter(Mandatory=$false,Position=24)]
[Switch]$FireForget,
[Parameter(Mandatory=$false,Position=25)]
[String]$FFwrapper="Modules\FireForget\FFwrapper.ps1",
[Parameter(Mandatory=$false,Position=26)]
[hashtable]$FFArgs=@{delayedStart = $true; maxDelay = 3600; killSwitch = $true; killDelay = 1800; VDIcheck = $false; CPUpriority = "Idle" },
[Parameter(Mandatory=$false,Position=27)]
[String]$FFStagePath="Modules\FFStaging\",
[Parameter(Mandatory=$false,Position=28)]
[String]$SafeWord="safeword"
)
# First check to see if Kansa is already in use on the current #
# machine. If it is, alert the user and exit. #
if (Test-Path $PSScriptRoot\IN_USE_BY_*){
$BusyUser = Get-ChildItem $PSScriptRoot\IN_USE_BY_* | select Name
$BusyUser = $BusyUser.Name.Split("_")[-1]
Write-Verbose "$BusyUser is using Kansa on this machine, exiting..."
Exit
}
else {
$BusyFileName = "IN_USE_BY_$env:USERNAME"
New-Item -Path $PSScriptRoot -Name $BusyFileName -ItemType "file" | Out-Null
}
# Opening with a Try so the Finally block at the bottom will always call
# the Exit-Script function and clean up things as needed.
Try {
# Since Kansa provides so much useful information through Write-Vebose, we
# want it to run with that flag enabled by default. This behavior can be
# overridden by passing the -Quiet flag. This is scoped only to this context,
# so we don't need to reset it when we're done.
if(!$Quiet) {
$VerbosePreference = "Continue"
}
function FuncTemplate {
<#
.SYNOPSIS
Default function template, copy when making new function
#>
Param(
[Parameter(Mandatory=$False,Position=0)]
[String]$ParamTemplate=$Null
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
# Non-terminating errors can be checked via
if ($Error) {
# Write the $Error to the $Errorlog
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}$Recpt | ConvertTo-Json -Depth $JSONDepth -Compress | Set-Content -Encoding $Encoding $Outfile
Try {
<#
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)"
}
<# End FuncTemplate #>
function Exit-Script {
<#
.SYNOPSIS
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 Kansa.ps1 has crashed." | Add-Content -Encoding $Encoding $ErrorLog
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
if (Test-Path($ErrorLog)) {
Write-Output "Script completed with warnings or errors. See ${ErrorLog} for details."
}
if (!(Get-ChildItem -Force $OutputPath)) {
# $OutputPath is empty, nuke it
"Output path was created, but Kansa finished with no hits, no runs and no errors. Nuking 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()
# ToDo: There should probably be some error handling in this function.
Write-Debug ('$ModulePath argument is {0}.' -f $ModulePath)
# 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 = @()
# Need to maintain the order for "order of volatility"
$ModuleHash = New-Object System.Collections.Specialized.OrderedDictionary
if (Test-Path($ModuleScript)) {
if (!(ls $ModuleScript | Select-Object -ExpandProperty PSIsContainer)) {
# User may have provided full path to a .ps1 module, which is how you run a single module explicitly
$ModuleHash.Add((ls $ModuleScript), $ModuleArgs)
$Module = ls $ModuleScript | Select-Object -ExpandProperty BaseName
Write-Verbose "Running module: `n$Module $ModuleArgs"
Return $ModuleHash
}
}
$ModConf = $ModulePath + "\" + "Modules.conf"
if (Test-Path($Modconf)) {
Write-Verbose "Found ${ModulePath}\Modules.conf."
# ignore blank and commented lines, trim misc. white space
Get-Content $ModulePath\Modules.conf | Foreach-Object { $_.Trim() } | ? { $_ -gt 0 -and (!($_.StartsWith("#"))) } | Foreach-Object { $Module = $_
# 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 ${ModulePath}\Modules.conf: $ModuleScript. Skipping." | Add-Content -Encoding $Encoding $ErrorLog
} else {
# module found add it and its arguments to the $ModuleHash
$ModuleHash.Add((ls $ModPath), $Moduleargs)
}
}
} else {
# 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 Get-LoggingConf {
<#
.SYNOPSIS
If $OutputFormat is either GL or Splunk, load the proper configuration so that
the output reaches the proper destination.
#>
Param(
[Parameter(Mandatory=$True,Position=0)]
[String]$OutputFormat,
[Parameter(Mandatory=$False,Position=1)]
[String]$LoggingConf = ".\logging.conf"
)
Write-Debug "Results will be sent to $($OutputFormat)"
$Error.Clear()
# ToDo: There should probably be some error handling in this function.
if (Test-Path($LoggingConf)) {
Write-Verbose "Found logging.conf"
# We should only grab lines that correspond with our desired destination, otherwise, data will just fly off in to the ether
# and you'll be stuck analyzing JSON files.
if ($OutputFormat -eq "splunk") {
Get-Content $LoggingConf | Foreach-Object { $_.Trim() } | ? {$_.StartsWith('spl') -and (!($_.StartsWith("#")))}
}
elseif ($OutputFormat -eq "gl") {
Get-Content $LoggingConf | Foreach-Object { $_.Trim() } | ? {$_.StartsWith('gl') -and (!($_.StartsWith("#")))}
}
}
}
function Load-AD {
# 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 | ? { $_.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 {
# 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) {
# 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) {
# 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 {
# 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
}
# Iterate through targets, cleaning up AD Replication errors
# In some AD environments, when there are duplicate object names, AD will add the objectGUID to the Name
# displayed in the format of "hostname\0ACNF:ObjectGUID". If you expand the property Name, you get 2 lines
# returned, the hostname, and then CNF:ObjectGUID. This code will look for hosts with more than one line and return
# 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.count) targets found`n"
if($Targets.GetType().Name -match "String"){
$tmp = New-Object System.Collections.ArrayList
if($Targets.count -eq 1){ $Targets = @($Targets)}
[void]$tmp.Add($Targets)
return [System.Collections.ArrayList]$tmp
}else{
return $Targets
}
} else {
Write-Verbose "Get-Targets function found no targets. Checking for errors."
}
if ($Error) { # 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.
Collector module output are named after the module and any
arguments.
#>
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 Kansa that a module depends on some
binary and what the name of the binary is. If Kansa 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 Kansa 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.
3) Added support for an OUTPUT directive, allowing a module to
override the OutputFormat specified when calling KANSA
#>
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|OUTPUT" | Foreach-Object { $Directive = $_
if ( $Directive -match "(^BINDEP|^# BINDEP) (.*)" ) {
$DirectiveHash.Add("BINDEP", $($matches[2]))
}
if ( $Directive -match "(^DATADIR|^# DATADIR) (.*)" ) {
$DirectiveHash.Add("DATADIR", $($matches[2]))
}
if ( $Directive -match "(^OUTPUT|^# OUTPUT) (.*)" ) {
$DirectiveHash.Add("OUTPUT", $($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 Kansa 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,
[Parameter(Mandatory=$False,Position=4)]
[Array]$LogConf
)
Write-Debug "Entering $($MyInvocation.MyCommand)"
$Error.Clear()
$myModuleName = $Modules.Keys | Select -First 1 | Select-Object -ExpandProperty BaseName
# Create our sessions with targets
if ($Credential) {
if ($UseSSL) {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -UseSSL -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile -OpenTimeout 1500 -OperationTimeout 5000 -CancelTimeout 5000) -Credential $Credential
} else {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile -OpenTimeout 1500 -OperationTimeout 5000 -CancelTimeout 5000) -Credential $Credential
}
} else {
if ($UseSSL) {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -UseSSL -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile -OpenTimeout 1500 -OperationTimeout 5000 -CancelTimeout 5000)
} else {
$PSSessions = New-PSSession -ComputerName $Targets -Port $Port -Authentication $Authentication -SessionOption (New-PSSessionOption -NoMachineProfile -OpenTimeout 1500 -OperationTimeout 5000 -CancelTimeout 5000)
}
}
# Check for and log errors
if ($Error) {
$Error | Add-Content -Encoding $Encoding $ErrorLog
$Error.Clear()
}
if ($PSSessions) {
$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 = "" }
# 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) {
#$PSScriptRoot is parsed as literal string into $bindep from the module script rather than the directory path, below is a fix to replace the literal '$PSScriptRoot' with the directory path required
if($bindep -match "PSScriptRoot"){
$bindep = $bindep.Trim("$")
$bindep = $bindep.Replace("PSScriptRoot", $PSScriptRoot)
}
# Send-File only supports a single destination at a time, so we have to loop this.
# Fix for Issue 146, originally suggested by sc2pyro.
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
}
}
}
}
# set up external logging parameters to be used below
if ($LogConf) {
$LogConf | foreach {
$logAssign = $_ -split '='
New-Variable -Name $logAssign[0] -Value $logAssign[1] -Force
}
}
# 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.`n"
} 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.`n"
}
# Wait-Job does return data to stdout, add $suppress = to start of next line, if needed
# Each set of jobs has a 10 minute timeout to prevent endless stalling
$Job | Wait-Job -Timeout $JobTimeout
if ($Job.State -contains "Running"){ Write-Verbose "Job timeout exceeded, continuing...`n" }
# set up our output location
$GetlessMod = $($ModuleName -replace "Get-")
# Long paths prevent output from being written, so we truncate $ArgFileName to accomodate
# We're estimating the output path because at this point, we don't know what the hostname
# is and it is part of the path. Hostnames are 15 characters max, so we assume worst case.
# Per http://msdn.microsoft.com/en-us/library/aa365247.aspx#maxpath, maximum
# path length should be 260 characters.
Set-Variable -Name MAXPATH -Value 260
Set-Variable -Name EXTLENGTH -Value 4
Set-Variable -Name SEPARATOR -Value 1
Set-Variable -Name CMPNAMELENGTH -Value 15
# Esitmate path length without arguments
$EstOutPathLength = $OutputPath.Length + $GetlessMod.Length + $SEPARATOR + $CMPNAMELENGTH + $SEPARATOR + $GetlessMod.Length + $EXTLENGTH
# Clean up $ArgFileName
$ArgFileName = $ArgFileName -replace "'","-" -replace "--","-"
if ($EstOutPathLength + $ArgFileName.Length + $ArgFileName.Length -gt $MAXPATH)
{
$MaxArgLength = [int]($MAXPATH - $EstOutPathLength) / 2 # Divide by two because args are in folder and file name
$OrigArgFileName = $ArgFileName
$ArgFileName = $ArgFileName.Substring(0, $MaxArgLength)
"WARNING: ${GetlessMod}'s output path contains arguments that were passed to it. Those arguments were truncated from $OrigArgFileName to $ArgFileName to accommodate Window's MAXPATH limitations of 260 characters."
}
# Set Module output directory based on module name and arg file name (if present)
$ModuleOutputDir = ($GetlessMod + $ArgFileName)
# Add module output directory to collection of all module output dirs, avoid duplicates
if ($ModulePathColl -notcontains $ModuleOutputDir){
[void] $ModulePathColl.add($ModuleOutputDir)
}
# Create module output directory on disk if it doesn't exist
$pathExists = Test-Path $OutputPath\$ModuleOutputDir
if ($pathExists -eq $False){
[void] (New-Item -Path $OutputPath -name $ModuleOutputDir -ItemType Directory)
}
$Job.ChildJobs | Foreach-Object { $ChildJob = $_
$Recpt = Receive-Job $ChildJob
# Remove excess properties that we don't need in the module output
foreach ($i in $Recpt){
if(-not ([string]::IsNullOrEmpty($i))){
$i.PSObject.Properties.Remove("PSShowComputerName")
$i.PSObject.Properties.Remove("RunspaceId")
}
}
# 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()
}
# Now that we know our hostname, let's double check our path length, if it's too long, we'll write an error
# 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
Continue
}
try{
# save the data
switch -Wildcard ($OutputFormat) {
"*csv" {
$Outfile = $Outfile + ".csv"
$Recpt | Export-Csv -NoTypeInformation -Encoding $Encoding $Outfile
}
"*json" {
$Outfile = $Outfile + ".json"
$Recpt | ConvertTo-Json -Depth $JSONDepth -Compress | Set-Content -Encoding $Encoding $Outfile
}
"*tsv" {
$Outfile = $Outfile + ".tsv"
# 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
}
"*gl" {
# We're going to handle output to a GrayLog instance in two ways... we're going to attempt to send it to the GrayLog server
# and we're also going to write the output to JSON files.
$Outfile = $Outfile + ".json"
$Recpt | ConvertTo-Json -Depth $JSONDepth | Set-Content -Encoding $Encoding $Outfile
# The next line tells Kansa where to send the data; hopefully you've entered the proper information in logging.conf
$glurl = "http://${glServerName}:$glServerPort/gelf"
# The next few lines define how we're going to handle information output from the various Kansa modules, then ship it off to GL
ForEach ($item in $Recpt){
$body = @{
message = $item
facility = "main"
host = $Target.Split(":")[0]
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri $glurl -Body $body
}
} # end GL definition
"*splunk" {
# We're going to handle output to a Splunk instance in two ways... we're going to attempt to send it to a Splunk instance
# and we're also going to write the output to JSON files. Much like the above GrayLog example.
$Outfile = $Outfile + ".json"
$Recpt | ConvertTo-Json -Depth $JSONDepth | Set-Content -Encoding $Encoding $Outfile
# Parameters for the next line are configured in logging.conf... don't say I didn't warn you
$url = "http://${splServerName}:$splServerPort/services/collector/event"
$header = @{Authorization = "Splunk $splHECToken"}
# The next few lines define how we're going to handle information output from the various Kansa modules and then ship it off to Splunk
ForEach ($item in $Recpt){
$body = @{
event = $item
source = "$GetlessMod"
sourcetype = "_json"
host = $ChildJob.Location
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri $url -Headers $header -Body $body
}
} # end Splunk definition
<# Following output formats are no longer supported in Kansa
"*bin" {
$Outfile = $Outfile + ".bin"
$Recpt | Set-Content -Encoding Byte $Outfile
}
"*zip" {
# Compression should be done in the collector
# Default collector template has a function
# 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
}
}
}catch{("Caught:{0}" -f $_)}
}
# Release memory reserved for Job data since we don't need it any more.
# Fixes bug number 73.
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}
}
}
}
}
Remove-PSSession $PSSessions