PowerShell: Working with XML

PowerShell seems to have two built-in ways of dealing with XML:

Clixml

The first is to use the Export-Clixml and Import-Clixml cmdlets. These are an easy way to save and load PowerShell objects for later use. For example, if you have an array of objects that has been created during script execution, you can save it to disk and then load it back at a later date exactly as it was originally.

Save:

$Processes = Get-Process
$Processes | Export-Clixml -Path "D:\TD2\Processes.xml"

Load:

$Processes = Import-Clixml -Path "D:\TD2\Processes.xml"

However, the XML this produces is not quite so human-readable as we’d maybe like:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Diagnostics.Process</T>
      <T>System.ComponentModel.Component</T>
      <T>System.MarshalByRefObject</T>
      <T>System.Object</T>
    </TN>
    <ToString>System.Diagnostics.Process (conhost)</ToString>
    <Props>
      <I32 N="BasePriority">8</I32>
      <I32 N="HandleCount">60</I32>
      <I32 N="Id">2624</I32>
      <S N="MachineName">.</S>
      <Obj N="MainWindowHandle" RefId="1">
        <TN RefId="1">
          <T>System.IntPtr</T>
          <T>System.ValueType</T>
          <T>System.Object</T>
        </TN>
        <ToString>0</ToString>
      </Obj>
      <S N="MainWindowTitle"></S>
      <I32 N="NonpagedSystemMemorySize">7664</I32>
      <I64 N="NonpagedSystemMemorySize64">7664</I64>
      <I32 N="PagedMemorySize">1949696</I32>
      <I64 N="PagedMemorySize64">1949696</I64>
      <I32 N="PagedSystemMemorySize">114064</I32>
      <I64 N="PagedSystemMemorySize64">114064</I64>

But that might not be a problem if you’re not intending to look at it, and just load it straight back in to PowerShell for further processing, or if your object is fairly small and uncomplicated and thus is able to be deciphered by your brain without too much work.

XML

The alternative is to convert your object into XML and then save it:

$Processes = Get-Process
$ProcessesXML = $Processes | ConvertTo-Xml
$ProcessesXML.Save("D:\TD2\Processes.xml")

This can also be re-loaded too:

[xml]$ProcessesXML = Get-Content -Path "D:\TD2\ProcessesXML.xml"

This XML is a bit more human-readable:

<?xml version="1.0"?>
<Objects>
  <Object Type="System.Diagnostics.Process">
    <Property Name="__NounName" Type="System.String">Process</Property>
    <Property Name="Name" Type="System.String">conhost</Property>
    <Property Name="Handles" Type="System.Int32">60</Property>
    <Property Name="VM" Type="System.Int32">58322944</Property>
    <Property Name="WS" Type="System.Int32">9347072</Property>
    <Property Name="PM" Type="System.Int32">1949696</Property>
    <Property Name="NPM" Type="System.Int32">7664</Property>
    <Property Name="Path" Type="System.Object" />
    <Property Name="Company" Type="System.Object" />
    <Property Name="CPU" Type="System.Double">4.90625</Property>
    <Property Name="FileVersion" Type="System.Object" />
    <Property Name="ProductVersion" Type="System.Object" />
    <Property Name="Description" Type="System.Object" />
    <Property Name="Product" Type="System.Object" />
    <Property Name="BasePriority" Type="System.Int32">8</Property>

Differences

The Clixml output file for 60 running processes is 3.9MB whereas the ConvertTo-Xml one is 488KB. Also, whereas the Clixml object remains as a data type of System.Array Object[], the XML becomes a System.Xml.XmlNode XmlDocument. You can still work with the latter using PowerShell but the data structure, and thus the code required, is different to a regular array object.

Loading

