O O OSOT, What Did You Do?!


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 fileOptionally, 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 HTMLHtmlToOpenXml
+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
pathBrowse buttons
One glorious "Convert" button
A status label that tries to be encouraging and not panic too hard when things break
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:
Control | Name | Description |
TextBox | txtXmlPath | Path to the XML file |
Button | btnBrowseXml | Opens a file dialog for XML |
TextBox | txtXsltPath | Path to the XSLT file |
Button | btnBrowseXslt | Opens a file dialog for XSLT |
TextBox | txtOutputPath | Path to save .docx |
Button | btnBrowseOut | Opens a Save dialog for .docx |
TextBox | txtHtmlPath | Optional: Save intermediate HTML |
Button | btnOpenHtmlPath | Browse for HTML output file |
Button | btnConvert | Converts everything |
Label | lblStatus | Shows 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 Section | Description | How XSLT Transforms It |
<sequence> (root) | Holds metadata and everything else | Title (@name ) and top-level info in <h1> |
<sequence>@description/@author/@version | General info about the template | Rendered in a summary paragraph at the top |
<globalVarList> | Global environment variables | Shows AutoLogin name + masked password info |
<globalVarList>/osCollection/osEntry | OS support info: name, version, architecture | Listed as bullet points under "Supported OS" |
<changeLog> / <log> | Version history, notes | Converted into <ul> with version + content |
<group> | Optimization categories (can nest) | Recursively rendered with <div class="group"> |
<step> | Single configuration action | Rendered 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 action | Shown 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
The word file even has headings for easy lookup :
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
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.