O O OSOT, What Did You Do?!

Mark PlatteMark Platte
13 min read

O O OSOT, What Did You Do?!

Converting XML with XSLT to Word so Humans Can Finally Read It

Ever stared at an OSOT export XML and thought, "this was clearly meant to be machine-readable only and even the machine is judging me"?

Yeah. Me too.

So I built a small VB.NET WinForms tool that finally does what the OSOT (Omnissa OS Optimization Tool) should have done from the start:

Convert that mess into a readable Word document.
With formatting. With structure. Without a migraine.

What it does

It’s simple:

  • You pick your XML export from OSOT (or any other structured config)

  • You pick your XSLT transform (I made one to make OSOT readable but go wild)

  • You pick where to save the resulting .docx Word file

  • Optionally, you can also save the intermediate .html if you're into that kind of thing

Click. Boom. Readable documentation.

Under the Hood

It uses:

  • XslCompiledTransform to apply the XSLT and turn XML into HTML

  • HtmlToOpenXml + OpenXml SDK to turn the HTML into a clean .docx

  • Temp files to keep things tidy (unless you ask for the HTML)

Yes, I could have used Word Interop.
No, I don't hate myself that much.

The GUI

Nothing fancy just enough:

  • Textboxes for XML, XSLT, HTML output, and .docx path

  • Browse buttons

  • One glorious "Convert" button

  • A status label that tries to be encouraging and not panic too hard when things break

image.png

Why?

Because:

  • Copy-pasting from OSOT to Word manually is for masochists

  • Every time someone screenshots XML in a presentation, an angel loses its wings

  • We need to document optimizations for security reviews, audits, or just to feel like we’re still in control

So Here Comes the Code Deep Dive

So you want to compile it yourself and even know what all the buttons do?

Let’s go.

But first...

Setting Up the Project (So It Doesn’t Crash on Start)

Before you run off hitting F5 and wondering why HtmlToOpenXml sounds like a virus here’s how to set up the project properly:

Step 1: Create the Project

  • Open Visual Studio

  • Create a new Windows Forms App (.NET Framework)
    Yes, classic .NET Framework, not .NET Core or .NET 6 this tool is retro-cool.

Step 2: Install the Required NuGet Packages

Open the Package Manager Console or use NuGet GUI and install:

Install-Package DocumentFormat.OpenXml 
Install-Package HtmlToOpenXml

No need for Interop. No need for Word to be installed.
Just these two and you’re good.

Step 3: Add the Controls to the Form

Here’s what you slap on your form:

ControlNameDescription
TextBoxtxtXmlPathPath to the XML file
ButtonbtnBrowseXmlOpens a file dialog for XML
TextBoxtxtXsltPathPath to the XSLT file
ButtonbtnBrowseXsltOpens a file dialog for XSLT
TextBoxtxtOutputPathPath to save .docx
ButtonbtnBrowseOutOpens a Save dialog for .docx
TextBoxtxtHtmlPathOptional: Save intermediate HTML
ButtonbtnOpenHtmlPathBrowse for HTML output file
ButtonbtnConvertConverts everything
LabellblStatusShows status

Just align them vertically and you’re done.
It’s not the UX of the decade, but it works.

All ready here we go the full code !

Imports System.Xml.Xsl
Imports DocumentFormat.OpenXml.Packaging
Imports HtmlToOpenXml
Imports DocumentFormat.OpenXml

