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/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/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/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..55e8eb6 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.11.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/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 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