From b70616674456c618b10317d3bb3d4197deafeb8f Mon Sep 17 00:00:00 2001 From: Seth Van Niekerk Date: Thu, 12 Nov 2026 12:26:33 -0500 Subject: [PATCH] feat(codeql): detect shared directory creation capability Flag use of the Dispatcharr watched-directory helper when filesystem_write is missing, and test manifest-version handling and plugin-data cleanup. --- .../queries/python/FilesystemWriteUsage.ql | 24 +++++++++++++++++++ .../FilesystemWriteUsage.expected | 1 + .../FilesystemWriteUsage.qlref | 1 + .../python/tests/FilesystemWriteUsage/bad.py | 1 + .../python/tests/FilesystemWriteUsage/good.py | 3 +++ .../pluginctl/src/pluginctl/capabilities.py | 2 +- .github/pluginctl/tests/test_capabilities.py | 1 + .github/pluginctl/tests/test_sarif.py | 19 +++++++++++++++ 8 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .github/codeql/queries/python/FilesystemWriteUsage.ql create mode 100644 .github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.expected create mode 100644 .github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.qlref create mode 100644 .github/codeql/queries/python/tests/FilesystemWriteUsage/bad.py create mode 100644 .github/codeql/queries/python/tests/FilesystemWriteUsage/good.py diff --git a/.github/codeql/queries/python/FilesystemWriteUsage.ql b/.github/codeql/queries/python/FilesystemWriteUsage.ql new file mode 100644 index 00000000..5ab34777 --- /dev/null +++ b/.github/codeql/queries/python/FilesystemWriteUsage.ql @@ -0,0 +1,24 @@ +/** + * @name Plugin shared directory creation capability use + * @description Dispatcharr's watched directory creation helper requires the filesystem_write capability. + * @kind problem + * @problem.severity warning + * @precision medium + * @id plugin/capability-contract/filesystem-write + * @tags security external/cwe/cwe-269 + */ + +import python + +from Call call, Subscript helper, Subscript dispatcharr, StringLiteral helperKey, + StringLiteral dispatcharrKey, Name context +where + call.getFunc() = helper and + helper.getObject() = dispatcharr and + helper.getIndex() = helperKey and + helperKey.getText() = "ensure_directory" and + dispatcharr.getObject() = context and + context.getId() = "context" and + dispatcharr.getIndex() = dispatcharrKey and + dispatcharrKey.getText() = "dispatcharr" +select call, "Creating a watched directory requires the `filesystem_write` capability." diff --git a/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.expected b/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.expected new file mode 100644 index 00000000..2eb41ea4 --- /dev/null +++ b/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.expected @@ -0,0 +1 @@ +| bad.py:1:1:1:51 | Subscript() | Creating a watched directory requires the `filesystem_write` capability. | diff --git a/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.qlref b/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.qlref new file mode 100644 index 00000000..f477c0c4 --- /dev/null +++ b/.github/codeql/queries/python/tests/FilesystemWriteUsage/FilesystemWriteUsage.qlref @@ -0,0 +1 @@ +FilesystemWriteUsage.ql diff --git a/.github/codeql/queries/python/tests/FilesystemWriteUsage/bad.py b/.github/codeql/queries/python/tests/FilesystemWriteUsage/bad.py new file mode 100644 index 00000000..1f5e1e44 --- /dev/null +++ b/.github/codeql/queries/python/tests/FilesystemWriteUsage/bad.py @@ -0,0 +1 @@ +context["dispatcharr"]["ensure_directory"]("logos") diff --git a/.github/codeql/queries/python/tests/FilesystemWriteUsage/good.py b/.github/codeql/queries/python/tests/FilesystemWriteUsage/good.py new file mode 100644 index 00000000..6eedc311 --- /dev/null +++ b/.github/codeql/queries/python/tests/FilesystemWriteUsage/good.py @@ -0,0 +1,3 @@ +context["dispatcharr"]["remove_directory"](context["data_dir"] + "/snapshot") +context["other"]["ensure_directory"]("logos") +other["dispatcharr"]["ensure_directory"]("logos") diff --git a/.github/pluginctl/src/pluginctl/capabilities.py b/.github/pluginctl/src/pluginctl/capabilities.py index 00f6922a..6f39a6a2 100644 --- a/.github/pluginctl/src/pluginctl/capabilities.py +++ b/.github/pluginctl/src/pluginctl/capabilities.py @@ -77,7 +77,7 @@ "min_manifest_version": 1, "max_manifest_version": None, "runtime_enforced": True, - "codeql_rules": [], + "codeql_rules": ["plugin/capability-contract/filesystem-write"], }, "celery_dispatch": { "group": "system", diff --git a/.github/pluginctl/tests/test_capabilities.py b/.github/pluginctl/tests/test_capabilities.py index c57a214c..b3f46630 100644 --- a/.github/pluginctl/tests/test_capabilities.py +++ b/.github/pluginctl/tests/test_capabilities.py @@ -70,6 +70,7 @@ def test_each_runtime_enforced_static_rule_maps_to_its_capability(): "network_listener": "plugin/capability-contract/network-listener", "subprocess": "plugin/capability-contract/subprocess", "outbound_network": "plugin/capability-contract/outbound-network", + "filesystem_write": "plugin/capability-contract/filesystem-write", "celery_dispatch": "plugin/capability-contract/celery-dispatch", } for capability_id, rule_id in expected.items(): diff --git a/.github/pluginctl/tests/test_sarif.py b/.github/pluginctl/tests/test_sarif.py index e2924d80..e1009143 100644 --- a/.github/pluginctl/tests/test_sarif.py +++ b/.github/pluginctl/tests/test_sarif.py @@ -221,6 +221,25 @@ def test_sandbox_bypass_rules_respect_schema_ranges(tmp_path, monkeypatch): assert not sarif.sandbox_result_requires_review(result(retired_rule)) +def test_filesystem_write_contract_respects_manifest_and_capability(tmp_path, monkeypatch): + plugin_dir = tmp_path / "plugins" / "demo" + plugin_dir.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + result = { + "ruleId": "plugin/capability-contract/filesystem-write", + "locations": [{"physicalLocation": {"artifactLocation": {"uri": "plugins/demo/plugin.py"}, + "region": {"startLine": 1}}}], + } + for version, declared, requires_review in ( + (0, [], False), (1, [], False), (2, [], True), + (2, ["filesystem_write"], False), + ): + (plugin_dir / "plugin.json").write_text(json.dumps({ + "manifest_version": version, "capabilities": declared, + }), encoding="utf-8") + assert sarif.contract_result_requires_review(result) == requires_review + + def test_capability_contract_accepts_declared_or_legacy_manifests(tmp_path, monkeypatch): plugin_dir = tmp_path / "plugins" / "demo" plugin_dir.mkdir(parents=True)