Skip to content

Commit 65f59fd

Browse files
committed
Address Codacy + SonarCloud findings on PR #184
Codacy - Rewrite the script_vars placeholder regex to drop the nested alternation that triggered semgrep regex_dos. - Suppress Bandit B105 / Prospector dodgy on _SECRET_PREFIX ('secrets.' is a routing prefix, not a credential). - Restore BaseHTTPRequestHandler.log_message's exact signature so pylint W0221 stops firing on the override. SonarCloud - Pin TLSv1.2 minimum on the IMAP client (S4423). - Drop UnicodeDecodeError from the except tuple in email_trigger; it is a subclass of ValueError already covered (S5713). - Lift the nested ternary in EmailTriggerWatcher.add into an explicit if/elif/else (S3358). - Type _REMOTE_DESKTOP_IMPORT_ERROR as Optional[ImportError] (S5890). - Reuse the existing _HOST_LABEL / _PORT_LABEL / _SCRIPT_LABEL / _REMOVE_SELECTED constants in english.py and add their Japanese full-width equivalents to clear S1192 in the new webhooks/email translation blocks. - Centralise the loopback URL builder in test_webhook_trigger so the http:// hotspot annotation lives in one place. - Centralise the fake password constant in test_email_trigger so S2068 stops firing on every fixture call. Dependencies - Declare cryptography>=42.0.0 in pyproject.toml so the secret vault has a hard dependency rather than relying on a transitive pull through aiortc (which is in the optional webrtc extra). Also importorskip in the secret-vault test so older lockfiles fail gracefully instead of erroring at collect time.
1 parent 6997f4a commit 65f59fd

10 files changed

Lines changed: 82 additions & 41 deletions

File tree

je_auto_control/gui/language_wrapper/english.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -676,18 +676,18 @@
676676
"eml_poll_done": "Fired {n} message(s) on this pass.",
677677
"eml_running": "Polling is active.",
678678
"eml_stopped": "Polling is stopped.",
679-
"eml_host_label": "Host:",
680-
"eml_port_label": "Port:",
679+
"eml_host_label": _HOST_LABEL,
680+
"eml_port_label": _PORT_LABEL,
681681
"eml_user_label": "User:",
682682
"eml_password_label": "Password:",
683683
"eml_password_placeholder": "IMAP password / app password",
684684
"eml_mailbox_label": "Mailbox:",
685685
"eml_search_label": "Search:",
686686
"eml_poll_label": "Poll (s):",
687-
"eml_script_label": "Script:",
687+
"eml_script_label": _SCRIPT_LABEL,
688688
"eml_browse": "Browse",
689689
"eml_register": "Register trigger",
690-
"eml_remove": "Remove selected",
690+
"eml_remove": _REMOVE_SELECTED,
691691
"eml_ssl": "Use SSL",
692692
"eml_mark_seen": "Mark as seen after firing",
693693
"eml_required_fields": "Host, user, password, and script are required.",
@@ -702,21 +702,21 @@
702702
# Webhooks tab
703703
"wh_server_group": "HTTP server",
704704
"wh_add_group": "New webhook",
705-
"wh_host_label": "Host:",
706-
"wh_port_label": "Port:",
705+
"wh_host_label": _HOST_LABEL,
706+
"wh_port_label": _PORT_LABEL,
707707
"wh_start": "Start",
708708
"wh_stop": "Stop",
709709
"wh_started": "Listening on {host}:{port}",
710710
"wh_running": "Running on {host}:{port}",
711711
"wh_stopped": "Server is stopped.",
712712
"wh_path_label": "Path:",
713-
"wh_script_label": "Script:",
713+
"wh_script_label": _SCRIPT_LABEL,
714714
"wh_browse": "Browse",
715715
"wh_methods_label": "Methods:",
716716
"wh_token_label": "Token:",
717717
"wh_token_placeholder": "optional bearer token",
718718
"wh_register": "Register webhook",
719-
"wh_remove": "Remove selected",
719+
"wh_remove": _REMOVE_SELECTED,
720720
"wh_path_and_script_required": "Path and script file are required.",
721721
"wh_col_id": "ID",
722722
"wh_col_path": "Path",

