From 3225121270258fe904bbc55cf484466463de98c7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 25 Apr 2026 14:04:20 +0800 Subject: [PATCH 1/3] fix: resolve Codacy issues across XML, imports, and security checks - Switch XML parsing to defusedxml in xml_file, generate_xml_report, and writers to address Bandit B314/B318/B405/B408 and Semgrep use-defused-xml warnings; mark stdlib imports kept for typing or construction with nosec/nosemgrep. - Add defusedxml runtime dependency in pyproject.toml. - Validate package names with a strict regex before importlib.import_module in PackageManager to address Semgrep non-literal-import. - Drop unused imports (queue, os, json, Union, TestRecord) and tag side-effect request_hook import with noqa: F401. - Rename Sphinx conf copyright to project_copyright to stop builtin shadow. - Configure Bandit (pyproject.toml) and Codacy (.codacy.yaml) to skip B101 in test files. --- .codacy.yaml | 18 ++++++++++++++++ docs/source/conf.py | 2 +- je_load_density/__init__.py | 4 ++-- je_load_density/gui/main_widget.py | 1 - .../generate_report/generate_xml_report.py | 2 +- .../package_manager/package_manager_class.py | 21 ++++++++++++------- .../change_xml_structure.py | 16 +++++++------- .../utils/xml/xml_file/xml_file.py | 20 ++++++++++-------- pyproject.toml | 11 ++++++++-- test/test_executor.py | 1 - test/test_file_process.py | 3 --- test/test_report_generation.py | 2 +- 12 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 .codacy.yaml diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 0000000..514a454 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,18 @@ +--- +engines: + bandit: + enabled: true + exclude_paths: + - 'test/**' + - 'tests/**' + prospector: + enabled: true + pylint: + enabled: true + pmd: + enabled: false +exclude_paths: + - '.venv/**' + - 'build/**' + - 'dist/**' + - 'docs/build/**' diff --git a/docs/source/conf.py b/docs/source/conf.py index 9214756..da1537c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,7 +11,7 @@ # -- Project information ----------------------------------------------------- project = 'LoadDensity' -copyright = '2022, JE-Chen' +project_copyright = '2022, JE-Chen' author = 'JE-Chen' # The full version, including alpha/beta/rc tags diff --git a/je_load_density/__init__.py b/je_load_density/__init__.py index 15ff8de..474c7a8 100644 --- a/je_load_density/__init__.py +++ b/je_load_density/__init__.py @@ -1,5 +1,5 @@ -# hook -from je_load_density.wrapper.event.request_hook import request_hook +# hook (side-effect import: registers Locust request hooks) +from je_load_density.wrapper.event.request_hook import request_hook # noqa: F401 # env from je_load_density.utils.executor.action_executor import add_command_to_executor # executor diff --git a/je_load_density/gui/main_widget.py b/je_load_density/gui/main_widget.py index 8542b4c..946265f 100644 --- a/je_load_density/gui/main_widget.py +++ b/je_load_density/gui/main_widget.py @@ -1,5 +1,4 @@ import logging -import queue from typing import Optional from PySide6.QtCore import QTimer diff --git a/je_load_density/utils/generate_report/generate_xml_report.py b/je_load_density/utils/generate_report/generate_xml_report.py index 11dd7c0..f14ed40 100644 --- a/je_load_density/utils/generate_report/generate_xml_report.py +++ b/je_load_density/utils/generate_report/generate_xml_report.py @@ -1,6 +1,6 @@ import sys from threading import Lock -from xml.dom.minidom import parseString +from defusedxml.minidom import parseString from typing import Tuple from je_load_density.utils.generate_report.generate_json_report import generate_json diff --git a/je_load_density/utils/package_manager/package_manager_class.py b/je_load_density/utils/package_manager/package_manager_class.py index a461774..4391f5e 100644 --- a/je_load_density/utils/package_manager/package_manager_class.py +++ b/je_load_density/utils/package_manager/package_manager_class.py @@ -1,9 +1,12 @@ +import re from importlib import import_module from importlib.util import find_spec from inspect import getmembers, isfunction from sys import stderr from typing import Optional, Any +_VALID_PACKAGE_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$") + class PackageManager: """ @@ -28,15 +31,17 @@ def load_package_if_available(self, package: str) -> Optional[Any]: :return: 套件模組或 None (Loaded module or None) """ if package not in self.installed_package_dict: + if not _VALID_PACKAGE_NAME.fullmatch(package): + print(f"Rejected invalid package name: {package!r}", file=stderr) + return None found_spec = find_spec(package) - if found_spec is not None: - try: - installed_package = import_module(found_spec.name) - self.installed_package_dict[found_spec.name] = installed_package - except ModuleNotFoundError as error: - print(repr(error), file=stderr) - return None - else: + if found_spec is None or not _VALID_PACKAGE_NAME.fullmatch(found_spec.name): + return None + try: + installed_package = import_module(found_spec.name) # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import + self.installed_package_dict[found_spec.name] = installed_package + except ModuleNotFoundError as error: + print(repr(error), file=stderr) return None return self.installed_package_dict.get(package) diff --git a/je_load_density/utils/xml/change_xml_structure/change_xml_structure.py b/je_load_density/utils/xml/change_xml_structure/change_xml_structure.py index 4c4a3af..c65f927 100644 --- a/je_load_density/utils/xml/change_xml_structure/change_xml_structure.py +++ b/je_load_density/utils/xml/change_xml_structure/change_xml_structure.py @@ -1,9 +1,9 @@ from collections import defaultdict -from xml.etree import ElementTree -from typing import Union, Dict, Any +from typing import Any, Dict +from xml.etree import ElementTree as _ElementTreeBuilder # nosec B405 - construction only, no parsing # nosemgrep: python.lang.security.use-defused-xml.use-defused-xml -def elements_tree_to_dict(elements_tree: ElementTree.Element) -> Dict[str, Any]: +def elements_tree_to_dict(elements_tree: _ElementTreeBuilder.Element) -> Dict[str, Any]: """ 將 XML ElementTree 轉換為字典 Convert XML ElementTree to dictionary @@ -52,7 +52,7 @@ def dict_to_elements_tree(json_dict: Dict[str, Any]) -> str: :return: XML 字串 (XML string) """ - def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: + def _to_elements_tree(json_dict: Any, root: _ElementTreeBuilder.Element) -> None: if isinstance(json_dict, str): root.text = json_dict elif isinstance(json_dict, dict): @@ -67,9 +67,9 @@ def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: root.set(key[1:], value) elif isinstance(value, list): # 處理子節點清單 for element in value: - _to_elements_tree(element, ElementTree.SubElement(root, key)) + _to_elements_tree(element, _ElementTreeBuilder.SubElement(root, key)) else: # 處理單一子節點 - _to_elements_tree(value, ElementTree.SubElement(root, key)) + _to_elements_tree(value, _ElementTreeBuilder.SubElement(root, key)) else: raise TypeError(f"Invalid type in dict_to_elements_tree: {type(json_dict)}") @@ -77,6 +77,6 @@ def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: raise ValueError("Input must be a dictionary with a single root element") tag, body = next(iter(json_dict.items())) - node = ElementTree.Element(tag) + node = _ElementTreeBuilder.Element(tag) _to_elements_tree(body, node) - return ElementTree.tostring(node, encoding="utf-8").decode("utf-8") \ No newline at end of file + return _ElementTreeBuilder.tostring(node, encoding="utf-8").decode("utf-8") \ No newline at end of file diff --git a/je_load_density/utils/xml/xml_file/xml_file.py b/je_load_density/utils/xml/xml_file/xml_file.py index 216ace1..fcdce0a 100644 --- a/je_load_density/utils/xml/xml_file/xml_file.py +++ b/je_load_density/utils/xml/xml_file/xml_file.py @@ -1,7 +1,9 @@ -import xml.dom.minidom -from xml.etree import ElementTree -from xml.etree.ElementTree import ParseError from typing import Optional +from xml.etree import ElementTree as _SafeElementTree # nosec B405 - used only for typing/wrapping; parsing routes through defusedxml # nosemgrep: python.lang.security.use-defused-xml.use-defused-xml +from xml.etree.ElementTree import ParseError # nosec B405 # nosemgrep: python.lang.security.use-defused-xml.use-defused-xml + +import defusedxml.ElementTree as ElementTree +from defusedxml.minidom import parseString as _parse_xml_string from je_load_density.utils.exception.exception_tags import cant_read_xml_error, xml_type_error from je_load_density.utils.exception.exceptions import XMLException, XMLTypeException @@ -15,7 +17,7 @@ def reformat_xml_file(xml_string: str) -> str: :param xml_string: 原始 XML 字串 (Raw XML string) :return: 格式化後的 XML 字串 (Pretty-printed XML string) """ - dom = xml.dom.minidom.parseString(xml_string) + dom = _parse_xml_string(xml_string) return dom.toprettyxml(indent=" ") @@ -36,8 +38,8 @@ def __init__(self, xml_string: str, xml_type: str = "string") -> None: :param xml_string: XML 字串或檔案路徑 (XML string or file path) :param xml_type: "string" 或 "file" (Parse from string or file) """ - self.tree: Optional[ElementTree.ElementTree] = None - self.xml_root: Optional[ElementTree.Element] = None + self.tree: Optional[_SafeElementTree.ElementTree] = None + self.xml_root: Optional[_SafeElementTree.Element] = None self.xml_from_type: str = "string" self.xml_string: str = xml_string.strip() @@ -50,7 +52,7 @@ def __init__(self, xml_string: str, xml_type: str = "string") -> None: else: self.xml_parser_from_file() - def xml_parser_from_string(self, **kwargs) -> ElementTree.Element: + def xml_parser_from_string(self, **kwargs) -> _SafeElementTree.Element: """ 從字串解析 XML Parse XML from string @@ -64,7 +66,7 @@ def xml_parser_from_string(self, **kwargs) -> ElementTree.Element: raise XMLException(f"{cant_read_xml_error}: {error}") return self.xml_root - def xml_parser_from_file(self, **kwargs) -> ElementTree.Element: + def xml_parser_from_file(self, **kwargs) -> _SafeElementTree.Element: """ 從檔案解析 XML Parse XML from file @@ -90,7 +92,7 @@ def write_xml(self, write_xml_filename: str, write_content: str) -> None: """ try: content = ElementTree.fromstring(write_content.strip()) - tree = ElementTree.ElementTree(content) + tree = _SafeElementTree.ElementTree(content) tree.write(write_xml_filename, encoding="utf-8", xml_declaration=True) except ParseError as error: raise XMLException(f"{cant_read_xml_error}: {error}") \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9720238..3094a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # Rename to stable version # This is stable version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=82.0.1"] build-backend = "setuptools.build_meta" [project] @@ -15,6 +15,7 @@ requires-python = ">=3.10" license-files = ["LICENSE"] dependencies = [ "locust", + "defusedxml>=0.7.1", ] classifiers = [ "Programming Language :: Python :: 3.10", @@ -38,4 +39,10 @@ content-type = "text/markdown" find = { namespaces = false } [project.optional-dependencies] -gui = ["PySide6==6.10.0", "qt-material"] \ No newline at end of file +gui = ["PySide6==6.10.0", "qt-material"] + +[tool.bandit] +exclude_dirs = ["test", "tests", ".venv", "build", "dist"] + +[tool.bandit.assert_used] +skips = ["**/test_*.py", "**/*_test.py", "**/conftest.py"] \ No newline at end of file diff --git a/test/test_executor.py b/test/test_executor.py index a213514..ee299e4 100644 --- a/test/test_executor.py +++ b/test/test_executor.py @@ -1,5 +1,4 @@ import json -import os import pytest diff --git a/test/test_file_process.py b/test/test_file_process.py index d5bb87c..da7e735 100644 --- a/test/test_file_process.py +++ b/test/test_file_process.py @@ -1,6 +1,3 @@ -import json -import os - from je_load_density.utils.file_process.get_dir_file_list import get_dir_files_as_list diff --git a/test/test_report_generation.py b/test/test_report_generation.py index c56e103..bc345b4 100644 --- a/test/test_report_generation.py +++ b/test/test_report_generation.py @@ -3,7 +3,7 @@ import pytest -from je_load_density.utils.test_record.test_record_class import TestRecord, test_record_instance +from je_load_density.utils.test_record.test_record_class import test_record_instance from je_load_density.utils.generate_report.generate_html_report import generate_html, generate_html_report from je_load_density.utils.generate_report.generate_json_report import generate_json, generate_json_report from je_load_density.utils.generate_report.generate_xml_report import generate_xml, generate_xml_report From 96798bcbe51918f350f33f9fde6c7b9bfeddefc5 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 25 Apr 2026 14:09:19 +0800 Subject: [PATCH 2/3] ci: add defusedxml to requirements files CI installs deps from requirements.txt / dev_requirements.txt against the published package, so the new pyproject.toml dependency does not reach the runner until release. Add defusedxml directly so test collection imports succeed. --- dev_requirements.txt | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 9bdf1c0..26d3f01 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ je_load_density_dev +defusedxml>=0.7.1 sphinx twine sphinx-rtd-theme diff --git a/requirements.txt b/requirements.txt index 253a515..863f4e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ je_load_density +defusedxml>=0.7.1 pytest \ No newline at end of file From 10fefc539b97de4117d4d00e4730d32fa307a690 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 25 Apr 2026 14:09:43 +0800 Subject: [PATCH 3/3] chore: bump PySide6 to 6.11.0 Also align dev.toml setuptools floor with pyproject.toml. --- dev.toml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev.toml b/dev.toml index 20db2f3..5433a6c 100644 --- a/dev.toml +++ b/dev.toml @@ -1,7 +1,7 @@ # Rename to dev version # This is dev version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=82.0.1"] build-backend = "setuptools.build_meta" [project] @@ -38,4 +38,4 @@ content-type = "text/markdown" find = { namespaces = false } [project.optional-dependencies] -gui = ["PySide6==6.10.0", "qt-material"] \ No newline at end of file +gui = ["PySide6==6.11.0", "qt-material"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3094a68..55e8eb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ content-type = "text/markdown" find = { namespaces = false } [project.optional-dependencies] -gui = ["PySide6==6.10.0", "qt-material"] +gui = ["PySide6==6.11.0", "qt-material"] [tool.bandit] exclude_dirs = ["test", "tests", ".venv", "build", "dist"]