fix(intunewin): align Detection.xml with IntuneWinAppUtil.exe reference format

Four structural defects in the generated Detection.xml caused Intune's upload
wizard to silently fail metadata parsing (empty form, OK button greyed):

1. Missing ToolVersion="1.8.6.0" XML attribute on ApplicationInfo — the wizard
   uses this to validate the package was produced by a compatible tool.
2. Spurious xmlns="http://schemas.microsoft.com/IntuneWin" namespace — changes
   element identity for Intune's XML parser (reference emits no namespace).
3. <?xml version="1.0" ?> declaration header — reference uses OmitXmlDeclaration=true.
4. Extra <MacAlgorithm> child element inside EncryptionInfo — not present in
   the reference FileEncryptionInfo model (svrooij/ContentPrep verified).

Fix: switched from toprettyxml() to tostring(xml_declaration=False)+indent(),
added ToolVersion attribute, removed xmlns and MacAlgorithm.
Tests updated to assert the corrected reference format; all 114 pass.

Root cause verified against svrooij/ContentPrep Packager.cs + ApplicationInfo.cs
(open-source C# reference implementation of IntuneWinAppUtil.exe).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:48:36 +02:00
co-authored by Claude Sonnet 4.6
parent 46cfde00e0
commit 77162466d5
2 changed files with 59 additions and 32 deletions
+36 -25
View File
@@ -2,7 +2,13 @@
Byte-level validation tests for the .intunewin file format.
These tests serve as the format specification: if they pass, the byte layout is correct.
The only remaining validation is a real Intune upload (manual, Phase 5 gate).
The only remaining validation is a real Intune upload (manual, Phase 10 gate).
Detection.xml format follows the IntuneWinAppUtil.exe reference exactly:
- ToolVersion is an XML *attribute* on <ApplicationInfo> (not a child element)
- No xmlns namespace (reference uses [XmlRoot("ApplicationInfo")] with no Namespace param)
- No <?xml?> declaration header (reference uses OmitXmlDeclaration=true)
- No MacAlgorithm element (not present in reference FileEncryptionInfo model)
"""
import base64
import hashlib
@@ -16,10 +22,7 @@ import pytest
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from imptune.generators.intunewin_builder import build_intunewin
NAMESPACE = "http://schemas.microsoft.com/IntuneWin"
from imptune.generators.intunewin_builder import _TOOL_VERSION, build_intunewin
@pytest.fixture
@@ -59,30 +62,22 @@ def package_contents(built_package):
def _get_xml_text(tree, tag):
"""Get text of a direct child element (with namespace)."""
elem = tree.find(f"{{{NAMESPACE}}}{tag}")
if elem is None:
# Try without namespace as fallback
elem = tree.find(tag)
"""Get text of a direct child element (no namespace — reference omits xmlns)."""
elem = tree.find(tag)
return elem.text if elem is not None else None
def _get_encryption_info(tree):
"""Get EncryptionInfo sub-element."""
enc = tree.find(f"{{{NAMESPACE}}}EncryptionInfo")
if enc is None:
enc = tree.find("EncryptionInfo")
return enc
"""Get EncryptionInfo sub-element (no namespace — reference omits xmlns)."""
return tree.find("EncryptionInfo")
def _get_enc_text(tree, tag):
"""Get text of a child of EncryptionInfo."""
"""Get text of a child of EncryptionInfo (no namespace)."""
enc = _get_encryption_info(tree)
if enc is None:
return None
elem = enc.find(f"{{{NAMESPACE}}}{tag}")
if elem is None:
elem = enc.find(tag)
elem = enc.find(tag)
return elem.text if elem is not None else None
@@ -108,32 +103,48 @@ class TestOuterZipStructure:
class TestDetectionXml:
def test_detection_xml_valid(self, package_contents):
"""Detection.xml is valid XML with ApplicationInfo root element in the correct namespace."""
"""Detection.xml is valid XML with ApplicationInfo root element and ToolVersion attribute.
Reference format: <ApplicationInfo ToolVersion="1.8.6.0"> with NO xmlns namespace.
Presence of xmlns would change element identity for Intune's XML parser, causing
silent metadata-parse failure in the upload wizard.
"""
tree = package_contents["tree"]
assert tree.tag == f"{{{NAMESPACE}}}ApplicationInfo", (
f"Root element must be ApplicationInfo with namespace {NAMESPACE}, got {tree.tag}"
assert tree.tag == "ApplicationInfo", (
f"Root element must be plain 'ApplicationInfo' (no xmlns namespace), got {tree.tag!r}"
)
assert tree.get("ToolVersion") == _TOOL_VERSION, (
f"ApplicationInfo must have ToolVersion attribute = {_TOOL_VERSION!r}, "
f"got {tree.get('ToolVersion')!r}"
)
def test_detection_xml_fields(self, package_contents):
"""Detection.xml contains all required elements including all 8 EncryptionInfo sub-elements."""
"""Detection.xml contains all required elements matching the reference FileEncryptionInfo model.
The reference model has 7 EncryptionInfo sub-elements (MacAlgorithm is NOT present).
"""
tree = package_contents["tree"]
# Direct children
for field in ("Name", "UnencryptedContentSize", "FileName", "SetupFile"):
assert _get_xml_text(tree, field) is not None, f"Missing field: {field}"
# EncryptionInfo sub-elements (all 8 required)
# EncryptionInfo sub-elements — 7 required (MacAlgorithm absent per reference schema)
for field in (
"EncryptionKey",
"MacKey",
"InitializationVector",
"Mac",
"MacAlgorithm",
"ProfileIdentifier",
"FileDigest",
"FileDigestAlgorithm",
):
assert _get_enc_text(tree, field) is not None, f"Missing EncryptionInfo/{field}"
# MacAlgorithm must NOT be present (not in reference FileEncryptionInfo model)
assert _get_enc_text(tree, "MacAlgorithm") is None, (
"EncryptionInfo/MacAlgorithm must NOT be present — not in reference schema"
)
def test_setup_file_in_detection_xml(self, package_contents):
"""SetupFile element matches the setup_file argument passed to build_intunewin."""
tree = package_contents["tree"]