Parse Powershell XML Output with xpath

When working with xpaths and parsing an XML file how can you account for a node that has spaces in the name?
The XML in question is the output of a powershell command which gives the following

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Management.Automation.PSCustomObject</T>
      <T>System.Object</T>
    </TN>
    <MS>
      <S N="State">AzureAdJoined</S>
      <S N="Status">YES</S>
    </MS>
  </Obj>
  <Obj RefId="1">
    <TNRef RefId="0" />
    <MS>
      <S N="State">EnterpriseJoined</S>
      <S N="Status">NO</S>
    </MS>
  </Obj>
  <Obj RefId="2">
    <TNRef RefId="0" />
    <MS>
      <S N="State">DomainJoined</S>
      <S N="Status">YES</S>
    </MS>
  </Obj>
  <Obj RefId="3">
    <TNRef RefId="0" />
    <MS>
      <S N="State">DomainName</S>
      <S N="Status">BABGSETC</S>
    </MS>
  </Obj>
  <Obj RefId="4">
    <TNRef RefId="0" />
    <MS>
      <S N="State">TenantName</S>
      <S N="Status">GameStop,</S>
      <S N="P3">Inc.</S>
    </MS>
  </Obj>
  <Obj RefId="5">
    <TNRef RefId="0" />
    <MS>
      <S N="State">TenantId</S>
      <S N="Status">88807067-f283-4e67-95a7-48db7278c141</S>
    </MS>
  </Obj>
  <Obj RefId="6">
    <TNRef RefId="0" />
    <MS>
      <S N="State">Idp</S>
      <S N="Status">login.windows.net</S>
    </MS>
  </Obj>
  <Obj RefId="7">
    <TNRef RefId="0" />
    <MS>
      <S N="State">AadRecoveryEnabled</S>
      <S N="Status">NO</S>
    </MS>
  </Obj>
  <Obj RefId="8">
    <TNRef RefId="0" />
    <MS>
      <S N="State">Executing</S>
      <S N="Status">Account</S>
      <S N="P3">Name</S>
      <S N="P4">INTUNEVM-3A9A82\admin</S>
    </MS>
  </Obj>
  <Obj RefId="9">
    <TNRef RefId="0" />
    <MS>
      <S N="State">KeySignTest</S>
      <S N="Status">PASSED</S>
    </MS>
  </Obj>
</Objs>

Then using the fixlet debugger I am trying to get the Value of the “S N” node related to the Status of the “S N” node for State AzureAdJoined

node names of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "/") of xml document of file "C:\Users\601140\Desktop\outfile.xml"

This gets me to the root and returns #document but when I try to move to the next node of using the following

node names of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "/'a:Obj RefId'/") of xml document of file "C:\Users\601140\Desktop\outfile.xml"

or the following

node names of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "/a:Obj RefId/") of xml document of file "C:\Users\601140\Desktop\outfile.xml"

I Just get an error of

Error: The expression could not be evaluated: Windows Error 0x80004005: Unspecified error

In this case, the node name is simply “S”, and “N” is an attribute of the node.

There’s a really helpful post from @brolly33 on dealing with namespaces in relevance at Xpath and node value woes on how the Namespace works with our inspectors, and why you need to prefix all of your nodes with “a:” (because in this query I coerce “a” to be the shorthand for that MS namespace schema)

Q: node names of child nodes of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS") of xml document of file "c:\temp\test.xml"
A: S
A: S
A: S
A: S

This one was actually a bit of a bear to figure out, so here’s the breakdown. The final form is

Q: node values of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/../a:S[@N='Status']/text()") of xml document of file "c:\temp\test.xml"
A: YES
T: 1467

I’m working my way up to a pattern that I think I’ll be reusing to parse XML. Start out by figuring out what the nodes are at the root of the document. I’ll display its node name, nde value, the attribute “N” that’s of interest, and the names of its child nodes. From the root, we can see that the root node name is “Objs” and it has several “Obj” child nodes:

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "*") of xml document of file "c:\temp\test.xml"
A: Objs, None, None, Obj;Obj;Obj;Obj;Obj;Obj;Obj;Obj;Obj;Obj

