It has been a while since my last Friday Fun post. These are articles that use PowerShell in fun and off-beat ways. The goal is to demonstrate techniques and concepts not necessarily give you something ready for production. Today, I'm going to modify PowerShell output to hide, or redact, potentially sensitive information. I might want to do this if I am running a command and I'm going to save the results to a file or printer.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
For example, I can run a command like this in PowerShell.
If I print this or send it to a file, I might not want the computername to be shown. Or maybe even my name. I want PowerShell to handle this for me. In short, I need to replace strings like 'Prospero' and 'Jeff Hicks' with some like XXX and 'Joe Doe'.
PowerShell has a Replace operator. Or I can use regular expressions. Naturally, I wenbt with the latter.
In order to accomplish this, I need to take the command output and convert it to an array of strings. My function, will take pipeline input. And because I wanted to include formatted output as well, I can't do any string replacements until all the pipelined input is complete. In other words, instead of having my string manipulation in the Process scriptblock, I'll move it to the End scriptblock. This means I need a mechanism to keep track of the incoming pipeline.
In the Begin block, I'll define a generic list object.
$in = [System.Collections.Generic.list[object]]::new()
In the Process block, I'll add each incoming object to the list.
$in.Add($inputobject)
Then in the End block I can turn it into an array of strings.
$out = ($in | Out-String).Split("`r`n")
Because I wanted my code to be flexible, I'll use a hashtable of values.
$global:Redacted = @{$env:COMPUTERNAME = "REDACTED";"Jeff Hicks"="Roy Biv" }
The key is the text I want to find and the value is the replacement text. I am ignoring case in my function. The function will find my computername in any casing and replace it with REDACTED. I went around in circles a few times finding the right regex approach. I wanted to take into account the target string being a standalone value as well as part of a longer string. You'll have to test to see if my approach meets your needs. One thing I will mention is that the length of the replacement string should no longer than the target string. Ideally, they should be identical.
Because I might want to redact several strings, I'll enumerate the hashtable using the GetEnumerator() method. I'll build a regex pattern based on the key and then look for matches. If any are found, I'll replace text.
foreach ($item in $redacted.getenumerator()) {
[regex]$r = "(\b)?$($item.key)(\S+)?(\b)?"
$fixme = [System.Text.RegularExpressions.Regex]::Matches($out,$r,"IgnoreCase")
foreach ($fix in $fixme) {
Write-Verbose "Replacing $($item.key) with $($item.value)"
#replace the value and pad the length difference
$new = ($fix.value -replace $item.key, $item.value).PadRight($item.key.length)
#update the output string
$out = $out -replace $fix.value, $new
} #foreach fix
} #foreach redact item
My regex pattern is basically the hashtable key. It might be preceded by a word boundary (\b). The '?' indicates the \b pattern is optional. The key may optionally be follow by one or more non-whitespace characters (\S+)?. This should handle situations where the key is part of a longer words like 'prospero.local'. The replacement should be 'REDACTED.local'. Finally,there might be another word boundary (\b)?. After looping though the matches, the function writes the clean text to the pipeline.
Here's how it looks in action.
Remember, this is only redacting the output. Here's the complete file.
#requires -version 5.1
#the value should be less than or equal to the key
$global:Redacted = @{$env:COMPUTERNAME = "REDACTED";"Jeff Hicks"="Roy Biv" }
Function Out-Redacted {
[cmdletbinding()]
[outputtype("System.string")]
[alias("or")]
Param(
[Parameter(ValueFromPipeline)]
[object]$InputObject,
[Parameter(HelpMessage = "Specify the redacted hashtable")]
[ValidateScript({$_.keys.count -gt 0})]
[hashtable]$Redacted = $global:Redacted
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
#initialize a list to hold the incoming objects
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Initializing a list"
$in = [System.Collections.Generic.list[object]]::new()
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $InputObject"
#add each pipelined object to the list
$in.Add($inputobject)
} #process
End {
#take the data in $in and create an array of strings
$out = ($in | Out-String).Split("`r`n")
Write-Verbose "Processing $($out.count) lines"
foreach ($item in $redacted.getenumerator()) {
[regex]$r = "(\b)?$($item.key)(\S+)?(\b)?"
$fixme = [System.Text.RegularExpressions.Regex]::Matches($out,$r,"IgnoreCase")
foreach ($fix in $fixme) {
Write-Verbose "Replacing $($item.key) with $($item.value)"
#replace the value and pad the length difference
$new = ($fix.value -replace $item.key, $item.value).PadRight($item.key.length)
#update the output string
$out = $out -replace $fix.value, $new
} #foreach fix
} #foreach redact item
#write the string result
$out
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Out-Redacted
The Redacted parameter uses a default value of a globally defined hashtable. When I dot-source this file, $Redacted is created in my session. I can modify this hashtable as much as needed without having to worry about the function.
Have Fun!
2 thoughts on “Friday Fun: Redacting with PowerShell”
Comments are closed.