1

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?

sigfried
  • 49
  • 1
  • 6

2 Answers2

1

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)
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • many thanks!! That worked.. by the way I guess that $records variable can be left out, correct? – sigfried Feb 05 '23 at 11:21
  • Yes record is not needed. I wasn't clear if you just need the startBoundary or other info. You can use Descendants to just get startboundary from the doc. – jdweng Feb 05 '23 at 12:01
  • ok, got it. In my case the input and output file are the same. But the xDoc.Save() method throws an error saying that "The process cannot access the file because it is being used by another process." Am I bound to save it with different name and then delete the original or is there another solution? I read that there should be a close method somewhere but in C#. I don't see any close method in PowerShell.. else I will write two more lines to save with different name, then delete, then rename.. and thanks again – sigfried Feb 05 '23 at 14:20
  • 1
    XDocument does not have a close method. So writing to new file and rename is a good solution. – jdweng Feb 05 '23 at 14:53
  • sorry to bug you again.. the sample xml used above contains only 2 TaskV2 but in reality the real xml file contains 3. I only need to modify the first 2, not the third. I thought I could simply do like this: foreach($taskV2 in $tasksV2.Where({$_.Attribute('name').Value -ne 'wsus-extraMaintenance'})) { rest of the code } and from console it seems it works, as I only get two Write-Hosts as per your code. But the xml file written at the end has amended also the one $taskV2 that was excluded. How to exclude a TaskV2 from the rest? – sigfried Feb 05 '23 at 17:30
  • You can force a psobject to an array by surrounding with `@(.......)`. So you can use : @($StartBoundary)[1] – jdweng Feb 05 '23 at 18:08
  • xml linq is the newer net library for xml. XmlDocument is the older net library that take more instructions to do same as linq. I have more statements because my code returns more values. – jdweng Feb 08 '23 at 04:07
  • it's a pity SO does not allow multiple answers. Again, both solutions work, I've chosen the one by mklement0 simply because shorter. Nothing else. – sigfried Feb 08 '23 at 11:02
1

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775