Lets take the following XML and show how to read it in and then work with the data:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE nmaprun>
<?xml-stylesheet href="file:///C:/Program Files (x86)/Nmap/nmap.xsl" type="text/xsl"?>
<!-- Nmap 7.00 scan initiated Wed Dec 09 15:11:22 2015 as: &quot;C:\\Program Files (x86)\\Nmap\\nmap.exe&quot; -T4 -O -F -oX C:\\Users\\admin\\AppData\\Local\\Temp\\192.168.0.0_16.xml 192.168.0.0/16 -->
<nmaprun scanner="nmap" args="&quot;C:\\Program Files (x86)\\Nmap\\nmap.exe&quot; -T4 -O -F -oX C:\\Users\\admin\\AppData\\Local\\Temp\\192.168.0.0_16.xml 192.168.0.0/16" start="1449673882" startstr="Wed Dec 09 15:11:22 2015" version="7.00" xmloutputversion="1.04">
  <scaninfo type="syn" protocol="tcp" numservices="100" services="7,9,13,21-23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,631,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152-49157"/>
  <verbose level="0"/>
  <debugging level="0"/>
  <host starttime="1449676122" endtime="1449676408"><status state="up" reason="echo-reply" reason_ttl="127"/>
    <address addr="192.168.72.159" addrtype="ipv4"/>
    <hostnames>
    </hostnames>
    <ports>
      <extraports state="closed" count="96">
   	  <extrareasons reason="resets" count="96"/>
      </extraports>
      <port protocol="tcp" portid="135"><state state="open" reason="syn-ack" reason_ttl="127"/><service name="msrpc" method="table" conf="3"/></port>
      <port protocol="tcp" portid="139"><state state="open" reason="syn-ack" reason_ttl="127"/><service name="netbios-ssn" method="table" conf="3"/></port>
      <port protocol="tcp" portid="445"><state state="open" reason="syn-ack" reason_ttl="127"/><service name="microsoft-ds" method="table" conf="3"/></port>
      <port protocol="tcp" portid="8080"><state state="open" reason="syn-ack" reason_ttl="127"/><service name="http-proxy" method="table" conf="3"/></port>
    </ports>
    <os>
      <portused state="open" proto="tcp" portid="135"/>
      <portused state="closed" proto="tcp" portid="7"/>
      <portused state="closed" proto="udp" portid="41581"/>
      <osmatch name="Microsoft Windows XP SP2 or SP3, or Windows Server 2003" accuracy="100" line="73014">
        <osclass type="general purpose" vendor="Microsoft" osfamily="Windows" osgen="XP" accuracy="100"><cpe>cpe:/o:microsoft:windows_xp</cpe></osclass>
        <osclass type="general purpose" vendor="Microsoft" osfamily="Windows" osgen="2003" accuracy="100"><cpe>cpe:/o:microsoft:windows_server_2003</cpe></osclass>
      </osmatch>
    </os>
    <distance value="2"/>
    <tcpsequence index="258" difficulty="Good luck!" values="FA12EFAD,7E843454,C8329553,EDC8DB4A,45C8D135,A5AFE021"/>
    <ipidsequence class="Incremental" values="1044,1045,1046,1047,1048,104E"/>
    <tcptssequence class="zero timestamp" values="0,0,0,0,0,0"/>
    <times srtt="10928" rttvar="9937" to="100000"/>
  </host>
  <runstats><finished time="1449681321" timestr="Wed Dec 09 17:15:21 2015" elapsed="7439.88" summary="Nmap done at Wed Dec 09 17:15:21 2015; 65536 IP addresses (27926 hosts up) scanned in 7439.88 seconds" exit="success"/><hosts up="27926" down="37610" total="65536"/>
  </runstats>
</nmaprun>

Which is the output from nmap using the -oX option, edited to only show one host. The following PowerShell pulls out some of the host attributes for display:

[xml]$XML = Get-Content -Path $XMLFile
foreach($Machine in $XML.ChildNodes.host){
    $IP4Address = ($Machine.address | Where-Object -Property addrtype -eq "ipv4").addr
    if($IP4Address -ne $null){
        $NICVendor = ($Machine.address | Where-Object -Property addrtype -eq "mac").vendor
        $OSMatch = ""
        foreach($OS in $Machine.os.osmatch){
            if($OSMatch -eq ""){
                $OSMatch = $OS.name
            }else{
                $OSMatch = $OSMatch+", "+$OS.name
            }
        }
        Write-Host $IPAddress, $NICVendor, $OSMatch
    }
}

Modifying

There seem to be several ways to do this. Here’s the one I’m currently using for adding an extra child item with a few attributes:

[XML]$SMXML = Get-Content -Path $ServerManagerXMLFile
$NewServer = $SMXML.CreateElement("ServerInfo")
$SMXML.ServerList.AppendChild($NewServer) | Out-Null
$NewServer.SetAttribute("name",$RDServer.Server)
$NewServer.SetAttribute("status","1")
$NewServer.SetAttribute("lastUpdateTime",[string](Get-Date -Format s))
$NewServer.SetAttribute("locale","en-GB")
$SMXML = $SMXML.OuterXml.Replace(" xmlns=`"`"","")
$SMXML.Save($ServerManagerXMLFile)

This was mostly from here, but didn’t mention the annoying extra xmlns attribute that gets added (and which I’m removing above)

This entry was posted in PowerShell, Windows and tagged , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

2 Responses to PowerShell: Working with XML

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s