Public Class Form1
    Private Sub btnBrowseXml_Click(sender As Object, e As EventArgs) Handles btnBrowseXml.Click
        Using ofd As New OpenFileDialog()
            ofd.Filter = "XML Files|*.xml"
            If ofd.ShowDialog() = DialogResult.OK Then
                txtXmlPath.Text = ofd.FileName
            End If
        End Using
    End Sub

    Private Sub btnBrowseXslt_Click(sender As Object, e As EventArgs) Handles btnBrowseXslt.Click
        Using ofd As New OpenFileDialog()
            ofd.Filter = "XSLT Files|*.xsl;*.xslt"
            If ofd.ShowDialog() = DialogResult.OK Then
                txtXsltPath.Text = ofd.FileName
            End If
        End Using
    End Sub

    Private Sub btnBrowseOut_Click(sender As Object, e As EventArgs) Handles btnBrowseOut.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "Word Documents|*.docx"
            If sfd.ShowDialog() = DialogResult.OK Then
                txtOutputPath.Text = sfd.FileName
            End If
        End Using
    End Sub

    Private Sub btnConvert_Click(sender As Object, e As EventArgs) Handles btnConvert.Click
        lblStatus.Text = ""
        Dim xmlPath = txtXmlPath.Text
        Dim xsltPath = txtXsltPath.Text
        Dim outputDocx = txtOutputPath.Text

        If Not System.IO.File.Exists(xmlPath) OrElse Not System.IO.File.Exists(xsltPath) Then
            lblStatus.Text = "Please select valid XML and XSLT files."
            Return
        End If

        Try
            ' Step 1: Transform XML → HTML (save to temp first)
            Dim htmlTemp = System.IO.Path.GetTempFileName() & ".html"
            Dim xslt As New XslCompiledTransform()
            xslt.Load(xsltPath)
            xslt.Transform(xmlPath, htmlTemp)

            ' If HTML path textbox has value, copy the result
            If Not String.IsNullOrWhiteSpace(txtHtmlPath.Text) Then
                System.IO.File.Copy(htmlTemp, txtHtmlPath.Text, overwrite:=True)
            End If

            ' Step 2: Convert HTML → Word from temp
            Dim htmlContent = System.IO.File.ReadAllText(htmlTemp)
            Using doc = WordprocessingDocument.Create(outputDocx, WordprocessingDocumentType.Document)
                Dim mainPart = doc.AddMainDocumentPart()
                mainPart.Document = New DocumentFormat.OpenXml.Wordprocessing.Document(New DocumentFormat.OpenXml.Wordprocessing.Body())

                Dim converter As New HtmlConverter(mainPart)
                converter.ParseHtml(htmlContent)
            End Using

            lblStatus.Text = "Conversion successful!"
        Catch ex As Exception
            lblStatus.Text = "Error: " & ex.Message
        End Try
    End Sub

    Private Sub lblStatus_Click(sender As Object, e As EventArgs) Handles lblStatus.Click

    End Sub

    Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles txtHtmlPath.TextChanged

    End Sub

    Private Sub btnOpenHtmlPath_Click(sender As Object, e As EventArgs) Handles btnOpenHtmlPath.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "HTML Files|*.html"
            If sfd.ShowDialog() = DialogResult.OK Then
                txtHtmlPath.Text = sfd.FileName
            End If
        End Using
    End Sub
End Class

That's all oh, ok I promised a slightly more extensive explanation

So let's break things up

 Private Sub btnBrowseXml_Click(sender As Object, e As EventArgs) Handles btnBrowseXml.Click
        Using ofd As New OpenFileDialog()
            ofd.Filter = "XML Files|*.xml"
            If ofd.ShowDialog() = DialogResult.OK Then
                txtXmlPath.Text = ofd.FileName
            End If
        End Using
    End Sub

    Private Sub btnBrowseXslt_Click(sender As Object, e As EventArgs) Handles btnBrowseXslt.Click
        Using ofd As New OpenFileDialog()
            ofd.Filter = "XSLT Files|*.xsl;*.xslt"
            If ofd.ShowDialog() = DialogResult.OK Then
                txtXsltPath.Text = ofd.FileName
            End If
        End Using
    End Sub

    Private Sub btnBrowseOut_Click(sender As Object, e As EventArgs) Handles btnBrowseOut.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "Word Documents|*.docx"
            If sfd.ShowDialog() = DialogResult.OK Then
                txtOutputPath.Text = sfd.FileName
            End If
        End Using
    End Sub
    Private Sub btnOpenHtmlPath_Click(sender As Object, e As EventArgs) Handles btnOpenHtmlPath.Click
        Using sfd As New SaveFileDialog()
            sfd.Filter = "HTML Files|*.html"
            If sfd.ShowDialog() = DialogResult.OK Then
                txtHtmlPath.Text = sfd.FileName
            End If
        End Using
    End Sub
End Class

The Browse Buttons: XML, XSLT, DOCX Same Energy, Different Filter

These three buttons all do the same thing:

  • Open a file dialog

  • Let you pick a file

  • Drop the path into the right textbox

They just differ in what file types they allow you to pick:

ofd -> open file dialog the you can open a file from here standard windows form.

ofd.Filter = "XML Files|*.xml"

Lets you select the XML file you're converting.
It fills in txtXmlPath.Text so you don’t have to type out some Windows path from 1996

ofd.Filter = "XSLT Files|*.xsl;*.xslt"

Same logic, but for the XSLT transform.
Yes, this is the file that takes your structured chaos and turns it into readable HTML.

sfd -> save file dialog the you can save a file here standard windows form.

sfd.Filter = "Word Documents|*.docx"

This time it’s a SaveFileDialog (not Open), so you can choose where to save your glorious .docx.
No, it won’t overwrite unless you tell it to we're not animals.