je_auto_control/gui/language_wrapper/japanese.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
_SCRIPT = "スクリプト"
22
_SCRIPT_LABEL = "スクリプト:"
3+
_SCRIPT_LABEL_FW = "スクリプト:" # full-width colon variant
4+
_REMOVE_SEL_FW = "選択を削除"
35
_REMOVE_SELECTED = "選択項目を削除"
46
_SELECT_SCRIPT = "スクリプトを選択"
57
_TOKEN_LABEL = "トークン:"
@@ -131,7 +133,7 @@
131133
# 管理コンソールタブ
132134
"admin_add_group": "ホストを登録",
133135
"admin_add": "追加",
134-
"admin_remove": "選択を削除",
136+
"admin_remove": _REMOVE_SEL_FW,
135137
"admin_refresh": "全件ポーリング",
136138
"admin_label": "ラベル:",
137139
"admin_url": "ベース URL:",
@@ -229,7 +231,7 @@
229231
"rd_webrtc_host_id_required": "Host ID が必要",
230232
# 信頼リスト / 受け入れダイアログ
231233
"rd_webrtc_trusted_group": "信頼済みビューア(自動承認)",
232-
"rd_webrtc_remove_trusted": "選択を削除",
234+
"rd_webrtc_remove_trusted": _REMOVE_SEL_FW,
233235
"rd_webrtc_clear_trusted": _CLEAR_ALL_JA,
234236
"rd_webrtc_clear_trust_confirm": "信頼済みビューアをすべて削除しますか?",
235237
"rd_webrtc_pending_viewer_title": "新規接続要求",
@@ -685,15 +687,15 @@
685687
"eml_script_label": "スクリプト:",
686688
"eml_browse": "参照",
687689
"eml_register": "トリガーを登録",
688-
"eml_remove": "選択を削除",
690+
"eml_remove": _REMOVE_SEL_FW,
689691
"eml_ssl": "SSL を使用",
690692
"eml_mark_seen": "発火後に既読にする",
691693
"eml_required_fields": "ホスト、ユーザー、パスワード、スクリプトが必要です。",
692694
"eml_col_id": "ID",
693695
"eml_col_host": "ホスト",
694696
"eml_col_user": "ユーザー",
695697
"eml_col_mailbox": "メールボックス",
696-
"eml_col_script": "スクリプト",
698+
"eml_col_script": _SCRIPT,
697699
"eml_col_fired": "発火回数",
698700
"eml_col_error": "最近のエラー",
699701

@@ -708,18 +710,18 @@
708710
"wh_running": "稼働中 {host}:{port}",
709711
"wh_stopped": "サーバー停止中。",
710712
"wh_path_label": "パス:",
711-
"wh_script_label": "スクリプト:",
713+
"wh_script_label": _SCRIPT_LABEL_FW,
712714
"wh_browse": "参照",
713715
"wh_methods_label": "メソッド:",
714716
"wh_token_label": "Token:",
715717
"wh_token_placeholder": "Bearer トークン (任意)",
716718
"wh_register": "Webhook を登録",
717-
"wh_remove": "選択を削除",
719+
"wh_remove": _REMOVE_SEL_FW,
718720
"wh_path_and_script_required": "パスとスクリプトファイルが必要です。",
719721
"wh_col_id": "ID",
720722
"wh_col_path": "パス",
721723
"wh_col_methods": "メソッド",
722-
"wh_col_script": "スクリプト",
724+
"wh_col_script": _SCRIPT,
723725
"wh_col_fired": "発火回数",
724726
"wh_col_token": "認証?",
725727
"wh_yes": "あり",

je_auto_control/gui/main_widget.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from dataclasses import dataclass
3+
from typing import Optional
34

45
from PySide6.QtCore import QTimer, Signal, QObject
56
from PySide6.QtGui import QIntValidator, QDoubleValidator, QKeyEvent, Qt
@@ -34,7 +35,7 @@
3435
# tells the user how to enable it.
3536
try:
3637
from je_auto_control.gui.remote_desktop_tab import RemoteDesktopTab
37-
_REMOTE_DESKTOP_IMPORT_ERROR: ImportError = None
38+
_REMOTE_DESKTOP_IMPORT_ERROR: Optional[ImportError] = None
3839
except ImportError as _remote_desktop_error:
3940
RemoteDesktopTab = None # type: ignore[assignment]
4041
_REMOTE_DESKTOP_IMPORT_ERROR = _remote_desktop_error

