Files
chipsec-chipsec/tests/xml_validator.py
Nathaniel Mitchell c35e42a6c0 Extend xml validator script to check for invisible characters (#2747)
* Extend xml validator script to check for invisible characters
* Extend cfg_checker to walk subdirs
* Fix iommu IRTA register size
2026-05-26 15:15:26 -07:00

271 lines
11 KiB
Python

# CHIPSEC: Platform Security Assessment Framework
# Copyright (c) 2025, Intel Corporation
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; Version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import re
from unicodedata import name as unicodename
import xml.etree.ElementTree as ET
from typing import Dict, List, Tuple
import os.path as op
import sys
from os import listdir
sys.path.append(op.abspath(op.join(__file__, "..", "..")))
from chipsec.library.file import get_main_dir
from chipsec.library.defines import is_hex
CFG_PATH = op.join(get_main_dir(), 'chipsec', 'cfg') # Where to look for config files
"""
Usage: python xml_validator.py [config_file1] [config_file2] ...
Run with no arguments to validate all XML config files in the default cfg directory.
"""
class ConfigAttributeValidator:
"""Validates XML configuration attributes against expected data types"""
def __init__(self):
# Define expected attribute types based on _config_convert_data function
self.integer_attrs = {'dev', 'fun', 'vid', 'did', 'rid', 'offset',
'bit', 'size', 'port', 'msr', 'value', 'address',
'fixed_address', 'base_align', 'align_bits', 'mask',
'reg_align', 'limit_align', 'regh_align', 'width', 'reg'}
self.boolean_attrs = {'req_pch'}
self.int_list_attrs = {'bus'}
self.str_list_attrs = {'config'}
self.range_list_attrs = {'detection_value'}
self.validation_errors = []
self.passed_file_count = 0
self.file_count = 0
# Regex matching invisible/non-printable Unicode characters:
# \x00-\x08\x0b\x0c\x0e-\x1f - C0 control chars (excluding \t \n \r)
# \x7f-\x9f - DEL + C1 control chars
# \xad - Soft hyphen
# \u200b-\u200f - Zero-width and directional marks
# \u2028-\u202f - Line/paragraph separators, embedding controls
# \u2060-\u206f - Word joiner, invisible operators
# \ufeff - BOM / zero-width no-break space
# \ufff0-\uffff - Specials block
self._invisible_re = re.compile(
r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\xad'
r'\u200b-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]'
)
def validate_integer_value(self, value: str) -> bool:
"""Validate if a string can be converted to integer (base 10 or 16)"""
try:
int(value, 0)
return True
except ValueError:
return False
def validate_boolean_value(self, value: str) -> bool:
"""Validate if a string represents a boolean value"""
return value.lower() in ('true', 'false')
def validate_integer_list(self, value: str) -> bool:
"""Validate comma-separated integer values"""
try:
for item in value.split(','):
int(item.strip(), 0)
return True
except ValueError:
return False
def validate_range_format(self, value: str) -> bool:
"""Validate range format (wildcards, ranges, single values)"""
try:
for item in value.split(','):
item = item.strip()
if item.upper().endswith('*'):
int(item.replace('*', '0'), 0)
elif '-' in item:
parts = item.split('-', 1)
if len(parts) != 2:
return False
int(parts[0], 0)
int(parts[1], 0)
else:
int(item, 0)
return True
except ValueError:
return False
def validate_attribute(self, attr_name: str, attr_value: str, element_tag: str, did_is_range: bool = False) -> bool:
"""Validate a single attribute based on its expected type"""
# Handle special case where 'did' can be range format
if did_is_range and attr_name == 'did':
if not self.validate_range_format(attr_value):
self.validation_errors.append(
f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'"
)
return False
elif attr_name in self.integer_attrs:
if not self.validate_integer_value(attr_value):
self.validation_errors.append(
f"Element '{element_tag}': Attribute '{attr_name}' must be integer: '{attr_value}'"
)
return False
elif attr_name in self.boolean_attrs:
if not self.validate_boolean_value(attr_value):
self.validation_errors.append(
f"Element '{element_tag}': Attribute '{attr_name}' must be boolean (true/false): '{attr_value}'"
)
return False
elif attr_name in self.int_list_attrs:
if not self.validate_integer_list(attr_value):
self.validation_errors.append(
f"Element '{element_tag}': Attribute '{attr_name}' must be comma-separated integers: '{attr_value}'"
)
return False
elif attr_name in self.range_list_attrs:
if not self.validate_range_format(attr_value):
self.validation_errors.append(
f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'"
)
return False
return True
def validate_no_invisible_chars(self, text: str, context: str) -> bool:
"""Check for invisible or non-printable Unicode characters in text"""
valid = True
for match in self._invisible_re.finditer(text):
ch = match.group()
pos = match.start()
char_name = unicodename(ch, f'U+{ord(ch):04X}')
self.validation_errors.append(
f"{context}: Invisible character '{char_name}' (U+{ord(ch):04X}) at position {pos}"
)
valid = False
return valid
def validate_xml_element(self, element: ET.Element, did_is_range: bool = False) -> bool:
"""Validate all attributes of an XML element"""
element_valid = True
for attr_name, attr_value in element.attrib.items():
if not self.validate_attribute(attr_name, attr_value, element.tag, did_is_range):
element_valid = False
if not self.validate_no_invisible_chars(
attr_value, f"Element '{element.tag}', attribute '{attr_name}'"
):
element_valid = False
if element.text and not self.validate_no_invisible_chars(
element.text, f"Element '{element.tag}' text content"
):
element_valid = False
if element.tail and not self.validate_no_invisible_chars(
element.tail, f"Element '{element.tag}' tail text"
):
element_valid = False
return element_valid
def validate_xml_file(self, file_path: str) -> Tuple[bool, List[str]]:
"""Validate an entire XML configuration file"""
self.validation_errors = []
try:
tree = ET.parse(file_path)
root = tree.getroot()
except ET.ParseError as e:
self.validation_errors.append(f"XML parsing error in '{file_path}': {e}")
return False, self.validation_errors
except FileNotFoundError:
self.validation_errors.append(f"File not found: '{file_path}'")
return False, self.validation_errors
file_valid = True
# Validate different element types with appropriate settings
for element in root.iter():
if element.tag in ('device', 'sku'):
# These elements can have 'did' as range
if not self.validate_xml_element(element, did_is_range=True):
file_valid = False
else:
if not self.validate_xml_element(element):
file_valid = False
return file_valid, self.validation_errors
def print_validation_report(self, file_path: str):
"""Print a validation report for a configuration file"""
is_valid, errors = self.validate_xml_file(file_path)
self.file_count += 1
message = "\t"
if is_valid:
message += f"✓ Configuration file is valid for: {file_path}"
self.passed_file_count += 1
else:
message += f"✗ Found {len(errors)} validation errors in {file_path}:"
for error in errors:
message += f"\n\t- {error}"
print(message)
def print_summary(self):
"""Print a summary of validation results"""
print(f"\nValidation Summary: {self.passed_file_count}/{self.file_count} files passed validation.")
def validate_configuration_files(file_paths: List[str]) -> Dict[str, bool]:
"""Validate multiple configuration files and return results"""
validator = ConfigAttributeValidator()
results = {}
print("Validation Report:\n")
for file_path in file_paths:
is_valid, _ = validator.validate_xml_file(file_path)
results[file_path] = is_valid
validator.print_validation_report(file_path)
validator.print_summary()
return results
def find_xml_files(paths: List[str]) -> List[str]:
files = []
dirs = []
for path in paths:
for cfg_file in listdir(path):
filepath = op.join(path, cfg_file)
if op.isdir(filepath):
dirs.append(filepath)
if filepath.endswith('.xml'):
files.append(filepath)
if dirs:
files.extend(find_xml_files(dirs))
return files
def find_base_config_dirs() -> List[str]:
vid_list = [f for f in listdir(CFG_PATH) if op.isdir(op.join(CFG_PATH, f)) and is_hex(f)]
base_dirs = [op.join(CFG_PATH, vid) for vid in vid_list]
return base_dirs
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
results = validate_configuration_files(find_xml_files(find_base_config_dirs()))
else:
results = validate_configuration_files(sys.argv[1:])
if all(results.values()):
sys.exit(0)
else:
sys.exit(1)