@misterhuydo/sentinel 1.6.13 → 1.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cairn/.hint-lock +1 -1
- package/.cairn/session.json +2 -2
- package/package.json +1 -1
- package/python/sentinel/__init__.py +1 -1
- package/python/sentinel/__pycache__/config_loader.cpython-311.pyc +0 -0
- package/python/sentinel/__pycache__/slack_bot.cpython-311.pyc +0 -0
- package/python/sentinel/config_loader.py +2 -0
- package/python/sentinel/slack_bot.py +41 -0
- package/python/tests/test_bot_noise_filter.py +51 -0
- package/python/tests/test_config_loader.py +15 -0
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-04-
|
|
1
|
+
2026-04-28T09:31:55.053Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-04-
|
|
3
|
-
"checkpoint_at": "2026-04-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-04-28T09:29:24.499Z",
|
|
3
|
+
"checkpoint_at": "2026-04-28T09:29:24.501Z",
|
|
4
4
|
"active_files": [
|
|
5
5
|
"J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
|
|
6
6
|
"J:\\Projects\\Sentinel\\cli\\lib\\test.js"
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.6.
|
|
1
|
+
__version__ = "1.6.14"
|
|
Binary file
|
|
Binary file
|
|
@@ -85,6 +85,7 @@ class SentinelConfig:
|
|
|
85
85
|
auto_commit: bool = False # project-level default: push directly to main (no PR); repos can override
|
|
86
86
|
auto_release: bool = False # project-level default: trigger CI/CD pipeline after push; repos can override
|
|
87
87
|
auto_raise_issues: bool = False # if True, auto-fix detected log errors without waiting for human to raise an issue
|
|
88
|
+
bot_watcher_noise_pattern: str = "" # regex; matching bot-alert text is dropped before queueing as an issue
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
@dataclass
|
|
@@ -284,6 +285,7 @@ class ConfigLoader:
|
|
|
284
285
|
c.auto_release = d["AUTO_RELEASE"].lower() == "true"
|
|
285
286
|
if "AUTO_RAISE_ISSUES" in d:
|
|
286
287
|
c.auto_raise_issues = d["AUTO_RAISE_ISSUES"].lower() == "true"
|
|
288
|
+
c.bot_watcher_noise_pattern = d.get("BOT_WATCHER_NOISE_PATTERN", "").strip()
|
|
287
289
|
self.sentinel = c
|
|
288
290
|
|
|
289
291
|
def _load_log_sources(self):
|
|
@@ -55,6 +55,30 @@ class _Session:
|
|
|
55
55
|
_sessions: dict[str, _Session] = {}
|
|
56
56
|
_sessions_lock = asyncio.Lock()
|
|
57
57
|
|
|
58
|
+
# Cached compiled regex for bot-watcher noise filter — recompiled when the
|
|
59
|
+
# config value changes (so SIGHUP reload picks up edits without a restart).
|
|
60
|
+
_noise_filter_cache: tuple[str, "object | None"] = ("", None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_noise_filter(pattern: str):
|
|
64
|
+
"""Return a compiled re.Pattern for the noise filter, or None if disabled/invalid."""
|
|
65
|
+
global _noise_filter_cache
|
|
66
|
+
cached_pattern, cached_re = _noise_filter_cache
|
|
67
|
+
if pattern == cached_pattern:
|
|
68
|
+
return cached_re
|
|
69
|
+
if not pattern:
|
|
70
|
+
_noise_filter_cache = ("", None)
|
|
71
|
+
return None
|
|
72
|
+
import re as _re
|
|
73
|
+
try:
|
|
74
|
+
compiled = _re.compile(pattern, _re.IGNORECASE)
|
|
75
|
+
except _re.error as e:
|
|
76
|
+
logger.warning("BOT_WATCHER_NOISE_PATTERN is not a valid regex (%s) — filter disabled", e)
|
|
77
|
+
_noise_filter_cache = (pattern, None)
|
|
78
|
+
return None
|
|
79
|
+
_noise_filter_cache = (pattern, compiled)
|
|
80
|
+
return compiled
|
|
81
|
+
|
|
58
82
|
|
|
59
83
|
async def _get_or_create_session(user_id: str, user_name: str, channel: str) -> _Session:
|
|
60
84
|
async with _sessions_lock:
|
|
@@ -272,6 +296,23 @@ async def _handle_bot_message(event: dict, client, cfg_loader, store) -> None:
|
|
|
272
296
|
if not text:
|
|
273
297
|
return
|
|
274
298
|
|
|
299
|
+
# Drop operational-noise alerts (e.g. wrong-PIN attempts, single transient
|
|
300
|
+
# SMS failures) before they enter the issues queue. Configurable via
|
|
301
|
+
# BOT_WATCHER_NOISE_PATTERN in sentinel.properties (regex, alternatives
|
|
302
|
+
# joined with `|`). Empty = disabled.
|
|
303
|
+
noise_re = _get_noise_filter(getattr(cfg_loader.sentinel, "bot_watcher_noise_pattern", ""))
|
|
304
|
+
if noise_re is not None and noise_re.search(text):
|
|
305
|
+
logger.info(
|
|
306
|
+
"Bot watcher: dropping noise-pattern match from %s (preview: %r)",
|
|
307
|
+
bot_id, text[:120].replace("\n", " "),
|
|
308
|
+
)
|
|
309
|
+
if ts:
|
|
310
|
+
try:
|
|
311
|
+
await client.reactions_add(channel=channel, timestamp=ts, name="mute")
|
|
312
|
+
except Exception:
|
|
313
|
+
pass # reactions:write scope may not be granted — non-fatal
|
|
314
|
+
return
|
|
315
|
+
|
|
275
316
|
# Find the project this bot is registered to
|
|
276
317
|
# Match on either the B-prefixed bot_id or the U-prefixed user ID
|
|
277
318
|
_user_id = event.get("user", "")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for the bot-watcher noise-pattern filter (slack_bot._get_noise_filter)."""
|
|
2
|
+
|
|
3
|
+
from sentinel import slack_bot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _reset_cache():
|
|
7
|
+
slack_bot._noise_filter_cache = ("", None)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_empty_pattern_disables_filter():
|
|
11
|
+
_reset_cache()
|
|
12
|
+
assert slack_bot._get_noise_filter("") is None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_invalid_regex_disables_filter(caplog):
|
|
16
|
+
_reset_cache()
|
|
17
|
+
import logging
|
|
18
|
+
with caplog.at_level(logging.WARNING):
|
|
19
|
+
assert slack_bot._get_noise_filter("(unbalanced") is None
|
|
20
|
+
assert any("not a valid regex" in r.message for r in caplog.records)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_pattern_compiles_and_caches():
|
|
24
|
+
_reset_cache()
|
|
25
|
+
p1 = slack_bot._get_noise_filter("Illegal pin logon|SMS delivery FAILED")
|
|
26
|
+
assert p1 is not None
|
|
27
|
+
p2 = slack_bot._get_noise_filter("Illegal pin logon|SMS delivery FAILED")
|
|
28
|
+
assert p1 is p2 # cache hit returns the same compiled object
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_pattern_change_recompiles():
|
|
32
|
+
_reset_cache()
|
|
33
|
+
p1 = slack_bot._get_noise_filter("foo")
|
|
34
|
+
p2 = slack_bot._get_noise_filter("bar")
|
|
35
|
+
assert p1 is not p2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_match_is_case_insensitive():
|
|
39
|
+
_reset_cache()
|
|
40
|
+
pat = slack_bot._get_noise_filter("Illegal pin logon")
|
|
41
|
+
assert pat.search("ILLEGAL PIN LOGON ATTEMPTED")
|
|
42
|
+
assert pat.search("user typed wrong PIN — Illegal Pin Logon attempted")
|
|
43
|
+
assert not pat.search("normal startup message")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_alternatives_match_either():
|
|
47
|
+
_reset_cache()
|
|
48
|
+
pat = slack_bot._get_noise_filter("Illegal pin logon|1 SMS delivery FAILED")
|
|
49
|
+
assert pat.search("Illegal pin logon attempted.")
|
|
50
|
+
assert pat.search(":no_entry: 1 SMS delivery FAILED in the last 5 minutes")
|
|
51
|
+
assert not pat.search("MsGraphMailSender threw exception")
|
|
@@ -74,6 +74,21 @@ def test_sentinel_config(config_dir):
|
|
|
74
74
|
assert cfg.github_token == "ghp_test"
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def test_bot_watcher_noise_pattern_default_empty(config_dir):
|
|
78
|
+
cfg = ConfigLoader(str(config_dir)).sentinel
|
|
79
|
+
assert cfg.bot_watcher_noise_pattern == ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_bot_watcher_noise_pattern_loaded(tmp_path):
|
|
83
|
+
(tmp_path / "log-configs").mkdir()
|
|
84
|
+
(tmp_path / "repo-configs").mkdir()
|
|
85
|
+
(tmp_path / "sentinel.properties").write_text(
|
|
86
|
+
"BOT_WATCHER_NOISE_PATTERN=Illegal pin logon|1 SMS delivery FAILED\n"
|
|
87
|
+
)
|
|
88
|
+
cfg = ConfigLoader(str(tmp_path)).sentinel
|
|
89
|
+
assert cfg.bot_watcher_noise_pattern == "Illegal pin logon|1 SMS delivery FAILED"
|
|
90
|
+
|
|
91
|
+
|
|
77
92
|
def test_log_sources(config_dir):
|
|
78
93
|
loader = ConfigLoader(str(config_dir))
|
|
79
94
|
assert "elprint-salescore" in loader.log_sources
|