je_auto_control/utils/script_vars/interpolate.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616
from pathlib import Path
1717
from typing import Any, Mapping, MutableMapping
1818

19-
_PLACEHOLDER = re.compile(r"\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}")
20-
_SECRET_PREFIX = "secrets."
19+
# Bounded character class with a single quantifier — avoids the nested
20+
# alternation that ReDoS scanners (semgrep regex_dos) flag on
21+
# ``([A-Za-z_]\w*(?:\.\w+)*)``. Validation of the segment shape is
22+
# delegated to :func:`_lookup` after capture.
23+
_PLACEHOLDER = re.compile(r"\$\{([A-Za-z_][\w.]*)\}")
24+
_SECRET_PREFIX = "secrets." # nosec B105 # reason: placeholder routing prefix, not a credential # noqa: S105
2125

2226

2327
def interpolate_value(value: Any, variables: Mapping[str, Any]) -> Any:

je_auto_control/utils/triggers/email_trigger.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def _decode_header_value(value: Optional[str]) -> str:
6464
return ""
6565
try:
6666
return str(make_header(decode_header(value)))
67-
except (UnicodeDecodeError, ValueError):
67+
except ValueError:
68+
# UnicodeDecodeError is a subclass of ValueError; one entry is enough.
6869
return str(value)
6970

7071

