sigfried
sigfried

Reputation: 69

Amend XML object using PowerShell

I'm trying to edit a xml object in PowerShell but the xml object is quite hard to get to. I've read quite a few posts and articles and they all show easy xml objects or files. And they all work as long as the xml tree is simple and no tags with ':' inside them show up. In my case the xml object is obtained from an Active Directory GPO, through this command:

[xml]$report = Get-GPOReport -Guid 'BEE66288-DF38-4E32-A6F6-9DF13BABFDDF' -ReportType XML -Server "fqdn"

The xml that generates is quite long and full of sensitive data, so I had to trim it and sanitize it quite a lot, but it gives the idea:

<?xml version="1.0" encoding="utf-16"?>
<GPO xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.microsoft.com/GroupPolicy/Settings">
  <Computer>
    <VersionDirectory>51</VersionDirectory>
    <VersionSysvol>51</VersionSysvol>
    <Enabled>true</Enabled>
    <ExtensionData>
      <Extension xmlns:q1="http://www.microsoft.com/GroupPolicy/Settings/Files" xsi:type="q1:FilesSettings">
        <q1:FilesSettings clsid="{215B2E53-57CE-475c-80FE-9EEC14635851}">
        </q1:FilesSettings>
      </Extension>
      <Name>Files</Name>
    </ExtensionData>
    <ExtensionData>
      <Extension xmlns:q2="http://www.microsoft.com/GroupPolicy/Settings/ScheduledTasks" xsi:type="q2:ScheduledTasksSettings">
        <q2:ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">
          <q2:TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="VBS" image="1" changed="2022-12-01 09:28:19" uid="{6128F739-7B10-4D53-B8B2-4F7D8D518B39}">
            <q2:GPOSettingOrder>1</q2:GPOSettingOrder>
            <q2:Properties action="R" name="VBS" runAs="NT AUTHORITY\System" logonType="S4U">
              <q2:Task version="1.2">
                <q2:RegistrationInfo>
                  <q2:Description>VBS script installs windows update without rebooting</q2:Description>
                </q2:RegistrationInfo>
                <q2:Triggers>
                  <q2:CalendarTrigger>
                    <q2:Enabled>true</q2:Enabled>
                    <q2:StartBoundary>2022-03-27T14:30:00</q2:StartBoundary>
                    <q2:ExecutionTimeLimit>PT4H</q2:ExecutionTimeLimit>
                  </q2:CalendarTrigger>
                </q2:Triggers>
              </q2:Task>
            </q2:Properties>
            <q2:Filters />
          </q2:TaskV2>
          <q2:TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="PS" image="1" changed="2022-12-01 09:28:05" uid="{27A6954B-DC81-42CE-ACA3-FB70CD1DDC98}">
            <q2:GPOSettingOrder>2</q2:GPOSettingOrder>
            <q2:Properties action="R" name="PS" runAs="NT AUTHORITY\System" logonType="S4U">
              <q2:Task version="1.2">
                <q2:RegistrationInfo>
                  <q2:Description>PS script to schedule reboot</q2:Description>
                </q2:RegistrationInfo>
                <q2:Triggers>
                  <q2:CalendarTrigger>
                    <q2:Enabled>true</q2:Enabled>
                    <q2:StartBoundary>2022-03-27T14:30:05</q2:StartBoundary>
                    <q2:ExecutionTimeLimit>PT4H</q2:ExecutionTimeLimit>
                  </q2:CalendarTrigger>
                </q2:Triggers>
              </q2:Task>
            </q2:Properties>
            <q2:Filters />
          </q2:TaskV2>
        </q2:ScheduledTasks>
      </Extension>
      <Name>Scheduled Tasks</Name>
    </ExtensionData>
  </Computer>
</GPO>

