|
| 1 | +# CHIPSEC: Platform Security Assessment Framework |
| 2 | +# Copyright (c) 2025, Intel Corporation |
| 3 | +# |
| 4 | +# This program is free software; you can redistribute it and/or |
| 5 | +# modify it under the terms of the GNU General Public License |
| 6 | +# as published by the Free Software Foundation; Version 2. |
| 7 | +# |
| 8 | +# This program is distributed in the hope that it will be useful, |
| 9 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +# GNU General Public License for more details. |
| 12 | + |
| 13 | +import xml.etree.ElementTree as ET |
| 14 | +from typing import Dict, List, Tuple |
| 15 | +import os.path as op |
| 16 | +import sys |
| 17 | +from os import listdir |
| 18 | + |
| 19 | +sys.path.append(op.abspath(op.join(__file__, "..", ".."))) |
| 20 | +from chipsec.library.file import get_main_dir |
| 21 | +from chipsec.library.defines import is_hex |
| 22 | + |
| 23 | +CFG_PATH = op.join(get_main_dir(), 'chipsec', 'cfg') # Where to look for config files |
| 24 | + |
| 25 | +""" |
| 26 | +Usage: python xml_validator.py [config_file1] [config_file2] ... |
| 27 | +Run with no arguments to vlidatate all XML config files in the default cfg directory. |
| 28 | +""" |
| 29 | + |
| 30 | +class ConfigAttributeValidator: |
| 31 | + """Validates XML configuration attributes against expected data types""" |
| 32 | + |
| 33 | + def __init__(self): |
| 34 | + # Define expected attribute types based on _config_convert_data function |
| 35 | + self.integer_attrs = {'dev', 'fun', 'vid', 'did', 'rid', 'offset', |
| 36 | + 'bit', 'size', 'port', 'msr', 'value', 'address', |
| 37 | + 'fixed_address', 'base_align', 'align_bits', 'mask', |
| 38 | + 'reg_align', 'limit_align', 'regh_align', 'width', 'reg'} |
| 39 | + |
| 40 | + self.boolean_attrs = {'req_pch'} |
| 41 | + self.int_list_attrs = {'bus'} |
| 42 | + self.str_list_attrs = {'config'} |
| 43 | + self.range_list_attrs = {'detection_value'} |
| 44 | + self.validation_errors = [] |
| 45 | + self.passed_file_count = 0 |
| 46 | + self.file_count = 0 |
| 47 | + |
| 48 | + |
| 49 | + def validate_integer_value(self, value: str) -> bool: |
| 50 | + """Validate if a string can be converted to integer (base 10 or 16)""" |
| 51 | + try: |
| 52 | + int(value, 0) |
| 53 | + return True |
| 54 | + except ValueError: |
| 55 | + return False |
| 56 | + |
| 57 | + def validate_boolean_value(self, value: str) -> bool: |
| 58 | + """Validate if a string represents a boolean value""" |
| 59 | + return value.lower() in ('true', 'false') |
| 60 | + |
| 61 | + def validate_integer_list(self, value: str) -> bool: |
| 62 | + """Validate comma-separated integer values""" |
| 63 | + try: |
| 64 | + for item in value.split(','): |
| 65 | + int(item.strip(), 0) |
| 66 | + return True |
| 67 | + except ValueError: |
| 68 | + return False |
| 69 | + |
| 70 | + def validate_range_format(self, value: str) -> bool: |
| 71 | + """Validate range format (wildcards, ranges, single values)""" |
| 72 | + try: |
| 73 | + for item in value.split(','): |
| 74 | + item = item.strip() |
| 75 | + if item.upper().endswith('*'): |
| 76 | + int(item.replace('*', '0'), 0) |
| 77 | + elif '-' in item: |
| 78 | + parts = item.split('-', 1) |
| 79 | + if len(parts) != 2: |
| 80 | + return False |
| 81 | + int(parts[0], 0) |
| 82 | + int(parts[1], 0) |
| 83 | + else: |
| 84 | + int(item, 0) |
| 85 | + return True |
| 86 | + except ValueError: |
| 87 | + return False |
| 88 | + |
| 89 | + def validate_attribute(self, attr_name: str, attr_value: str, element_tag: str, did_is_range: bool = False) -> bool: |
| 90 | + """Validate a single attribute based on its expected type""" |
| 91 | + # Handle special case where 'did' can be range format |
| 92 | + if did_is_range and attr_name == 'did': |
| 93 | + if not self.validate_range_format(attr_value): |
| 94 | + self.validation_errors.append( |
| 95 | + f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'" |
| 96 | + ) |
| 97 | + return False |
| 98 | + elif attr_name in self.integer_attrs: |
| 99 | + if not self.validate_integer_value(attr_value): |
| 100 | + self.validation_errors.append( |
| 101 | + f"Element '{element_tag}': Attribute '{attr_name}' must be integer: '{attr_value}'" |
| 102 | + ) |
| 103 | + return False |
| 104 | + elif attr_name in self.boolean_attrs: |
| 105 | + if not self.validate_boolean_value(attr_value): |
| 106 | + self.validation_errors.append( |
| 107 | + f"Element '{element_tag}': Attribute '{attr_name}' must be boolean (true/false): '{attr_value}'" |
| 108 | + ) |
| 109 | + return False |
| 110 | + elif attr_name in self.int_list_attrs: |
| 111 | + if not self.validate_integer_list(attr_value): |
| 112 | + self.validation_errors.append( |
| 113 | + f"Element '{element_tag}': Attribute '{attr_name}' must be comma-separated integers: '{attr_value}'" |
| 114 | + ) |
| 115 | + return False |
| 116 | + elif attr_name in self.range_list_attrs: |
| 117 | + if not self.validate_range_format(attr_value): |
| 118 | + self.validation_errors.append( |
| 119 | + f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'" |
| 120 | + ) |
| 121 | + return False |
| 122 | + |
| 123 | + return True |
| 124 | + |
| 125 | + def validate_xml_element(self, element: ET.Element, did_is_range: bool = False) -> bool: |
| 126 | + """Validate all attributes of an XML element""" |
| 127 | + element_valid = True |
| 128 | + |
| 129 | + for attr_name, attr_value in element.attrib.items(): |
| 130 | + if not self.validate_attribute(attr_name, attr_value, element.tag, did_is_range): |
| 131 | + element_valid = False |
| 132 | + |
| 133 | + return element_valid |
| 134 | + |
| 135 | + def validate_xml_file(self, file_path: str) -> Tuple[bool, List[str]]: |
| 136 | + """Validate an entire XML configuration file""" |
| 137 | + self.validation_errors = [] |
| 138 | + |
| 139 | + try: |
| 140 | + tree = ET.parse(file_path) |
| 141 | + root = tree.getroot() |
| 142 | + except ET.ParseError as e: |
| 143 | + self.validation_errors.append(f"XML parsing error in '{file_path}': {e}") |
| 144 | + return False, self.validation_errors |
| 145 | + except FileNotFoundError: |
| 146 | + self.validation_errors.append(f"File not found: '{file_path}'") |
| 147 | + return False, self.validation_errors |
| 148 | + |
| 149 | + file_valid = True |
| 150 | + |
| 151 | + # Validate different element types with appropriate settings |
| 152 | + for element in root.iter(): |
| 153 | + if element.tag in ('device', 'sku'): |
| 154 | + # These elements can have 'did' as range |
| 155 | + if not self.validate_xml_element(element, did_is_range=True): |
| 156 | + file_valid = False |
| 157 | + else: |
| 158 | + if not self.validate_xml_element(element): |
| 159 | + file_valid = False |
| 160 | + |
| 161 | + return file_valid, self.validation_errors |
| 162 | + |
| 163 | + def print_validation_report(self, file_path: str): |
| 164 | + """Print a validation report for a configuration file""" |
| 165 | + is_valid, errors = self.validate_xml_file(file_path) |
| 166 | + self.file_count += 1 |
| 167 | + message = "\t" |
| 168 | + if is_valid: |
| 169 | + message += f"✓ Configuration file is valid for: {file_path}" |
| 170 | + self.passed_file_count += 1 |
| 171 | + else: |
| 172 | + message += f"✗ Found {len(errors)} validation errors in {file_path}:" |
| 173 | + for error in errors: |
| 174 | + message += f"\n\t- {error}" |
| 175 | + print(message) |
| 176 | + |
| 177 | + def print_summary(self): |
| 178 | + """Print a summary of validation results""" |
| 179 | + print(f"\nValidation Summary: {self.passed_file_count}/{self.file_count} files passed validation.") |
| 180 | + |
| 181 | + |
| 182 | +def validate_configuration_files(file_paths: List[str]) -> Dict[str, bool]: |
| 183 | + """Validate multiple configuration files and return results""" |
| 184 | + validator = ConfigAttributeValidator() |
| 185 | + results = {} |
| 186 | + print("Validation Report:\n") |
| 187 | + for file_path in file_paths: |
| 188 | + is_valid, _ = validator.validate_xml_file(file_path) |
| 189 | + results[file_path] = is_valid |
| 190 | + validator.print_validation_report(file_path) |
| 191 | + validator.print_summary() |
| 192 | + return results |
| 193 | + |
| 194 | + |
| 195 | +def find_xml_files(paths: List[str]) -> List[str]: |
| 196 | + files = [] |
| 197 | + dirs = [] |
| 198 | + for path in paths: |
| 199 | + for cfg_file in listdir(path): |
| 200 | + filepath = op.join(path, cfg_file) |
| 201 | + if op.isdir(filepath): |
| 202 | + dirs.append(filepath) |
| 203 | + if filepath.endswith('.xml'): |
| 204 | + files.append(filepath) |
| 205 | + if dirs: |
| 206 | + files.extend(find_xml_files(dirs)) |
| 207 | + return files |
| 208 | + |
| 209 | + |
| 210 | +def find_base_config_dirs() -> List[str]: |
| 211 | + vid_list = [f for f in listdir(CFG_PATH) if op.isdir(op.join(CFG_PATH, f)) and is_hex(f)] |
| 212 | + base_dirs = [op.join(CFG_PATH, vid) for vid in vid_list] |
| 213 | + return base_dirs |
| 214 | + |
| 215 | + |
| 216 | +if __name__ == "__main__": |
| 217 | + import sys |
| 218 | + |
| 219 | + if len(sys.argv) < 2: |
| 220 | + results = validate_configuration_files(find_xml_files(find_base_config_dirs())) |
| 221 | + else: |
| 222 | + results = validate_configuration_files(sys.argv[1:]) |
| 223 | + |
| 224 | + if all(results.values()): |
| 225 | + sys.exit(0) |
| 226 | + else: |
| 227 | + sys.exit(1) |
0 commit comments