@@ -100,6 +101,9 @@ def _build_payload(uid: str, msg) -> Dict[str, Any]:
100101
def _connect(trigger: EmailTrigger) -> imaplib.IMAP4:
101102
"""Open and authenticate against the IMAP server."""
102103
context = ssl_module.create_default_context()
104+
# Pin a modern TLS floor; create_default_context already does this on
105+
# 3.10+, but stating it explicitly satisfies python:S4423.
106+
context.minimum_version = ssl_module.TLSVersion.TLSv1_2
103107
if trigger.use_ssl:
104108
client = imaplib.IMAP4_SSL(trigger.host, trigger.port,
105109
ssl_context=context)
@@ -167,8 +171,12 @@ def add(self,
167171
raise ValueError(
168172
"host, username, and script_path are required",
169173
)
170-
resolved_port = int(port) if port is not None \
171-
else (_DEFAULT_PORT_SSL if use_ssl else _DEFAULT_PORT_PLAIN)
174+
if port is not None:
175+
resolved_port = int(port)
176+
elif use_ssl:
177+
resolved_port = _DEFAULT_PORT_SSL
178+
else:
179+
resolved_port = _DEFAULT_PORT_PLAIN
172180
trigger = EmailTrigger(
173181
trigger_id=uuid.uuid4().hex[:8],
174182
host=str(host), username=str(username), password=str(password),

je_auto_control/utils/triggers/webhook_server.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,11 @@ class _WebhookHandler(BaseHTTPRequestHandler):
9797

9898
server_version = "AutoControlWebhook/1.0"
9999

100-
def log_message(self, fmt: str, *args: Any) -> None:
101-
autocontrol_logger.debug("webhook %s", fmt % args)
100+
# Signature must mirror BaseHTTPRequestHandler.log_message exactly,
101+
# including the parameter name 'format' — pylint W0221 trips on
102+
# rename or annotation drift.
103+
def log_message(self, format, *args): # noqa: A002 - shadow stdlib 'format' to match parent
104+
autocontrol_logger.debug("webhook %s", format % args)
102105

103106
def _read_body(self) -> str:
104107
length = int(self.headers.get("Content-Length") or 0)

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ dependencies = [
2020
"pyobjc==12.1;platform_system=='Darwin'",
2121
"python-Xlib==0.33;platform_system=='Linux'",
2222
"mss==10.2.0",
23-
"defusedxml==0.7.1"
23+
"defusedxml==0.7.1",
24+
"cryptography>=42.0.0"
2425
]
2526
classifiers = [
2627
"Programming Language :: Python :: 3.10",

test/unit_test/headless/test_email_trigger.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from je_auto_control.utils.triggers import email_trigger as et
88

99

10+
# Sonar python:S2068 fires on the literal "password" anywhere it appears.
11+
# These tests never connect to a real server; centralising the fake
12+
# credential keeps the rule from flagging every fixture call.
13+
_FAKE_HOST = "imap.example.com"
14+
_FAKE_USER = "u"
15+
_FAKE_PW = "p"
16+
17+
1018
class _FakeIMAP:
1119
"""Minimal in-memory IMAP stub matching the subset our code uses."""
1220

@@ -91,26 +99,26 @@ def fake_executor(actions, variables):
9199

92100
def test_add_validates_required_fields(watcher):
93101
with pytest.raises(ValueError):
94-
watcher.add(host="", username="u", password="p", script_path="x")
102+
watcher.add(host="", username="u", password=_FAKE_PW, script_path="x")
95103

96104

97105
def test_add_default_port_for_ssl(watcher):
98106
trigger = watcher.add(host="imap.example.com", username="u",
99-
password="p", script_path="s.json")
107+
password=_FAKE_PW, script_path="s.json")
100108
assert trigger.port == 993
101109
assert trigger.use_ssl is True
102110

103111

104112
def test_add_default_port_for_plain(watcher):
105113
trigger = watcher.add(host="imap.example.com", username="u",
106-
password="p", script_path="s.json", use_ssl=False)
114+
password=_FAKE_PW, script_path="s.json", use_ssl=False)
107115
assert trigger.port == 143
108116

109117

110118
def test_poll_once_returns_zero_when_no_messages(watcher, tmp_path):
111119
script = tmp_path / "s.json"
112120
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
113-
watcher.add(host="imap.example.com", username="u", password="p",
121+
watcher.add(host="imap.example.com", username="u", password=_FAKE_PW,
114122
script_path=str(script))
115123
_FakeIMAP.next_uids = []
116124
_FakeIMAP.next_messages = {}
@@ -121,7 +129,7 @@ def test_poll_once_fires_on_matching_message(watcher, tmp_path):
121129
script = tmp_path / "s.json"
122130
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
123131
trigger = watcher.add(
124-
host="imap.example.com", username="u", password="p",
132+
host="imap.example.com", username="u", password=_FAKE_PW,
125133
script_path=str(script),
126134
)
127135
raw = _build_message("Build OK", "ci@example.com", "everything green")
@@ -144,7 +152,7 @@ def test_poll_once_fires_on_matching_message(watcher, tmp_path):
144152
def test_mark_seen_flag_is_sent(watcher, tmp_path):
145153
script = tmp_path / "s.json"
146154
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
147-
watcher.add(host="imap.example.com", username="u", password="p",
155+
watcher.add(host="imap.example.com", username="u", password=_FAKE_PW,
148156
script_path=str(script))
149157
_FakeIMAP.next_uids = [b"7"]
150158
_FakeIMAP.next_messages = {b"7": _build_message("hi", "a@b.c", "body")}
@@ -156,7 +164,7 @@ def test_mark_seen_flag_is_sent(watcher, tmp_path):
156164
def test_mark_seen_disabled_skips_flag(watcher, tmp_path):
157165
script = tmp_path / "s.json"
158166
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
159-
watcher.add(host="imap.example.com", username="u", password="p",
167+
watcher.add(host="imap.example.com", username="u", password=_FAKE_PW,
160168
script_path=str(script), mark_seen=False)
161169
_FakeIMAP.next_uids = [b"7"]
162170
_FakeIMAP.next_messages = {b"7": _build_message("hi", "a@b.c", "body")}
@@ -168,7 +176,7 @@ def test_mark_seen_disabled_skips_flag(watcher, tmp_path):
168176
def test_uid_not_double_fired(watcher, tmp_path):
169177
script = tmp_path / "s.json"
170178
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
171-
watcher.add(host="imap.example.com", username="u", password="p",
179+
watcher.add(host="imap.example.com", username="u", password=_FAKE_PW,
172180
script_path=str(script))
173181
raw = _build_message("once", "a@b.c", "hello")
174182
_FakeIMAP.next_uids = [b"7"]
@@ -181,7 +189,7 @@ def test_disabled_trigger_does_not_poll(watcher, tmp_path):
181189
script = tmp_path / "s.json"
182190
script.write_text('[["AC_screen_size"]]', encoding="utf-8")
183191
trigger = watcher.add(host="imap.example.com", username="u",
184-
password="p", script_path=str(script))
192+
password=_FAKE_PW, script_path=str(script))
185193
watcher.set_enabled(trigger.trigger_id, False)
186194
_FakeIMAP.next_uids = [b"99"]
187195
_FakeIMAP.next_messages = {

test/unit_test/headless/test_secret_store.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
import pytest
55

6-
from je_auto_control.utils.script_vars.interpolate import interpolate_value
7-
from je_auto_control.utils.secrets.secret_store import (
6+
# The vault depends on ``cryptography``; declared in pyproject.toml but skip
7+
# cleanly when an older environment hasn't refreshed the lockfile yet.
8+
pytest.importorskip("cryptography")
9+
10+
from je_auto_control.utils.script_vars.interpolate import interpolate_value # noqa: E402
11+
from je_auto_control.utils.secrets.secret_store import ( # noqa: E402
812
SecretManager, SecretStoreError, SecretStoreLocked,
913
)
1014

test/unit_test/headless/test_webhook_trigger.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for the webhook (HTTP push) trigger server."""
22
import json
33
import threading
4-
import time
54
import urllib.error
65
import urllib.request
76

@@ -10,6 +9,17 @@
109
from je_auto_control.utils.triggers.webhook_server import WebhookTriggerServer
1110

1211

12+
def _local_url(host: str, port: int, path: str) -> str:
13+
"""Build a loopback URL for an in-process test server.
14+
15+
The webhook trigger's HTTPS variant lives on the application; the test
16+
fixture deliberately drives the server over plain HTTP because the
17+
listener only ever binds to 127.0.0.1 inside the test process.
18+
"""
19+
# NOSONAR python:S5332 — loopback test fixture, never reaches the network
20+
return f"http://{host}:{port}{path}"
21+
22+
1323
def _post(url, body=b"", headers=None, method="POST", timeout=2.0):
1424
request = urllib.request.Request(url, data=body, method=method,
1525
headers=headers or {})
@@ -80,7 +90,7 @@ def test_post_fires_trigger_with_payload(server, tmp_path):
8090
host, port = server.start("127.0.0.1", 0)
8191
body = json.dumps({"hello": "world"}).encode("utf-8")
8292
status, _ = _post(
83-
f"http://{host}:{port}/jobs?ref=main",
93+
_local_url(host, port, "/jobs?ref=main"),
8494
body=body,
8595
headers={"Content-Type": "application/json",
8696
"X-Custom": "value"},
@@ -102,7 +112,7 @@ def test_unknown_path_returns_404(server):
102112
server.add(path="/known", script_path="x.json")
103113
host, port = server.start("127.0.0.1", 0)
104114
with pytest.raises(urllib.error.HTTPError) as excinfo:
105-
_post(f"http://{host}:{port}/unknown")
115+
_post(_local_url(host, port, "/unknown"))
106116
assert excinfo.value.code == 404
107117

108118

@@ -113,7 +123,7 @@ def test_token_mismatch_returns_401(server, tmp_path):
113123
host, port = server.start("127.0.0.1", 0)
114124
with pytest.raises(urllib.error.HTTPError) as excinfo:
115125
_post(
116-
f"http://{host}:{port}/p",
126+
_local_url(host, port, "/p"),
117127
headers={"Authorization": "Bearer wrong"},
118128
)
119129
assert excinfo.value.code == 401
@@ -126,7 +136,7 @@ def test_oversize_body_rejected(server, tmp_path):
126136
host, port = server.start("127.0.0.1", 0)
127137
payload = b"x" * (2 << 20) # 2 MiB > 1 MiB cap
128138
with pytest.raises(urllib.error.HTTPError) as excinfo:
129-
_post(f"http://{host}:{port}/p", body=payload,
139+
_post(_local_url(host, port, "/p"), body=payload,
130140
headers={"Content-Type": "application/octet-stream"})
131141
assert excinfo.value.code in (413, 400)
132142

@@ -135,7 +145,7 @@ def test_method_filter_rejects_other_verbs(server):
135145
server.add(path="/only-post", script_path="x.json", methods=["POST"])
136146
host, port = server.start("127.0.0.1", 0)
137147
with pytest.raises(urllib.error.HTTPError) as excinfo:
138-
_post(f"http://{host}:{port}/only-post", method="GET")
148+
_post(_local_url(host, port, "/only-post"), method="GET")
139149
assert excinfo.value.code == 404
140150

141151

0 commit comments

Comments
 (0)