my goal is to 'select' the lines <q2:StartBoundary>2022-03-27T14:30:05</q2:StartBoundary> for each node, then edit them and put them back in the object. Basically I need to change the date/time and save it back in the GPO. I can do the saving back in the GPO by myself. My objective here is to be able to select those lines. I've tried with:

$nodes = $report.GPO.ChildNodes.Item(0)

but I can only get as far as the ExtensionData tag. After that the content gets empty, and I have nothing to manipulate. I've also tried

$nodes = $report.GPO.Computer.ExtensionData.Extension | Where-Object {$_.type -eq 'q2:ScheduledTasksSettings'}

but same thing. Also this:

$xpath = "/GPO/Computer/ExtensionData/Extension/q2:ScheduledTasks/q2:TaskV2"
$nodesX = Select-Xml -Xml $report -XPath $xpath

but I get an error: Select-Xml : Namespace Manager or XsltContext needed. This query has a prefix, variable, or user-defined function

Any hint?

Upvotes: 1

Views: 157

Answers (2)

mklement0
mklement0

Reputation: 437062

You were on the right track. To make your code work:

  • Drill down to the parent element(s) of interest via a property path, which - thanks to PowerShell's member-access enumeration feature - implicitly iterates over all elements that have the same name at a given level of the hierarchy.

    • Do not use namespace prefixes such as q2: - PowerShell's convenient adaption of the XML DOM ([xml] (System.Xml.XmlDocument)) with dot notation (see this answer) is not namespace-aware and ignores namespaces and their prefixes.
  • Process the resulting elements with ForEach-Object and update the child element of interest for each.

    • Note that even if the same value is to be assigned to all elements of interest, assigning directly to a property resulting in/from member-access enumeration does not work, because the latter only supports getting values - see this answer for details; e.g.:

      # OK - *getting* all <date> values.
      ([xml] '<d><date>1970</date><date>1971</date></d>').d.date
      
      # !! NOT SUPPORTED - *setting* all <date> values.
      ([xml] '<d><date>1970</date><date>1971</date></d>').d.date = '2001'
      

Therefore:

$report.GPO.Computer.ExtensionData.Extension.ScheduledTasks.TaskV2.Properties.Task.Triggers.CalendarTrigger | 
  ForEach-Object { $_.StartBoundary = Get-Date -Format s }

Note: It doesn't apply to the sample XML at hand, but it is possible for XML elements to be inaccessible with dot notation due to name collisions, necessitating a workaround, notably when trying to access <Item> child elements on an array of elements resulting from member-access enumeration - see this answer.


As for the error you saw with Select-Xml:

  • Unlike the dot-notation DOM adaptation, Select-Xml does require explicit handling of namespaces - both by declaring all namespaces up front, and by needing to select elements by namespace-qualified names.

  • See this answer for an example.

Upvotes: 1

jdweng
jdweng

Reputation: 34421

Try xml linq

using assembly System 
using assembly System.Linq
using assembly System.Xml.Linq 

$inputFilename = "c:\temp\test.xml"
$outputFilename = "c:\temp\test1.xml"

$reader = [System.IO.StreamReader]::new($inputFilename)
# skip first line with utf-16
$reader.ReadLine()
$xDoc = [System.Xml.Linq.XDocument]::Load($reader)
$today = [DateTime]::Now
$tasksV2 = $xDoc.Descendants().Where( {$_.Name.LocalName -eq "TaskV2"})
foreach($taskV2 in $tasksV2)
{
   $name = $taskV2.Attribute("name").Value

   $startBoundary = $xDoc.Descendants().Where( {$_.Name.LocalName -eq "StartBoundary"})
   $oldDate = $startBoundary[0].Value
   $todayStr = $today.ToString("yyyy-MM-ddTHH:mm:ss")
   Write-Host "Task Name = " $name ",Old Date = " $oldDate ",Today Date = " $todayStr
   $startBoundary.SetValue($todayStr)
}
$xDoc.Save($outputFilename)

Upvotes: 1

Related Questions