Bonus Round: btnOpenHtmlPath_Click

For When You Want the Ugly Middle Step Too

But honestly... I kinda like the HTML more than the DOC.

This one’s optional, but super handy.

You know how the app transforms your XML + XSLT into HTML before stuffing it into Word?
Well, this lets you save that intermediate HTML so you can open it in a browser, check the layout, or just admire your <step> elements without crying.

Same pattern as before:

  • Open Save dialog

  • Filter = .html

  • Set txtHtmlPath.Text so the rest of the app knows where to drop the HTML

No mystery here. It’s just honest plumbing for visibility freaks (or for when Word doesn’t do what it’s told).

 Private Sub btnConvert_Click(sender As Object, e As EventArgs) Handles btnConvert.Click
        lblStatus.Text = ""
        Dim xmlPath = txtXmlPath.Text
        Dim xsltPath = txtXsltPath.Text
        Dim outputDocx = txtOutputPath.Text

        If Not System.IO.File.Exists(xmlPath) OrElse Not System.IO.File.Exists(xsltPath) Then
            lblStatus.Text = "Please select valid XML and XSLT files."
            Return
        End If

        Try
            ' Step 1: Transform XML → HTML (save to temp first)
            Dim htmlTemp = System.IO.Path.GetTempFileName() & ".html"
            Dim xslt As New XslCompiledTransform()
            xslt.Load(xsltPath)
            xslt.Transform(xmlPath, htmlTemp)

            ' If HTML path textbox has value, copy the result
            If Not String.IsNullOrWhiteSpace(txtHtmlPath.Text) Then
                System.IO.File.Copy(htmlTemp, txtHtmlPath.Text, overwrite:=True)
            End If

            ' Step 2: Convert HTML → Word from temp
            Dim htmlContent = System.IO.File.ReadAllText(htmlTemp)
            Using doc = WordprocessingDocument.Create(outputDocx, WordprocessingDocumentType.Document)
                Dim mainPart = doc.AddMainDocumentPart()
                mainPart.Document = New DocumentFormat.OpenXml.Wordprocessing.Document(New DocumentFormat.OpenXml.Wordprocessing.Body())

                Dim converter As New HtmlConverter(mainPart)
                converter.ParseHtml(htmlContent)
            End Using

            lblStatus.Text = "Conversion successful!"
        Catch ex As Exception
            lblStatus.Text = "Error: " & ex.Message
        End Try
    End Sub

btnConvert_Click

The Button That Actually Does Something

This is where the magic happens.
You click “Convert”, and it does the full workflow in two steps:

Dim htmlTemp = System.IO.Path.GetTempFileName() & ".html"
Dim xslt As New XslCompiledTransform()
xslt.Load(xsltPath)
xslt.Transform(xmlPath, htmlTemp)
Step 1 -> HTML

This uses good ol’ XslCompiledTransform to convert the XML using the XSLT you picked.
We save the result to a temp HTML file because nobody wants to clutter up C:\ just to feel powerful.

Optional: Save HTML Somewhere Nicer

If Not String.IsNullOrWhiteSpace(txtHtmlPath.Text) Then
    System.IO.File.Copy(htmlTemp, txtHtmlPath.Text, overwrite:=True)
End If

If you filled in the “Save HTML as...” box, we copy the temp file over to your desired path.

Why?
Because sometimes Word screws up the layout and you just want the raw HTML.
Also, maybe you’re weird like me and prefer HTML over .docx.

Step 2: HTML → DOCX
Dim htmlContent = System.IO.File.ReadAllText(htmlTemp)
Using doc = WordprocessingDocument.Create(outputDocx, WordprocessingDocumentType.Document)
    Dim mainPart = doc.AddMainDocumentPart()
    mainPart.Document = New DocumentFormat.OpenXml.Wordprocessing.Document(New DocumentFormat.OpenXml.Wordprocessing.Body())
    Dim converter As New HtmlConverter(mainPart)
    converter.ParseHtml(htmlContent)
End Using

ow we take that HTML and jam it into a Word file using HtmlToOpenXml.

  • It creates a new .docx

  • Adds the main document part

  • Parses your pretty HTML and makes it vaguely printable

And yes all of this works without Word installed.
Thanks OpenXML SDK.

Status Reporting

lblStatus.Text = "Conversion successful!" Yeeey it worked

lblStatus.Text = "Error: " & ex.Message No clue but something didn't cut it well not my problem.

Either way, you get feedback.
No spinning circles.

So What Is This XSL File?

And no it’s not “XLS”, unless you want Excel to open it and scream internally.

