///|
let word_content_types : Array[String] = [
docm_ct, docx_ct, vba_ct, word_vba_data_ct, "application/vnd.openxmlformats-package.relationships+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml",
"application/vnd.openxmlformats-officedocument.theme+xml", "application/vnd.openxmlformats-package.core-properties+xml",
"application/vnd.openxmlformats-officedocument.extended-properties+xml",
]
///|
let word_relationship_types : Array[String] = [
office_rel + "officeDocument",
office_rel + "styles",
office_rel + "settings",
office_rel + "fontTable",
office_rel + "webSettings",
office_rel + "theme",
office_rel + "extended-properties",
vba_rel,
word_vba_data_rel,
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
]
///|
let word_attribute_names : Array[String] = [
"space", "val", "rsidR", "rsidRPr", "rsidRDefault", "rsidP", "rsidDel", "rsidSect",
"w", "h", "orient", "top", "right", "bottom", "left", "header", "footer", "gutter",
"num", "equalWidth", "sep", "linePitch", "charSpace", "type", "ascii", "hAnsi",
"cs", "eastAsia", "asciiTheme", "hAnsiTheme", "cstheme", "eastAsiaTheme", "hint",
"bidi", "firstLine", "hanging", "before", "after", "line", "lineRule", "styleId",
"default", "customStyle", "name", "count", "defLockedState", "defUIPriority", "defSemiHidden",
"defUnhideWhenUsed", "defQFormat", "locked", "uiPriority", "semiHidden", "unhideWhenUsed",
"qFormat", "percent", "spelling", "grammar", "bg1", "t1", "bg2", "t2", "accent1",
"accent2", "accent3", "accent4", "accent5", "accent6", "hyperlink", "followedHyperlink",
"usb0", "usb1", "usb2", "usb3", "csb0", "csb1", "clear",
]
///|
fn controlled_vba_parts(pkg : Package) -> Array[String] {
let result : Array[String] = []
for finding in pkg.audit.findings {
result.push(finding.part)
if is_word_format(pkg.audit.format) {
let companion = relationship_part(finding.part)
if pkg.archive.get(companion[:]) is Some(_) {
result.push(companion)
for
i in pkg.graph.graph.nodes[pkg.graph.node_index[finding.part]].outbound {
result.push(pkg.audit.relationships[i].resolved)
}
}
}
}
result.sort()
result
}
///|
fn validate_word_profile(pkg : Package) -> Unit raise {
let report = pkg.audit
guard pkg.graph.graph.coverage_gaps.length() == 0 else {
raise Refused(
"Word graph coverage gap: " + pkg.graph.graph.coverage_gaps[0],
)
}
for r in report.relationships {
guard !r.external && word_relationship_types.contains(r.kind) else {
raise Refused("external or unsupported Word relationship")
}
let target = report.parts[pkg.graph.part_index[r.resolved]]
let expected = if r.kind == vba_rel {
target.content_type == vba_ct
} else if r.kind == word_vba_data_rel {
target.content_type == word_vba_data_ct
} else if r.kind == office_rel + "officeDocument" {
target.content_type == docm_ct || target.content_type == docx_ct
} else if r.kind == office_rel + "theme" {
target.content_type ==
"application/vnd.openxmlformats-officedocument.theme+xml"
} else if r.kind == office_rel + "extended-properties" {
target.content_type ==
"application/vnd.openxmlformats-officedocument.extended-properties+xml"
} else if r.kind.has_suffix("/core-properties") {
target.content_type ==
"application/vnd.openxmlformats-package.core-properties+xml"
} else {
target.content_type ==
"application/vnd.openxmlformats-officedocument.wordprocessingml." +
r.kind[office_rel.length():].to_owned() +
"+xml"
}
guard expected else {
raise Refused("Word relationship/content-type conflict")
}
if r.kind == word_vba_data_rel {
guard report.findings.any(fn(f) { f.part == r.source }) else {
raise Refused("Word VBA metadata not owned by a declared VBA project")
}
} else if [
office_rel + "officeDocument",
office_rel + "extended-properties",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
].contains(r.kind) {
guard r.source == "/" else {
raise Refused("Word package metadata source mismatch")
}
} else {
guard r.source == report.main_part else {
raise Refused("unsupported Word relationship source")
}
}
}
guard report.findings.length() <= 1 else {
raise Refused("multiple Word VBA projects unsupported")
}
for f in report.findings {
guard report.format == "DOCM" else {
raise Refused("VBA in non-macro Word main type")
}
let incoming = graph_incoming(pkg.graph, f.part)
guard incoming.length() == 1 &&
incoming[0].source == report.main_part &&
incoming[0].kind == vba_rel else {
raise Refused("shared/orphan Word VBA or conflicting evidence")
}
guard get_part(pkg.archive, f.part).has_prefix(
b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1",
) else {
raise Refused("Word VBA payload is not CFB")
}
let outgoing = pkg.graph.graph.nodes[pkg.graph.node_index[f.part]].outbound
guard outgoing.length() <= 1 else {
raise Refused("unsupported Word VBA dependency set")
}
for i in outgoing {
let relation = report.relationships[i]
guard relation.kind == word_vba_data_rel && !relation.external else {
raise Refused("unsupported Word VBA companion")
}
let owners = graph_incoming(pkg.graph, relation.resolved)
guard owners.length() == 1 && owners[0].source == f.part else {
raise Refused("shared Word VBA metadata")
}
guard pkg.archive.get(relationship_part(relation.resolved)[:]) is None else {
raise Refused("nested Word VBA metadata dependencies unsupported")
}
}
guard !pkg.graph.graph.xml_references.any(fn(reference) {
let r = report.relationships[reference.relationship_index]
r.resolved == f.part ||
outgoing.any(fn(i) { report.relationships[i].resolved == r.resolved })
}) else {
raise Refused("retained XML references Word VBA controlled parts")
}
}
let removed = controlled_vba_parts(pkg)
for part in report.parts {
if part.name == "[Content_Types].xml" {
continue
}
guard word_content_types.contains(part.content_type) else {
raise Refused("unknown Word part content type")
}
if part.content_type == vba_ct {
continue
}
if part.content_type == word_vba_data_ct {
guard removed.contains(part.name) else {
raise Refused("orphan Word VBA supporting metadata")
}
}
if part.name.has_suffix(".rels") {
guard part.content_type ==
"application/vnd.openxmlformats-package.relationships+xml" else {
raise Refused("Word relationship part content type mismatch")
}
continue
}
if part.content_type.contains(".theme+") ||
part.content_type.contains("core-properties") ||
part.content_type.contains("extended-properties") {
validate_static_xml_part(pkg, part)
} else {
validate_word_xml_part(pkg, part)
}
}
}
///|
fn validate_word_xml_part(pkg : Package, part : PartRecord) -> Unit raise {
guard pkg.xml_nodes.get(part.name) is Some(nodes) else {
raise Refused("Word XML payload name unsupported")
}
let root = if part.name == pkg.audit.main_part {
"document"
} else if part.content_type == word_vba_data_ct {
"vbaSuppData"
} else if part.content_type.contains(".styles+") {
"styles"
} else if part.content_type.contains(".settings+") {
"settings"
} else if part.content_type.contains(".fontTable+") {
"fonts"
} else {
"webSettings"
}
guard nodes[0].element.name.local_name == root else {
raise Refused("Word XML root/content type mismatch")
}
let parents : Array[String] = []
for node in nodes {
let ns = node.element.name.namespace_uri.unwrap_or("")
let name = node.element.name.local_name
while parents.length() > node.depth {
ignore(parents.pop())
}
let parent = if parents.length() > 0 {
parents[parents.length() - 1]
} else {
""
}
let permitted = if root == "document" {
ns == word_ns &&
(match name {
"document" => node.depth == 0
"body" => parent == "document"
"p" | "sectPr" => parent == "body"
"r" | "pPr" => parent == "p"
"t" | "rPr" | "br" | "cr" | "tab" => parent == "r"
"b" | "i" | "u" | "sz" | "szCs" | "rFonts" | "lang" | "color" =>
parent == "rPr"
"spacing"
| "jc"
| "ind"
| "pStyle"
| "keepNext"
| "keepLines"
| "widowControl" => parent == "pPr"
"pgSz" | "pgMar" | "cols" | "docGrid" => parent == "sectPr"
_ => false
})
} else if root == "vbaSuppData" {
ns == word_macro_ns &&
(match name {
"vbaSuppData" => node.depth == 0
"mcds" => parent == "vbaSuppData"
"mcd" => parent == "mcds"
_ => false
})
} else if root == "settings" {
if ns == word_math_ns {
(name == "mathPr" && parent == "settings") ||
(
parent == "mathPr" &&
[
"mathFont", "brkBin", "brkBinSub", "smallFrac", "dispDef", "lMargin",
"rMargin", "defJc", "wrapIndent", "intLim", "naryLim",
].contains(name)
)
} else if ns == word_office_ns {
(
parent == "shapeDefaults" &&
["shapedefaults", "shapelayout"].contains(name)
) ||
(name == "idmap" && parent == "shapelayout")
} else if ns == word_ns {
(name == "settings" && node.depth == 0) ||
(
parent == "settings" &&
[
"zoom", "proofState", "defaultTabStop", "characterSpacingControl", "compat",
"rsids", "themeFontLang", "clrSchemeMapping", "shapeDefaults", "decimalSymbol",
"listSeparator",
].contains(name)
) ||
(parent == "rsids" && ["rsidRoot", "rsid"].contains(name))
} else {
false
}
} else if root == "fonts" {
ns == word_ns &&
(
(name == "fonts" && node.depth == 0) ||
(name == "font" && parent == "fonts") ||
(
parent == "font" &&
["panose1", "charset", "family", "pitch", "sig", "altName"].contains(
name,
)
)
)
} else if root == "webSettings" {
ns == word_ns &&
(
(name == "webSettings" && node.depth == 0) ||
(name == "optimizeForBrowser" && parent == "webSettings")
)
} else {
ns == word_ns &&
[
"styles", "docDefaults", "rPrDefault", "pPrDefault", "rPr", "pPr", "latentStyles",
"lsdException", "style", "name", "basedOn", "next", "link", "uiPriority",
"semiHidden", "unhideWhenUsed", "qFormat", "rsid", "rFonts", "lang", "sz",
"szCs", "spacing", "tblPr", "tblInd", "tblCellMar", "top", "left", "bottom",
"right", "b", "i", "u", "color", "jc", "ind", "keepNext", "keepLines", "widowControl",
].contains(name)
}
guard permitted else {
raise Refused("unsupported Word XML feature or placement: " + name)
}
parents.push(name)
for a in node.element.attributes {
let ans = a.name.namespace_uri.unwrap_or("")
if ns == word_office_ns {
guard (
ans == "urn:schemas-microsoft-com:vml" &&
a.name.local_name == "ext" &&
a.value == "edit"
) ||
(
ans == "" &&
(
(name == "shapedefaults" && a.name.local_name == "spidmax") ||
(name == "idmap" && a.name.local_name == "data")
)
) else {
raise Refused("unsupported Office layout attribute")
}
} else {
guard ans == ns ||
(
ans == "http://www.w3.org/XML/1998/namespace" &&
a.name.local_name == "space" &&
name == "t"
) else {
raise Refused("unsupported Word XML attribute namespace")
}
guard !["id", "embed", "link"].contains(a.name.local_name) ||
(ns == word_ns && root == "styles" && name == "link") else {
raise Refused("unsupported Word relationship attribute")
}
let permitted_attribute = if ns == word_macro_ns {
["macroName", "name", "bEncrypt", "cmg"].contains(a.name.local_name)
} else if ns == word_math_ns {
a.name.local_name == "val"
} else {
word_attribute_names.contains(a.name.local_name)
}
guard permitted_attribute else {
raise Refused("unsupported Word attribute: " + a.name.local_name)
}
}
}
}
}