Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Friday Fun: Redacting with PowerShell

Posted on January 28, 2022January 28, 2022

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.

Manage and Report Active Directory, Exchange and Microsoft 365 with
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.

get-ciminstance example

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.

redacted powershell sample
redacted powershell sample 2

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.

modifying the redacted hashtable

Have Fun!


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

2 thoughts on “Friday Fun: Redacting with PowerShell”

  1. Pingback: Friday Fun: Redacting with PowerShell - The Lonely Administrator - Syndicated Blogs - IDERA Community
  2. Pingback: Data Masking with Powershell – Curated SQL

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d