XSL stands for eXtensible Stylesheet Language.
It’s what tells the app:
“Here’s how to turn this XML tree into something vaguely human-readable.”

More precisely, we’re using XSLT the "T" stands for Transform because it lets you define rules that convert XML into HTML, plain text, or even another XML format.

Think of it like:

“Hey XML, every time you see a <step>, I want a row in a table with that stuff. Oh and wrap it in some <html><body> so browsers and Word don’t lose their minds.”

In short

  • you write rules once (in .xsl)

  • The app uses those rules to transform your config

  • It spits out clean, styled HTML

  • Then we convert that HTML into .docx using OpenXML

It’s like templating for structured data without the JS, without the browser, and without Excel trying to open the wrong file.

Template ( don't worry you can download it too)

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="html" encoding="UTF-8" indent="yes"/>

  <xsl:template match="/">
    <html>
      <head>
        <title><xsl:value-of select="/sequence/@name"/></title>
        <style>
          body { font-family: Arial, sans-serif; background: #f8f8f8; color: #333; }
          h1 { background: #444; color: #fff; padding: 10px; }
          h2 { color: #222; margin-top: 30px; }
          h3 { color: #444; margin-top: 15px; }
          .section, .group, .step { margin-left: 20px; padding: 5px; }
          .group { background: #eee; padding: 10px; margin: 10px 0; border-left: 4px solid #bbb; }
          .step { background: #fff; border: 1px solid #ccc; margin: 10px 0; padding: 10px; }
          .desc { font-size: 0.9em; color: #666; margin-bottom: 10px; display: block; }
          table { border-collapse: collapse; margin-top: 10px; width: 100%; }
          th, td { border: 1px solid #ccc; padding: 6px; text-align: left; vertical-align: top; }
          th { background: #ddd; }
          ul { margin-left: 20px; }
        </style>
      </head>
      <body>
        <h1><xsl:value-of select="/sequence/@name"/></h1>
        <p><b>Description:</b> <xsl:value-of select="/sequence/@description"/></p>
        <p><b>Author:</b> <xsl:value-of select="/sequence/@author"/></p>
        <p><b>Version:</b> <xsl:value-of select="/sequence/@version"/></p>

        <h2>Settings</h2>
        <div class="section">
          <p><b>AutoLogin User:</b> <xsl:value-of select="/sequence/globalVarList/autoLogin/alUserName"/></p>
          <p><b>AutoLogin Password:</b>
            <xsl:choose>
              <xsl:when test="string-length(/sequence/globalVarList/autoLogin/alPassword) > 0">[Set]</xsl:when>
              <xsl:otherwise>[Not Set]</xsl:otherwise>
            </xsl:choose>
          </p>

          <h3>Supported OS Entries</h3>
          <ul>
            <xsl:for-each select="/sequence/globalVarList/osCollection/osEntry">
              <li>
                <b><xsl:value-of select="@name"/></b>
                (<xsl:value-of select="Version"/> / ProductType <xsl:value-of select="ProductType"/>)
                <ul>
                  <xsl:for-each select="osEntry">
                    <li><xsl:value-of select="@name"/> (<xsl:value-of select="OSArchitecture"/>)</li>
                  </xsl:for-each>
                </ul>
              </li>
            </xsl:for-each>
          </ul>
        </div>

        <h2>Change Log</h2>
        <ul>
          <xsl:for-each select="/sequence/changeLog/log">
            <li>
              <b>Version <xsl:value-of select="@version"/>:</b>
              <pre><xsl:value-of select="."/></pre>
            </li>
          </xsl:for-each>
        </ul>

        <h2>Optimization Groups</h2>
        <xsl:apply-templates select="/sequence/group"/>
      </body>
    </html>
  </xsl:template>

  <!-- Recursive group handling -->
  <xsl:template match="group">
    <div class="group">
      <h3><xsl:value-of select="@name"/></h3>
      <span class="desc"><xsl:value-of select="@description"/></span>
      <xsl:apply-templates select="group|step"/>
    </div>
  </xsl:template>

  <!-- Step display with actions in a table -->
  <xsl:template match="step">
    <div class="step">
      <b><xsl:value-of select="@name"/></b>
      <span class="desc"><xsl:value-of select="@description"/></span>

      <xsl:if test="action">
        <table>
          <thead>
            <tr>
              <th>Type</th>
              <th>Command</th>
              <th>Parameters</th>
            </tr>
          </thead>
          <tbody>
            <xsl:for-each select="action">
              <tr>
                <td><xsl:value-of select="type"/></td>
                <td><xsl:value-of select="command"/></td>
                <td>
                  <xsl:for-each select="params/*">
                    <b><xsl:value-of select="name()"/>:</b>
                    <xsl:value-of select="."/><br/>
                  </xsl:for-each>
                </td>
              </tr>
            </xsl:for-each>
          </tbody>
        </table>
      </xsl:if>
    </div>
  </xsl:template>

</xsl:stylesheet>

What It Does

This XSLT takes the exported XML and generates clean, styled HTML that:

• Shows the optimization sequence metadata (name, author, version)
• Displays global variables like AutoLogin credentials
• Lists supported operating systems with version, type, and architecture
• Includes the full change log, with version notes
• Recursively walks through groups and nested groups
• Renders each step with:

  • Name & description

  • A table of actions: type, command, and all parameters

No JavaScript. No external files. Just raw HTML styled with embedded CSS.
And yes it works great as input for conversion to .docx later.

What’s in the XML and How the XSLT Handles It

XML SectionDescriptionHow XSLT Transforms It
<sequence> (root)Holds metadata and everything elseTitle (@name) and top-level info in <h1>
<sequence>@description/@author/@versionGeneral info about the templateRendered in a summary paragraph at the top
<globalVarList>Global environment variablesShows AutoLogin name + masked password info
<globalVarList>/osCollection/osEntryOS support info: name, version, architectureListed as bullet points under "Supported OS"
<changeLog> / <log>Version history, notesConverted into <ul> with version + content
<group>Optimization categories (can nest)Recursively rendered with <div class="group">
<step>Single configuration actionRendered in a white box with name + description
<action> inside <step>Specific command (e.g. registry edit)Displayed in a table: Type, Command, Parameters
<params> inside <action>List of key/value settings for the actionShown as bold labels inside the "Parameters" cell

Tested On

This was tested using the default optimization template for:

• Windows 10 (1809–22H2)
• Windows 11 (22H2)
• Windows Server 2019
• Windows Server 2022

If you're using a heavily customized OSOT export:
Run it through and see what happens.
Worst case: HTML spaghetti. Best case: instant documentation.

Final Thoughts

This tool won’t solve world hunger.
It won’t stop OSOT from generating XML that looks like it came from an AI trained on registry trees.
But it will help you stop apologizing every time you send someone your optimization config and they ask,

“Wait, what’s a <nodeId> doing in my face?”

With just a few clicks, you go from raw XML chaos to readable HTML and optionally a Word doc, if your manager insists on it being in .docx.

Feel free to:

• Try it
• Modify it
• Fork it
• Or just shamelessly steal the XSLT and say you built it I won’t judge.

At the very least, you’ll look organized.

And sometimes, in IT, that’s already half the win.

Here some bonus images showing the HTML and DOCX format

image.png

image.png

The word file even has headings for easy lookup :

image.png

About the Files

The files are a bit bigger this time because the source zip includes all required NuGet packages so you can compile it without digging through missing dependency hell.

In principle, just open the .sln (solution file) in Visual Studio and hit build.
If something still yells at you, make sure the following packages are properly referenced:

DocumentFormat.OpenXml
HtmlToOpenXml
• (Optional) System.Xml.Xsl – usually already available

Once those are sorted, you're good to go.

Source:
Source Zip

Template:
Template

Rather use power shell and find the HTML sufficient ?

$xslt = New-Object System.Xml.Xsl.XslCompiledTransform
$xslt.load('template.xslt')
$xslt.Transform('Source.xml, 'Export.html')

And a shoutout to _benwa: for the snippet in a reddit reply!

Enjoy

That’s it a simple tool to make OSOT exports actually human-readable (and optionally Word-friendly).
It’s not glamorous, but it gets the job done and sometimes, that’s exactly what we need.

As always, feedback is very much appreciated.
Did it work for your custom template? Did it break in a hilariously unhelpful way? Let me know.

Enjoy!

Mark

0
Subscribe to my newsletter

Read articles from Mark Platte directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mark Platte
Mark Platte

Mark Platte Born in January, 1984.I currently work as an IT architect in a hospital environment, with a career shaped by hands-on experience in application development, storage engineering, and infrastructure design. My roots are in software, but over time I moved deeper into system architecture, working closely with storage platforms, virtualization, and security especially in regulated and research-intensive environments. I have a strong focus on building stable, secure, and manageable IT solutions, particularly in complex environments where clinical systems, research data, and compliance requirements intersect. I’m especially experienced in enterprise storage design, backup strategies, and performance tuning, often acting as the bridge between engineering teams and long-term architectural planning. I enjoy solving difficult problems and still believe most issues in IT can be fixed with enough determination, focus, and sometimes budget. It’s that drive to find solutions that keeps me motivated.