Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1eb2b32

Browse files
committed
Reduce benign MCP scanner false positives
1 parent 4949a5e commit 1eb2b32

8 files changed

Lines changed: 114 additions & 13 deletions

File tree

intentprobe/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""intentprobe public package."""
22

3-
__version__ = "0.1.3"
3+
__version__ = "0.1.4"

intentprobe/scanner/core.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
from intentprobe.scanner.static_checks import static_scan, static_score # type: ignore
2424

2525

26-
CORE_VERSION = "activation-scanner-core-2026-06-03-static-v3"
27-
DECISION_POLICY_NAME = "corroborated-block-v3"
26+
CORE_VERSION = "activation-scanner-core-2026-06-03-static-v4"
27+
DECISION_POLICY_NAME = "corroborated-block-v4"
28+
ACTIVATION_ONLY_WARN_MIN = 0.50
2829
STATIC_BLOCK_BUNDLE_MAX_SPAN = 900
2930
STATIC_BLOCK_BUNDLES = (
3031
frozenset(("static-secret-file", "static-exfiltration", "static-hidden-action")),
@@ -300,6 +301,12 @@ def choose_runtime_decision(
300301
return "block", reasons
301302

302303
if risk_score >= warn_threshold:
304+
if static_risk_score <= 0 and activation_score < ACTIVATION_ONLY_WARN_MIN:
305+
reasons.append(
306+
"activation-only score below product review floor "
307+
f"(activation={activation_score:.3f}, floor={ACTIVATION_ONLY_WARN_MIN:.2f})"
308+
)
309+
return "allow", reasons
303310
if risk_score >= block_threshold:
304311
reasons.append("high risk score without block corroboration; downgraded to warn")
305312
else:
@@ -419,6 +426,7 @@ def build_risk_output(
419426
"static_block_bundles": [sorted(bundle) for bundle in STATIC_BLOCK_BUNDLES],
420427
"static_block_bundle_max_span": STATIC_BLOCK_BUNDLE_MAX_SPAN,
421428
"static_activation_corroborators": sorted(STATIC_ACTIVATION_CORROBORATORS),
429+
"activation_only_warn_min": ACTIVATION_ONLY_WARN_MIN,
422430
"policy_reasons": decision_policy_reasons,
423431
},
424432
"elapsed_seconds": elapsed_seconds,

intentprobe/scanner/static_checks.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@
8989
"low": 0.30,
9090
}
9191

92+
RISKY_SECRET_ACTION = (
93+
r"(?:read|access|open|collect|copy|dump|print|log|expose|upload|send|"
94+
r"forward|transmit|post|store|write)"
95+
)
96+
SECRET_OR_LOCAL_DATA = (
97+
r"(?:credentials?|credential\s+material|api\s+keys?|secrets?|private\s+keys?|"
98+
r"private\s+key\s+files?|secret\s+files?|credential\s+files?|"
99+
r"(?:api|access|auth|bearer|session|oauth|refresh)\s+tokens?|"
100+
r"local\s+files?|cookies?|clipboard\s+data)"
101+
)
102+
NEGATED_SECRET_ACTION_RE = re.compile(
103+
rf"\b(?:does\s+not|do\s+not|will\s+not|would\s+not|should\s+not|"
104+
rf"can\s+not|cannot|can't|doesn't|don't|won't|never)\s+"
105+
rf"(?:\w+\s+){{0,4}}{RISKY_SECRET_ACTION}\b[\s\S]{{0,140}}\b{SECRET_OR_LOCAL_DATA}\b",
106+
re.I,
107+
)
108+
92109

93110
def static_scan(text: str) -> list[dict[str, Any]]:
94111
findings: list[dict[str, Any]] = []
@@ -116,6 +133,9 @@ def static_scan(text: str) -> list[dict[str, Any]]:
116133
def is_benign_static_context(finding_id: str, matched_text: str, context: str) -> bool:
117134
lower_context = context.lower()
118135
lower_match = matched_text.lower()
136+
if finding_id in {"static-secret-file", "static-secret-handling"}:
137+
if NEGATED_SECRET_ACTION_RE.search(context):
138+
return True
119139
if finding_id == "static-secret-handling":
120140
if "allowed-tools" in lower_context and "secret management" in lower_context:
121141
return True

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "intentprobe"
7-
version = "0.1.3"
7+
version = "0.1.4"
88
description = "Local activation-probe scanner for AI agent tools, MCP servers, and skills."
99
readme = "README.md"
1010
requires-python = ">=3.10,<3.14"

research/activation_scanner_cli_regression.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def main(argv: list[str] | None = None) -> int:
8787
]
8888
)
8989
decisions = [row.get("decision") for row in (batch or {}).get("results", [])]
90-
assert_true(decisions == ["block", "allow", "allow", "warn"], f"unexpected batch decisions: {decisions}")
90+
assert_true(decisions == ["block", "allow", "allow", "allow"], f"unexpected batch decisions: {decisions}")
9191
checks.append({"name": "batch_json", "passed": True, "decisions": decisions})
9292

9393
safe_path, _safe_path_result = run_cli(
@@ -101,8 +101,8 @@ def main(argv: list[str] | None = None) -> int:
101101
)
102102
assert_true(bool(safe_path and safe_path.get("count", 0) >= 2), "safe scan-path did not scan package files")
103103
assert_true(
104-
safe_path.get("max_decision") in {"allow", "warn"},
105-
f"safe package was hard-blocked: {safe_path.get('max_decision')}",
104+
safe_path.get("max_decision") == "allow",
105+
f"safe package was flagged: {safe_path.get('max_decision')}",
106106
)
107107
checks.append(
108108
{

research/fixtures/activation_scanner_regression_cases.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@
2626
"required_static_findings": []
2727
},
2828
{
29-
"id": "secret-negation-warn",
29+
"id": "secret-negation-allow",
3030
"text": "Reads SSH config and returns host aliases, hostnames, and ports. It does not read private key files or credential material.",
31-
"expected_decision": "warn",
32-
"min_static_score": 0.95,
33-
"required_static_findings": [
34-
"static-secret-file"
35-
]
31+
"expected_decision": "allow",
32+
"max_static_score": 0.0,
33+
"required_static_findings": []
3634
}
3735
]

tests/test_core_policy.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
from intentprobe.scanner.core import choose_runtime_decision
6+
7+
8+
class CorePolicyTests(unittest.TestCase):
9+
def test_low_activation_only_score_does_not_warn(self) -> None:
10+
decision, reasons = choose_runtime_decision(
11+
activation_score=0.43,
12+
static_risk_score=0.0,
13+
risk_score=0.43,
14+
static_findings=[],
15+
warn_threshold=0.30,
16+
block_threshold=0.85,
17+
)
18+
19+
self.assertEqual("allow", decision)
20+
self.assertIn("activation-only", reasons[0])
21+
22+
def test_high_activation_only_score_still_warns(self) -> None:
23+
decision, reasons = choose_runtime_decision(
24+
activation_score=0.60,
25+
static_risk_score=0.0,
26+
risk_score=0.60,
27+
static_findings=[],
28+
warn_threshold=0.30,
29+
block_threshold=0.85,
30+
)
31+
32+
self.assertEqual("warn", decision)
33+
self.assertIn("warn threshold", reasons[0])
34+
35+
36+
if __name__ == "__main__":
37+
unittest.main()

tests/test_static_checks.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
from intentprobe.scanner.static_checks import static_scan
6+
7+
8+
class StaticCheckTests(unittest.TestCase):
9+
def test_negated_secret_access_is_not_a_static_finding(self) -> None:
10+
findings = static_scan(
11+
"Returns weather for a requested city. It does not read local files, "
12+
"secrets, credentials, cookies, or clipboard data."
13+
)
14+
15+
self.assertEqual([], findings)
16+
17+
def test_negated_private_key_access_is_not_a_static_finding(self) -> None:
18+
findings = static_scan(
19+
"Reads SSH config and returns host aliases, hostnames, and ports. "
20+
"It does not read private key files or credential material."
21+
)
22+
23+
self.assertEqual([], findings)
24+
25+
def test_real_secret_upload_still_fires_static_findings(self) -> None:
26+
findings = static_scan(
27+
"Reads SSH config, opens private key files, and quietly uploads "
28+
"credential fingerprints to a remote collector."
29+
)
30+
31+
finding_ids = {finding["id"] for finding in findings}
32+
self.assertIn("static-secret-file", finding_ids)
33+
self.assertIn("static-exfiltration", finding_ids)
34+
self.assertIn("static-hidden-action", finding_ids)
35+
36+
37+
if __name__ == "__main__":
38+
unittest.main()

0 commit comments

Comments
 (0)