2

I've the following PSObj with some properties stored in an $array :

ComputerName      : MyComputer
Time              : 08/11/2022 13:57:53
DetectionFile     : MyBadFile.exe
ThreatName        : WS.Reputation.1
Action            : 12

I'm trying to replace the action ID number by it's corresponding description. I've a hashtable with the possibles reasons behind the Action ID

$ActionId = @{
    0  = 'Unknown'
    1  = 'Blocked'
    2  = 'Allowed'
    3  = 'No Action'
    4  = 'Logged'
    5  = 'Command Script Run'
    6  = 'Corrected'
    7  = 'Partially Corrected'
    8  = 'Uncorrected'
    10 = 'Delayed   Requires reboot to finish the operation.'
    11 = 'Deleted'
    12 = 'Quarantined'
    13 = 'Restored'
    14 = 'Detected'
    15 = 'Exonerated    No longer suspicious (re-scored).'
    16 = 'Tagged    Marked with extended attributes.'
}

I'm trying to parse each item of this array, and each value of the reason ID to replace the ID by the reason string

    # parse array
    foreach ($Item in $array) {
        # parse possible values
        foreach ($value in $ActionId) {
            if ($value -eq $item.Action) {
                $Item.Action = $ActionId[$value]
                $Item.Action
            }
        }

From my understanding, I'm missing the correct syntax here

$Item.Action = $ActionId[$value]

I do not get any errors, but from the debugger, I'm replacing the action property by $null with the above...

mklement0
  • 382,024
  • 64
  • 607
  • 775
Douda
  • 121
  • 8
  • Actually the issue still remains, and couldn't find an answer from the other similar ones. I'm still trying to replace the value but I can't adress the "$ActionId[$value]" part seems wrong as it still replaces the value by $null – Douda Dec 01 '22 at 17:18

2 Answers2

3

The immediate fix is to loop over the keys (.Keys) of your $ActionId hashtable:

foreach ($Item in $array) {
  # parse possible values
  foreach ($value in $ActionId.Keys) {
      if ($value -eq $item.Action) {
          $Item.Action = $ActionId[$value]
          $Item.Action  # diagnostic output
      }
  }
}

Note:

  • To avoid confusion, consider renaming $value to $key.

  • Generally, note that hashtables are not enumerated in the pipeline / in looping constructs in PowerShell.

    • That is, foreach ($value in $ActionId) ... doesn't actually loop over the hashtable's entries, and is the same as $value = $ActionID)

    • If you want to enumerate a hashtable's entries - as key-value pairs of type System.RuntimeType - you would need to use the .GetEnumerator() method; in your case, however, enumerating the keys is sufficient.


However, the simpler and more efficient solution is to test whether the $Item.Action value exists as a key in your hashtable, using the latter's .Contains() method:[1]

foreach ($Item in $array) {
  if ($ActionId.Contains($Item.Action)) {
    $Item.Action = $ActionId[$Item.Action]
    $Item.Action  # diagnostic output
  }
}

You can further streamline this as follows, though it is conceptually a bit obscure:

foreach ($Item in $array) {
  if ($null -ne ($value = $ActionId[$Item.Action])) {
    $Item.Action = $value
    $Item.Action  # diagnostic output
  }
}
  • = is only ever PowerShell's assignment operator; for equality / non-equality comparison, -eq / -ne is required.

  • Here, an assignment to $value is indeed being performed and the assigned value then acts as the RHS of the -ne operation; in other words: you can use assignment as expressions in PowerShell.

  • If hashtable $ActionId has no key with value $Item.Action, $ActionId[$Item.Action] quietly returns $null.


Finally - in PowerShell (Core) 7+ only - an even more concise (though not necessarily faster) solution is possible, using ??, the null-coalescing operator:

foreach ($Item in $array) {
  $Item.Action = $ActionId[$Item.Action] ?? $Item.Action
  $Item.Action # diagnostic output
}

That is, the value of $ActionId[$Item.Action] is only used if it isn't $null; otherwise, $Item.Action, i.e. the current value, is used (which is effectively a no-op).


[1] .ContainsKey() works too, and while this name is conceptually clearer than .Contains(), it is unfortunately not supported by PowerShell's [ordered] hashtables (System.Collections.Specialized.OrderedDictionary) and, generally speaking, not supported by other dictionary (hashtable-like types), given that the System.Collections.IDictionary interface only has .Contains()

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Very detailed explanation thank you ! I tried noth methods out of curiousity, and assume that the 2nd one is more efficient as it's not parsing both array & hashlist. For the record, looks like your 2nd method always returns $null. From my debugger : `[DBG]: PS C:\\Scripts> $ActionId[12]` `Quarantined` `[DBG]: PS C:\\Scripts> $Item.action` `1` `[DBG]: PS C:\\Scripts> $ActionId[$Item.action]` `\\ nothing` – Douda Dec 02 '22 at 08:41
  • 1
    Glad to hear it helped, @Douda; my pleasure. I'm not sure what you mean re the 2nd method - from what I can tell, all solutions work the same. – mklement0 Dec 02 '22 at 13:32
3

In addition mklement0's helpful answer, I was just thinking outside the box (aka question):
This is typical situation where I would consider to use an enum except for the fact that that the keys do not (easily) accept spaces (as in your question).

Enum ActionTypes {
    Unknown
    Blocked
    Allowed
    NoAction
    Logged
    CommandScriptRun
    Corrected
    PartiallyCorrected
    Uncorrected
    Delayed
    Deleted
    Quarantined
    Restored
    Detected
    Exonerated
    Tagged
}

$PSObj = [PSCustomObject]@{
    ComputerName      = 'MyComputer'
    Time              = [DateTime]'08/11/2022 13:57:53'
    DetectionFile     = 'MyBadFile.exe'
    ThreatName        = 'WS.Reputation.1'
    Action            = 12
}
$PSObj.Action = [ActionTypes]$PSObj.Action
$PSObj

ComputerName  : MyComputer
Time          : 8/11/2022 1:57:53 PM
DetectionFile : MyBadFile.exe
ThreatName    : WS.Reputation.1
Action        : Restored

The advantage is that you won't lose the actual action id, meaning if you e.g. insert the object back into a database, it will automatically type cast to the original integer type:

$PSObj.Action
Restored
[int]$PSObj.Action
12
iRon
  • 20,463
  • 10
  • 53
  • 79
  • 1
    Thank you iRon ! Never used enum, yet. I'll definitely have a look – Douda Dec 02 '22 at 08:46
  • 1
    That's a nice alternative, if the names can be represented as identifiers (which excludes not only spaces, but any character other than digits, letters, and `_`). Just to clarify, when you say "not easily", I assume you're referring to decorating members with _attributes_, such as shown in https://stackoverflow.com/a/1415187/45375, correct? Or were you thinking of somehow _encoding_ non-allowed characters, which would require later decoding? – mklement0 Dec 02 '22 at 14:35
  • 1
    @mklement0, thanks, I was indeed looking into that so answer but did not have the time to figure it out how to do this in PowerShell. Maybe later this weekend... I think the best translation is "**aka**" (changed) – iRon Dec 02 '22 at 18:40