Stepping one level down the tree, I’ll look for a:Objs/a:Obj

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs//a:Obj") of xml document of file "c:\temp\test.xml"
A: Obj, None, None, TN;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS
A: Obj, None, None, TNRef;MS

From the file we see that ‘AzureADJoined’ is a child of an MS node so we walk down that level

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS") of xml document of file "c:\temp\test.xml"
A: MS, None, None, S;S
A: MS, None, None, S;S
A: MS, None, None, S;S
A: MS, None, None, S;S
A: MS, None, None, S;S;S
A: MS, None, None, S;S
A: MS, None, None, S;S
A: MS, None, None, S;S
A: MS, None, None, S;S;S;S
A: MS, None, None, S;S

The node we are looking for is some kind of “S”, so one level deeper:

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S") of xml document of file "c:\temp\test.xml"
A: S, None, State, #text
A: S, None, Status, #text
A: S, None, State, #text
A: S, None, Status, #text
A: S, None, State, #text
A: S, None, Status, #text
A: S, None, State, #text
A: S, None, Status, #text
A: S, None, State, #text
A: S, None, Status, #text

At this point we finally get some values for the “N” attribute - “State” and “Status”, and finally some of the child nodes are just text (their names showing as #text). ‘AzureAdJoined’ is a child of a “State” node, so let’s filter to the “S” nodes where the “N” attribute is equal to ‘State’. Let’s also return the node values of the child nodes (#text) instead of the node names:

Q: (node name of it, node value of it | “None”, node value of attribute “N” of it | “None”, (concatenation “;” of node values of child nodes of it) | “None” ) of xpaths (“xmlns:a=‘http://schemas.microsoft.com/powershell/2004/04’”, “a:Objs/a:Obj/a:MS/a:S[@N=‘State’]”) of xml document of file "c:\temp\test.xml"
A: S, None, State, AzureAdJoined
A: S, None, State, EnterpriseJoined
A: S, None, State, DomainJoined
A: S, None, State, DomainName
A: S, None, State, TenantName
A: S, None, State, TenantId
A: S, None, State, Idp
A: S, None, State, AadRecoveryEnabled
A: S, None, State, Executing
A: S, None, State, KeySignTest

Next, lets add an xpath filter for only the node with a text() value of ‘AzureAdJoined’:

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node values of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']") of xml document of file "c:\temp\test.xml"
A: S, None, State, AzureAdJoined

From that path, we need to go up one level to find the parent node of the ‘S’ node with text AzureAdJoined. I’ll also switch back to the node names of its child nodes

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/..") of xml document of file "c:\temp\test.xml"
A: MS, None, None, S;S

So finally, we are at the parent node (MS), that has two child nodes “S”. One of those two has the N attribute of ‘State’ (with text() of ‘AzureAdJoined’), the other node is the ‘S’ node with an ‘N’ attribute of ‘Status’ (and with text() value ‘YES’)
Let’s filter to the one with the ‘Status’:

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node names of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/../a:S[@N='Status']") of xml document of file "c:\temp\test.xml"
A: S, None, Status, #text

…and switch it to give us the node value of that child node…

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node values of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/../a:S[@N='Status']") of xml document of file "c:\temp\test.xml"
A: S, None, Status, YES

So we’ve found the exact node we want, just append the /text() to the xpath query to get the text value instead of the node containing the text…

Q: (node name of it, node value of it | "None", node value of attribute "N" of it | "None", (concatenation ";" of node values of child nodes of it) | "None" ) of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/../a:S[@N='Status']/text()") of xml document of file "c:\temp\test.xml"
A: #text, YES, None,

and now we just trim off the extra properties at the left of our relevance, since we only need that node value in the end

Q: node values of xpaths ("xmlns:a='http://schemas.microsoft.com/powershell/2004/04'", "a:Objs/a:Obj/a:MS/a:S[@N='State' and text()='AzureAdJoined']/../a:S[@N='Status']/text()") of xml document of file "c:\temp\test.xml"
A: YES
T: 1467

edit: When I wrote this back out for example, I left the query starting at the “*” node instead of the “a:Objs” node. Fixed it.

5 Likes