diff --git a/.github/workflows/code-checkers.yml b/.github/workflows/code-checkers.yml index 495ebab..e8e621d 100644 --- a/.github/workflows/code-checkers.yml +++ b/.github/workflows/code-checkers.yml @@ -72,7 +72,7 @@ jobs: uv run semgrep scan \ --config p/default \ --config p/security-audit \ - --config semgrep.yml \ + --config .semgrep \ --error \ --exclude-rule python.flask.security.audit.directly-returned-format-string.directly-returned-format-string \ --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag diff --git a/.semgrep/python-enforce-class-pascal-case.yaml b/.semgrep/python-enforce-class-pascal-case.yaml new file mode 100644 index 0000000..13a09ce --- /dev/null +++ b/.semgrep/python-enforce-class-pascal-case.yaml @@ -0,0 +1,14 @@ +rules: + - id: python-enforce-class-pascal-case + languages: [python] + severity: WARNING + message: "Class `$CLASS` must respect PascalCase style." + + patterns: + - pattern: | + class $CLASS(...): + ... + + - metavariable-regex: + metavariable: $CLASS + regex: '^(?!_?([A-Z][a-z0-9]+)+$)' diff --git a/.semgrep/python-enforce-enum-uppercase-members.yaml b/.semgrep/python-enforce-enum-uppercase-members.yaml new file mode 100644 index 0000000..ee83307 --- /dev/null +++ b/.semgrep/python-enforce-enum-uppercase-members.yaml @@ -0,0 +1,30 @@ +rules: + - id: python-enforce-enum-uppercase-members + languages: [python] + severity: WARNING + message: "Enum member `$MEMBER` must respect UPPER_CASE style." + patterns: + - pattern-either: + - pattern: "$MEMBER = $VAL" + - pattern: "$MEMBER: $TYPE = $VAL" + + - metavariable-regex: + metavariable: $MEMBER + regex: '^(?![A-Z][A-Z0-9_]*$)' + + - pattern-either: + - pattern-inside: | + class $ENUM(..., Enum, ...): + ... + - pattern-inside: | + class $ENUM(..., IntEnum, ...): + ... + - pattern-inside: | + class $ENUM(..., StrEnum, ...): + ... + - pattern-inside: | + class $FLAG(..., Flag, ...): + ... + - pattern-inside: | + class $FLAG(..., IntFlag, ...): + ... diff --git a/.semgrep/python-enforce-exec-path-prefix.yaml b/.semgrep/python-enforce-exec-path-prefix.yaml new file mode 100644 index 0000000..4ca2c64 --- /dev/null +++ b/.semgrep/python-enforce-exec-path-prefix.yaml @@ -0,0 +1,17 @@ +rules: + - id: python-enforce-exec-path-prefix + languages: [python] + severity: WARNING + message: "Executable path variable must use `EXEC_PATH` or `_EXEC_PATH` prefix." + patterns: + - pattern-either: + - pattern: "$VAR = \"$PATH\"" + - pattern: "$VAR: $TYPE = \"$PATH\"" + + - metavariable-regex: + metavariable: $PATH + regex: "^/(usr/)?(bin|sbin)/.+" + + - metavariable-regex: + metavariable: $VAR + regex: "^(?!(?:_)?EXEC_PATH)" diff --git a/.semgrep/python-enforce-file-variable-suffix.yaml b/.semgrep/python-enforce-file-variable-suffix.yaml new file mode 100644 index 0000000..86d3788 --- /dev/null +++ b/.semgrep/python-enforce-file-variable-suffix.yaml @@ -0,0 +1,39 @@ +rules: + - id: python-enforce-file-variable-suffix + languages: [python] + severity: WARNING + message: "File object `$FILE` must be named `file` or end with `_file`." + patterns: + - pattern-either: + - pattern: | + with open(...) as $FILE: + ... + - pattern: | + with ..., open(...) as $FILE, ...: + ... + - pattern: | + with Path(...).open(...) as $FILE: + ... + - pattern: | + with ..., Path(...).open(...) as $FILE, ...: + ... + - pattern: $FILE = open(...) + - pattern: $FILE = Path(...).open(...) + - patterns: + - pattern-either: + - pattern: | + with $PATH.open(...) as $FILE: + ... + - pattern: | + with ..., $PATH.open(...) as $FILE, ...: + ... + - pattern: $FILE = $PATH.open(...) + - metavariable-regex: + metavariable: $PATH + regex: '^(.*[._])?path$' + + - focus-metavariable: $FILE + + - metavariable-regex: + metavariable: $FILE + regex: '^(?!(file|\w+_file)$)' diff --git a/.semgrep/python-forbid-direct-logging-import.yaml b/.semgrep/python-forbid-direct-logging-import.yaml new file mode 100644 index 0000000..2be6b59 --- /dev/null +++ b/.semgrep/python-forbid-direct-logging-import.yaml @@ -0,0 +1,13 @@ +rules: + - id: python-forbid-direct-logging-import + languages: [python] + severity: WARNING + message: "Import from `xcp_storage.log`, not from `logging`." + paths: + exclude: + - src/xcp_storage/log.py + pattern-either: + - pattern: import logging + - pattern: import logging.$X + - pattern: from logging import $X + - pattern: from logging.$X import $Y diff --git a/.semgrep/python-forbid-direct-typing-import.yaml b/.semgrep/python-forbid-direct-typing-import.yaml new file mode 100644 index 0000000..e729414 --- /dev/null +++ b/.semgrep/python-forbid-direct-typing-import.yaml @@ -0,0 +1,13 @@ +rules: + - id: python-forbid-direct-typing-import + languages: [python] + severity: WARNING + message: "Import from `xcp_storage.typing`, not from `typing` or `typing_extensions`." + paths: + exclude: + - src/xcp_storage/typing/ + pattern-either: + - pattern: from typing import $X + - pattern: from typing_extensions import $X + - pattern: import typing + - pattern: import typing_extensions diff --git a/.semgrep/python-forbid-import-of-from-only-modules.yaml b/.semgrep/python-forbid-import-of-from-only-modules.yaml new file mode 100644 index 0000000..629fa82 --- /dev/null +++ b/.semgrep/python-forbid-import-of-from-only-modules.yaml @@ -0,0 +1,19 @@ +rules: + - id: python-forbid-import-of-from-only-modules + languages: [python] + severity: WARNING + message: "This module is always imported with `from X import ...`, not with `import X`." + pattern-regex: |- + (?x) + ^[ \t]*import[ \t]+( + abc + | cryptography(\.\w+)* + | dataclasses + | enum + | functools + | pathlib + | types + | tests(\.\w+)+ + | unittest\.mock + | xcp_storage\.(?!log\b)(?!rpc\.(api|modules)\b)\w+(\.\w+)* + )\b diff --git a/.semgrep/python-forbid-subprocess-usage.yaml b/.semgrep/python-forbid-subprocess-usage.yaml new file mode 100644 index 0000000..b597211 --- /dev/null +++ b/.semgrep/python-forbid-subprocess-usage.yaml @@ -0,0 +1,26 @@ +rules: + - id: python-forbid-subprocess-usage + languages: [python] + severity: WARNING + message: "Run commands with `run_command` from `xcp_storage.utils.process`, not directly with `subprocess` or `os`." + paths: + include: + - src/ + exclude: + - src/xcp_storage/utils/process.py + pattern-either: + - pattern: import subprocess + - pattern: import subprocess.$X + - pattern: from subprocess import $X + - pattern: from subprocess.$X import $Y + - pattern: pty.spawn(...) + - patterns: + - pattern: os.$FUNC(...) + - metavariable-regex: + metavariable: $FUNC + regex: '^(system|popen|exec.*|spawn.*|posix_spawn.*)$' + - patterns: + - pattern: asyncio.$FUNC(...) + - metavariable-regex: + metavariable: $FUNC + regex: '^create_subprocess_(exec|shell)$' diff --git a/semgrep.yml b/.semgrep/python-missing-final-on-constants.yaml similarity index 100% rename from semgrep.yml rename to .semgrep/python-missing-final-on-constants.yaml diff --git a/pyproject.toml b/pyproject.toml index 3b2072c..c414659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,6 +191,43 @@ section-order = [ "typing" ] +[tool.ruff.lint.flake8-import-conventions] +# Modules that are always imported with `import X`, never with `from X import ...`. +banned-from = [ + "asyncio", + "contextlib", + "datetime", + "errno", + "fcntl", + "inspect", + "ipaddress", + "json", + "logging", + "logging.handlers", + "multiprocessing", + "multiprocessing.synchronize", + "os", + "pytest", + "re", + "select", + "signal", + "socket", + "ssl", + "struct", + "subprocess", + "sys", + "threading", + "time", + "uuid" +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +# Modules that are always imported with `import X as Y`. +"xcp_storage.log" = "log" +"xcp_storage.rpc.api" = "api" +"xcp_storage.rpc.modules.drbd" = "drbd" +"xcp_storage.rpc.modules.echo" = "echo" + [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/src/xcp_storage/rpc/server.py b/src/xcp_storage/rpc/server.py index 65c914f..d075d8a 100644 --- a/src/xcp_storage/rpc/server.py +++ b/src/xcp_storage/rpc/server.py @@ -15,7 +15,7 @@ import ssl # Import API mods to ensure ApiDispatcher is exported with all public methods. -import xcp_storage.rpc.api # noqa: F401 +import xcp_storage.rpc.api # noqa: F401, ICN001 from xcp_storage.rpc.dispatcher import ApiDispatcher from xcp_storage.utils.json.rpc.server import JsonRpcServer