diff --git a/tests/tools/test_tirith_security.py b/tests/tools/test_tirith_security.py index 53b7e2893a48..664cfe4b7015 100644 --- a/tests/tools/test_tirith_security.py +++ b/tests/tools/test_tirith_security.py @@ -1397,6 +1397,31 @@ def test_dev_lookalike_tld_suppressed(self, mock_cfg, mock_run): assert result["action"] == "allow" assert result["findings"] == [] + @patch("tools.tirith_security.subprocess.run") + @patch("tools.tirith_security._load_security_config") + def test_modal_run_lookalike_domain_suppressed(self, mock_cfg, mock_run): + """modal.run and its subdomains are trusted .run false positives.""" + mock_cfg.return_value = _CFG + findings = [{"rule_id": "lookalike_tld", + "evidence": [{"raw": "cpe-research--cpe-web.modal.run"}], + "message": "Domain uses '.run' TLD"}] + mock_run.return_value = _mock_run(2, _json_stdout(findings, ".run TLD warning")) + result = check_command_security("curl https://cpe-research--cpe-web.modal.run") + assert result["action"] == "allow" + assert result["findings"] == [] + + @patch("tools.tirith_security.subprocess.run") + @patch("tools.tirith_security._load_security_config") + def test_arbitrary_run_lookalike_tld_preserved(self, mock_cfg, mock_run): + """Regression: .run is generic/open and must not be blanket-suppressed.""" + mock_cfg.return_value = _CFG + findings = [{"rule_id": "lookalike_tld", "value": "malicious-installer.run", + "message": "Domain uses '.run' TLD"}] + mock_run.return_value = _mock_run(2, _json_stdout(findings, ".run TLD warning")) + result = check_command_security("curl https://malicious-installer.run") + assert result["action"] == "warn" + assert len(result["findings"]) == 1 + @patch("tools.tirith_security.subprocess.run") @patch("tools.tirith_security._load_security_config") def test_mov_lookalike_tld_preserved(self, mock_cfg, mock_run): @@ -1451,42 +1476,46 @@ def test_matching_value_field(self): def test_matching_dev_tld(self): assert self.fn({"rule_id": "lookalike_tld", "value": ".dev"}) - def test_bare_run_tld_not_matched(self): - """A bare .run TLD token is the whole public Identity Digital registry, - not Modal — it must NOT be suppressed (only modal.run is).""" + def test_bare_run_tld_not_suppressed(self): + """A bare .run token is not enough: .run is an open generic TLD.""" assert not self.fn({"rule_id": "lookalike_tld", "value": ".run"}) - def test_run_message_field_not_matched(self): - """A generic '.run' TLD warning is not a Modal endpoint; keep the warn.""" + def test_run_message_field_not_suppressed_without_trusted_domain(self): + """Generic '.run' warnings stay active without modal.run evidence.""" assert not self.fn({"rule_id": "lookalike_tld", - "message": "Domain uses '.run' TLD"}) + "message": "Domain uses '.run' TLD"}) - def test_arbitrary_run_host_not_matched(self): - """An arbitrary .run host (potential phishing) is not suppressed.""" + def test_arbitrary_run_host_not_suppressed(self): + """Regression: arbitrary .run hosts still warn (not blanket-safe).""" assert not self.fn({"rule_id": "lookalike_tld", "value": "attacker.run"}) + assert not self.fn({"rule_id": "lookalike_tld", "value": "evil.run"}) def test_matching_modal_run_domain(self): assert self.fn({"rule_id": "lookalike_tld", "value": "cpe-research--cpe-web.modal.run"}) + def test_matching_modal_run_subdomain(self): + assert self.fn({"rule_id": "lookalike_tld", "value": "foo.modal.run"}) + def test_matching_bare_modal_run_domain(self): assert self.fn({"rule_id": "lookalike_tld", "value": "modal.run"}) - def test_lookalike_modal_run_suffix_not_matched(self): - """A host that only ends the label with ...notmodal.run is not Modal.""" + def test_evilmodal_run_not_suppressed(self): + """Only modal.run's registrable domain and subdomains are trusted.""" assert not self.fn({"rule_id": "lookalike_tld", "value": "evilmodal.run"}) - def test_modal_run_deeper_suffix_not_matched(self): - """modal.run followed by a further risky TLD must not be suppressed.""" + def test_run_deeper_suffix_not_matched(self): + """A terminal .run followed by a further risky TLD must not be + suppressed — the real TLD is .zip, not .run.""" assert not self.fn({"rule_id": "lookalike_tld", "value": "modal.run.evil.zip"}) def test_run_as_subdomain_label_not_matched(self): """`.run` as a non-terminal label does not count as safe.""" assert not self.fn({"rule_id": "lookalike_tld", "value": "foo.run.example.zip"}) - def test_real_tirith_schema_modal_run_evidence(self): + def test_real_tirith_schema_run_evidence_suppressed_for_modal(self): """Real Tirith schema-v3 output: the host lives in evidence[].raw and - the description is generic, so the Modal carve-out must read evidence.""" + the description is generic. A modal.run host is suppressed.""" finding = { "rule_id": "lookalike_tld", "severity": "MEDIUM", @@ -1496,8 +1525,8 @@ def test_real_tirith_schema_modal_run_evidence(self): } assert self.fn(finding) - def test_real_tirith_schema_arbitrary_run_evidence_not_matched(self): - """Same schema, but an arbitrary .run host in evidence stays warned.""" + def test_real_tirith_schema_arbitrary_run_evidence_not_suppressed(self): + """Same schema, arbitrary .run host in evidence — still warns.""" finding = { "rule_id": "lookalike_tld", "severity": "MEDIUM", diff --git a/tools/tirith_security.py b/tools/tirith_security.py index 205f91efb6bf..c1fa2bc440b3 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -857,18 +857,16 @@ def check_command_security(command: str) -> dict: return {"action": action, "findings": findings, "summary": summary} -# gTLDs that Tirith's lookalike_tld heuristic flags but which are legitimate, -# HTTPS-only registries in wide production use. .app and .dev are -# Google-operated gTLDs on the HSTS preload list (browsers force HTTPS), used -# by many production services (e.g. *.workers.dev, *.web.app). Suppressing -# their warnings removes false positives without weakening protection against -# filename-collision TLDs (.zip, .mov) used in real phishing. +# gTLDs that Tirith's lookalike_tld heuristic flags but which we treat as +# known false positives for this agent's normal workload. .app and .dev are +# Google-operated, HTTPS-only gTLDs on the HSTS preload list (browsers force +# HTTPS), used by many production services (e.g. *.workers.dev, *.web.app). # -# NOTE: .run is deliberately NOT in this list. Unlike .app/.dev it is a -# generic Identity Digital (Binky Moon) registry, not HTTPS-only, so blanket -# suppression would silence the lookalike warning for any .run host -# (e.g. ``attacker.run``). We only carve out Modal's own endpoints below. +# Do not blanket-suppress generic open registries such as .run: those collide +# with common executable extensions (installer.run, setup.run). Trusted .run +# services must be allowlisted by registrable domain below instead. _SAFE_LOOKALIKE_TLDS = (".app", ".dev") +_SAFE_LOOKALIKE_DOMAINS = ("modal.run",) # Match a safe gTLD only as a complete TLD token, never as a substring of a # longer label. Without the trailing boundary, a substring check would let a @@ -882,33 +880,32 @@ def check_command_security(command: str) -> dict: r"(?![A-Za-z0-9_-])(?!\.\w)" ) -# Modal web endpoints all live under the ``modal.run`` registrable domain -# (e.g. ``cpe-research--cpe-web.modal.run``), a routine target for health -# checks and API calls from this agent, and the reason the lookalike_tld -# heuristic fires on ``.run`` in normal use. We suppress the warning only for -# this specific registrable domain — the host must be ``modal.run`` itself or -# end in ``.modal.run`` as a terminal label — so an arbitrary ``attacker.run`` -# still preserves the warn action. The trailing negative lookahead keeps a -# deeper suffix like ``modal.run.evil.zip`` from matching. -_MODAL_RUN_RE = re.compile(r"(?:^|[^A-Za-z0-9_.-])(?:[a-z0-9-]+\.)*modal\.run(?![A-Za-z0-9_.-])") +# Match trusted registrable domains, plus any subdomain, only when the trusted +# domain is the terminal host suffix. This suppresses ``modal.run`` and +# ``foo.modal.run`` but not ``evilmodal.run`` or ``modal.run.evil.zip``. +_SAFE_DOMAIN_RE = re.compile( + r"(? bool: """Return True if this finding is a lookalike_tld warning we treat as a - known false positive: a terminal ``.app`` / ``.dev`` gTLD, or a host under - Modal's ``modal.run`` registrable domain (see ``_SAFE_LOOKALIKE_TLDS`` / - ``_MODAL_RUN_RE``). + known false positive: a terminal ``.app``/``.dev`` TLD or a trusted + registrable domain such as ``modal.run`` and its subdomains. Inspects the top-level string fields Tirith may use to carry the TLD/host (``value``/``tld``/``detail``/``description``/``message``) AND the ``evidence`` list, whose entries carry the actual matched host in a ``raw``/``value``/``url`` field (real Tirith schema v3 output puts the host only in ``evidence[].raw`` — the ``description`` is generic, e.g. "Domain - uses '.run' TLD ...", so the Modal carve-out would never fire without - reading evidence). The safe TLD must appear as a distinct terminal token; - substrings like ``example.dev.zip`` (where ``.zip`` is the real, unsafe - TLD) and arbitrary ``.run`` hosts such as ``attacker.run`` are not - suppressed. + uses '.run' TLD ...", so reading evidence is required). The safe TLD/domain + must appear as a distinct terminal token; substrings like + ``example.dev.zip``, ``evilmodal.run``, or ``foo.run.evil.zip`` (where + ``.zip`` is the real, unsafe TLD) are not suppressed. """ if not isinstance(finding, dict): return False @@ -919,7 +916,7 @@ def _is_safe_text(val) -> bool: if val is None: return False text = str(val).lower() - return bool(_SAFE_TLD_RE.search(text) or _MODAL_RUN_RE.search(text)) + return bool(_SAFE_TLD_RE.search(text) or _SAFE_DOMAIN_RE.search(text)) for field in ("value", "tld", "detail", "description", "message"): if _is_safe_text(finding.get(field)):