-
Notifications
You must be signed in to change notification settings - Fork 3
/
strippy.ps1
1692 lines (1451 loc) · 77 KB
/
strippy.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
#Requires -Version 5
<#
.SYNOPSIS
Tool for bulk sanitising text-based files
.DESCRIPTION
Use this tool to automate the replacement of sensitive data with generic strings in text-based files.
This tool requires configuration to describe where sensitive data will be present, or explicit examples of known sensitive data. Sensitive data is 'described' to Strippy using Regular Expressions (regex) that match log patterns known to hold contain. These regex rules are run across all input files, the matching strings brought together, labelled and then replaced with their new generic label across all input files. This method allows for 'known unknown' capture of sensitive data and replacement of data in 'unknown known' but known scenario.
Step by Step usage process:
1) Determine file(s) that require sanitisation
2) Group the input files together - same folder will do
3a) If IP Addresses are the only sensitive data no configuration strippy.conf file is required. Skip to Step 5
3b) If the expected location of sensitive data is known then creation of a new .conf file is required. This can be done quickly with the -makeconfig flag
3c) If all sensitive data is known (eg. specific server names) then this should also be placed in a .conf file, but as a specifically matched string. Creating a basic .conf file can be done with the -makeconfig flag.
4) Populate the strippy.conf file with rules describing where sensitive data will be found in logs
5) Execute strippy.ps1 with the -file argument pointing to the target file/folder/zip
Strippy will look for a strippy.conf file to use in:
a. The file pointed to by -configfile
b. The same folder as the target file/folder // NOT IMPLEMENTED
c. It's own storage directory
6) Strippy will perform the sanitisation and save the output to <input>.sanitised or -AlternateOutputFolder
7) Keylist.txt is created in the same folder as the target file/folder. This file contains all sanitised sensitive data with the generic string replacement. This is for consultation should a reader of the sanitised file discuss 'Address53'
Sanitisation rules added to the .conf file should look like this:
"<regex>"="<alias>" where:
<regex> is a (not escaped) regex query and;
<alias> is the text that will replace the first group inside the regex query.
A complete example is: "hostname=(.*?),"="Hostname"
This example would transform 'hostname=SecretHostname,' into 'hostname=Hostname1,'
Strippy configuration files for specific tools live in the configFiles directory: https://github.com/cavejay/Strippy/tree/master/configFiles
Make use of the tool by reading the examples from: get-help .\strippy.ps1 -examples
If you haven't already then you'll need to change your execution policy to run this tool.
You can do this temporarily by using the following:
powershell [-noexit] -executionpolicy Unrestricted -File .\strippy.ps1 <args>
Or permanently by opening Powershell and running the following:
Set-ExecutionPolicy Unrestricted https://ss64.com/ps/set-executionpolicy.html
.EXAMPLE
C:\PS> .\strippy.ps1 .\logs
This will sanitise only the files directly in .\logs using a default config file.
Output files will be in the .\logs.sanitised folder and the keylist created for this run will be found in the directory you ran the script.
.EXAMPLE
C:\PS> .\strippy.ps1 .\logs\server.1.log
In this case only one file has been specified for sanitisation.
The output in this case would be to .\logs\server.1.sanitised.log file and a keylist file .\KeyList.txt
.EXAMPLE
C:\PS> .\strippy.ps1 ..\otherlogs\servers\oldlog.log -KeyFile .\KeyList.txt
This would process the oldlog.log file like any other file, but will load in the keys already found from a key list file.
This means you can process files at different times but still have their keys matchup. Once done, this usecase will output a new keylist that contains all the keys from KeyList.txt and any new keys found in the oldlog.log file.
.EXAMPLE
C:\PS> .\strippy.ps1 .\logs -Recurse
If you need to sanitise an entire file tree, use the -Recurse flag to iterate through each file in a folder and it's subfolders. Strippy's maximum depth at 2.4.0 is capped at 20 directories.
This will output sanitised files to .\logs.sanitised
.EXAMPLE
C:\PS> D:\strippy\strippy.ps1 "C:\Program Files\Dynatrace\CAS\Server\logs" -configFile 'D:\strippy\batchjob.strippy.conf' -Recurse -Silent -alternateOutputFolder "C:\sanitised-$(get-date -UFormat %s)"
This example shows how you might integrate strippy in an automation scheme. The -Silent flag stops output to stdout, preventing the need for a stdout redirect.
The -alternateOutputFolder flag allows redirection of the sanitised files to a custom folder.
.NOTES
Author: Michael Ball
Version: 2.4.0 - 20200208
Compatability: Powershell 5+
.LINK
https://github.com/cavejay/Strippy
#>
# Todo
# combine AlternateKeyListOutput and keylistfile config settings.
# Dealing with selections of files a la "server.*.log" or similar
# Use PS-InlineProgress or check and then use it if we're in the exe version
# Switch to add strippy to your ps profile for quick running
# Have option for diagnotics file or similar that shows how many times each rule was hit
# Publish to dxs wiki
# Have a blacklist of regexs.
# More intelligent capitalisation resolution.
# Move from jobs to runspaces?
# Switch used to create a single file strippy. ie, edit the script's code with the config rules etc.
<# Maintenance Todo list
- Time global sanitise against running all the rules against each and every line in the files.
- use powershell options for directory and file edits
#>
[CmdletBinding(DefaultParameterSetName = 'input')]
param (
<# Help flags that return help information for -h -help types #>
# Help flags that return help information for -h types
[Parameter(Position = 0, ParameterSetName = "help")][Switch] $h,
[Parameter(ParameterSetName = "help")][Switch] $help,
<# Config things #>
# Creates a barebones strippy.conf file for the user to fill edit
[Parameter(Position = 0, ParameterSetName = "makeconfig", Mandatory = $false)][Switch] $makeConfig,
<# Input Settings #>
# The File or Folder you wish to sanitise
[Parameter(Position = 0, ParameterSetName = "input")][String] $File,
# Specifies a previously generated keylist file to import keys from for this sanitisation
[Parameter(Position = 1, ParameterSetName = "input")][String] $KeyFile,
# Specifies a config file to use rather than the default local file or no file at all
[Parameter(Position = 2, ParameterSetName = "input")][String] $ConfigFile,
# Looks for log files throughout a directory tree rather than only in the first level
[Parameter(ParameterSetName = "input")][Switch] $Recurse = $false,
# Destructively sanitises the file. There is no warning for this switch. If you use it, it's happened
[Parameter(ParameterSetName = "input")][Switch] $InPlace = $false,
# Should Strippy handle .zip archives it finds in the file/folders?
[Parameter(ParameterSetName = "input")][switch] $unpackZip,
# Do not include the sanitisation meta data in output
[Parameter(ParameterSetName = "input")][switch] $noHeaderInOutput = $false,
# A shortcut for -AlternateKeylistOutput
[Parameter(ParameterSetName = "input")][String] $ko,
# Specifies an alternate name and path for the keylist file
[Parameter(ParameterSetName = "input")][String] $AlternateKeyListOutput = $ko,
# A shortcut for -AlternateOutputFolder
[Parameter(ParameterSetName = "input")][String] $o,
# Specifies an alternate path or file for the sanitised file
[Parameter(ParameterSetName = "input")][String] $AlternateOutputFolder = $o,
# The tool will run silently, without printing to the terminal and exit with an error if it needed user input
[Parameter(ParameterSetName = "input")][Switch] $Silent = $false,
# How threaded can this process become?
[Parameter(ParameterSetName = "input")][int] $MaxThreads = 5,
<# Logging Settings #>
# Perform logging for this execution
[Switch] $log = $false,
# The specific log file to log to. This is useless unless the log switch is used
[String] $logfile = ".\strippy.log",
# Show absolutely all log messages. This will create much larger logs
[Switch] $showDebug = $false,
# How big can a log file get before it's shuffled
[int] $MaxLogFileSize = 10MB,
# Max number of log files created by the script
[int] $LogHistory = 5
)
$_startTime = get-date
$recurseDepth = @(0, 20)[$script:Recurse -eq $true]
$ignoredFileTypes = 'gif', 'jpeg', 'png', 'jpg', 'exe', 'tif', '7z', 'py' | ForEach-Object { ".$_" }
$zipFilesToIgnore = 'plugins.zip', 'remote_plugins.zip'
## Setup Log functions
function shuffle-logs ($MaxSize, $LogFile = $script:logfile, $MaxFiles = $script:LogHistory) {
if (!(Test-Path $LogFile)) {
return # if the log file doesn't exist then we don't need to do anything
}
elseif ((Get-Item $logfile).Length -le $MaxSize) {
return # the log file is still too small
}
# Get the name of the file
$n = ((Split-Path -Leaf -Resolve $logFile) -split '\.')[-2]
# Find all the files that fit that name
$logfiles = Get-ChildItem (split-path $LogFile) -Filter "$n.*log"
# When moving files make sure nothing else is accessing them. This is a bit of overkill but could be necessary.
if ($mtx.WaitOne(500)) {
# Shuffle the file numbers up
($MaxFiles - 1)..1 | ForEach-Object {
move-item "$n.$_.log" "$n.$($_+1).log" -Force -ErrorAction SilentlyContinue
}
$timestamp = Get-Date -format "yy-MM-dd HH:mm:ss.fff"
$logMessage = ("LOG SHUFFLE " + $timestamp + " Continued in next log file")
$logMessage | Out-File -FilePath $LogFile -Force -Append
move-item $logFile "$n.1.log"
# Start a new file
new-item -ItemType file -Path $LogFile | Out-Null;
[void]$mtx.ReleaseMutex()
}
}
# Create a mutex for the rest of the execution
$mtx = New-Object System.Threading.Mutex($false, "LoggerMutex")
# Enum to show what type of log it should be
Enum LEnum {
Trace
Warning
Debug
Error
Question # Use this to show a prompt for user input
Message # This is the log type that's printed and coloured
}
<#
logfunction. Default params will log to file with date
https://www.sapien.com/blog/2015/01/05/enumerators-in-windows-powershell-5-0/
#>
function log ([String] $Stage, [LEnum] $Type = [LEnum]::Trace, [String] $String, [System.ConsoleColor] $Colour, [String] $Logfile = $script:logfile) {
# Return instantly if this isn't output and we're not logging
if (!$script:log -and @([LEnum]::Message, [LEnum]::Question, [LEnum]::Warning, [LEnum]::Error) -notcontains $type) { return }
# Return instantly if this is a debug message and we're not showing debug
if (!$script:showDebug -and $type -eq [Lenum]::Debug) { return }
shuffle-logs $script:MaxLogFileSize $Logfile
# Deal with the colouring and metadata
switch ($Type) {
"Message" {
$1 = 'I'
$display = $true
$Colour = ($null, $Colour, 'WHITE' -ne $null)[0]
break
}
"Question" {
$1 = 'Q'
$display = $true
$Colour = ($null, $Colour, 'CYAN' -ne $null)[0]
break
}
"Debug" {
$1 = 'D'
break
}
"Error" {
$1 = 'E'
$Colour = ($null, $Colour, 'RED' -ne $null)[0]
$display = $true
$String = "ERROR: $string"
break
}
"Warning" {
$1 = 'W'
$Colour = ($null, $Colour, 'YELLOW' -ne $null)[0]
$display = $true
$String = "Warning: $string"
break
}
Default {
# Trace enums are default.
$1 = 'T'
}
}
# If we need to display the message check that we're not meant to be silent
if ($display -and -not $silent) {
# Error messages require a black background to stand out and mirror powershell's native errors
if ($type -eq [LEnum]::Error) {
write-host $String -foregroundcolor $Colour -BackgroundColor 'Black'
}
else {
write-host $String -foregroundcolor $Colour
}
}
# Check whether we're meant to log to file
if (!$script:log) {
return
}
else {
# assemble log message!
$stageSection = $(0..5 | % { $s = '' } { $s += @(' ', $Stage[$_])[[bool]$Stage[$_]] } { $s })
$timestamp = Get-Date -format "yy-MM-dd HH:mm:ss.fff"
$logMessage = ($1 + " " + $stageSection.toUpper() + " " + $timestamp + " " + $String)
try {
# This try is to deal specifically when we've destroyed the mutex.
if ($mtx.WaitOne()) {
# use Powershell native code. .NET functions don't offer enough improvement here.
$logMessage | Out-File -Filepath $Logfile -Append -Encoding utf8
[void]$mtx.ReleaseMutex()
}
# consider doing something here like:
# if waiting x ms then continue but build a buffer. Check each time the buffer is added to until a max is reached and wait to add that
# Sometimes the mutex might have been destroyed already (like when we're finishing up) so work with what we've got
}
catch [ObjectDisposedException] {
"$logMessage - NoMutex" | Out-File -FilePath $logFile -Append -Encoding utf8
}
}
}
function replace-null ($valIfNull, [Parameter(ValueFromPipeline = $true)]$TestVal) {
return ($null, $TestVal, $valIfNull -ne $null)[0]
}
function show-path ($path) {
$path = if ($path -eq '' -or $null -eq $path) {
"Unset"
}
else {
try {
(Resolve-Path $path -ErrorAction Stop).path
}
catch {
"$path (unresolveable)"
}
}
return $path
}
# Help flag checks
if ($h -or $help) {
log params trace "Strippy was started help flags. Showing the get-help output for the script and exiting"
Get-Help $(join-path $(Get-Location) $MyInvocation.MyCommand.Name) -Detailed
exit 0
}
# Usage todo need to make this usable without the reliance on get-help or powershell in general.
if ( $File -eq "" -and -not $makeConfig ) {
log params trace "Strippy was started with no file. Showing the get-help output for the script and exiting"
Get-Help $(join-path $(Get-Location) $MyInvocation.MyCommand.Name) -Detailed
exit 0
}
log init Trace "`r`n`r`n"
log init Trace "-=H|||||||| Starting Strippy Execution |||||||||H=-"
log init Trace " || Author: michael.ball@dynatrace.com ||"
log init Trace " || Version: 2.4.0 ||"
log params Trace "Strippy was started with the parameters:"
log params Trace "Sanitisation Target: $(show-path $file)" # try to resolve the file here. Show nothing if it fails
log params Trace "Key file: $(@('Unset',(show-path $KeyFile))[$KeyFile -ne ''])"
log params Trace "Config file: $(@('Unset',(show-path $ConfigFile))[$ConfigFile -ne ''])"
log params Trace "Silent Mode: $Silent"
log params Trace "Recursive Searching: $Recurse"
log params Trace "In-place sanitisation: $InPlace"
log params Trace "No sanitisation header: $noHeaderInOutput"
log params Trace "Unpack encountered zip files $unpackZip"
log params Trace "Creating a new Config file: $MakeConfig"
log params Trace "Showing Debug logging: $showDebug"
log params Trace "Alternate Keylist Output file: $(@('Unset',(show-path $AlternateKeyListOutput))[$AlternateKeyListOutput -ne ''])"
log params Trace "Alternate Output folder files: $(@('Unset',(show-path $AlternateOutputFolder))[$AlternateOutputFolder -ne ''])"
log params Trace "Maximum parrallel threads: $MaxThreads"
log params Trace "Logging enabled: $log"
log params Trace "Destination Logfile: $(show-path $logfile)" # try to resolve the file here. Show nothing if it fails
log params Trace "Max log file size: $MaxLogFileSize"
log params Trace "Number of Log files kept: $LogHistory"
# Special Variables: (Not overwritten by config files)
# If this script is self contained then all config is specified in the script itself and config files are not necessary or requested for.
# This cuts down the amount of files necessary to move between computers and makes it easier to give to someone and say "run this"
# $SelfContained = $false # Not really implemented yet.
## Variables: (Over written by any config file and include all the command line variables)
# Priority of inputs: Default -> Configfile -> cmdline input
$Config = @{ }
$Config.IgnoredStrings = @('/0:0:0:0:0:0:0:0', '0.0.0.0', '127.0.0.1', 'name', 'applications', "", "unknown", "null", ".")
$Config.SanitisedFileFirstline = "This file was Sanitised at {0}.{1}=={1}{1}"
$Config.KeyListFirstline = "This keylist was created at {0}.{1}"
$Config.KeyFileName = "KeyList.txt"
log params debug "Default Ignored Strings: `"$($Config.IgnoredStrings -join '", "')`""
log params debug "Default Sanitised file header: $($Config.SanitisedFileFirstLine)"
log params debug "Default Keylist/file header: $($Config.KeyListFirstLine)"
log params debug "Default Keyfile name: $($Config.KeyFileName)"
######################################################################
# Important Pre-script things like usage, setup and commands that change the flow of the tool
# General config
$PWD = get-location
log params debug "Initial running location: $PWD"
$_tp = 992313 # top Progress
log params debug "Special ID for top progress bar: $_tp"
$_env = $script:log, $script:showDebug, $(resolve-path $script:logfile -ErrorAction 'SilentlyContinue').path, $script:MaxLogFileSize, $script:LogHistory
log params debug "Created `$_env variable to pass logging environment to jobs (log, showDebug, logfile, maxLogFileSize): $($_env -join ', ')"
# Flags
# Valid Types are basic,delete and list
# @{type=basic; regex=String; replacement=String}
# @{type=list; regex=String; replacement=String; delimiter=String}
# @{type=delete; regex=String}
$Config.flags = New-Object System.Collections.ArrayList
# Added to every list of flags to cover IPs and UNC's
$defaultFlags = New-Object System.Collections.ArrayList
$defaultFlags.AddRange(@(
@{type = "basic"; regex = "((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))[^\d]"; replacement = 'Address' },
@{type = "basic"; regex = "\\\\([\w\-.]*?)\\"; replacement = "Hostname" }
))
log params debug "Default flags/rules: $defaultFlags"
#Check if we're _just_ creating a default config file
if ( $MakeConfig ) {
log mkconf trace "Script launched with the -MakeConfig switch. Script will attempt to make a new, default config file before exiting."
$confloc = Join-Path $( Get-Location ) 'strippy.conf'
log mkconf trace "We're going to make the config file here: $confloc"
# Apologies if you're trying to read this next string.
$defaultConfig = "; Strippy Config file`r`n;Recurse=true`r`n;InPlace=false`r`n;Silent=false`r`n;MaxThreads=5`r`n`r`n[ Config ]`r`nIgnoredStrings=""/0:0:0:0:0:0:0:0"",""0.0.0.0"",""127.0.0.1"",""name"",""applications"","""",""unknown"",""null"","".""`r`n`r`n; These settings can use braces to include dynamic formatting:`r`n; {0} = Date/Time at processing`r`n; {1} = NewLine`r`n; #notimplemented {2} = Depends on context. Name of specific file being processed where relevant otherwise it`s the name of the Folder/File provided to Strippy `r`nSanitisedFileFirstLine=""This file was Sanitised at {0}.``r``n==``r``n``r``n""`r`nKeyListFirstLine=""This keylist was created at {0}.""`r`n;KeyFileName=""Keylist.txt""`r`n;AlternateOutputFolder="".\sanitisedoutput""`r`n`r`n[ Rules ]`r`n;""Some Regex String here""=""Replacement here""`r`n""((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))[^\d]""=""Address""`r`n""\\\\([\w\-.]*?)\\""=""Hostname""`r`n"
log mkconf trace "We're going to give it this content:`r`n$defaultConfig"
# Check to make sure we're not overwriting someone's config file
if ( Test-Path $( $confloc ) ) {
log mkconf debug "There is already a file at: $confloc. Polling user for action"
log Mkconf question "A config file already exists. Would you like to overwrite it with the default?"
$ans = Read-Host "y/n> (n) "
log Mkconf trace "User answered with: '$ans'"
if ( $ans -ne 'y' ) {
log Mkconf message "Didn't overwrite the current config file. Exiting script"
exit 0
}
else {
log Mkconf message "You overwrote a config file that contained the following. Use this to recreate the file if you stuffed up:`r`n$([IO.file]::ReadAllText($confloc))"
}
}
$defaultConfig | Out-File -Encoding ascii $confloc -ErrorAction Stop
log Mkconf message "Generated config file: $confloc"
exit 0
}
# Check we're dealing with an actual file
if ( -not (Test-Path $File) ) {
log params error "$File does not exist. Exiting script"
exit -1
}
log timing trace "[Start] Loading function definitions"
#########################################################################
# Function definitions
# This is a dupe of the same function in the JobFunctions Scriptblock
function eval-config-string ([string] $str) {
log evlcfs trace "config string |$str| is getting eval'd"
# Check if we actually need to do this?
if ($str -notmatch "\{\d\}") {
log evlcfs trace "Config string did not contain any eval-able components"
return $str
}
# Lets make an array filled with the possible substitions. This is what will need to be updated for future versions
$arrayOfFills = @($(get-date).ToString(), "`r`n")
$matches = [regex]::Matches($str, "\{\d\}")
$out = $str
foreach ($m in $matches.groups.value) {
log evlcfs debug "Replacing $m with $($arrayOfFills[([int][string]$m[1])])"
$out = $out -replace [regex]::Escape($m), $arrayOfFills[([int][string]$m[1])]
}
log evlcfs trace "Eval'd to: $out"
return $out
}
# This is also a dupe of the function in the JobFunctions Scriptblock :( I can't figure out how to join the 2
function Get-PathTail ([string] $d1, [string] $d2) {
if ($d1 -eq $d2) { return split-Path -Leaf $d1 }
#codemagicthing
[String]::Join('', $(if ($d1 -gt $d2) {
$d1[$($d2.length)..$($d1.length - 1)]
}
else {
$d2[$($d1.length)..$($d2.length - 1)]
}
))
}
function output-keylist ($finalKeyList, $listOfSanitisedFiles, [switch]$quicksave) {
log timing trace "[START] Saving Keylist to disk"
$kf = join-path $PWD "KeyList.txt"
# We have Keys?
if ( $finalKeyList.Keys.Count -ne 0) {
# Do we need to put them somewhere else?
if ( $AlternateKeyListOutput ) {
Set-Location $PWD # Should maybe not use PWD here todo
New-Item -Force "$AlternateKeyListOutput" | Out-Null
$kf = $( Get-Item "$AlternateKeyListOutput" ).FullName
}
if (!$quicksave) { log outkey message "`r`nExporting KeyList to $kf" }
$KeyOutfile = (eval-config-string $script:config.KeyListFirstline) + "`r`n" + $( $finalKeyList.GetEnumerator() | Sort-Object -Property name | Out-String )
$KeyOutfile += "List of files using this Key:`n$( $listOfSanitisedFiles | Out-String)"
$KeyOutfile | Out-File -Encoding ascii $kf -Force
}
else {
log outkey Warning "No Keys were found to show or output. There will be no key file"
}
log timing trace "[END] Saving Keylist to disk"
}
# This should be run before the script is closed
function Clean-Up {
PARAM ([Switch] $NoExit = $false)
log timing trace "[START] Script Cleanup"
# output-keylist # This should no longer be needed.
if ($NoExit) { log clnup debug "Cleanup function run with -NoExit arg. Will not exit after running" }
## Cleanup
log clnup Debug "Returning preferences to original state"
$VerbosePreference = $oldVerbosityPref
$DebugPreference = $oldDebugPref
$InformationPreference = $oldInfoPref
log clnup debug "Return shell to original position"
Set-Location $PWD
log clnup trace "Destroying logging Mutex"
$mtx.Dispose()
log timing trace "[END] Script Cleanup"
if (!$NoExit) {
exit 0
}
}
## Process Config file
function proc-config-file ( $cf ) {
log timing trace "[START] Processing of Config File"
$stages = @('Switches', 'Config', 'Rules')
$validLineKey = @('IgnoredStrings', 'SanitisedFileFirstLine', 'KeyListFirstLine', 'KeyFilename', 'AlternateKeyListOutput', 'AlternateOutputFolder')
$stage = 0; $lineNum = 0
$config = @{flags = @() }
$lines = $cf -split "`r?`n"
ForEach ( $line in $lines ) {
$lineNum++
# Do some checks about the line we're on
if ( $line -match "^\s*;" ) {
log prccnf trace "skipped comment: $line"
continue
}
elseif ($line -eq '') {
log prccnf trace "skipped empty line: $linenum"
continue
}
# Check if this is a header
if ( $line -match "^\s*\[ [\w\s]* \].*$" ) {
# is it a valid header structure?
$matches = [regex]::Matches($line, "^\s*\[ ([\w\s]*) \].*$")
if ($matches.groups -and $matches.groups.length -gt 1) { } else {
log prccnf trace "We found the '[]' for a header but something went wrong"
log prccnf error "CONFIG: Error with Header on line $lineNum`: $line"
exit -1
}
$headerVal = $matches.groups[1].value
# bump the stage if we found a valid header
if ( $stages[$stage + 1] -eq $headerVal ) {
log prccnf trace "Moving to $($stages[$stage+1]) due to line $linenum`: $line"
$stage++
}
elseif ( $stages -notcontains $headerVal ) {
log prccnf trace "Tried to move to stage '$headval' at the wrong time on line $linenum`: $line"
log prccnf error "Valid head '$headerval' in the wrong position on line $linenum`: $line"
exit -1
}
else {
log prccnf trace "Tried to move to unknown stage '$headval' on line $linenum`: $line"
log prccnf error "Invalid header '$headerval' on line $linenum`: $line"
exit -1
}
continue # if we're still here move to the next line
}
# Check if this is a valid config line
if ( $line -match "^.*=.*$" ) {
$matches = [regex]::Matches($line, "^(.*?)=(.*)$")
if ( $matches.groups -and $matches.groups.length -ne 3 ) {
log prccnf trace "Invalid config line. not enough values"
log prccnf error "Invalid config line. Incorrect format/grouping on line $linenum`: $line"
exit -1
}
$lineKey = $matches.groups[1].value
$lineValue = $matches.groups[2].value
# If we're not reading rules and we don't recognise the key, show a warning
if ( $stages[$stage] -eq "Config" -and $validLineKey -notcontains $lineKey ) {
log prccnf trace "We did not recognise the key '$lineKey' we won't exit but will generate a warning"
log prccnf warning "Unrecognised config setting. '$lineKey' on line $linenum`: $line"
}
}
else {
# if we didn't match the above then we're broke so quit
log prccnf trace "Could not parse line $linenum as a ini config line. Creating an error"
log prccnf error "Unable to parse config on line $linenum`: $line"
exit -1
}
# Action lines based on stage
switch ( $stages[$stage] ) {
'Switches' {
# \\todo this is super dodge and needs more validation :( please improve
# Use a switch for easy adding if there's more
switch ( $lineKey ) {
'MaxThreads' {
if ($lineValue -match "\d*") {
$config.MaxThreads = [convert]::ToInt32($lineValue, 10)
}
else {
log prccnf trace "MaxThreads value was not a valid numeric form. Will show a warning and continue with default"
log prccnf warning "Maxthreads value was not a valid number. Contining with default value: $script:MaxThreads"
}
}
'Silent' { $Config.Silent = $lineValue -eq "true" }
'Recurse' { $Config.Recurse = $lineValue -eq "true" }
'InPlace' { $Config.InPlace = $lineValue -eq "true" }
Default {
log prccnf warning "Unknown configuration setting '$lineKey' found on line $linenum`: $line"
}
}
}
'Config' {
# Binary Option
if ($lineValue -eq "true" -or $lineValue -eq "false") {
$config[$lineKey] = $lineValue -match "true"
# Array option
}
elseif ( $line -match "^.*=(.*)(,.*)*$" ) {
$config[$lineKey] = ($lineValue[1..($lineValue.length - 2)] -join '') -split "`",\s*`"" -replace '\\"', '"'
# String option
}
elseif ($lineValue[0] -eq '"' -and $lineValue[-1] -eq '"') {
$Config[$lineKey] = $lineValue[1..($lineValue.length - 2)] -join ''
log prccnf trace "Line $linenum stored: Setting: $lineKey, Value: $lineValue"
}
else {
log prccnf warning "Unrecognised config format on line $linenum`: $line. It Does not seem to be a string, bool or array and so will be ignored"
}
}
'Rules' {
# rule w/ delimiter for lists
if ( $line -match '^".+"=".+"\s*,\s*".+"$') {
# re-find the key/value incase there are '=' in the key
$matches = [regex]::Matches($line, '^"(.+?)"="(.+)","(.+)"$')
$lineKey = $matches.groups[1].value
$lineValue = $matches.groups[2].value
$lineKeyDelimiter = $matches.groups[3].value
# Add the rule to the flags array
$config.flags += @{type = "list"; regex = $lineKey; replacement = $lineValue; delimiter = $lineKeyDelimiter }
} # Normal Rules
elseif ( $line -match '^".*"=".*"$' ) {
# re-find the key/value incase there are '=' in the key
$matches = [regex]::Matches($line, '^"(.*?)"="(.*)"$')
$lineKey = $matches.groups[1].value
$lineValue = $matches.groups[2].value
# Add the rule to the flags array
$config.flags += @{type = "basic"; regex = $lineKey; replacement = $lineValue }
} # 'delete' rules -
elseif ($line -match '^".*"=\\delete\s*$') {
$flagtoremoveentirely = $([regex]::Matches($line, '^"(.*?)"=\\delete$')).groups[1].value
$config.flags += @{type = "delete"; regex = $flagtoremoveentirely }
}
else {
log prccnf warning "Invalid Rule found on line $linenum. It doesn't appear to be wrapped with '`"' and will not be processed.
Found as Key: |$lineKey| & Value: |$lineValue|"
}
}
Default {
log prccnf error "Something went wrong on line $($lineNum): $line"
exit -1
}
}
}
log prccnf trace "config is here`n$($config | Out-String)`n`n"
# todo log all keys here. Debugging is difficult if we can't see everything.
# $config.origin = $ConfigFile # store where the config is from
log timing trace "[END] Processing of Config File"
return $config
}
# Process a KeyFile... This doen't really work at the moment. :S
function proc-keyfile ( [string] $kf ) {
log timing trace "[START] Processing KeyFile"
log prckyf warning "Sanitising files based on a pre-made keyfile is not currently supported. Many apologies if this affects a workflow"
$importedKeylist = @{ }
$kfLines = [IO.file]::ReadAllLines($kf)
# Find length of keylist
$startOfFileList = $kfLines.IndexOf("List of files using this Key:") + 1
$endOfKeyList = $startOfFileList - 4
if ( $startOfFileList -eq 0 ) {
log prckyf error "Invalid format for KeyFile ($KeyFile)`nCan't find list of output files"
log timing trace "[END] Processing KeyFile"
exit -1
}
$dataLines = $kfLines[4..$endOfKeyList]
foreach ($d in $dataLines) {
$d = $d -replace '\s+', ' ' -split "\s"
if ( $d.Length -ne 3) {
log prckyf error "Invalid format for KeyFile ($KeyFile)`nKey and Value lines are invalid"
log timing trace "[END] Processing KeyFile"
exit -1
}
log prckyf trace "Found Key: $($d[0]) & Value: $($d[1])"
$k = $d[0]; $v = $d[1]
if ( $k -eq "" -or $v -eq "") {
log prckyf error "Invalid format for KeyFile ($KeyFile)`nKeys and Values cannot be empty"
log timing trace "[END] Processing KeyFile"
exit -1
}
$importedKeylist[$k] = $v
}
foreach ($d in $kfLines[$startOfFileList..$( $kfLines.Length - 2 )]) {
$script:listOfSanitisedFiles += $d;
}
log timing trace "[END] Processing KeyFile"
$importedKeylist = @{ } # see function head for comment about this not super working
return $importedKeylist
}
# Group all the functions that we'll need to run in Jobs as a scriptblock
$JobFunctions = {
# Enum to show what type of log it should be
Enum LEnumJ {
Trace
Warning
Debug
Error
}
# mtx used to share logging file
$mtx = [System.Threading.Mutex]::OpenExisting("LoggerMutex")
function shuffle-logs ($MaxSize, $LogFile = $script:logfile, $MaxFiles = $script:LogHistory) {
if (!(Test-Path $LogFile)) {
return # if the log file doesn't exist then we don't need to do anything
}
elseif ((Get-Item $logfile).Length -le $MaxSize) {
return # the log file is still too small
}
# Get the name of the file
$n = ((Split-Path -Leaf -Resolve $logFile) -split '\.')[-2]
# Find all the files that fit that name
$logfiles = Get-ChildItem (split-path $LogFile) -Filter "$n.*log"
# When moving files make sure nothing else is accessing them. This is a bit of overkill but could be necessary.
if ($mtx.WaitOne(500)) {
# Shuffle the file numbers up
($MaxFiles - 1)..1 | ForEach-Object {
move-item "$n.$_.log" "$n.$($_+1).log" -Force -ErrorAction SilentlyContinue
}
$timestamp = Get-Date -format "yy-MM-dd HH:mm:ss.fff"
$logMessage = ("LOG SHUFFLE " + $timestamp + " Continued in next log file")
$logMessage | Out-File -FilePath $LogFile -Force -Append
move-item $logFile "$n.1.log"
# Start a new file
new-item -ItemType file -Path $LogFile | Out-Null;
[void]$mtx.ReleaseMutex()
}
}
# Copy of $Script:Log function
function log {
[CmdletBinding()]
PARAM (
[Parameter (Mandatory)][String] $Stage,
[Parameter (Mandatory)][LEnumJ] $Type = [LEnumJ]::Trace,
[Parameter (Mandatory)][String] $String,
[System.ConsoleColor] $Colour,
[String] $Logfile = $script:logfile
)
# Return instantly if we're not logging
if (!$script:log) { return }
# Return instantly if this is a debug message and we're not showing debug
if ($type -eq [LenumJ]::Debug -and !$script:showDebug) { return }
shuffle-logs $script:MaxLogFileSize $Logfile
# Deal with the colour
switch ($Type) {
"Debug" {
$1 = 'D'
break
}
"Error" {
$1 = 'E'
$Colour = ($null, $Colour, 'RED' -ne $null)[0]
$String = "ERROR: $string"
break
}
"Warning" {
$1 = 'W'
$Colour = ($null, $Colour, 'YELLOW' -ne $null)[0]
$String = "Warning: $string"
break
}
Default {
# Trace enums are default.
$1 = 'T'
}
}
$stageSection = $(0..5 | % { $s = '' } { $s += @(' ', $Stage[$_])[[bool]$Stage[$_]] } { $s })
$timestamp = Get-Date -format "yy-MM-dd HH:mm:ss.fff"
$logMessage = ($1 + " " + $stageSection.toUpper() + " " + $timestamp + " [JOB_$($script:JobId)] " + $String)
if ($mtx.WaitOne()) {
$logMessage | Out-File -Filepath $logfile -Append -Encoding utf8
[void]$mtx.ReleaseMutex()
}
# consider doing something here like:
# if waiting x ms then continue but build a buffer. Check each time the buffer is added to until a max is reached and wait to add that
}
function log-job-start () {
log jobenv trace "Job '$Script:JobName' started with Id: $Script:JobId"
log jobenv trace "Logging enabled: $($script:log)"
log jobenv trace "Showing Debug messages: $($script:showDebug)"
log jobenv trace "Logfile: $($script:logfile)"
log jobenv trace "Max log file size: $($script:MaxLogFileSize)"
log jobenv trace "Number of Historical Logs:$($script:LogHistory)"
}
function eval-config-string ([string] $str) {
log evlcfs trace "config string |$str| is getting eval'd"
# Check if we actually need to do this?
if ($str -notmatch "\{\d\}") {
log evlcfs trace "Config string did not contain any eval-able components"
return $str
}
# Lets make an array filled with the possible substitions. This is what will need to be updated for future versions
$arrayOfFills = @($(get-date).ToString(), "`r`n")
$matches = [regex]::Matches($str, "\{\d\}")
$out = $str
foreach ($m in $matches.groups.value) {
log evlcfs debug "Replacing $m with $($arrayOfFills[([int][string]$m[1])])"
$out = $out -replace [regex]::Escape($m), $arrayOfFills[([int][string]$m[1])]
}
log evlcfs trace "Eval'd to: $out"
return $out
}
function Get-PathTail ([string] $d1, [string] $d2) {
if ($d1 -eq $d2) { return split-Path -Leaf $d1 }
#codemagicthing
[String]::Join('', $($d2[$($d1.length)..$($d2.length - 1)], $d1[$($d2.length)..$($d1.length - 1)])[$d1 -gt $d2])
}
function Save-File ( [string] $file, [string] $content, [string] $rootFolder, [string] $OutputFolder, [bool] $inPlace ) {
log timing trace "[START] Saving sanitised file to disk"
$filenameOUT = ''
if ( -not $InPlace ) {
# Create output file's name
$name = Split-Path $file -Leaf -Resolve
$filenameParts = $name -split '\.'
$sanitisedName = $filenameParts[0..$( $filenameParts.Length - 2 )] -join '.'
$sanitisedName += '.sanitised.' + $filenameParts[ $( $filenameParts.Length - 1 ) ]
if ($rootFolder) {
log svfile trace "Sanitising a folder, foldername is $rootFolder"
$locality = Get-PathTail $(Split-Path $file) $rootFolder
log svfile trace "File is $locality from the root folder"
$filenameOUT = Join-Path $OutputFolder $locality
$filenameOUT = Join-Path $filenameOUT $sanitisedName
}
else {
$filenameOUT = Join-Path $OutputFolder $sanitisedName
}
}
else {
log svfile trace "Overwriting original file at $file"
$filenameOUT = $file
}
# Create the file if it doesn't already exist
if (!(test-path $filenameOUT)) {
New-Item -Force $filenameOUT | Out-Null
}
$content | Out-File -force -Encoding ascii $filenameOUT
log svfile trace "Written out to $filenameOUT"
log timing trace "[END] Saving sanitised file to disk"
# Return name of sanitised file for use by the keylist
return "$( $(Get-Date).toString() ) - $filenameOUT"
}
## Sanitises a file and stores sanitised data in a key
function Sanitise ( [string] $SanitisedFileFirstLine, $finalKeyList, [string] $content, [string] $filename) {
log Snitis trace "Sanitising file: $filename"
# Process file for items found using tokens in descending order of length.
# This will prevent smaller things ruining the text that longer keys would have replaced and leaving half sanitised tokens
$count = 0
foreach ( $key in $( $finalKeyList.GetEnumerator() | Sort-Object { $_.Value.Length } -Descending )) {
log Snitis debug " Substituting $($key.value) -> $($key.key)"
Write-Progress -Activity "Sanitising $filename" -Status "Removing $($key.value)" -Completed -PercentComplete (($count++ / $finalKeyList.count) * 100)
# Do multiple replaces with different types of the string to catch weird/annoying cases
$content = $content.Replace($key.value, $key.key)
$content = $content.Replace($key.value.toString().toUpper(), $key.key)
$content = $content.Replace($key.value.toString().toLower(), $key.key)
}
Write-Progress -Activity "Sanitising $filename" -Completed -PercentComplete 100
# Add first line to show sanitation //todo this doesn't really work :/
$header = eval-config-string $SanitisedFileFirstLine
$content = $header + $content
return $content
}
## Build the key table for all the files
function Find-Keys ( [string] $fp, $flags, [System.Collections.Generic.HashSet[String]]$IgnoredStringsSet) {
log timing trace "[START] Finding Keys from $fp"
# dictionary to populate with <key><kind> values
$Keys = New-Object 'System.Collections.Generic.Dictionary[String,String]'
# Remove entire lines that we don't want.
if ($flags | Where-Object -Property type -EQ -Value delete) {
$totalRemovalFlags = ($flags | Where-Object -Property type -EQ -Value delete).regex -join '|'
log fndkys trace "Filtering out lines that match ($totalRemovalFlags)"
$f = [IO.file]::ReadAllLines( $fp ) -notmatch $totalRemovalFlags -join "`r`n"
}
else {
$f = [IO.file]::ReadAllLines( $fp ) -join "`r`n"
}
# Process file for tokens
$count = 1
foreach ( $token in ($flags | Where-Object -Property type -ne -Value delete) ) {
Write-Progress -Activity "Scouting $fp" -Status "$($token.regex)" -Completed -PercentComplete (($count++ / $flags.count) * 100)
$pattern = $token.regex
$kind = $token.replacement
log fndkys trace "Using '$pattern' to find matches"
$matches = [regex]::matches($f, $pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
log fndkys trace "Finished using '$pattern' to find matches"
# Collect all the match Groups that work
$matchGroups = $matches | where-object {$_.groups.length -gt 1} | Foreach-object { $_.groups[1].value }
# If we're looking for a list, split the list into it's elements and continue logic with those individual parts
if ($token.type -eq "list") {
$_matchGroups = $matchesGroups # preserve original list of matches
$matchGroups = $_matchesGroups | ForEach-Object {
$_ -split $token.delimiter
}
}
# Grab the value for each match, if it doesn't have a key make one
foreach ( $mval in $matchGroups ) {
log fndkys debug "Matched: $mval"
# remove any weird extra new-lines
$mval = $mval -replace "[`r|`n]",""
# Do we have a key already?
if ( $Keys.containsKey( $mval ) ) {
log fndkys debug "Recognised as: $($keys[$mval]) - $mval"
} # Check the match against the list of ignored strings
elseif ( $IgnoredStringsSet.contains( $mval ) ) {
log fndkys trace "Found ignored string: $mval"
} # Save the key and it's associated kind
else {
$Keys.add($mval, $kind)
log fndkys trace "Made new key entry: $($keys[$mval]) - $mval"
}
}
}
# Set the bar to full for manage-job
Write-Progress -Activity "Scouting $fp" -Completed -PercentComplete 100
log fndkys trace "Keys: ${$keys | Format-Table}"
log timing trace "[END] Finding Keys from $fp"
return $keys
}
}
# Takes a list of files, some context and a list of regex rules and returns all the matches to those regex rules
function Scout-Stripper ($files, $flags, [string] $rootFolder, [int] $PCompleteStart, [int] $PCompleteEnd) {
log timing trace "[START] Scouting file(s) with rules"
$q = New-Object System.Collections.Queue
ForEach ($file in $files) {
$name = "Finding Keys in $(Get-PathTail $rootFolder $file)"
$ScriptBlock = {
PARAM($file, $flags, $IgnoredStringsSet, $_env)