From 175e0a2495e2c4a2f5680eab31943ab5a63a0940 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Tue, 21 Apr 2026 23:57:12 +0800 Subject: [PATCH 01/35] Annotate SonarCloud security hotspots with NOSONAR justifications --- automation_file/local/tar_ops.py | 7 +++++-- automation_file/remote/ftp/client.py | 6 +++++- tests/test_checksum.py | 4 +++- tests/test_cross_backend.py | 5 ++++- tests/test_http_client.py | 2 +- tests/test_tar_ops.py | 9 ++++++--- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/automation_file/local/tar_ops.py b/automation_file/local/tar_ops.py index 672e74e..36ab503 100644 --- a/automation_file/local/tar_ops.py +++ b/automation_file/local/tar_ops.py @@ -57,7 +57,8 @@ def create_tar( target_path.parent.mkdir(parents=True, exist_ok=True) try: - with tarfile.open(str(target_path), mode) as archive: + # Write mode — not extraction. + with tarfile.open(str(target_path), mode) as archive: # NOSONAR python:S5042 archive.add(str(src_path), arcname=src_path.name) except (OSError, tarfile.TarError) as err: raise TarException(f"create_tar failed: {err}") from err @@ -76,7 +77,9 @@ def extract_tar(source: str, target_dir: str) -> list[str]: extracted: list[str] = [] try: - with tarfile.open(str(src_path), "r:*") as archive: + # _verify_members rejects traversal / escaping symlinks / hardlinks before any + # extract, and PEP 706 filter="data" is applied when available (3.10.12+ / 3.11.4+ / 3.12+). + with tarfile.open(str(src_path), "r:*") as archive: # NOSONAR python:S5042 _verify_members(archive, dest) for member in archive.getmembers(): if _TAR_FILTER_SUPPORTED: diff --git a/automation_file/remote/ftp/client.py b/automation_file/remote/ftp/client.py index 142f826..984687f 100644 --- a/automation_file/remote/ftp/client.py +++ b/automation_file/remote/ftp/client.py @@ -42,7 +42,11 @@ def __init__(self) -> None: def later_init(self, options: FTPConnectOptions | None = None, **kwargs: Any) -> FTP: """Open an FTP control connection. TLS is negotiated when ``tls=True``.""" opts = options if options is not None else FTPConnectOptions(**kwargs) - ftp: FTP = FTP_TLS(timeout=opts.timeout) if opts.tls else FTP(timeout=opts.timeout) + # Plaintext FTP is opt-in via tls=False; FTPS is the default when tls=True. + if opts.tls: + ftp: FTP = FTP_TLS(timeout=opts.timeout) + else: + ftp = FTP(timeout=opts.timeout) # NOSONAR python:S5332 try: ftp.connect(opts.host, opts.port, timeout=opts.timeout) if opts.tls and isinstance(ftp, FTP_TLS): diff --git a/tests/test_checksum.py b/tests/test_checksum.py index 781cfbf..4b3b885 100644 --- a/tests/test_checksum.py +++ b/tests/test_checksum.py @@ -35,7 +35,9 @@ def test_file_checksum_streams_large_file(tmp_path: Path) -> None: def test_file_checksum_md5(tmp_path: Path) -> None: target = tmp_path / "a.bin" target.write_bytes(b"hi") - assert file_checksum(target, algorithm="md5") == hashlib.md5(b"hi").hexdigest() + # Verifies the library accepts any hashlib algorithm — not a security use of MD5. + expected = hashlib.md5(b"hi").hexdigest() # NOSONAR python:S4790 + assert file_checksum(target, algorithm="md5") == expected def test_file_checksum_unknown_algorithm(tmp_path: Path) -> None: diff --git a/tests/test_cross_backend.py b/tests/test_cross_backend.py index 870fcae..2e71aec 100644 --- a/tests/test_cross_backend.py +++ b/tests/test_cross_backend.py @@ -42,8 +42,11 @@ def test_missing_local_source_returns_false(tmp_path: Path) -> None: def test_unknown_source_scheme_raises() -> None: + # The target path is unused — the call must fail on the source scheme + # before touching the filesystem. + unused_target = "/tmp/x" # NOSONAR python:S5443 with pytest.raises(CrossBackendException): - copy_between("gopher://a/b", "/tmp/x") + copy_between("gopher://a/b", unused_target) def test_unknown_target_scheme_raises(tmp_path: Path) -> None: diff --git a/tests/test_http_client.py b/tests/test_http_client.py index d8ff381..fe9d248 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -18,7 +18,7 @@ def _ensure_echo_registered() -> None: def _base_url(server) -> str: host, port = server.server_address - return f"http://{host}:{port}" + return f"http://{host}:{port}" # NOSONAR python:S5332 — loopback test server; TLS not in scope. def test_client_executes_action_round_trip() -> None: diff --git a/tests/test_tar_ops.py b/tests/test_tar_ops.py index ec76159..c58178b 100644 --- a/tests/test_tar_ops.py +++ b/tests/test_tar_ops.py @@ -39,7 +39,8 @@ def test_create_and_extract_gz(sample_dir: Path, tmp_path: Path) -> None: def test_create_uncompressed(sample_dir: Path, tmp_path: Path) -> None: archive = tmp_path / "plain.tar" create_tar(str(sample_dir), str(archive), compression=None) - with tarfile.open(str(archive), "r") as tf: + # Reading an archive we just wrote in this test — not untrusted input. + with tarfile.open(str(archive), "r") as tf: # NOSONAR python:S5042 assert any(name.endswith("a.txt") for name in tf.getnames()) @@ -72,7 +73,8 @@ def test_extract_missing_archive_raises(tmp_path: Path) -> None: def test_extract_rejects_path_traversal(tmp_path: Path) -> None: archive = tmp_path / "evil.tar" - with tarfile.open(str(archive), "w") as tf: + # Write mode; fixture builds a malicious archive to exercise the guard. + with tarfile.open(str(archive), "w") as tf: # NOSONAR python:S5042 info = tarfile.TarInfo(name="../escape.txt") info.size = 0 tf.addfile(info, None) @@ -83,7 +85,8 @@ def test_extract_rejects_path_traversal(tmp_path: Path) -> None: def test_extract_rejects_absolute_symlink(tmp_path: Path) -> None: archive = tmp_path / "evil.tar" - with tarfile.open(str(archive), "w") as tf: + # Write mode; fixture builds a malicious archive to exercise the guard. + with tarfile.open(str(archive), "w") as tf: # NOSONAR python:S5042 info = tarfile.TarInfo(name="link") info.type = tarfile.SYMTYPE info.linkname = "/etc/passwd" From 132264ae1c573199d16504ec49ca3783744b0590 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 22 Apr 2026 00:20:42 +0800 Subject: [PATCH 02/35] Add Traditional and Simplified Chinese Sphinx docs Mirror the English tree under docs/source.zh-TW and docs/source.zh-CN with translated index, architecture, and usage pages; keep automodule directives identical so docstring content flows through unchanged. Add html-zh-TW / html-zh-CN / html-all targets to Makefile and make.bat, and a language switcher line in all three index.rst files. --- docs/Makefile | 10 +- docs/make.bat | 18 + docs/source.zh-CN/api/client.rst | 5 + docs/source.zh-CN/api/core.rst | 65 ++++ docs/source.zh-CN/api/index.rst | 18 + docs/source.zh-CN/api/local.rst | 29 ++ docs/source.zh-CN/api/notify.rst | 8 + docs/source.zh-CN/api/progress.rst | 11 + docs/source.zh-CN/api/project.rst | 8 + docs/source.zh-CN/api/remote.rst | 145 ++++++++ docs/source.zh-CN/api/scheduler.rst | 16 + docs/source.zh-CN/api/server.rst | 14 + docs/source.zh-CN/api/trigger.rst | 12 + docs/source.zh-CN/api/ui.rst | 81 ++++ docs/source.zh-CN/api/utils.rst | 11 + docs/source.zh-CN/architecture.rst | 253 +++++++++++++ docs/source.zh-CN/conf.py | 54 +++ docs/source.zh-CN/index.rst | 45 +++ docs/source.zh-CN/usage.rst | 557 ++++++++++++++++++++++++++++ docs/source.zh-TW/api/client.rst | 5 + docs/source.zh-TW/api/core.rst | 65 ++++ docs/source.zh-TW/api/index.rst | 18 + docs/source.zh-TW/api/local.rst | 29 ++ docs/source.zh-TW/api/notify.rst | 8 + docs/source.zh-TW/api/progress.rst | 11 + docs/source.zh-TW/api/project.rst | 8 + docs/source.zh-TW/api/remote.rst | 145 ++++++++ docs/source.zh-TW/api/scheduler.rst | 16 + docs/source.zh-TW/api/server.rst | 14 + docs/source.zh-TW/api/trigger.rst | 12 + docs/source.zh-TW/api/ui.rst | 81 ++++ docs/source.zh-TW/api/utils.rst | 11 + docs/source.zh-TW/architecture.rst | 253 +++++++++++++ docs/source.zh-TW/conf.py | 54 +++ docs/source.zh-TW/index.rst | 45 +++ docs/source.zh-TW/usage.rst | 557 ++++++++++++++++++++++++++++ docs/source/index.rst | 2 + 37 files changed, 2693 insertions(+), 1 deletion(-) create mode 100644 docs/source.zh-CN/api/client.rst create mode 100644 docs/source.zh-CN/api/core.rst create mode 100644 docs/source.zh-CN/api/index.rst create mode 100644 docs/source.zh-CN/api/local.rst create mode 100644 docs/source.zh-CN/api/notify.rst create mode 100644 docs/source.zh-CN/api/progress.rst create mode 100644 docs/source.zh-CN/api/project.rst create mode 100644 docs/source.zh-CN/api/remote.rst create mode 100644 docs/source.zh-CN/api/scheduler.rst create mode 100644 docs/source.zh-CN/api/server.rst create mode 100644 docs/source.zh-CN/api/trigger.rst create mode 100644 docs/source.zh-CN/api/ui.rst create mode 100644 docs/source.zh-CN/api/utils.rst create mode 100644 docs/source.zh-CN/architecture.rst create mode 100644 docs/source.zh-CN/conf.py create mode 100644 docs/source.zh-CN/index.rst create mode 100644 docs/source.zh-CN/usage.rst create mode 100644 docs/source.zh-TW/api/client.rst create mode 100644 docs/source.zh-TW/api/core.rst create mode 100644 docs/source.zh-TW/api/index.rst create mode 100644 docs/source.zh-TW/api/local.rst create mode 100644 docs/source.zh-TW/api/notify.rst create mode 100644 docs/source.zh-TW/api/progress.rst create mode 100644 docs/source.zh-TW/api/project.rst create mode 100644 docs/source.zh-TW/api/remote.rst create mode 100644 docs/source.zh-TW/api/scheduler.rst create mode 100644 docs/source.zh-TW/api/server.rst create mode 100644 docs/source.zh-TW/api/trigger.rst create mode 100644 docs/source.zh-TW/api/ui.rst create mode 100644 docs/source.zh-TW/api/utils.rst create mode 100644 docs/source.zh-TW/architecture.rst create mode 100644 docs/source.zh-TW/conf.py create mode 100644 docs/source.zh-TW/index.rst create mode 100644 docs/source.zh-TW/usage.rst diff --git a/docs/Makefile b/docs/Makefile index 01e66b5..b304cc6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = _build -.PHONY: help html clean +.PHONY: help html clean html-zh-TW html-zh-CN html-all help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) @@ -12,5 +12,13 @@ help: html: @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) +html-zh-TW: + @$(SPHINXBUILD) -b html source.zh-TW "$(BUILDDIR)/html-zh-TW" $(SPHINXOPTS) + +html-zh-CN: + @$(SPHINXBUILD) -b html source.zh-CN "$(BUILDDIR)/html-zh-CN" $(SPHINXOPTS) + +html-all: html html-zh-TW html-zh-CN + clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) diff --git a/docs/make.bat b/docs/make.bat index 45d7073..cf6d346 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -18,9 +18,27 @@ if errorlevel 9009 ( exit /b 1 ) +if "%1" == "html-zh-TW" goto build-zh-TW +if "%1" == "html-zh-CN" goto build-zh-CN +if "%1" == "html-all" goto build-all + %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end +:build-zh-TW +%SPHINXBUILD% -b html source.zh-TW %BUILDDIR%\html-zh-TW %SPHINXOPTS% %O% +goto end + +:build-zh-CN +%SPHINXBUILD% -b html source.zh-CN %BUILDDIR%\html-zh-CN %SPHINXOPTS% %O% +goto end + +:build-all +%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -b html source.zh-TW %BUILDDIR%\html-zh-TW %SPHINXOPTS% %O% +%SPHINXBUILD% -b html source.zh-CN %BUILDDIR%\html-zh-CN %SPHINXOPTS% %O% +goto end + :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% diff --git a/docs/source.zh-CN/api/client.rst b/docs/source.zh-CN/api/client.rst new file mode 100644 index 0000000..36ab808 --- /dev/null +++ b/docs/source.zh-CN/api/client.rst @@ -0,0 +1,5 @@ +Client SDK +========== + +.. automodule:: automation_file.client.http_client + :members: diff --git a/docs/source.zh-CN/api/core.rst b/docs/source.zh-CN/api/core.rst new file mode 100644 index 0000000..f91dfdc --- /dev/null +++ b/docs/source.zh-CN/api/core.rst @@ -0,0 +1,65 @@ +Core +==== + +.. automodule:: automation_file.core.action_registry + :members: + +.. automodule:: automation_file.core.action_executor + :members: + +.. automodule:: automation_file.core.dag_executor + :members: + +.. automodule:: automation_file.core.callback_executor + :members: + +.. automodule:: automation_file.core.package_loader + :members: + +.. automodule:: automation_file.core.json_store + :members: + +.. automodule:: automation_file.core.retry + :members: + +.. automodule:: automation_file.core.quota + :members: + +.. automodule:: automation_file.core.checksum + :members: + +.. automodule:: automation_file.core.manifest + :members: + +.. automodule:: automation_file.core.fim + :members: + +.. automodule:: automation_file.core.audit + :members: + +.. automodule:: automation_file.core.crypto + :members: + +.. automodule:: automation_file.core.metrics + :members: + +.. automodule:: automation_file.core.substitution + :members: + +.. automodule:: automation_file.core.config_watcher + :members: + +.. automodule:: automation_file.core.plugins + :members: + +.. automodule:: automation_file.core.config + :members: + +.. automodule:: automation_file.core.secrets + :members: + +.. automodule:: automation_file.exceptions + :members: + +.. automodule:: automation_file.logging_config + :members: diff --git a/docs/source.zh-CN/api/index.rst b/docs/source.zh-CN/api/index.rst new file mode 100644 index 0000000..672f7a3 --- /dev/null +++ b/docs/source.zh-CN/api/index.rst @@ -0,0 +1,18 @@ +API 参考 +======== + +.. toctree:: + :maxdepth: 2 + + core + local + remote + server + client + trigger + scheduler + notify + progress + project + ui + utils diff --git a/docs/source.zh-CN/api/local.rst b/docs/source.zh-CN/api/local.rst new file mode 100644 index 0000000..2b41168 --- /dev/null +++ b/docs/source.zh-CN/api/local.rst @@ -0,0 +1,29 @@ +本地操作 +======== + +.. automodule:: automation_file.local.file_ops + :members: + +.. automodule:: automation_file.local.dir_ops + :members: + +.. automodule:: automation_file.local.zip_ops + :members: + +.. automodule:: automation_file.local.sync_ops + :members: + +.. automodule:: automation_file.local.safe_paths + :members: + +.. automodule:: automation_file.local.shell_ops + :members: + +.. automodule:: automation_file.local.tar_ops + :members: + +.. automodule:: automation_file.local.json_edit + :members: + +.. automodule:: automation_file.local.conditional + :members: diff --git a/docs/source.zh-CN/api/notify.rst b/docs/source.zh-CN/api/notify.rst new file mode 100644 index 0000000..15b4aa1 --- /dev/null +++ b/docs/source.zh-CN/api/notify.rst @@ -0,0 +1,8 @@ +通知 +==== + +.. automodule:: automation_file.notify.sinks + :members: + +.. automodule:: automation_file.notify.manager + :members: diff --git a/docs/source.zh-CN/api/progress.rst b/docs/source.zh-CN/api/progress.rst new file mode 100644 index 0000000..e7a9306 --- /dev/null +++ b/docs/source.zh-CN/api/progress.rst @@ -0,0 +1,11 @@ +进度与取消 +========== + +传输的可选仪表化。对 :func:`~automation_file.download_file`、 +:func:`s3_upload_file` 或 :func:`s3_download_file` 传入 +``progress_name="