Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 45 additions & 16 deletions tests/tools/test_tirith_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
53 changes: 25 additions & 28 deletions tools/tirith_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",)
Comment on lines 868 to +869

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include .run in the safe TLD set

For a non-Modal .run service such as curl https://api.some-service.run, Tirith's sole lookalike_tld finding still isn't suppressible because .run is excluded here and the only .run allowlist entry is modal.run; check_command_security therefore continues returning warn, so the blanket .run suppression described by this change does not take effect outside Modal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally scoped to modal.run only. Widening suppression back to the whole .run TLD is exactly the security-high gemini flagged on this same line: .run is an open public registry AND a common Linux executable extension (setup.run, installer.run), so blanket-suppressing it lets malicious-installer.run evade the lookalike warning. Non-Modal .run services warning is the intended, safer default; trusted domains get added to _SAFE_LOOKALIKE_DOMAINS individually. The higher-severity finding wins here.


# 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
Expand All @@ -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"(?<![A-Za-z0-9_-])"
r"(?:[A-Za-z0-9-]+\.)*(?:"
+ "|".join(re.escape(domain) for domain in _SAFE_LOOKALIKE_DOMAINS)
+ r")"
r"(?![A-Za-z0-9_-])(?!\.\w)"
)


def _is_safe_lookalike_tld_finding(finding: dict) -> 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
Expand All @@ -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)):
Expand Down
Loading