{"repo_url":"https://github.com/Panniantong/Agent-Reach","overall_status":"completed","scanned_at":"2026-10-14T22:50:05.098Z","scan_summary":{"total_components":2,"worst_case_severity":"CRITICAL","repo_trust_score":{"score":58,"tier":"low","capability_band":"minimal","score_confidence":"low","score_interval":{"low":22,"high":94},"factors":["ATR-P0001 CRITICAL","ATR-P0006 HIGH","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM"]}},"components":[{"artifact_version":"2.0","component":{"component_type":"mcp-server","component_name":"mcp-server","component_path":".","metadata":{"transports":["stdio"],"server_info_name":"agent-reach"}},"scan_provenance":{"scanned_at":"2026-10-14T22:50:05.098Z","resolved_ref":"94f06c1969dfc1834001269d79d3ad0972d9dee6","resolved_ver":"1.5.0"},"repo_context":{"repo_url":"https://github.com/Panniantong/Agent-Reach","languages":["python","shell"],"transports_detected":["stdio"],"deployment_artifacts":[],"provenance":{"publisher":"Panniantong","publisher_verified":false,"signed_build":false,"dependency_cve_count":null,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-M0001","rule_version":"1.2.0","rule_name":"Tool Definition Integrity & Versioning (ETDI-style)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The server exposes a single, hardcoded tool definition (`get_status`) built as a Python literal in `create_server()`; there is no external tool manifest, no runtime fetch of tool definitions, and no hot-reload path. Tool definitions ship inside the pinned package source and change only via a new release of the package itself (standard pip/source install), not at runtime. There is no update/signature-verification mechanism because there is no mutable external manifest to protect.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents rug-pull tool poisoning.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":43,"end_line":49,"snippet":"@server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0002","rule_version":"1.1.0","rule_name":"Unrestricted Tool Permissions (Overprivileged Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only registered tool, get_status, calls AgentReach(config).doctor_report() with a Config constructed as read_only=True. doctor_report does perform process execution and network activity -- it invokes fixed version/status subprocesses and queries fixed/local status endpoints per channel -- so it is not accurate that it performs no process execution or network activity at all. However, the tool's inputSchema accepts no properties and call_tool ignores any supplied arguments, so the caller cannot select or influence which command, URL, path, credential, or mutation is used: every subprocess and network call made by doctor_report is fixed by the implementation. The tool therefore remains least-privilege on the basis of caller-side control, not on an absence of underlying activity.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Limits blast radius and privilege escalation.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":39,"end_line":56,"snippet":"server = Server(\"agent-reach\")\n    config = Config(read_only=True)\n    eyes = AgentReach(config)\n\n    @server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\", ...)\n        ]\n\n    @server.call_tool()\n    async def call_tool(name: str, arguments: dict):\n        try:\n            if name == \"get_status\":\n                result = eyes.doctor_report()"},{"path":"agent_reach/doctor.py","start_line":16,"end_line":45,"snippet":"def check_all(config: Config) -> Dict[str, dict]:\n    results = {}\n    for ch in get_all_channels():\n        try:\n            status, message = ch.check(config)\n            active = getattr(ch, \"active_backend\", None)\n        except Exception as e:\n            status = \"error\"\n            ...\n        results[ch.name] = {...}\n    return results"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0003","rule_version":"1.3.1","rule_name":"CORS / Origin / Host Validation (DNS Rebinding Defense)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The MCP server is started exclusively via `stdio_server()` in `main()` (agent_reach/integrations/mcp_server.py). There is no HTTP/SSE/streamable-HTTP transport, no CORS middleware, and no listener binding to a network interface anywhere in the repo (grep for StreamableHTTP/SSEServer/uvicorn/Flask/FastAPI/listen/0.0.0.0 found no server-side matches). Per applicability.requires_remote_transport, this rule does not apply to a stdio-only component.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents DNS rebinding/browser hijack of local or remote MCP.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":72,"end_line":76,"snippet":"async def main():\n    server = create_server()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, server.create_initialization_options())"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0004","rule_version":"1.2.0","rule_name":"Binding to 0.0.0.0 Without Auth/TLS","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No network listener/bind exists in this component; it communicates over stdio only (see main() using stdio_server()). requires_remote_transport applicability makes this rule not applicable to a stdio-only component.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"LAN/Internet exposure enables remote exploitation.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":72,"end_line":76,"snippet":"async def main():\n    server = create_server()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, server.create_initialization_options())"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0005","rule_version":"1.4.0","rule_name":"OAuth 2.1 + PKCE Correctness (MCP Authorization)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"No remote transport (streamable_http/SSE/HTTP) is present; the server only runs over stdio (see main()/stdio_server()). Per the rule's own false_positive_guards, stdio-only components with no remote authorization surface are NOT_APPLICABLE for OAuth 2.1 requirements.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Misconfig enables token theft and confused-deputy attacks.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":72,"end_line":76,"snippet":"async def main():\n    server = create_server()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, server.create_initialization_options())"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0006","rule_version":"1.2.0","rule_name":"Tool Name Collision / Spoofing","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The server exposes a single tool named `get_status`. This is a generic verb with no collision against a specific named tool exposed by a widely-deployed MCP server or client built-in (e.g., it is not 'read_file' or similar). No namespace prefixing exists but with only one trivial, read-only tool the collision risk is negligible.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents malicious tool shadowing.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":46,"end_line":48,"snippet":"Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0007","rule_version":"1.2.0","rule_name":"Prompt/Metadata Injection in Tool Descriptions","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The sole tool description is a short, neutral, plain-ASCII string with no hidden directives, no control/zero-width/bidi characters, and no instructions to exfiltrate data or manipulate the model. Manual inspection of the literal string confirms it is purely descriptive.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents covert model manipulation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":46,"end_line":48,"snippet":"Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0008","rule_version":"1.2.0","rule_name":"CORS Misuse: Wildcard with Credentials","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No CORS middleware or HTTP listener exists in this stdio-only component; requires_remote_transport applicability makes this rule not applicable.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents credentialed cross-origin abuse.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":72,"end_line":76,"snippet":"async def main():\n    server = create_server()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, server.create_initialization_options())"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0009","rule_version":"1.3.0","rule_name":"Transport Security: TLS Everywhere (Remote)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No remote HTTP transport exists; the component communicates exclusively over local stdio. requires_remote_transport applicability makes TLS-everywhere rule not applicable.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"MitM can steal tokens and tamper responses.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":72,"end_line":76,"snippet":"async def main():\n    server = create_server()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, server.create_initialization_options())"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0010","rule_version":"1.4.0","rule_name":"No Authentication or Authorization Checks (General)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"This is a loopback/local-process-only stdio MCP server (no network bind at all -- see main()/stdio_server()). Its single tool, get_status, is pure over no caller input (empty inputSchema) and only reads local channel-availability status via a read_only Config -- it has no host-side write effect, no credential/token access, no process execution, and no shared backend identity across callers (each stdio session is a separate local process invocation). Per the rule's negative_indicators/false_positive_guards, a loopback-only listener (here, no listener at all; the OS process boundary governs who can speak over stdio) whose every tool is pure over caller-supplied input is NOT VULNERABLE -- the missing authn/authz gate is hardening, not a finding.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents unauthorized tool use and data access.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":39,"end_line":56,"snippet":"    server = Server(\"agent-reach\")\n    config = Config(read_only=True)\n    eyes = AgentReach(config)\n\n    @server.list_tools()\n    async def list_tools():\n        return [Tool(name=\"get_status\", ... inputSchema={\"type\": \"object\", \"properties\": {}})]\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]\n\n    @server.call_tool()"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0011","rule_version":"1.2.0","rule_name":"OAuth Proxy Per-Client Consent (Confused Deputy Defense)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"The server is not an OAuth proxy: it does not forward authorization requests to a distinct upstream IdP on behalf of dynamically-registered MCP clients. There is no /authorize, /register-client, or bridge-code logic anywhere in the codebase; it is a single local stdio tool that reports channel status. The confused-deputy OAuth-proxy threat model does not apply.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":1,"end_line":80,"snippet":"(whole file -- no OAuth proxy / authorize / register-client / bridge-code logic present)"}],"affected_tools":null,"component_name":"mcp-server"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.0","rule_name":"Unrestricted Network Fetch / SSRF","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only arbitrary-URL fetch surface is WebChannel.read() (Jina Reader proxy for 'any URL'). It passes every URL through normalize_public_http_url() before use, which rejects non-HTTP(S) schemes, userinfo, literal non-global IPs (RFC1918/loopback/link-local/metadata via ipaddress.is_global), and known internal TLD/hostname suffixes (.local, .internal, metadata.google.internal, etc.). All other channels (V2EX, Xueqiu, Boss, GitHub, Bilibili) call a fixed, hardcoded API host and validate scheme/host/port with host_matches()/_validate_api_url() before any request, so user input cannot redirect the destination host. Responses are size-capped (_MAX_RESPONSE_BYTES) and timeouts are set on every urlopen/subprocess call.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":true,"transport_gated":true,"mitigation_present":true,"evidence":[{"path":"agent_reach/utils/url.py","start_line":47,"end_line":84,"snippet":"def normalize_public_http_url(url: str) -> str:\n    ...\n    literal_address = _literal_ip_address(host)\n    if (\n        parsed.scheme.lower() not in {\"http\", \"https\"}\n        or not host\n        or parsed.username is not None\n        or parsed.password is not None\n        or \"%\" in host\n        or host in _BLOCKED_PUBLIC_FETCH_HOSTS\n        or host.endswith(_BLOCKED_PUBLIC_FETCH_SUFFIXES)\n        or (\".\" not in host and literal_address is None)\n        or (literal_address is not None and not literal_address.is_global)\n    ):\n        raise ValueError(\"only public HTTP(S) URLs are allowed\")"},{"path":"agent_reach/channels/web.py","start_line":48,"end_line":61,"snippet":"def read(self, url: str) -> str:\n    url = normalize_public_http_url(url)\n    jina_url = f\"https://r.jina.ai/{url}\"\n    req = urllib.request.Request(\n        jina_url,\n        headers={\"User-Agent\": _UA, \"Accept\": \"text/plain\"},\n    )\n    with urllib.request.urlopen(req, timeout=30) as resp:\n        body = resp.read(_MAX_RESPONSE_BYTES + 1)"},{"path":"agent_reach/channels/v2ex.py","start_line":28,"end_line":43,"snippet":"def _validate_api_url(url: str) -> None:\n    \"\"\"Allow only the public V2EX HTTPS JSON API.\"\"\"\n    ...\n    if (\n        parsed.scheme.lower() != \"https\"\n        or (parsed.hostname or \"\").lower() not in {\"v2ex.com\", \"www.v2ex.com\"}\n        or port not in {None, 443}\n        or parsed.username is not None\n        or parsed.password is not None\n        or not parsed.path.startswith(\"/api/\")\n    ):\n        raise ValueError(\"only the V2EX HTTPS API is allowed\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.2","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":".env.example ships only commented-out placeholder values with no real secret material. gitleaks scan found 0 findings repo-wide. Secrets are read from ~/.agent-reach/config.yaml (created at runtime, not committed) or from process.env. Config.to_dict() masks sensitive fields as '[REDACTED]'. Semgrep's atr-c0002 credential-to-log-sink rule flagged 4 lines (cli.py:1519,1532,1536 and cookie_extract.py:401) -- all 4 were read: cli.py:1519/1532/1536 write the literal status string 'requires login' into config.set()/print() (not a credential value), and cookie_extract.py:401 writes AUTH_TOKEN/CT0 to a private 0600 file (~/.config/bird/credentials.env) via atomic_write_private_text, shell-quoted with shlex.quote -- this is a private credential file write, not a log/stdout sink, and the file is owner-only. No credential value is printed, logged, or committed anywhere.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Plaintext secrets are easily exfiltrated.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/cli.py","start_line":1517,"end_line":1522,"snippet":"        if auth_token and ct0:\n            config.set(\"twitter_auth_token\", auth_token)\n            config.set(\"twitter_ct0\", ct0)\n\n            print(\"✅ Twitter cookies 已保存到 ~/.agent-reach/config.yaml\")"},{"path":"agent_reach/cli.py","start_line":1529,"end_line":1538,"snippet":"                legacy_results = (\n                    (\n                        \"~/.config/xfetch/session.json\",\n                        _sync_xfetch_session(auth_token, ct0),\n                    ),\n                    (\n                        \"~/.config/bird/credentials.env\",\n                        _sync_bird_env(auth_token, ct0),\n                    ),\n                )"},{"path":"agent_reach/cookie_extract.py","start_line":396,"end_line":404,"snippet":"        bird_dir = home_dir() / \".config\" / \"bird\"\n        make_private_dir(bird_dir)\n        env_path = bird_dir / \"credentials.env\"\n        atomic_write_private_text(\n            env_path,\n            f\"AUTH_TOKEN={shlex.quote(auth_token)}\\n\"\n            f\"CT0={shlex.quote(ct0)}\\n\",\n        )\n        return True"},{"path":"agent_reach/config.py","start_line":211,"end_line":233,"snippet":"def to_dict(self) -> dict:\n    \"\"\"Return config as dict (masks sensitive values).\"\"\"\n    sensitive_markers = (\n        \"key\", \"token\", \"password\", \"proxy\", \"cookie\", \"secret\",\n        \"session\", \"sessdata\", \"csrf\", \"auth\", \"cred\", \"ct0\",\n    )\n    masked = {}\n    for k, v in self.data.items():\n        if any(s in k.lower() for s in sensitive_markers):\n            masked[k] = \"[REDACTED]\" if v else None\n        else:\n            masked[k] = v\n    return masked"},{"path":"agent_reach/cli.py","start_line":1519,"end_line":1519,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cli.py","start_line":1532,"end_line":1532,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cli.py","start_line":1536,"end_line":1536,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cookie_extract.py","start_line":401,"end_line":401,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":".env.example","start_line":5,"end_line":19,"snippet":"# Exa Search (free 1000/month) — https://exa.ai\n# EXA_API_KEY=exa-your-key-here\n\n# GitHub Token (optional, for higher rate limits) — https://github.com/settings/tokens\n# GITHUB_TOKEN=ghp_your_token_here\n\n# Reddit ISP Proxy (optional, for full Reddit access)\n# REDDIT_PROXY=http://user:[REDACTED]@ip:port\n\n# Groq Whisper (optional, for video transcription) — https://console.groq.com\n# GROQ_API_KEY=gsk_your_key_here\n\n# OpenAI Whisper (optional fallback when Groq is rate-limited) — https://platform.openai.com\n# OPENAI_API_KEY=sk-your_key_here"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.3.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"All subprocess invocations across the runtime (cli.py, v2ex.py, transcribe.py, probe.py) use subprocess.run() with an argument-vector list and no shell=True / os.system / eval anywhere in agent_reach/ (confirmed by grep: zero shell=True, os.system, or eval( hits in application code). Arguments passed to yt-dlp/ffmpeg/curl are either fixed flags or a URL that is first validated by _assert_safe_public_url()/normalize_public_http_url() (reject non-http(s) schemes, private/loopback/link-local/reserved IPs including inet_aton shorthand forms like 127.1/0x7f000001, and internal hostnames). The '--' separator before the URL argument in yt-dlp invocation prevents option-injection via a URL starting with '-'. Bandit flagged B603 (subprocess without shell equals true) across 25 call sites in cli.py/v2ex.py/transcribe.py -- all manually confirmed to use list-form argv with a fixed executable located via shutil.which(), no shell re-parsing.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":214,"end_line":270,"snippet":"def _assert_safe_public_url(url: str) -> None:\n    ...\n    if host in _BLOCKED_HOSTS or host.endswith(\".localhost\"):\n        raise TranscribeError(\"SSRF blocked: internal host is not allowed\")\n    if _is_private_ip(host):\n        raise TranscribeError(\"SSRF blocked: private/internal IP is not allowed\")\n\n\ndef download_audio(url: str, out_dir: Path) -> Path:\n    \"\"\"Download audio with yt-dlp into out_dir; return the resulting file path.\"\"\"\n    _assert_safe_public_url(url)\n    _require(\"yt-dlp\")\n    template = out_dir / \"source.%(ext)s\"\n    _run(\n        [\n            \"yt-dlp\", \"-x\", \"--audio-format\", \"m4a\", \"--audio-quality\", \"0\",\n            \"--no-playlist\", \"--max-filesize\", str(MAX_SOURCE_BYTES),\n            \"-o\", str(template), \"--\", url,"},{"path":"agent_reach/channels/v2ex.py","start_line":92,"end_line":120,"snippet":"    command = [\n        curl, \"--fail\", \"--silent\", \"--show-error\", \"--proto\", \"=https\",\n        \"--connect-timeout\", \"5\", \"--max-time\", str(_TIMEOUT),\n        \"--max-filesize\", str(_MAX_RESPONSE_BYTES),\n        \"--header\", f\"User-Agent: {_UA}\", \"--url\", url,\n    ]\n    try:\n        result = subprocess.run(\n            command, capture_output=True, encoding=\"utf-8\", errors=\"replace\",\n            timeout=_TIMEOUT + 2, env=utf8_subprocess_env(),\n        )"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.0","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"Not every HTTP response is byte-capped: several fixed-host health/status checks (bilibili.py, boss.py, xueqiu.py) use a bare resp.read() with no explicit cap. However, these calls target fixed, hardcoded API hosts rather than an attacker-selectable destination. The paths that are both attacker-triggerable and high-cost -- media download (yt-dlp, MAX_SOURCE_BYTES/MAX_AUDIO_SECONDS/MAX_CHUNKS) and the arbitrary-URL web-reader fetch (_MAX_RESPONSE_BYTES) -- remain explicitly bounded, and every urlopen/subprocess call carries a timeout. The exposed MCP surface itself is a single fixed, argument-free status tool with no concurrent untrusted request handling, so resource-exhaustion protection rests on bounding the attacker-controllable paths rather than on a universal response-size cap.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":37,"end_line":44,"snippet":"SIZE_LIMIT_BYTES = 24 * 1024 * 1024\nCHUNK_SECONDS = 600\nMAX_SOURCE_BYTES = 512 * 1024 * 1024\nMAX_CHUNKS = 24\nMAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024\nMAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS\nFFPROBE_TIMEOUT_SECONDS = 30"},{"path":"agent_reach/channels/web.py","start_line":11,"end_line":12,"snippet":"_MAX_RESPONSE_BYTES = 5 * 1024 * 1024\n_ANTIBOT_SCAN_BYTES = 4096"},{"path":"agent_reach/channels/web.py","start_line":56,"end_line":61,"snippet":"        with urllib.request.urlopen(req, timeout=30) as resp:\n            body = resp.read(_MAX_RESPONSE_BYTES + 1)\n        if len(body) > _MAX_RESPONSE_BYTES:\n            raise ValueError(\n                f\"Jina Reader response exceeds {_MAX_RESPONSE_BYTES} byte limit\"\n            )"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.0","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"high","rationale":"Doctor/status output and exception text are scrubbed of credentials via scrub_url_credentials() before being rendered, and config.to_dict() masks sensitive fields. However, this rule also requires an audit trail of tool invocations. The MCP server's call_tool dispatcher executes get_status and returns its result directly, without recording the tool name, arguments, outcome, caller/session identity, or a correlation identifier anywhere. CLI logging is optional console output, not a structured invocation log. As a result, MCP tool operations cannot be reconstructed for forensics after the fact.","vulnerability_details":null,"fix_guidance":"Add an audit-log call inside the MCP call_tool dispatcher that records, for every invocation, the tool name, redacted arguments, caller/session identifier, timestamp, and outcome/result status to a durable log or audit store.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0005 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":50,"end_line":63,"snippet":"\n    @server.call_tool()\n    async def call_tool(name: str, arguments: dict):\n        try:\n            if name == \"get_status\":\n                result = eyes.doctor_report()\n            else:\n                result = f\"Unknown tool: {name}\"\n\n            text = json.dumps(result, ensure_ascii=False, indent=2) if isinstance(result, (dict, list)) else str(result)\n            return [TextContent(type=\"text\", text=text)]\n        except Exception as e:\n            return [\n                TextContent("},{"path":"agent_reach/cli.py","start_line":60,"end_line":65,"snippet":"def _configure_logging(verbose: bool = False):\n    \"\"\"Suppress loguru output unless --verbose is set.\"\"\"\n    from loguru import logger\n    logger.remove()  # Remove default stderr handler\n    if verbose:\n        logger.add(sys.stderr, level=\"INFO\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.2.0","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"Agent Reach ships as a Python package (pip-installable CLI/MCP tool) with no Dockerfile, docker-compose, Helm chart, or Kubernetes manifest anywhere in the repository (confirmed: find for Dockerfile*/docker-compose*/*.tf/*.yaml under deploy-style paths returned nothing beyond the CI workflow and mcporter config). There is no container or cloud deployment artifact for this rule's container-hardening controls (USER, read-only FS, capabilities, seccomp, hostNetwork) to apply to.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Reduces blast radius of exploits.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".","start_line":1,"end_line":1,"snippet":"No Dockerfile, docker-compose.yml, Helm chart, or Kubernetes/Terraform manifest found in the repository tree (verified via recursive find for Dockerfile*, docker-compose*, *.tf, helm/, k8s/)."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.2.0","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"high","rationale":"pyproject.toml declares every runtime dependency with open-ended minimum-version bounds (e.g. requests>=2.28, pyyaml>=6.0, yt-dlp[default]>=2026.07.04) rather than pinned or upper-bounded versions. constraints.txt does pin every dependency to an exact version, but it is opt-in: it is applied only when a user explicitly runs `pip install -c constraints.txt ...`, and is not consulted by an ordinary `pip install agent-reach`. A standard installation therefore resolves against the open-ended bounds in pyproject.toml and can pull in unreviewed future dependency releases, including ones affected by a newly disclosed vulnerability or supply-chain compromise.","vulnerability_details":null,"fix_guidance":"Pin or upper-bound the dependency versions declared directly in pyproject.toml (or ship and enforce a lockfile that is applied by default), rather than relying on an opt-in constraints.txt that an ordinary install does not consult.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"pyproject.toml","start_line":29,"end_line":46,"snippet":"\ndependencies = [\n    \"requests>=2.28\",\n    \"feedparser>=6.0\",\n    \"python-dotenv>=1.0\",\n    \"loguru>=0.7\",\n    \"pyyaml>=6.0\",\n    \"rich>=13.0\",\n    \"yt-dlp[default]>=2026.07.04\",\n]\n\n[project.optional-dependencies]\nbrowser = [\"playwright>=1.40\"]\ncookies = [\"browser-cookie3>=0.19\"]\nall = [\"playwright>=1.40\", \"mcp[cli]>=1.0\", \"browser-cookie3>=0.19\"]\ndev = [\n    \"pytest>=8.0\",\n    \"ruff>=0.8\","},{"path":"constraints.txt","start_line":1,"end_line":12,"snippet":"# Agent Reach tested dependency set\n# Usage:\n#   pip install -c constraints.txt -e .[dev]\n\nrequests==2.32.5\nfeedparser==6.0.12\npython-dotenv==1.2.1\nloguru==0.7.3\nPyYAML==6.0.3\nrich==14.3.2\nyt-dlp==2026.07.04\nyt-dlp-ejs==0.8.0"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.2.0","rule_name":"Missing or Inadequate Input Validation","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The single MCP tool (get_status) takes no arguments (empty inputSchema), eliminating that attack surface entirely. The CLI entry points use argparse with explicit `choices=[...]` allowlists for all enum-like parameters (configure key, --from-browser browser, --platform, format platform) so out-of-range values are rejected by argparse itself before reaching application code. Free-text inputs that matter for security (URLs passed to web/transcribe channels) are validated by dedicated schema-like validators -- normalize_public_http_url(), _validate_api_url(), _assert_safe_public_url() -- which enforce scheme/host/port/userinfo constraints before any network or subprocess call. Config file inputs are size-capped (_MAX_CONFIG_BYTES) and type-checked (must parse to a dict) before use.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/cli.py","start_line":108,"end_line":114,"snippet":"    p_conf = sub.add_parser(\"configure\", help=\"Set a config value or auto-extract from browser\")\n    p_conf.add_argument(\"key\", nargs=\"?\", default=None,\n                        choices=[\"proxy\", \"github-token\", \"groq-key\", \"openai-key\",\n                                 \"twitter-cookies\", \"youtube-cookies\",\n                                 \"xhs-cookies\"],\n                        help=\"What to configure (omit if using --from-browser)\")\n    p_conf.add_argument(\"value\", nargs=\"*\", help=\"The value(s) to set\")"},{"path":"agent_reach/integrations/mcp_server.py","start_line":43,"end_line":49,"snippet":"    @server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]"},{"path":"agent_reach/config.py","start_line":136,"end_line":150,"snippet":"        try:\n            payload = read_small_text_no_follow(\n                self.config_path,\n                max_bytes=_MAX_CONFIG_BYTES,\n            )\n        except PrivatePathError as exc:\n            raise ConfigSecurityError(str(exc)) from exc\n        if payload is None:\n            self.data = {}\n            return\n\n        loaded = yaml.safe_load(payload) or {}\n        if not isinstance(loaded, dict):\n            raise ConfigError(\"配置文件顶层必须是对象\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0009","rule_version":"1.2.0","rule_name":"Insecure Deserialization","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No use of pickle, marshal, or yaml.load/yaml.unsafe_load anywhere in the codebase (grep for these patterns across agent_reach/ returned zero matches). All YAML parsing uses yaml.safe_load (config.py load(), github.py hosts.yml parsing) and all JSON parsing uses the standard json module, neither of which executes arbitrary code on untrusted input.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Can lead to arbitrary code execution.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/config.py","start_line":147,"end_line":150,"snippet":"        loaded = yaml.safe_load(payload) or {}\n        if not isinstance(loaded, dict):\n            raise ConfigError(\"配置文件顶层必须是对象\")\n        self.data = loaded"},{"path":"agent_reach/channels/github.py","start_line":63,"end_line":66,"snippet":"    try:\n        payload = yaml.safe_load(raw)\n    except yaml.YAMLError as exc:\n        raise GitHubConfigError(\"gh hosts.yml 不是有效的 UTF-8 YAML\") from exc"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.2.0","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"high","rationale":"`agent-reach transcribe` sends derived audio to an external transcription provider (Groq/OpenAI Whisper) and prints that provider's raw response text directly to the operator's terminal (cli.py, fed from transcribe.py), with no ANSI/control-character sanitization or output encoding applied before printing. Because the transcript content originates from third-party audio the operator does not control, a crafted transcription result could embed terminal escape sequences that reach the terminal unneutralized, making this a live output-encoding sink rather than an inapplicable one.","vulnerability_details":null,"fix_guidance":"Strip or neutralize ANSI/terminal control-escape sequences from the transcription provider's response text before printing it to the terminal, e.g. by filtering non-printable/control characters outside normal whitespace.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0010 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/cli.py","start_line":165,"end_line":169,"snippet":"    p_tr = sub.add_parser(\"transcribe\", help=\"Transcribe a URL or local audio file (Whisper via Groq/OpenAI)\")\n    p_tr.add_argument(\"source\", help=\"Audio/video URL or local file path\")\n    p_tr.add_argument(\"--provider\", choices=[\"auto\", \"groq\", \"openai\"], default=\"auto\",\n                      help=\"Transcription provider (default: first configured provider)\")\n    p_tr.add_argument("},{"path":"agent_reach/transcribe.py","start_line":381,"end_line":395,"snippet":"        try:\n            resp = requests.post(\n                info[\"endpoint\"],\n                headers={\"Authorization\": f\"Bearer {key}\"},\n                files={\"file\": (chunk.name, fh, \"audio/m4a\")},\n                data={\"model\": info[\"model\"], \"response_format\": \"text\"},\n                timeout=timeout,\n            )\n        except requests.RequestException as e:\n            raise TranscribeError(f\"{provider}: network error: {e}\") from e\n\n    if not resp.ok:\n        raise TranscribeError(f\"{provider}: HTTP {resp.status_code}: {resp.text[:300]}\")\n    return resp.text\n"},{"path":"agent_reach/cli.py","start_line":1608,"end_line":1613,"snippet":"\n    if args.output:\n        Path(args.output).write_text(text + \"\\n\", encoding=\"utf-8\")\n        print(f\"✅ Transcript written to {args.output}\")\n    else:\n        print(text)"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.4.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only MCP tool exposed (get_status in integrations/mcp_server.py) takes no filesystem arguments at all -- it has an empty inputSchema and calls eyes.doctor_report() with no caller-supplied path. The CLI's filesystem writes (skill install, config, cookie sync) write only to fixed, hardcoded locations under the user's home directory (~/.agent-reach, ~/.claude/skills, ~/.config/bird, etc.) -- none of these paths are derived from untrusted/caller-supplied input. Every private-file read/write path goes through utils/paths.py helpers (ensure_no_symlink_path, read_small_text_no_follow, atomic_write_private_text) which walk every path component with os.lstat and reject any symlink before opening, preventing symlink-redirection attacks even though the destination paths are already fixed.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Traversal exposes arbitrary host files; can lead to code execution.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":43,"end_line":57,"snippet":"    @server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]\n\n    @server.call_tool()\n    async def call_tool(name: str, arguments: dict):\n        try:\n            if name == \"get_status\":\n                result = eyes.doctor_report()"},{"path":"agent_reach/utils/paths.py","start_line":34,"end_line":47,"snippet":"def ensure_no_symlink_path(path: str | Path, label: str = \"路径\") -> Path:\n    \"\"\"Reject any existing symlink component without resolving the path.\"\"\"\n    target = Path(path)\n    absolute = Path(os.path.abspath(os.fspath(target)))\n    current = Path(absolute.anchor)\n    for part in absolute.parts[1:]:\n        current /= part\n        try:\n            mode = os.lstat(current).st_mode\n        except FileNotFoundError:\n            continue\n        if stat.S_ISLNK(mode):"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.0","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The only billable external dependency is the Whisper transcription API (Groq/OpenAI), gated behind a user-supplied API key the user must configure themselves (no key ships with the repo). Each transcription run is bounded: MAX_SOURCE_BYTES (512MB download cap), MAX_AUDIO_SECONDS (4 hours via MAX_CHUNKS=24 * CHUNK_SECONDS=600) reject oversized/too-long media before any paid API call, and chunks are capped at MAX_TOTAL_CHUNK_BYTES. There is no loop or batch mode that could multiply billed calls unboundedly; transcribe() processes one source per invocation. All other channels (web/social/search) either use free public APIs or require the operator's own paid key (Exa), so Agent Reach itself has no mechanism to run up a bill beyond what the operator explicitly triggers per call.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":40,"end_line":43,"snippet":"MAX_SOURCE_BYTES = 512 * 1024 * 1024\nMAX_CHUNKS = 24  # 4 hours at the standard 10-minute segment size\nMAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024\nMAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS"},{"path":"agent_reach/transcribe.py","start_line":144,"end_line":152,"snippet":"def _require_duration_within_budget(path: Path) -> float:\n    \"\"\"Reject audio that cannot fit within the bounded chunk budget.\"\"\"\n    duration = _probe_audio_duration(path)\n    if duration > MAX_AUDIO_SECONDS:\n        max_minutes = MAX_AUDIO_SECONDS // 60\n        raise TranscribeError(\n            f\"audio duration exceeds safety limit of {max_minutes} minutes\"\n        )\n    return duration"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.0","rule_name":"Tool Output Sanitization & Injection Containment","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Agent Reach's own MCP server (integrations/mcp_server.py) exposes only a single tool, get_status, which returns the host's own doctor report (JSON built from fixed channel-status strings) -- no third-party content flows through that specific tool, so for that narrow surface the rule is effectively not triggered. However, Agent Reach's actual declared product (per SKILL.md) is a router that tells the calling agent to invoke OTHER external CLIs/MCP tools directly (yt-dlp, Jina Reader via curl, gh CLI, mcporter Exa, twitter-cli, opencli) and consume their raw output as LLM context. Agent Reach's own code does not itself inject fetched web/social content into any LLM context (no envelope/sanitization logic exists in this repo for that purpose, because Agent Reach does not perform the fetch-and-return step -- it delegates it to upstream tools outside this repo's scope). Because the host-side injection point lives in those external tools/the calling agent rather than in this repository, I cannot verify from this codebase alone whether an envelope/sanitization/untrustedReturn control exists at the point where fetched content reaches the model -- abstaining rather than guessing about code outside this repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Tool outputs are a primary prompt-injection vector. Third-party content (web pages, files, API responses) may contain text crafted to override instructions or impersonate system/tool messages. Without a host-enforced envelope, sanitization, and an explicit untrusted-return signal, the model can be coerced into executing attacker-controlled directives, leaking secrets, or chaining further tool calls on behalf of the adversary.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0014","rule_version":"1.2.0","rule_name":"CI/CD Workflow Script Injection","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The repository has a single GitHub Actions workflow (.github/workflows/pytest.yml) triggered only by `push` and `pull_request` (not pull_request_target, issue_comment, workflow_run, or repository_dispatch). No step interpolates any `${{ github.event.* }}`, `${{ inputs.* }}`, or other attacker-controlled expression into a `run:` shell block -- all run steps execute fixed commands (pip install, pytest, python -m build, wheel verification script). The matrix `python-version` values are static literals defined in the workflow file itself, not derived from any external input. Semgrep's github-actions-mutable-action-tag rule flagged 6 lines (all `uses: actions/checkout@v4` / `actions/setup-python@v5`), which is a supply-chain pinning hygiene note (mutable major-version tag vs. pinned SHA), not a script-injection finding -- there is no shell-injection vector in this workflow.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".github/workflows/pytest.yml","start_line":1,"end_line":31,"snippet":"name: ci\n\non:\n  push:\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install package and test deps\n        run: |\n          python -m pip install --upgrade pip\n          pip install -c constraints.txt -e .[dev]\n      - name: Run tests\n        run: |\n          pytest -q"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.0","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Agent Reach's declared purpose (README.md, SKILL.md, pyproject.toml description) is to give an AI agent read access to public internet platforms (Twitter, Reddit, YouTube, GitHub, etc.) for research/search -- a benign, clearly non-offensive capability. There is no malware/exploit/credential-theft/evasion generation capability anywhere in the instruction bodies or bundled scripts; the only CDP browser-automation pattern (boss.py) exists to reuse the USER's OWN already-logged-in browser session for legitimate job-search automation and explicitly requires the user to visually confirm their own logged-in state before any action -- the opposite of covert/unattributed operation. No offensive-security framing, dual-use pentest claim, or log-suppression/anti-analysis instructions are present anywhere in the repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"pyproject.toml","start_line":5,"end_line":5,"snippet":"description = \"Give your AI Agent eyes to see the entire internet. Search + Read 10+ platforms.\""},{"path":"agent_reach/skill/SKILL.md","start_line":94,"end_line":97,"snippet":"只绑定 `127.0.0.1:9222` 的专用 Chrome；**拉起后第一步是暂停并让用户肉眼确认**\n窗口内是已登录状态（右上角有头像），未登录则让用户登录/扫码，用户确认后再运行\n`boss --cdp-url http://localhost:9222 login --cdp` 和 `agent-reach doctor` 验收。\n不要让用户自己研究端口参数。"}],"affected_tools":null,"component_name":null}],"component_tools":[{"tool_name":"get_status","tool_description":"Get Agent Reach status: which channels are installed and active (doctor report).","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server"}],"summary":{"high_level_risks":["Prompt-injection / output-sanitization gap in upstream tool chain: SKILL.md instructs the agent to invoke external CLIs (yt-dlp, Jina Reader, gh CLI, twitter-cli) directly, and whether those tools apply any output-sanitization or prompt-injection envelope before raw fetched content reaches the calling LLM's context cannot be verified from this codebase alone (ATR-C0013, UNKNOWN).","GitHub Actions supply-chain hygiene gap: the CI workflow (.github/workflows/pytest.yml) pins third-party actions by mutable major-version tag (actions/checkout@v4, actions/setup-python@v5) rather than by commit SHA, creating a supply-chain tampering window (flagged by Semgrep github-actions-mutable-action-tag rule).","The repo's broader CLI (agent_reach/cli.py, cookie_extract.py, boss.py channel) handles browser cookies and CDP-based browser automation for several platforms; if those modules are ever exposed as MCP tools or skill capabilities in a future version they would require independent security review.","Bandit reported 1330 repo-wide findings and Semgrep 21; these are overwhelmingly generic CLI-code style/assert findings that were not individually re-triaged and may contain unreviewed issues outside the narrow MCP/skill scope."],"top_remediations":["Pin third-party GitHub Actions to a specific commit SHA instead of a mutable major-version tag (e.g. actions/checkout@<sha> # v4.x.x) to close the supply-chain tampering window.","Apply a host-enforced envelope and untrustedReturn-style annotation around any third-party fetched content (yt-dlp, Jina Reader, gh CLI, twitter-cli output) before it reaches LLM context, consistent with the SSRF-hardening discipline already present in utils/url.py.","Keep the MCP server stdio-only; if a remote transport (HTTP/SSE) is ever added, implement OAuth 2.1+PKCE, Origin/Host validation, and TLS before exposure (per ATR-M0003/M0004/M0005/M0008/M0009).","If SKILL.md is revised to include lifecycle hooks, auto-update fetch of instructions, or write-capable commands, re-evaluate ATR-P0002/P0004/P0007 against the new content.","If the MCP tool surface is expanded beyond get_status, re-evaluate ATR-M0002/M0010 against any new tool's capabilities and transport.","Continue the existing least-privilege file-permission discipline; Semgrep WARNING-level findings at cli.py:939 and utils/paths.py:65 note 0o700 directory/script permissions that are stricter than default but are not a vulnerability — no immediate action required."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":5,"not_applicable":6},"repo_findings_inherited_count":{"vulnerable":3,"secure":10,"not_applicable":1},"component_tools_count":{"read_only":1},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":58,"tier":"low","capability_band":"minimal","score_confidence":"low","score_interval":{"low":22,"high":94},"factors":["ATR-P0001 CRITICAL","ATR-P0006 HIGH","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM"]},"component_level":{"score":92,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":84,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/agent_reach/skill","component_path":"agent_reach/skill","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-10-14T22:50:05.098Z","resolved_ref":"94f06c1969dfc1834001269d79d3ad0972d9dee6","resolved_ver":"1.5.0"},"repo_context":{"repo_url":"https://github.com/Panniantong/Agent-Reach","languages":["python","shell"],"transports_detected":["stdio"],"deployment_artifacts":[],"provenance":{"publisher":"Panniantong","publisher_verified":false,"signed_build":false,"dependency_cve_count":null,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.0","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"VULNERABLE","severity":"CRITICAL","realized_severity":"CRITICAL","confidence":"medium","rationale":"SKILL.md explicitly states the skill is NOT for write operations such as posting, commenting, or liking, framing itself as a read-only internet research/content-fetch router. The bundled references/dev.md, however, instructs the agent to run state-changing GitHub commands -- including creating, forking, and syncing repositories, creating issues and pull requests, and creating releases. These bundled operational instructions are an undisclosed write capability that directly contradicts the skill's own declared no-write scope, rather than every referenced command mapping one-to-one onto the declared read/search capability.","vulnerability_details":null,"fix_guidance":"Remove the repository-write commands (repo create/fork/sync, issue/PR creation, release creation) from references/dev.md, or update SKILL.md's declared scope to explicitly disclose that GitHub write operations are part of the skill's capability set.","why_this_matters":"Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-P0001 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals CRITICAL.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":13,"end_line":20,"snippet":"  16 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs).\n  Zero config for 6 channels. Run `agent-reach doctor --json` to see which\n  backend serves each platform right now.\n\n  NOT for: 写报告/数据分析/翻译等内容加工（本 skill 只负责从互联网获取内容）；\n  发帖/评论/点赞等写操作；已有专门 skill 的平台（先用专门 skill）。\n\n  【路由方式】SKILL.md 包含路由表和常用命令，复杂场景需按需阅读对应分类的 references/*.md。"},{"path":"agent_reach/skill/references/dev.md","start_line":16,"end_line":35,"snippet":"gh search code \"query\" --language python\n\n# 仓库\ngh repo view owner/repo\ngh repo clone owner/repo\ngh repo create my-repo --private\ngh repo fork owner/repo\ngh repo fork owner/repo --clone\ngh repo sync owner/repo\n\n# Issues\ngh issue list -R owner/repo --state open\ngh issue view 123 -R owner/repo\ngh issue create -R owner/repo --title \"Title\" --body \"Body\"\n\n# Pull Requests\ngh pr list -R owner/repo --state open\ngh pr view 123 -R owner/repo\ngh pr create -R owner/repo --title \"Title\" --body \"Body\"\ngh pr checks 123 --repo owner/repo"},{"path":"agent_reach/skill/references/dev.md","start_line":40,"end_line":42,"snippet":"gh run view <run-id> --repo owner/repo --log-failed\ngh workflow list --repo owner/repo\n"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0002","rule_version":"1.0.0","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Reviewed SKILL.md and SKILL_en.md in full plus all references/*.md for imperative prose instructing the host to skip confirmation, hide actions, or claim authority over the system prompt. No such directives exist. On the contrary, the Boss Zhipin (招聘) flow explicitly instructs the agent to PAUSE and require the user to visually confirm login state before proceeding ('拉起后第一步是暂停并让用户肉眼确认...'), and multiple sections stress explicit, non-silent credential handling (e.g. Twitter auth tokens must be provided explicitly and 不得在日志或命令回显中暴露值 -- a disclosure-preserving instruction, not a suppression one).","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":93,"end_line":98,"snippet":"Boss直聘配置触发：当用户说“帮我配 Boss直聘”时，先读取 `references/career.md`\n的 Boss 章节，然后在获得安装授权后运行\n`agent-reach install --env=local --system --channels=boss`。Agent 负责按系统启动\n只绑定 `127.0.0.1:9222` 的专用 Chrome；**拉起后第一步是暂停并让用户肉眼确认**\n窗口内是已登录状态（右上角有头像），未登录则让用户登录/扫码，用户确认后再运行\n`boss --cdp-url http://localhost:9222 login --cdp` 和 `agent-reach doctor` 验收。"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0003","rule_version":"1.2.0","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"This rule is applicable under its mechanism-agnostic scope: the skill explicitly handles credential material by routing user-provided Twitter auth tokens (TWITTER_AUTH_TOKEN/CT0) and imported same-domain cookies (e.g. XiaoHongShu) through registered platform clients, rather than having no credential-handling subject at all. This handling is nevertheless secure: the credentials are user-supplied and scoped to the intended platform/domain, the instructions explicitly require they not be exposed in logs or command echo, and no instruction in SKILL.md/SKILL_en.md or any references/*.md directs reading host credential stores (~/.ssh, ~/.aws, .env, keychain, cloud metadata), harvesting the live conversation/system-prompt context, or routing credentials to an unrelated or attacker-controlled endpoint.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":82,"end_line":89,"snippet":"Twitter 注意：`agent-reach configure twitter-cookies` 保存的 Cookie 只供\n`doctor` 检查配置是否齐全；`doctor` 不执行 `twitter status`，也不会设置当前\nShell。直接运行 `twitter` 前，必须在子进程环境中显式提供\n`TWITTER_AUTH_TOKEN` 和 `TWITTER_CT0`，不得在日志或命令回显中暴露值。\n\n小红书注意：Agent Reach 不替用户登录，也不读取浏览器 Cookie。"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0004","rule_version":"1.1.0","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"The skill ships no lifecycle/event hooks (no PreToolUse/PostToolUse hooks, no before_tool_callback), no settings.json auto-approve config, no git hooks, and no env-var hijack assignments (no GIT_EXTERNAL_DIFF/NODE_OPTIONS/LD_PRELOAD/PYTHONSTARTUP). The skill directory contains only SKILL.md/SKILL_en.md (prose) and references/*.md (prose). The bundled install mechanism (_install_skill in cli.py) only copies the static markdown files into the host's skill directory via an explicit `agent-reach skill --install` CLI command invoked by the user/agent -- it is not an automatic load-time or lifecycle-hook execution path the artifact itself triggers; invoking the host's own file-write tool through the normal consent-gated flow is not a format-native control-plane primitive under this rule's scope test.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":1,"end_line":24,"snippet":"(Entire SKILL.md/SKILL_en.md reviewed: frontmatter + prose only, no lifecycle hooks, no env exports, no entry scripts)"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0005","rule_version":"1.0.0","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"SKILL.md frontmatter declares no `allowed-tools`/capability list at all (Claude skill format here omits an explicit scope field), so there is no wildcard/catch-all declaration or OAuth-scope over-grant to evaluate. The skill's instructed capability set (invoking shell CLIs: twitter, bili, gh, yt-dlp, curl, opencli, mcporter) is intrinsic to its stated purpose -- a multi-platform internet research router that needs to shell out to per-platform CLIs to fetch content, which is exactly what the description discloses (web search/fetch across 16 named platforms). No bundled executable, postinstall hook, or hidden capability grant exists beyond this declared, broad-but-justified function.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":58,"end_line":78,"snippet":"## 零配置快速命令\n\n```bash\n# Exa 网页搜索\nmcporter call exa.web_search_exa query=\"query\" numResults=5\n\n# 通用网页阅读\ncurl -s \"https://r.jina.ai/URL\"\n\n# GitHub 搜索\ngh search repos \"query\" --sort stars --limit 10\n..."}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0006","rule_version":"1.1.0","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"A programmatic codepoint scan of SKILL.md, SKILL_en.md, and all references/*.md found zero zero-width, bidi-override/isolate, or Unicode Tag-block characters, and no ANSI/OSC escape sequences or forged chat-template/role tokens. However, SKILL.md's model-facing description instructs the host to use the skill whenever the user mentions any of 16 named platforms or shares any URL, and the body escalates this to an explicit directive that when this skill exists the host must use it and must not invent its own approach. This is a broad, overt, mandatory tool-routing directive aimed at the host model rather than purely descriptive capability text, and falls within this rule's scope covering concealed or overt directives embedded in the instruction carrier.","vulnerability_details":null,"fix_guidance":"Soften the mandatory 'MUST USE ... do not invent your own approach' language in SKILL.md's model-facing description/body to a non-binding recommendation, leaving the host model free to choose an alternative approach instead of being directed to always defer to this skill.","why_this_matters":"A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-P0006 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":3,"end_line":11,"snippet":"description: >\n  MUST USE when user wants to 调研/research/搜索/search/查/找/look up anything\n  on the internet — e.g. 全网调研 X / 帮我调研一下 X / 查一下 X / 搜搜 X /\n  看看大家怎么评价 X / X 上有什么讨论 / research this topic。\n\n  Also MUST USE when user mentions any platform or shares any URL/链接:\n  小红书/xiaohongshu/xhs, Twitter/推特/X, B站/bilibili, Reddit, Facebook,\n  Instagram, V2EX, LinkedIn/领英/Boss直聘/招聘/求职/jobs, YouTube, GitHub code search, 小宇宙播客,\n  雪球/股票行情, RSS feeds, or any web URL."},{"path":"agent_reach/skill/SKILL.md","start_line":27,"end_line":37,"snippet":"\n16 平台、多后端。**本 skill 存在时必须用它访问这些平台，不要自己发明方案。**\n\n## 常驻规则（全程适用）\n\n1. **动手前先体检**：多后端/登录态平台（小红书/Reddit/B站/Twitter/Facebook/Instagram/Boss直聘）先跑\n   `agent-reach doctor --json`。`active_backend` 有值时按它选命令组；`active_backend: null`\n   表示 Doctor 为避免触发浏览器 Cookie 读取或远端写入而没有做实时验证，不代表后端不存在。\n   Doctor 结果是「某一时刻的快照」，通道/登录态可能已变化；执行只读命令前若怀疑失效，\n   按对应 reference 的「体检与恢复」runbook 重新确认（如 career.md 的 Boss直聘 CDP 排查）。\n2. **声明你在用什么**：开始干活前说一句「使用 agent-reach 的 X 平台 / Y 后端」。"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0007","rule_version":"1.0.0","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The skill's model-facing instruction body ships as static files inside the component directory itself (SKILL.md / SKILL_en.md / references/*.md) -- there is no code within agent_reach/skill/ that fetches or rewrites these files at runtime; they are plain, non-executable markdown. The only install-time copy mechanism lives outside this component (in the CLI, not in the skill artifact itself) and copies these same static bundled files verbatim to the host's skill directory on an explicit, user-invoked `agent-reach skill --install` command -- it does not live-fetch the instruction text from a mutable remote endpoint, and it does not auto-overwrite an existing installation without preserving it. The artifact's own trust surface (this directory's content) is immutable at runtime.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":1,"end_line":24,"snippet":"---\nname: agent-reach\ndescription: >\n  MUST USE when user wants to 调研/research/搜索/search/查/找/look up anything\n  on the internet — e.g. 全网调研 X / 帮我调研一下 X / 查一下 X / 搜搜 X /\n  ...\n\n  Also MUST USE when user mentions any platform or shares any URL/链接:\n  小红书/xiaohongshu/xhs, Twitter/推特/X, B站/bilibili, Reddit, Facebook,\n  Instagram, V2EX, LinkedIn/领英/Boss直聘/招聘/求职/jobs, YouTube, GitHub code search, 小宇宙播客,\n  雪球/股票行情, RSS feeds, or any web URL.\n\n  16 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs)."}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"},{"rule_id":"ATR-P0008","rule_version":"1.0.0","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The skill's declared name/slug is `agent-reach`, matching exactly the canonical name of the project and its GitHub org/repo (github.com/Panniantong/Agent-Reach, confirmed via `git remote -v`). There is no near-miss/typosquat of a known brand, no trust-implying prefix, no version/tier-spoofing suffix, and no impersonation prose claiming to be an official/updated build of a different named tool. README explicitly notes 'PyPI 的同名包是另一个项目' (a same-named PyPI package is a different project) rather than claiming to BE that other package.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/skill/SKILL.md","start_line":1,"end_line":3,"snippet":"---\nname: agent-reach\ndescription: >"}],"affected_tools":null,"component_name":"skills-plugins/agent_reach/skill"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.0","rule_name":"Unrestricted Network Fetch / SSRF","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only arbitrary-URL fetch surface is WebChannel.read() (Jina Reader proxy for 'any URL'). It passes every URL through normalize_public_http_url() before use, which rejects non-HTTP(S) schemes, userinfo, literal non-global IPs (RFC1918/loopback/link-local/metadata via ipaddress.is_global), and known internal TLD/hostname suffixes (.local, .internal, metadata.google.internal, etc.). All other channels (V2EX, Xueqiu, Boss, GitHub, Bilibili) call a fixed, hardcoded API host and validate scheme/host/port with host_matches()/_validate_api_url() before any request, so user input cannot redirect the destination host. Responses are size-capped (_MAX_RESPONSE_BYTES) and timeouts are set on every urlopen/subprocess call.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":true,"transport_gated":true,"mitigation_present":true,"evidence":[{"path":"agent_reach/utils/url.py","start_line":47,"end_line":84,"snippet":"def normalize_public_http_url(url: str) -> str:\n    ...\n    literal_address = _literal_ip_address(host)\n    if (\n        parsed.scheme.lower() not in {\"http\", \"https\"}\n        or not host\n        or parsed.username is not None\n        or parsed.password is not None\n        or \"%\" in host\n        or host in _BLOCKED_PUBLIC_FETCH_HOSTS\n        or host.endswith(_BLOCKED_PUBLIC_FETCH_SUFFIXES)\n        or (\".\" not in host and literal_address is None)\n        or (literal_address is not None and not literal_address.is_global)\n    ):\n        raise ValueError(\"only public HTTP(S) URLs are allowed\")"},{"path":"agent_reach/channels/web.py","start_line":48,"end_line":61,"snippet":"def read(self, url: str) -> str:\n    url = normalize_public_http_url(url)\n    jina_url = f\"https://r.jina.ai/{url}\"\n    req = urllib.request.Request(\n        jina_url,\n        headers={\"User-Agent\": _UA, \"Accept\": \"text/plain\"},\n    )\n    with urllib.request.urlopen(req, timeout=30) as resp:\n        body = resp.read(_MAX_RESPONSE_BYTES + 1)"},{"path":"agent_reach/channels/v2ex.py","start_line":28,"end_line":43,"snippet":"def _validate_api_url(url: str) -> None:\n    \"\"\"Allow only the public V2EX HTTPS JSON API.\"\"\"\n    ...\n    if (\n        parsed.scheme.lower() != \"https\"\n        or (parsed.hostname or \"\").lower() not in {\"v2ex.com\", \"www.v2ex.com\"}\n        or port not in {None, 443}\n        or parsed.username is not None\n        or parsed.password is not None\n        or not parsed.path.startswith(\"/api/\")\n    ):\n        raise ValueError(\"only the V2EX HTTPS API is allowed\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.2","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":".env.example ships only commented-out placeholder values with no real secret material. gitleaks scan found 0 findings repo-wide. Secrets are read from ~/.agent-reach/config.yaml (created at runtime, not committed) or from process.env. Config.to_dict() masks sensitive fields as '[REDACTED]'. Semgrep's atr-c0002 credential-to-log-sink rule flagged 4 lines (cli.py:1519,1532,1536 and cookie_extract.py:401) -- all 4 were read: cli.py:1519/1532/1536 write the literal status string 'requires login' into config.set()/print() (not a credential value), and cookie_extract.py:401 writes AUTH_TOKEN/CT0 to a private 0600 file (~/.config/bird/credentials.env) via atomic_write_private_text, shell-quoted with shlex.quote -- this is a private credential file write, not a log/stdout sink, and the file is owner-only. No credential value is printed, logged, or committed anywhere.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Plaintext secrets are easily exfiltrated.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/cli.py","start_line":1517,"end_line":1522,"snippet":"        if auth_token and ct0:\n            config.set(\"twitter_auth_token\", auth_token)\n            config.set(\"twitter_ct0\", ct0)\n\n            print(\"✅ Twitter cookies 已保存到 ~/.agent-reach/config.yaml\")"},{"path":"agent_reach/cli.py","start_line":1529,"end_line":1538,"snippet":"                legacy_results = (\n                    (\n                        \"~/.config/xfetch/session.json\",\n                        _sync_xfetch_session(auth_token, ct0),\n                    ),\n                    (\n                        \"~/.config/bird/credentials.env\",\n                        _sync_bird_env(auth_token, ct0),\n                    ),\n                )"},{"path":"agent_reach/cookie_extract.py","start_line":396,"end_line":404,"snippet":"        bird_dir = home_dir() / \".config\" / \"bird\"\n        make_private_dir(bird_dir)\n        env_path = bird_dir / \"credentials.env\"\n        atomic_write_private_text(\n            env_path,\n            f\"AUTH_TOKEN={shlex.quote(auth_token)}\\n\"\n            f\"CT0={shlex.quote(ct0)}\\n\",\n        )\n        return True"},{"path":"agent_reach/config.py","start_line":211,"end_line":233,"snippet":"def to_dict(self) -> dict:\n    \"\"\"Return config as dict (masks sensitive values).\"\"\"\n    sensitive_markers = (\n        \"key\", \"token\", \"password\", \"proxy\", \"cookie\", \"secret\",\n        \"session\", \"sessdata\", \"csrf\", \"auth\", \"cred\", \"ct0\",\n    )\n    masked = {}\n    for k, v in self.data.items():\n        if any(s in k.lower() for s in sensitive_markers):\n            masked[k] = \"[REDACTED]\" if v else None\n        else:\n            masked[k] = v\n    return masked"},{"path":"agent_reach/cli.py","start_line":1519,"end_line":1519,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cli.py","start_line":1532,"end_line":1532,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cli.py","start_line":1536,"end_line":1536,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":"agent_reach/cookie_extract.py","start_line":401,"end_line":401,"snippet":null,"vulnerability":{"severity":"WARNING"}},{"path":".env.example","start_line":5,"end_line":19,"snippet":"# Exa Search (free 1000/month) — https://exa.ai\n# EXA_API_KEY=exa-your-key-here\n\n# GitHub Token (optional, for higher rate limits) — https://github.com/settings/tokens\n# GITHUB_TOKEN=ghp_your_token_here\n\n# Reddit ISP Proxy (optional, for full Reddit access)\n# REDDIT_PROXY=http://user:[REDACTED]@ip:port\n\n# Groq Whisper (optional, for video transcription) — https://console.groq.com\n# GROQ_API_KEY=gsk_your_key_here\n\n# OpenAI Whisper (optional fallback when Groq is rate-limited) — https://platform.openai.com\n# OPENAI_API_KEY=sk-your_key_here"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.3.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"All subprocess invocations across the runtime (cli.py, v2ex.py, transcribe.py, probe.py) use subprocess.run() with an argument-vector list and no shell=True / os.system / eval anywhere in agent_reach/ (confirmed by grep: zero shell=True, os.system, or eval( hits in application code). Arguments passed to yt-dlp/ffmpeg/curl are either fixed flags or a URL that is first validated by _assert_safe_public_url()/normalize_public_http_url() (reject non-http(s) schemes, private/loopback/link-local/reserved IPs including inet_aton shorthand forms like 127.1/0x7f000001, and internal hostnames). The '--' separator before the URL argument in yt-dlp invocation prevents option-injection via a URL starting with '-'. Bandit flagged B603 (subprocess without shell equals true) across 25 call sites in cli.py/v2ex.py/transcribe.py -- all manually confirmed to use list-form argv with a fixed executable located via shutil.which(), no shell re-parsing.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":214,"end_line":270,"snippet":"def _assert_safe_public_url(url: str) -> None:\n    ...\n    if host in _BLOCKED_HOSTS or host.endswith(\".localhost\"):\n        raise TranscribeError(\"SSRF blocked: internal host is not allowed\")\n    if _is_private_ip(host):\n        raise TranscribeError(\"SSRF blocked: private/internal IP is not allowed\")\n\n\ndef download_audio(url: str, out_dir: Path) -> Path:\n    \"\"\"Download audio with yt-dlp into out_dir; return the resulting file path.\"\"\"\n    _assert_safe_public_url(url)\n    _require(\"yt-dlp\")\n    template = out_dir / \"source.%(ext)s\"\n    _run(\n        [\n            \"yt-dlp\", \"-x\", \"--audio-format\", \"m4a\", \"--audio-quality\", \"0\",\n            \"--no-playlist\", \"--max-filesize\", str(MAX_SOURCE_BYTES),\n            \"-o\", str(template), \"--\", url,"},{"path":"agent_reach/channels/v2ex.py","start_line":92,"end_line":120,"snippet":"    command = [\n        curl, \"--fail\", \"--silent\", \"--show-error\", \"--proto\", \"=https\",\n        \"--connect-timeout\", \"5\", \"--max-time\", str(_TIMEOUT),\n        \"--max-filesize\", str(_MAX_RESPONSE_BYTES),\n        \"--header\", f\"User-Agent: {_UA}\", \"--url\", url,\n    ]\n    try:\n        result = subprocess.run(\n            command, capture_output=True, encoding=\"utf-8\", errors=\"replace\",\n            timeout=_TIMEOUT + 2, env=utf8_subprocess_env(),\n        )"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.0","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"Not every HTTP response is byte-capped: several fixed-host health/status checks (bilibili.py, boss.py, xueqiu.py) use a bare resp.read() with no explicit cap. However, these calls target fixed, hardcoded API hosts rather than an attacker-selectable destination. The paths that are both attacker-triggerable and high-cost -- media download (yt-dlp, MAX_SOURCE_BYTES/MAX_AUDIO_SECONDS/MAX_CHUNKS) and the arbitrary-URL web-reader fetch (_MAX_RESPONSE_BYTES) -- remain explicitly bounded, and every urlopen/subprocess call carries a timeout. The exposed MCP surface itself is a single fixed, argument-free status tool with no concurrent untrusted request handling, so resource-exhaustion protection rests on bounding the attacker-controllable paths rather than on a universal response-size cap.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":37,"end_line":44,"snippet":"SIZE_LIMIT_BYTES = 24 * 1024 * 1024\nCHUNK_SECONDS = 600\nMAX_SOURCE_BYTES = 512 * 1024 * 1024\nMAX_CHUNKS = 24\nMAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024\nMAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS\nFFPROBE_TIMEOUT_SECONDS = 30"},{"path":"agent_reach/channels/web.py","start_line":11,"end_line":12,"snippet":"_MAX_RESPONSE_BYTES = 5 * 1024 * 1024\n_ANTIBOT_SCAN_BYTES = 4096"},{"path":"agent_reach/channels/web.py","start_line":56,"end_line":61,"snippet":"        with urllib.request.urlopen(req, timeout=30) as resp:\n            body = resp.read(_MAX_RESPONSE_BYTES + 1)\n        if len(body) > _MAX_RESPONSE_BYTES:\n            raise ValueError(\n                f\"Jina Reader response exceeds {_MAX_RESPONSE_BYTES} byte limit\"\n            )"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.0","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"high","rationale":"Doctor/status output and exception text are scrubbed of credentials via scrub_url_credentials() before being rendered, and config.to_dict() masks sensitive fields. However, this rule also requires an audit trail of tool invocations. The MCP server's call_tool dispatcher executes get_status and returns its result directly, without recording the tool name, arguments, outcome, caller/session identity, or a correlation identifier anywhere. CLI logging is optional console output, not a structured invocation log. As a result, MCP tool operations cannot be reconstructed for forensics after the fact.","vulnerability_details":null,"fix_guidance":"Add an audit-log call inside the MCP call_tool dispatcher that records, for every invocation, the tool name, redacted arguments, caller/session identifier, timestamp, and outcome/result status to a durable log or audit store.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0005 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":50,"end_line":63,"snippet":"\n    @server.call_tool()\n    async def call_tool(name: str, arguments: dict):\n        try:\n            if name == \"get_status\":\n                result = eyes.doctor_report()\n            else:\n                result = f\"Unknown tool: {name}\"\n\n            text = json.dumps(result, ensure_ascii=False, indent=2) if isinstance(result, (dict, list)) else str(result)\n            return [TextContent(type=\"text\", text=text)]\n        except Exception as e:\n            return [\n                TextContent("},{"path":"agent_reach/cli.py","start_line":60,"end_line":65,"snippet":"def _configure_logging(verbose: bool = False):\n    \"\"\"Suppress loguru output unless --verbose is set.\"\"\"\n    from loguru import logger\n    logger.remove()  # Remove default stderr handler\n    if verbose:\n        logger.add(sys.stderr, level=\"INFO\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.2.0","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"Agent Reach ships as a Python package (pip-installable CLI/MCP tool) with no Dockerfile, docker-compose, Helm chart, or Kubernetes manifest anywhere in the repository (confirmed: find for Dockerfile*/docker-compose*/*.tf/*.yaml under deploy-style paths returned nothing beyond the CI workflow and mcporter config). There is no container or cloud deployment artifact for this rule's container-hardening controls (USER, read-only FS, capabilities, seccomp, hostNetwork) to apply to.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Reduces blast radius of exploits.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".","start_line":1,"end_line":1,"snippet":"No Dockerfile, docker-compose.yml, Helm chart, or Kubernetes/Terraform manifest found in the repository tree (verified via recursive find for Dockerfile*, docker-compose*, *.tf, helm/, k8s/)."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.2.0","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"high","rationale":"pyproject.toml declares every runtime dependency with open-ended minimum-version bounds (e.g. requests>=2.28, pyyaml>=6.0, yt-dlp[default]>=2026.07.04) rather than pinned or upper-bounded versions. constraints.txt does pin every dependency to an exact version, but it is opt-in: it is applied only when a user explicitly runs `pip install -c constraints.txt ...`, and is not consulted by an ordinary `pip install agent-reach`. A standard installation therefore resolves against the open-ended bounds in pyproject.toml and can pull in unreviewed future dependency releases, including ones affected by a newly disclosed vulnerability or supply-chain compromise.","vulnerability_details":null,"fix_guidance":"Pin or upper-bound the dependency versions declared directly in pyproject.toml (or ship and enforce a lockfile that is applied by default), rather than relying on an opt-in constraints.txt that an ordinary install does not consult.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"pyproject.toml","start_line":29,"end_line":46,"snippet":"\ndependencies = [\n    \"requests>=2.28\",\n    \"feedparser>=6.0\",\n    \"python-dotenv>=1.0\",\n    \"loguru>=0.7\",\n    \"pyyaml>=6.0\",\n    \"rich>=13.0\",\n    \"yt-dlp[default]>=2026.07.04\",\n]\n\n[project.optional-dependencies]\nbrowser = [\"playwright>=1.40\"]\ncookies = [\"browser-cookie3>=0.19\"]\nall = [\"playwright>=1.40\", \"mcp[cli]>=1.0\", \"browser-cookie3>=0.19\"]\ndev = [\n    \"pytest>=8.0\",\n    \"ruff>=0.8\","},{"path":"constraints.txt","start_line":1,"end_line":12,"snippet":"# Agent Reach tested dependency set\n# Usage:\n#   pip install -c constraints.txt -e .[dev]\n\nrequests==2.32.5\nfeedparser==6.0.12\npython-dotenv==1.2.1\nloguru==0.7.3\nPyYAML==6.0.3\nrich==14.3.2\nyt-dlp==2026.07.04\nyt-dlp-ejs==0.8.0"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.2.0","rule_name":"Missing or Inadequate Input Validation","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The single MCP tool (get_status) takes no arguments (empty inputSchema), eliminating that attack surface entirely. The CLI entry points use argparse with explicit `choices=[...]` allowlists for all enum-like parameters (configure key, --from-browser browser, --platform, format platform) so out-of-range values are rejected by argparse itself before reaching application code. Free-text inputs that matter for security (URLs passed to web/transcribe channels) are validated by dedicated schema-like validators -- normalize_public_http_url(), _validate_api_url(), _assert_safe_public_url() -- which enforce scheme/host/port/userinfo constraints before any network or subprocess call. Config file inputs are size-capped (_MAX_CONFIG_BYTES) and type-checked (must parse to a dict) before use.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/cli.py","start_line":108,"end_line":114,"snippet":"    p_conf = sub.add_parser(\"configure\", help=\"Set a config value or auto-extract from browser\")\n    p_conf.add_argument(\"key\", nargs=\"?\", default=None,\n                        choices=[\"proxy\", \"github-token\", \"groq-key\", \"openai-key\",\n                                 \"twitter-cookies\", \"youtube-cookies\",\n                                 \"xhs-cookies\"],\n                        help=\"What to configure (omit if using --from-browser)\")\n    p_conf.add_argument(\"value\", nargs=\"*\", help=\"The value(s) to set\")"},{"path":"agent_reach/integrations/mcp_server.py","start_line":43,"end_line":49,"snippet":"    @server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]"},{"path":"agent_reach/config.py","start_line":136,"end_line":150,"snippet":"        try:\n            payload = read_small_text_no_follow(\n                self.config_path,\n                max_bytes=_MAX_CONFIG_BYTES,\n            )\n        except PrivatePathError as exc:\n            raise ConfigSecurityError(str(exc)) from exc\n        if payload is None:\n            self.data = {}\n            return\n\n        loaded = yaml.safe_load(payload) or {}\n        if not isinstance(loaded, dict):\n            raise ConfigError(\"配置文件顶层必须是对象\")"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0009","rule_version":"1.2.0","rule_name":"Insecure Deserialization","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No use of pickle, marshal, or yaml.load/yaml.unsafe_load anywhere in the codebase (grep for these patterns across agent_reach/ returned zero matches). All YAML parsing uses yaml.safe_load (config.py load(), github.py hosts.yml parsing) and all JSON parsing uses the standard json module, neither of which executes arbitrary code on untrusted input.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Can lead to arbitrary code execution.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"agent_reach/config.py","start_line":147,"end_line":150,"snippet":"        loaded = yaml.safe_load(payload) or {}\n        if not isinstance(loaded, dict):\n            raise ConfigError(\"配置文件顶层必须是对象\")\n        self.data = loaded"},{"path":"agent_reach/channels/github.py","start_line":63,"end_line":66,"snippet":"    try:\n        payload = yaml.safe_load(raw)\n    except yaml.YAMLError as exc:\n        raise GitHubConfigError(\"gh hosts.yml 不是有效的 UTF-8 YAML\") from exc"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.2.0","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"high","rationale":"`agent-reach transcribe` sends derived audio to an external transcription provider (Groq/OpenAI Whisper) and prints that provider's raw response text directly to the operator's terminal (cli.py, fed from transcribe.py), with no ANSI/control-character sanitization or output encoding applied before printing. Because the transcript content originates from third-party audio the operator does not control, a crafted transcription result could embed terminal escape sequences that reach the terminal unneutralized, making this a live output-encoding sink rather than an inapplicable one.","vulnerability_details":null,"fix_guidance":"Strip or neutralize ANSI/terminal control-escape sequences from the transcription provider's response text before printing it to the terminal, e.g. by filtering non-printable/control characters outside normal whitespace.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0010 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"agent_reach/cli.py","start_line":165,"end_line":169,"snippet":"    p_tr = sub.add_parser(\"transcribe\", help=\"Transcribe a URL or local audio file (Whisper via Groq/OpenAI)\")\n    p_tr.add_argument(\"source\", help=\"Audio/video URL or local file path\")\n    p_tr.add_argument(\"--provider\", choices=[\"auto\", \"groq\", \"openai\"], default=\"auto\",\n                      help=\"Transcription provider (default: first configured provider)\")\n    p_tr.add_argument("},{"path":"agent_reach/transcribe.py","start_line":381,"end_line":395,"snippet":"        try:\n            resp = requests.post(\n                info[\"endpoint\"],\n                headers={\"Authorization\": f\"Bearer {key}\"},\n                files={\"file\": (chunk.name, fh, \"audio/m4a\")},\n                data={\"model\": info[\"model\"], \"response_format\": \"text\"},\n                timeout=timeout,\n            )\n        except requests.RequestException as e:\n            raise TranscribeError(f\"{provider}: network error: {e}\") from e\n\n    if not resp.ok:\n        raise TranscribeError(f\"{provider}: HTTP {resp.status_code}: {resp.text[:300]}\")\n    return resp.text\n"},{"path":"agent_reach/cli.py","start_line":1608,"end_line":1613,"snippet":"\n    if args.output:\n        Path(args.output).write_text(text + \"\\n\", encoding=\"utf-8\")\n        print(f\"✅ Transcript written to {args.output}\")\n    else:\n        print(text)"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.4.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only MCP tool exposed (get_status in integrations/mcp_server.py) takes no filesystem arguments at all -- it has an empty inputSchema and calls eyes.doctor_report() with no caller-supplied path. The CLI's filesystem writes (skill install, config, cookie sync) write only to fixed, hardcoded locations under the user's home directory (~/.agent-reach, ~/.claude/skills, ~/.config/bird, etc.) -- none of these paths are derived from untrusted/caller-supplied input. Every private-file read/write path goes through utils/paths.py helpers (ensure_no_symlink_path, read_small_text_no_follow, atomic_write_private_text) which walk every path component with os.lstat and reject any symlink before opening, preventing symlink-redirection attacks even though the destination paths are already fixed.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Traversal exposes arbitrary host files; can lead to code execution.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/integrations/mcp_server.py","start_line":43,"end_line":57,"snippet":"    @server.list_tools()\n    async def list_tools():\n        return [\n            Tool(name=\"get_status\",\n                 description=\"Get Agent Reach status: which channels are installed and active.\",\n                 inputSchema={\"type\": \"object\", \"properties\": {}}),\n        ]\n\n    @server.call_tool()\n    async def call_tool(name: str, arguments: dict):\n        try:\n            if name == \"get_status\":\n                result = eyes.doctor_report()"},{"path":"agent_reach/utils/paths.py","start_line":34,"end_line":47,"snippet":"def ensure_no_symlink_path(path: str | Path, label: str = \"路径\") -> Path:\n    \"\"\"Reject any existing symlink component without resolving the path.\"\"\"\n    target = Path(path)\n    absolute = Path(os.path.abspath(os.fspath(target)))\n    current = Path(absolute.anchor)\n    for part in absolute.parts[1:]:\n        current /= part\n        try:\n            mode = os.lstat(current).st_mode\n        except FileNotFoundError:\n            continue\n        if stat.S_ISLNK(mode):"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.0","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The only billable external dependency is the Whisper transcription API (Groq/OpenAI), gated behind a user-supplied API key the user must configure themselves (no key ships with the repo). Each transcription run is bounded: MAX_SOURCE_BYTES (512MB download cap), MAX_AUDIO_SECONDS (4 hours via MAX_CHUNKS=24 * CHUNK_SECONDS=600) reject oversized/too-long media before any paid API call, and chunks are capped at MAX_TOTAL_CHUNK_BYTES. There is no loop or batch mode that could multiply billed calls unboundedly; transcribe() processes one source per invocation. All other channels (web/social/search) either use free public APIs or require the operator's own paid key (Exa), so Agent Reach itself has no mechanism to run up a bill beyond what the operator explicitly triggers per call.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"agent_reach/transcribe.py","start_line":40,"end_line":43,"snippet":"MAX_SOURCE_BYTES = 512 * 1024 * 1024\nMAX_CHUNKS = 24  # 4 hours at the standard 10-minute segment size\nMAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024\nMAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS"},{"path":"agent_reach/transcribe.py","start_line":144,"end_line":152,"snippet":"def _require_duration_within_budget(path: Path) -> float:\n    \"\"\"Reject audio that cannot fit within the bounded chunk budget.\"\"\"\n    duration = _probe_audio_duration(path)\n    if duration > MAX_AUDIO_SECONDS:\n        max_minutes = MAX_AUDIO_SECONDS // 60\n        raise TranscribeError(\n            f\"audio duration exceeds safety limit of {max_minutes} minutes\"\n        )\n    return duration"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.0","rule_name":"Tool Output Sanitization & Injection Containment","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Agent Reach's own MCP server (integrations/mcp_server.py) exposes only a single tool, get_status, which returns the host's own doctor report (JSON built from fixed channel-status strings) -- no third-party content flows through that specific tool, so for that narrow surface the rule is effectively not triggered. However, Agent Reach's actual declared product (per SKILL.md) is a router that tells the calling agent to invoke OTHER external CLIs/MCP tools directly (yt-dlp, Jina Reader via curl, gh CLI, mcporter Exa, twitter-cli, opencli) and consume their raw output as LLM context. Agent Reach's own code does not itself inject fetched web/social content into any LLM context (no envelope/sanitization logic exists in this repo for that purpose, because Agent Reach does not perform the fetch-and-return step -- it delegates it to upstream tools outside this repo's scope). Because the host-side injection point lives in those external tools/the calling agent rather than in this repository, I cannot verify from this codebase alone whether an envelope/sanitization/untrustedReturn control exists at the point where fetched content reaches the model -- abstaining rather than guessing about code outside this repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Tool outputs are a primary prompt-injection vector. Third-party content (web pages, files, API responses) may contain text crafted to override instructions or impersonate system/tool messages. Without a host-enforced envelope, sanitization, and an explicit untrusted-return signal, the model can be coerced into executing attacker-controlled directives, leaking secrets, or chaining further tool calls on behalf of the adversary.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0014","rule_version":"1.2.0","rule_name":"CI/CD Workflow Script Injection","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The repository has a single GitHub Actions workflow (.github/workflows/pytest.yml) triggered only by `push` and `pull_request` (not pull_request_target, issue_comment, workflow_run, or repository_dispatch). No step interpolates any `${{ github.event.* }}`, `${{ inputs.* }}`, or other attacker-controlled expression into a `run:` shell block -- all run steps execute fixed commands (pip install, pytest, python -m build, wheel verification script). The matrix `python-version` values are static literals defined in the workflow file itself, not derived from any external input. Semgrep's github-actions-mutable-action-tag rule flagged 6 lines (all `uses: actions/checkout@v4` / `actions/setup-python@v5`), which is a supply-chain pinning hygiene note (mutable major-version tag vs. pinned SHA), not a script-injection finding -- there is no shell-injection vector in this workflow.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".github/workflows/pytest.yml","start_line":1,"end_line":31,"snippet":"name: ci\n\non:\n  push:\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install package and test deps\n        run: |\n          python -m pip install --upgrade pip\n          pip install -c constraints.txt -e .[dev]\n      - name: Run tests\n        run: |\n          pytest -q"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.0","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Agent Reach's declared purpose (README.md, SKILL.md, pyproject.toml description) is to give an AI agent read access to public internet platforms (Twitter, Reddit, YouTube, GitHub, etc.) for research/search -- a benign, clearly non-offensive capability. There is no malware/exploit/credential-theft/evasion generation capability anywhere in the instruction bodies or bundled scripts; the only CDP browser-automation pattern (boss.py) exists to reuse the USER's OWN already-logged-in browser session for legitimate job-search automation and explicitly requires the user to visually confirm their own logged-in state before any action -- the opposite of covert/unattributed operation. No offensive-security framing, dual-use pentest claim, or log-suppression/anti-analysis instructions are present anywhere in the repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"pyproject.toml","start_line":5,"end_line":5,"snippet":"description = \"Give your AI Agent eyes to see the entire internet. Search + Read 10+ platforms.\""},{"path":"agent_reach/skill/SKILL.md","start_line":94,"end_line":97,"snippet":"只绑定 `127.0.0.1:9222` 的专用 Chrome；**拉起后第一步是暂停并让用户肉眼确认**\n窗口内是已登录状态（右上角有头像），未登录则让用户登录/扫码，用户确认后再运行\n`boss --cdp-url http://localhost:9222 login --cdp` 和 `agent-reach doctor` 验收。\n不要让用户自己研究端口参数。"}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["Prompt-injection / output-sanitization gap in upstream tool chain: SKILL.md instructs the agent to invoke external CLIs (yt-dlp, Jina Reader, gh CLI, twitter-cli) directly, and whether those tools apply any output-sanitization or prompt-injection envelope before raw fetched content reaches the calling LLM's context cannot be verified from this codebase alone (ATR-C0013, UNKNOWN).","GitHub Actions supply-chain hygiene gap: the CI workflow (.github/workflows/pytest.yml) pins third-party actions by mutable major-version tag (actions/checkout@v4, actions/setup-python@v5) rather than by commit SHA, creating a supply-chain tampering window (flagged by Semgrep github-actions-mutable-action-tag rule).","The repo's broader CLI (agent_reach/cli.py, cookie_extract.py, boss.py channel) handles browser cookies and CDP-based browser automation for several platforms; if those modules are ever exposed as MCP tools or skill capabilities in a future version they would require independent security review.","Bandit reported 1330 repo-wide findings and Semgrep 21; these are overwhelmingly generic CLI-code style/assert findings that were not individually re-triaged and may contain unreviewed issues outside the narrow MCP/skill scope."],"top_remediations":["Pin third-party GitHub Actions to a specific commit SHA instead of a mutable major-version tag (e.g. actions/checkout@<sha> # v4.x.x) to close the supply-chain tampering window.","Apply a host-enforced envelope and untrustedReturn-style annotation around any third-party fetched content (yt-dlp, Jina Reader, gh CLI, twitter-cli output) before it reaches LLM context, consistent with the SSRF-hardening discipline already present in utils/url.py.","Keep the MCP server stdio-only; if a remote transport (HTTP/SSE) is ever added, implement OAuth 2.1+PKCE, Origin/Host validation, and TLS before exposure (per ATR-M0003/M0004/M0005/M0008/M0009).","If SKILL.md is revised to include lifecycle hooks, auto-update fetch of instructions, or write-capable commands, re-evaluate ATR-P0002/P0004/P0007 against the new content.","If the MCP tool surface is expanded beyond get_status, re-evaluate ATR-M0002/M0010 against any new tool's capabilities and transport.","Continue the existing least-privilege file-permission discipline; Semgrep WARNING-level findings at cli.py:939 and utils/paths.py:65 note 0o700 directory/script permissions that are stricter than default but are not a vulnerability — no immediate action required."]},"rollup":{"component_findings_count":{"vulnerable":2,"secure":5,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":3,"secure":10,"not_applicable":1},"component_tools_count":{},"worst_case_severity":"CRITICAL"},"trust_scores":{"repo_level":{"score":58,"tier":"low","capability_band":"minimal","score_confidence":"low","score_interval":{"low":22,"high":94},"factors":["ATR-P0001 CRITICAL","ATR-P0006 HIGH","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM"]},"component_level":{"score":69,"tier":"medium","capability_band":"minimal","score_confidence":"low","score_interval":{"low":38,"high":100},"factors":["ATR-P0001 CRITICAL","ATR-P0006 HIGH"]}}}]}