{"repo_url":"https://github.com/getsentry/toolkit","overall_status":"completed","scanned_at":"2026-11-08T16:37:18.261Z","scan_summary":{"total_components":23,"worst_case_severity":"CRITICAL","repo_trust_score":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]}},"components":[{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/create-pr","component_path":".agents/skills/create-pr","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"SKILL.md is a pure alias pointing to sentry-skills:pr-writer. No bundled scripts, no network/exec/credential access. The component directory holds only this file.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/create-pr/SKILL.md","start_line":10,"end_line":12,"snippet":"Use `sentry-skills:pr-writer` as the canonical skill for creating and editing pull requests.\n\nIf invoked via `create-pr`, run the same workflow and conventions documented in `sentry-skills:pr-writer`."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Alias text only, with no directives about confirmation, disclosure or authority.","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":".agents/skills/create-pr/SKILL.md","start_line":8,"end_line":8,"snippet":"This skill name is kept for compatibility."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Alias skill that reads no credential store or conversation context and has no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md. There are no scripts, hooks, host config or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares only name and description. There is no allowed-tools, wildcard, OAuth scope, bin or install hook.","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":".agents/skills/create-pr/SKILL.md","start_line":2,"end_line":3,"snippet":"name: create-pr\ndescription: Alias for sentry-skills:pr-writer. Use when users explicitly ask for \"create-pr\" or reference the legacy skill name. Redirects to the canonical PR writing workflow."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan of the model-facing text found no zero-width, bidi or Tag-block characters. There are no HTML comments, forged role tokens or ANSI escapes, and the text is plain ASCII.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/create-pr/SKILL.md","start_line":10,"end_line":10,"snippet":"Use `sentry-skills:pr-writer` as the canonical skill for creating and editing pull requests."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions ship inside the repo. There is no runtime fetch of the artifact's own instructions, no self-rewriting and no launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/create-pr/SKILL.md","start_line":6,"end_line":8,"snippet":"# Alias: create-pr\n\nThis skill name is kept for compatibility."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name 'create-pr' is a common-word name with no near-miss of a brand. It is published from the getsentry GitHub org and aliases a first-party Sentry skill.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/create-pr/SKILL.md","start_line":2,"end_line":2,"snippet":"name: create-pr"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/create-pr"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/logging-observability","component_path":".agents/skills/logging-observability","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Pure-prose SKILL.md; ships only references/guidance for reviewing logging code. No bundled scripts, network clients, exec or credential reads.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/logging-observability/SKILL.md","start_line":2,"end_line":3,"snippet":"name: logging-observability\ndescription: Review code for correct logging and error handling patterns. Use when reviewing code that handles errors..."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Grepped the SKILL.md for consent-suppression/disclosure-hiding phrases (never ask, auto-approve, skip confirmation, don't tell, pre-authorized): none found. YARA hits in the repo were in other files (packages/cli/AGENTS.md etc.), not this component.","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":".agents/skills/logging-observability/SKILL.md","start_line":6,"end_line":8,"snippet":"# Logging & Observability Correctness\n\nEnforce correct logging and error handling patterns in the sentry-mcp codebase."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Skill reads no host credential store or conversation context and has no outbound channel; no harvest+exfil pair is constructible.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Directory contains only SKILL.md; no bundled scripts, hooks, config or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Frontmatter declares only name/description; no allowed-tools, scopes, wildcards, bins or install hooks.","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":".agents/skills/logging-observability/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: logging-observability\ndescription: Review code for correct logging and error handling patterns...\n---"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan found no zero-width, bidi, Tag-block or ANSI characters; no HTML comments, forged chat-template tokens or host-directed injection imperatives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/logging-observability/SKILL.md","start_line":3,"end_line":3,"snippet":"description: Review code for correct logging and error handling patterns. Use when reviewing code that handles errors..."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions ship inside the bundle; no runtime fetch, self-updater, self-rewrite or launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/logging-observability/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: logging-observability\ndescription: Review code for correct logging...\n---"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Generic descriptive name; not a near-miss of a known brand; published in getsentry/toolkit repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/logging-observability/SKILL.md","start_line":2,"end_line":2,"snippet":"name: logging-observability"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/logging-observability"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/mcp-audit","component_path":".agents/skills/mcp-audit","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Pure-declarative skill: SKILL.md plus markdown references. No bundled scripts, network clients, exec or credential reads. The declared purpose (MCP spec audit) matches the content.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/mcp-audit/SKILL.md","start_line":2,"end_line":2,"snippet":"name: mcp-audit"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Grep of the skill for auto-approve, skip-confirmation and silent-action prose found nothing. The instructions are audit workflow guidance only.","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":".agents/skills/mcp-audit/SKILL.md","start_line":6,"end_line":8,"snippet":"# MCP Audit\n\nAudit an MCP server against the current released MCP specification and any repo-specific compatibility constraints."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill reads no host credential store or conversation context and ships no outbound channel; there is no harvest+exfil pair.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory contains only markdown files. It ships no hooks, scripts, dotenv, env assignments or settings.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares only name and description, with no allowed-tools, wildcard or OAuth scopes, and the skill ships no executables.","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":".agents/skills/mcp-audit/SKILL.md","start_line":1,"end_line":2,"snippet":"---\nname: mcp-audit"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan for zero-width, bidi and Tag-block characters over the skill files returned no matches. There are no forged role tokens or host-directed override imperatives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/mcp-audit/SKILL.md","start_line":8,"end_line":12,"snippet":"Read `references/spec-baseline.md` and `references/checklist.md` before making changes."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The instructions and references ship inside the bundle. There is no runtime fetch of the skill's own instructions, no self-updater and no launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/mcp-audit/SKILL.md","start_line":8,"end_line":8,"snippet":"Read `references/spec-baseline.md` and `references/checklist.md` before making changes."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name 'mcp-audit' is not a near-miss of a specific brand, and the artifact claims no official or fork status. The source org is getsentry.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/mcp-audit/SKILL.md","start_line":2,"end_line":2,"snippet":"name: mcp-audit"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-audit"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/mcp-qa","component_path":".agents/skills/mcp-qa","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"SKILL.md instructs running pnpm build/test/lint, the local sentry CLI, authenticated requests against production Sentry, and an optional curl to localhost:5173. These operations are explicitly declared and match the stated purpose of end-to-end QA for MCP tool changes. There are no bundled scripts, no concealment and no undisclosed third-party egress.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/mcp-qa/SKILL.md","start_line":12,"end_line":12,"snippet":"pnpm run tsc && pnpm run lint && pnpm run test"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill tells the agent to avoid live prod mutation unless a disposable resource exists. It has no instructions to skip confirmations, hide actions or override host policy.","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":".agents/skills/mcp-qa/SKILL.md","start_line":66,"end_line":66,"snippet":"For mutating tools, avoid live prod changes unless there is a disposable"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The skill directs the agent to use the local Sentry CLI's stored credentials (sentry auth status/login) and permits an access-token argument for authenticated requests against production Sentry. This delegated credential access is disclosed and functional for end-to-end MCP QA. The skill has no directive to read env or credential files, dump conversation context, or send secrets to an unrelated external collector; the only curl target is localhost.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md, which invokes pnpm/CLI commands through the host's normal tool flow. No bundled scripts, hooks, dotenv or exec-enabling env vars.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares no allowed-tools or scopes. It is a passive prose artifact with no executables or install hooks.","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":".agents/skills/mcp-qa/SKILL.md","start_line":2,"end_line":3,"snippet":"name: mcp-qa\ndescription: QA MCP tool changes with local CLI and real agent clients. Use when explicitly invoked via /mcp-qa or when asked to QA MCP tool changes end-to-end."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A scan of the skill directory found no zero-width, bidi or Tag-block characters, no HTML comments and no injection imperatives aimed at the host model.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/mcp-qa/SKILL.md","start_line":8,"end_line":9,"snippet":"Verify MCP tool behavior end-to-end before committing or creating a PR. Prefer\nagent-callable paths over browser or inspector workflows."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are static and bundled. The skill fetches no instructions, rewrites no manifest and has no unpinned npx/uvx launch.","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 -- or the package its launch entry runs -- 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":".agents/skills/mcp-qa/SKILL.md","start_line":12,"end_line":12,"snippet":"pnpm run tsc && pnpm run lint && pnpm run test"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name 'mcp-qa' is generic with no brand near-miss, and the skill makes no official or replacement claims.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/mcp-qa/SKILL.md","start_line":2,"end_line":2,"snippet":"name: mcp-qa"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/mcp-qa"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":7,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/openspec-apply-change","component_path":".agents/skills/openspec-apply-change","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Prose-only skill that drives the declared `openspec` CLI to implement tasks; no bundled code, no undeclared network/exec/credential access.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-apply-change/SKILL.md","start_line":2,"end_line":5,"snippet":"name: openspec-apply-change\ndescription: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.\nlicense: MIT\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"No consent-suppression prose; skill explicitly defers to the user via the AskUserQuestion tool when ambiguous.","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":".agents/skills/openspec-apply-change/SKILL.md","start_line":23,"end_line":23,"snippet":"   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only invokes the openspec CLI on local change artifacts; no credential/context harvesting or exfil channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only SKILL.md; ships no scripts, hooks, dotenv or env-hijack primitives.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Frontmatter has no allowed-tools or scopes; only declares the openspec CLI requirement; no executables or install hooks.","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":".agents/skills/openspec-apply-change/SKILL.md","start_line":1,"end_line":5,"snippet":"---\nname: openspec-apply-change\ndescription: Implement tasks from an OpenSpec change...\nlicense: MIT\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan clean (no concealment characters); no HTML comments or forged role tokens.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-apply-change/SKILL.md","start_line":3,"end_line":3,"snippet":"description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing..."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are bundled; no live-fetched manifest, no unpinned launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/openspec-apply-change/SKILL.md","start_line":1,"end_line":5,"snippet":"---\nname: openspec-apply-change\n...\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Name matches the upstream OpenSpec generated skill naming (openspec-apply-change); no near-miss, trust prefix or impersonation prose.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/openspec-apply-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-apply-change"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-apply-change"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/openspec-archive-change","component_path":".agents/skills/openspec-archive-change","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is prose instructions that call the declared `openspec` CLI. It bundles no scripts and has no network or credential access beyond its stated purpose.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-archive-change/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill defers to the host consent flow and asks the user before archiving.","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":".agents/skills/openspec-archive-change/SKILL.md","start_line":40,"end_line":41,"snippet":"   - Use **AskUserQuestion tool** to confirm user wants to proceed\n   - Proceed if user confirms"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill reads conversation context only to infer which change to archive, as stated at line 14 of SKILL.md. It uses that value locally for archival and does not export conversation content, collect credentials, or send data to any outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory holds only SKILL.md. It ships no hooks, scripts, env assignments or host-config writes.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares no allowed-tools, wildcards or OAuth scopes, and the skill ships no executables.","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":".agents/skills/openspec-archive-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-archive-change"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan found no zero-width, bidi or Tag characters. There are no forged role tokens or override directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-archive-change/SKILL.md","start_line":20,"end_line":20,"snippet":"   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The instructions ship in the bundle. There is no runtime fetch of the skill's own instructions, no self-update and no launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/openspec-archive-change/SKILL.md","start_line":7,"end_line":9,"snippet":"  author: openspec\n  version: \"1.0\"\n  generatedBy: \"1.4.1\""}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The name follows the upstream OpenSpec generator's canonical naming. It is not a near-miss of another brand and makes no official or fork claim.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/openspec-archive-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-archive-change"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-archive-change"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":7,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/openspec-explore","component_path":".agents/skills/openspec-explore","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is a prose-only thinking-partner skill with no bundled code, network access or credential access. Its prohibition covers implementing application code, not all writes: SKILL.md line 14 expressly permits user-requested OpenSpec planning artifacts. These disclosed planning writes and CLI calls are consistent with the skill's exploration purpose and involve no concealed or undeclared capability.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-explore/SKILL.md","start_line":14,"end_line":14,"snippet":"**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill restricts the agent (no implementing) and has no consent-bypass or disclosure-suppression prose.","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":".agents/skills/openspec-explore/SKILL.md","start_line":14,"end_line":14,"snippet":"If the user asks you to implement something, remind them to exit explore mode first and create a change proposal."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Prose-only thinking skill. It reads no credentials, dumps no context, and has no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md and no execution or control-plane primitives.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only requirement declared is the openspec CLI. There is no allowed-tools, wildcard or scope, and no bundled executables.","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":".agents/skills/openspec-explore/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A scan found no concealment codepoints, hidden HTML comments or forged chat tokens. The only directives are task-scoped behavioral guidance.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-explore/SKILL.md","start_line":12,"end_line":12,"snippet":"Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are static and bundled. There is no remote fetch, self-modification or launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/openspec-explore/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name matches the canonical openspec-explore skill generated by the openspec CLI (author: openspec). It is a correct name, not a near-miss, and makes no official or replacement claims.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/openspec-explore/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-explore"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-explore"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/openspec-propose","component_path":".agents/skills/openspec-propose","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Prose-only skill invoking the declared openspec CLI; behavior matches description.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-propose/SKILL.md","start_line":2,"end_line":5,"snippet":"name: openspec-propose\ndescription: Propose a new change with all artifacts generated in one step...\nlicense: MIT\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"No consent-suppression, disclosure-hiding or authority-override prose found by keyword search and read-through.","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":".agents/skills/openspec-propose/SKILL.md","start_line":12,"end_line":12,"snippet":"Propose a new change - create the change and generate all artifacts in one step."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only invokes the openspec CLI on local artifacts; no credential/context reads or outbound sink.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only SKILL.md; no format-native execution or control-plane primitives.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No allowed-tools/wildcards/scopes declared; passive prose artifact.","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":".agents/skills/openspec-propose/SKILL.md","start_line":1,"end_line":5,"snippet":"---\nname: openspec-propose\ndescription: Propose a new change with all artifacts generated in one step...\nlicense: MIT\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan clean; no hidden or forged directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-propose/SKILL.md","start_line":3,"end_line":3,"snippet":"description: Propose a new change with all artifacts generated in one step..."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are bundled; no live fetch, self-mutation or launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/openspec-propose/SKILL.md","start_line":1,"end_line":5,"snippet":"---\nname: openspec-propose\n...\ncompatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Canonical OpenSpec generated skill name; no impersonation signals.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/openspec-propose/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-propose"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-propose"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/openspec-sync-specs","component_path":".agents/skills/openspec-sync-specs","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is prose instructions that call the declared `openspec` CLI. It bundles no scripts and has no undeclared network or credential access.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-sync-specs/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill uses AskUserQuestion to let the user select a change. It has no directives to skip confirmation.","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":".agents/skills/openspec-sync-specs/SKILL.md","start_line":22,"end_line":22,"snippet":"   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill has no credential or context reads and no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory holds only SKILL.md. It ships no hooks, scripts or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares no allowed-tools, wildcards or scopes, and the skill ships no executables.","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":".agents/skills/openspec-sync-specs/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-sync-specs"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan found no concealment characters, forged role tokens or override directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/openspec-sync-specs/SKILL.md","start_line":22,"end_line":22,"snippet":"   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The instructions ship in the bundle. There is no live fetch, no self-mutation and no launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/openspec-sync-specs/SKILL.md","start_line":7,"end_line":9,"snippet":"  author: openspec\n  version: \"1.0\"\n  generatedBy: \"1.4.1\""}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The name follows the canonical OpenSpec naming. It is not a near-miss and makes no impersonation claim.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/openspec-sync-specs/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-sync-specs"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/openspec-sync-specs"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/skill-creator","component_path":".agents/skills/skill-creator","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"SKILL.md is a pure alias pointing to sentry-skills:skill-writer. No bundled code or undisclosed capability.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/skill-creator/SKILL.md","start_line":10,"end_line":12,"snippet":"Use `sentry-skills:skill-writer` as the canonical skill for creating and updating skills.\n\nIf invoked via `skill-creator`, run the same workflow and conventions documented in `sentry-skills:skill-writer`."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Alias text only, with no consent-related directives.","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":".agents/skills/skill-creator/SKILL.md","start_line":8,"end_line":8,"snippet":"This skill name is kept for compatibility."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Alias skill that reads no credential store or conversation context and has no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md. There are no scripts, hooks, host config or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares only name and description. There is no allowed-tools, wildcard, OAuth scope, bin or install hook.","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":".agents/skills/skill-creator/SKILL.md","start_line":2,"end_line":3,"snippet":"name: skill-creator\ndescription: Alias for sentry-skills:skill-writer. Use when users explicitly ask for \"skill-creator\" or reference the legacy skill name. Redirects to the canonical skill authoring workflow."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan of the model-facing text found no concealment characters, no HTML comments and no forged tokens.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/skill-creator/SKILL.md","start_line":10,"end_line":10,"snippet":"Use `sentry-skills:skill-writer` as the canonical skill for creating and updating skills."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions ship inside the repo. There is no runtime fetch, self-mutation or launch entry.","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 -- or the package its launch entry runs -- 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":".agents/skills/skill-creator/SKILL.md","start_line":6,"end_line":8,"snippet":"# Alias: skill-creator\n\nThis skill name is kept for compatibility."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name 'skill-creator' is a generic name with no brand near-miss. The publisher is the getsentry org (origin https://github.com/getsentry/toolkit).","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/skill-creator/SKILL.md","start_line":2,"end_line":2,"snippet":"name: skill-creator"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/skill-creator"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.agents/skills/testing-guidelines","component_path":".agents/skills/testing-guidelines","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Pure guidance document on writing tests; no capabilities beyond text.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/testing-guidelines/SKILL.md","start_line":2,"end_line":3,"snippet":"name: testing-guidelines\ndescription: Guide for writing tests. Use when adding new functionality, fixing bugs, or when tests are needed."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Testing guidance only; no directives about confirmation, disclosure or authority.","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":".agents/skills/testing-guidelines/SKILL.md","start_line":2,"end_line":3,"snippet":"name: testing-guidelines\ndescription: Guide for writing tests."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Pure guidance text; no reads of credentials or context, no egress.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only SKILL.md; no format-native execution or control-plane primitives.","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":[],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Frontmatter is name/description only; no grants, no bundled executables.","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":".agents/skills/testing-guidelines/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: testing-guidelines\ndescription: Guide for writing tests. Use when adding new functionality...\n---"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan clean; neutral descriptive text.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".agents/skills/testing-guidelines/SKILL.md","start_line":3,"end_line":3,"snippet":"description: Guide for writing tests. Use when adding new functionality, fixing bugs, or when tests are needed."}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Static bundled guidance; no runtime fetch or mutation.","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 -- or the package its launch entry runs -- 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":".agents/skills/testing-guidelines/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: testing-guidelines\ndescription: Guide for writing tests...\n---"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Generic name, no brand near-miss.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".agents/skills/testing-guidelines/SKILL.md","start_line":2,"end_line":2,"snippet":"name: testing-guidelines"}],"affected_tools":null,"component_name":"skills-plugins/.agents/skills/testing-guidelines"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.codex/skills/openspec-apply-change","component_path":".codex/skills/openspec-apply-change","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is prose instructions that call the declared `openspec` CLI. It bundles no scripts and has no undeclared capabilities.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-apply-change/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill asks the user to select when the change is ambiguous. It has no directives to skip confirmation.","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":".codex/skills/openspec-apply-change/SKILL.md","start_line":23,"end_line":23,"snippet":"   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill has no credential or context reads and no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory holds only SKILL.md. It ships no hooks, scripts or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares no allowed-tools, wildcards or scopes, and the skill ships no executables.","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":".codex/skills/openspec-apply-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-apply-change"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan found no concealment characters, forged role tokens or override directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-apply-change/SKILL.md","start_line":23,"end_line":23,"snippet":"   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The instructions ship in the bundle. There is no live fetch, no self-mutation and no launch entry.","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 -- or the package its launch entry runs -- 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":".codex/skills/openspec-apply-change/SKILL.md","start_line":7,"end_line":9,"snippet":"  author: openspec\n  version: \"1.0\"\n  generatedBy: \"1.4.1\""}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The name follows the canonical OpenSpec naming. It is not a near-miss and makes no impersonation claim.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".codex/skills/openspec-apply-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-apply-change"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-apply-change"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.codex/skills/openspec-archive-change","component_path":".codex/skills/openspec-archive-change","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Runs only the openspec CLI, mkdir and mv within the change directory, which matches the declared archive purpose. No egress or credential reads.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-archive-change/SKILL.md","start_line":85,"end_line":85,"snippet":"   mv \"<changeRoot>\" \"<planningHome.changesDir>/archive/YYYY-MM-DD-<name>\""}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill requires AskUserQuestion for change selection and for incomplete artifacts/tasks, and says not to guess or auto-select a change. It defers to the consent gate.","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":".codex/skills/openspec-archive-change/SKILL.md","start_line":25,"end_line":25,"snippet":"   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only local openspec and filesystem moves. No credential or context reads and no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md. It has no hooks, scripts or covert host-state writes. Archiving is its declared, user-confirmed function.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only requirement declared is the openspec CLI. There is no allowed-tools, wildcard or scope, and no bundled executables.","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":".codex/skills/openspec-archive-change/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A scan found no concealment codepoints, hidden comments or forged tokens.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-archive-change/SKILL.md","start_line":25,"end_line":25,"snippet":"   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are static and bundled. It does not edit its own SKILL.md (it only moves openspec change directories) and fetches nothing.","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 -- or the package its launch entry runs -- 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":".codex/skills/openspec-archive-change/SKILL.md","start_line":85,"end_line":85,"snippet":"   mv \"<changeRoot>\" \"<planningHome.changesDir>/archive/YYYY-MM-DD-<name>\""}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name is the canonical openspec-archive-change skill name from the openspec generator and is not a near-miss.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".codex/skills/openspec-archive-change/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-archive-change"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-archive-change"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.codex/skills/openspec-explore","component_path":".codex/skills/openspec-explore","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is text-only, but it instructs use of the openspec CLI and permits user-requested planning artifact writes (line 14). These effects fit the declared thinking-partner purpose, and no bundled executable or hidden capability was found.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-explore/SKILL.md","start_line":2,"end_line":3,"snippet":"name: openspec-explore\ndescription: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Exploration-mode prose with no consent-suppression or oversight-disabling directives (keyword search clean).","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":".codex/skills/openspec-explore/SKILL.md","start_line":2,"end_line":3,"snippet":"name: openspec-explore\ndescription: Enter explore mode - a thinking partner for exploring ideas..."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Prose-only exploration skill; no credential/context harvesting or egress.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only SKILL.md; no format-native execution or control-plane primitives.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No allowed-tools, scopes or wildcard grants; passive prose artifact.","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":".codex/skills/openspec-explore/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: openspec-explore\ndescription: Enter explore mode - a thinking partner for exploring ideas...\nlicense: MIT"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan clean; no hidden or forged directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-explore/SKILL.md","start_line":3,"end_line":3,"snippet":"description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Static bundled prose; no runtime fetch or mutation.","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 -- or the package its launch entry runs -- 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":".codex/skills/openspec-explore/SKILL.md","start_line":1,"end_line":4,"snippet":"---\nname: openspec-explore\ndescription: Enter explore mode...\nlicense: MIT"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Canonical OpenSpec generated skill name; no impersonation signals.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".codex/skills/openspec-explore/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-explore"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-explore"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.codex/skills/openspec-propose","component_path":".codex/skills/openspec-propose","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is prose instructions that call the declared `openspec` CLI. It bundles no scripts and has no undeclared capabilities.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-propose/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill asks the user via AskUserQuestion. It has no directives to skip confirmation.","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":".codex/skills/openspec-propose/SKILL.md","start_line":29,"end_line":29,"snippet":"   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill has no credential or context reads and no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory holds only SKILL.md. It ships no hooks, scripts or env assignments.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The frontmatter declares no allowed-tools, wildcards or scopes, and the skill ships no executables.","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":".codex/skills/openspec-propose/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-propose"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan found no concealment characters, forged role tokens or override directives.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-propose/SKILL.md","start_line":29,"end_line":29,"snippet":"   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The instructions ship in the bundle. There is no live fetch, no self-mutation and no launch entry.","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 -- or the package its launch entry runs -- 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":".codex/skills/openspec-propose/SKILL.md","start_line":7,"end_line":9,"snippet":"  author: openspec\n  version: \"1.0\"\n  generatedBy: \"1.4.1\""}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The name follows the canonical OpenSpec naming. It is not a near-miss and makes no impersonation claim.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".codex/skills/openspec-propose/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-propose"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-propose"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/.codex/skills/openspec-sync-specs","component_path":".codex/skills/openspec-sync-specs","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Runs openspec status and edits openspec/specs files, which matches the declared purpose. No egress, credential reads or concealment.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-sync-specs/SKILL.md","start_line":32,"end_line":32,"snippet":"   openspec status --change \"<name>\" --json"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill requires prompting for change selection and has no suppression prose.","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":".codex/skills/openspec-sync-specs/SKILL.md","start_line":26,"end_line":26,"snippet":"   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Only local openspec and spec file edits. No credential or context reads and no outbound channel.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The directory ships only SKILL.md. It has no hooks or scripts, and spec edits are its declared function.","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":[],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only requirement declared is the openspec CLI. There is no allowed-tools, wildcard or scope, and no bundled executables.","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":".codex/skills/openspec-sync-specs/SKILL.md","start_line":5,"end_line":5,"snippet":"compatibility: Requires openspec CLI."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A scan found no concealment codepoints, hidden comments or forged tokens.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":".codex/skills/openspec-sync-specs/SKILL.md","start_line":26,"end_line":26,"snippet":"   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose."}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Instructions are static and bundled. It edits only openspec/specs data files, not its own manifest, and fetches nothing.","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 -- or the package its launch entry runs -- 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":".codex/skills/openspec-sync-specs/SKILL.md","start_line":32,"end_line":32,"snippet":"   openspec status --change \"<name>\" --json"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name is the canonical openspec-sync-specs skill name from the openspec generator and is not a near-miss.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":".codex/skills/openspec-sync-specs/SKILL.md","start_line":2,"end_line":2,"snippet":"name: openspec-sync-specs"}],"affected_tools":null,"component_name":"skills-plugins/.codex/skills/openspec-sync-specs"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"mcp-server","component_name":"mcp-server/apps/docs-mcp","component_path":"apps/docs-mcp","metadata":{"transports":["streamable_http"]}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-M0001","rule_version":"1.2.1","rule_name":"Tool Definition Integrity & Versioning (ETDI-style)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Tool definitions are compiled into the Worker (apps/docs-mcp/src/mcp.ts registerTool calls) and deployed via wrangler; no remote tool manifest, hot reload or runtime-fetched definitions exist. Tools are fixed per deployed version.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents rug-pull tool poisoning.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"apps/docs-mcp/src/mcp.ts","start_line":190,"end_line":197,"snippet":"mcp.registerTool(\n    \"search-docs\",\n    {\n      description:\n        \"Search the public Sentry docs index. Returns deterministic ranked page titles...\",\n      inputSchema: searchDocsInputSchema.shape,"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0002","rule_version":"1.2.0","rule_name":"Unrestricted Tool Permissions (Overprivileged Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"All four tools (get-available-features, search-docs, query-docs, get-docs) are read-only and only reach docs.sentry.io (fetcher BASE_URL fixed) plus the OpenRouter LLM API. No fs, exec or admin actions are exposed. Caller-controlled inputs are zod-validated.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Limits blast radius and privilege escalation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"apps/docs-mcp/src/sentry-docs/fetcher.ts","start_line":4,"end_line":4,"snippet":"const BASE_URL = \"https://docs.sentry.io\";"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0003","rule_version":"1.3.3","rule_name":"CORS / Origin / Host Validation (DNS Rebinding Defense)","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"The Worker applies hono cors() with defaults and never inspects the Origin header before transport dispatch. isDocsMcpServiceRequest checks only the URL hostname, so a hostile web page can direct requests to the documented localhost development server (for example localhost:8788/mcp or 127.0.0.1:8788/internal/docs) and pass the gate. The tools perform outbound requests and use a shared model credential, so they are not pure-input tools covered by a loopback exception. Private production routing (workers_dev=false, no routes) reduces exposure but does not protect the shipped local HTTP path.","vulnerability_details":null,"fix_guidance":"Add explicit Origin validation (an allowlist, rejecting unexpected Origins) to the /mcp and /internal/docs handlers, and replace the default cors() configuration with one restricted to trusted origins.","why_this_matters":"Prevents DNS rebinding/browser hijack of local or remote MCP.","context_note":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-M0003 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/package.json","start_line":5,"end_line":7,"snippet":"  \"scripts\": {\n    \"dev\": \"wrangler dev --port 8788\",\n    \"build\": \"wrangler deploy --dry-run\","},{"path":"apps/docs-mcp/src/index.ts","start_line":27,"end_line":37,"snippet":"export const app = new Hono<{ Bindings: Env }>().use(cors());\n\napp.get(\"/health\", (c) => c.json({ status: \"ok\" }));\n\napp.all(\"/mcp\", async (c) => {\n  if (!isDocsMcpServiceRequest(c.req.raw)) {\n    return c.json(\n      { error: \"Docs MCP is only available through its service binding\" },\n      403,\n    );\n  }"},{"path":"apps/docs-mcp/src/access.ts","start_line":9,"end_line":13,"snippet":"export function isDocsMcpServiceRequest(request: Request): boolean {\n  const { url } = request;\n  const { hostname } = new URL(url);\n  return hostname === \"docs-mcp.internal\" || LOCAL_HOSTNAMES.has(hostname);\n}"},{"path":"apps/docs-mcp/src/index.ts","start_line":47,"end_line":55,"snippet":"  const transport = new StreamableHTTPTransport();\n  const mcp = createMcpServer(\n    c.env.OPENROUTER_API_KEY,\n    c.env.CF_VERSION_METADATA.id,\n  );\n\n  await mcp.connect(transport);\n\n  return await transport.handleRequest(c);"},{"path":"apps/docs-mcp/src/sentry-docs/fetcher.ts","start_line":58,"end_line":60,"snippet":"      const response = await fetch(request, {\n        signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n      });"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":198,"end_line":201,"snippet":"    async ({ limit, query }) => {\n      const start = Date.now();\n      try {\n        const result = await executeDocsSearch({ limit, query });"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0004","rule_version":"1.2.1","rule_name":"Binding to 0.0.0.0 Without Auth/TLS","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Cloudflare Worker; no socket listen/bind in code. Exposure is controlled by workers_dev=false and the service-binding hostname gate; there is no 0.0.0.0 bind and no Dockerfile/Ingress.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"LAN/Internet exposure enables remote exploitation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0005","rule_version":"1.4.2","rule_name":"OAuth 2.1 + PKCE Correctness (MCP Authorization)","finding":"VULNERABLE","severity":"CRITICAL","realized_severity":"HIGH","confidence":"medium","rationale":"The component exposes a remote streamable-HTTP MCP endpoint (/mcp via @hono/mcp StreamableHTTPTransport) but implements no OAuth 2.1 authorization: no protected-resource metadata, no PKCE, no token validation. Access is restricted only by a URL-hostname check meant for service-binding callers, which is not a verified credential and lets any accepted-hostname caller invoke paid agent operations with the Worker's key. This is a restricted architectural authorization gap rather than demonstrated Internet exploitation: workers_dev and preview URLs are disabled, no public route is configured, and the hostname gate returns 403 for other hostnames.","vulnerability_details":"Remote HTTP MCP transport with no OAuth 2.1 flow; only a hostname-based service-binding gate that relies on the platform preserving the caller's internal URL rather than a verified credential.","fix_guidance":"If the endpoint is ever exposed beyond the service binding, front it with OAuth 2.1 (resource metadata, PKCE) or a signed service-to-service token.","why_this_matters":"A remote MCP endpoint without OAuth 2.1 authorization can let anyone who reaches it, or who obtains a static key, call every tool it serves. A misconfigured OAuth flow enables token theft and confused-deputy attacks.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-M0005 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by both the scan and the independent adjudication. Realized severity HIGH (from CRITICAL: 40% context relief -- mitigation_present -40%).","within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"apps/docs-mcp/src/index.ts","start_line":31,"end_line":55,"snippet":"app.all(\"/mcp\", async (c) => {\n  if (!isDocsMcpServiceRequest(c.req.raw)) {\n    return c.json({ error: \"Docs MCP is only available through its service binding\" }, 403);\n  }\n  ...\n  const transport = new StreamableHTTPTransport();\n  const mcp = createMcpServer(c.env.OPENROUTER_API_KEY, c.env.CF_VERSION_METADATA.id);\n  await mcp.connect(transport);\n  return await transport.handleRequest(c);\n});"},{"path":"apps/docs-mcp/src/access.ts","start_line":9,"end_line":13,"snippet":"export function isDocsMcpServiceRequest(request: Request): boolean {\n  const { url } = request;\n  const { hostname } = new URL(url);\n  return hostname === \"docs-mcp.internal\" || LOCAL_HOSTNAMES.has(hostname);\n}"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":100,"end_line":105,"snippet":"try {\n        if (!apiKey) {\n          throw new Error(\"Set OPENROUTER_API_KEY on the docs-mcp worker\");\n        }\n\n        const result = await config.run(typedInput, apiKey);"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0006","rule_version":"1.2.1","rule_name":"Tool Name Collision / Spoofing","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"low","rationale":"Tool names (get-available-features, search-docs, query-docs, get-docs) are generic verbs scoped to Sentry docs; this server is consumed only through an internal service binding, not alongside third-party servers. No specific named collision with a widely-deployed server tool was established.","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":"apps/docs-mcp/src/mcp.ts","start_line":181,"end_line":181,"snippet":"name: \"get-available-features\","},{"path":"apps/docs-mcp/src/mcp.ts","start_line":266,"end_line":266,"snippet":"name: \"get-docs\","}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0007","rule_version":"1.3.1","rule_name":"Prompt/Metadata Injection in Tool Descriptions","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"All four registered tool descriptions in mcp.ts are neutral functional descriptions with no directives, hidden characters or exfiltration instructions.","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":"apps/docs-mcp/src/mcp.ts","start_line":227,"end_line":229,"snippet":"description:\n        \"Answer a free-form Sentry documentation question using fetched docs.sentry.io pages. The result contains a cited Markdown answer and its validated source URLs.\","}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0008","rule_version":"1.2.1","rule_name":"CORS Misuse: Wildcard with Credentials","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"hono cors() is used with defaults (Access-Control-Allow-Origin: * and credentials not enabled). No cookies or credentialed responses are used, so wildcard+credentials does not occur.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents credentialed cross-origin abuse.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"apps/docs-mcp/src/index.ts","start_line":27,"end_line":27,"snippet":"export const app = new Hono<{ Bindings: Env }>().use(cors());"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0009","rule_version":"1.3.1","rule_name":"Transport Security: TLS Everywhere (Remote)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runs on Cloudflare Workers where TLS is terminated by the platform; outbound fetches target https://docs.sentry.io. No plain-HTTP server or disabled certificate validation in code.","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":false,"mitigation_present":null,"evidence":[{"path":"apps/docs-mcp/src/sentry-docs/fetcher.ts","start_line":4,"end_line":4,"snippet":"const BASE_URL = \"https://docs.sentry.io\";"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0010","rule_version":"1.5.0","rule_name":"No Authentication or Authorization Checks (General)","finding":"VULNERABLE","severity":"CRITICAL","realized_severity":"CRITICAL","confidence":"medium","rationale":"Both /mcp and /internal/docs are gated only by isDocsMcpServiceRequest, which checks the URL hostname (docs-mcp.internal or localhost) and verifies no caller credential. In production, workers_dev and preview URLs are disabled and no public route is configured, so service-binding isolation holds. However, the shipped local development listener accepts unauthenticated requests addressed to localhost, including requests carrying a hostile Origin. The tools make outbound docs requests and use the operator's shared OpenRouter credential, so the loopback-only exception does not apply and local callers can reach model-backed tool dispatch. This is limited to local development exposure, not a demonstrated public production bypass.","vulnerability_details":null,"fix_guidance":"Require a verified credential (for example a shared secret header or signed service-to-service token) in the access check applied to /mcp and /internal/docs, including on the localhost development path, instead of relying on the URL hostname alone.","why_this_matters":"Prevents unauthorized tool use and data access.","context_note":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-M0010 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/package.json","start_line":5,"end_line":7,"snippet":"  \"scripts\": {\n    \"dev\": \"wrangler dev --port 8788\",\n    \"build\": \"wrangler deploy --dry-run\","},{"path":"apps/docs-mcp/src/access.ts","start_line":9,"end_line":13,"snippet":"export function isDocsMcpServiceRequest(request: Request): boolean {\n  const { url } = request;\n  const { hostname } = new URL(url);\n  return hostname === \"docs-mcp.internal\" || LOCAL_HOSTNAMES.has(hostname);\n}"},{"path":"apps/docs-mcp/src/index.ts","start_line":31,"end_line":37,"snippet":"app.all(\"/mcp\", async (c) => {\n  if (!isDocsMcpServiceRequest(c.req.raw)) {\n    return c.json(\n      { error: \"Docs MCP is only available through its service binding\" },\n      403,\n    );\n  }"},{"path":"apps/docs-mcp/src/index.ts","start_line":47,"end_line":55,"snippet":"  const transport = new StreamableHTTPTransport();\n  const mcp = createMcpServer(\n    c.env.OPENROUTER_API_KEY,\n    c.env.CF_VERSION_METADATA.id,\n  );\n\n  await mcp.connect(transport);\n\n  return await transport.handleRequest(c);"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":101,"end_line":105,"snippet":"        if (!apiKey) {\n          throw new Error(\"Set OPENROUTER_API_KEY on the docs-mcp worker\");\n        }\n\n        const result = await config.run(typedInput, apiKey);"},{"path":"apps/docs-mcp/src/agent/model-fallback.ts","start_line":34,"end_line":44,"snippet":"  const openrouter = createOpenRouter(apiKey ? { apiKey } : {});\n  const options =\n    typeof fallbackOrOptions === \"string\"\n      ? { fallbackModelId: fallbackOrOptions }\n      : fallbackOrOptions;\n  const chatModel = (modelId: string) =>\n    options.parallelToolCalls === undefined\n      ? openrouter(modelId)\n      : openrouter.chat(modelId, {\n          parallelToolCalls: options.parallelToolCalls,\n        });"}],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"},{"rule_id":"ATR-M0011","rule_version":"1.2.1","rule_name":"OAuth Proxy Per-Client Consent (Confused Deputy Defense)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The server is not an OAuth proxy (no /authorize, no client registration, no upstream IdP). Tools take no caller-supplied target for the shared OpenRouter credential; they only run LLM prompts over docs.sentry.io content.","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":[],"affected_tools":null,"component_name":"mcp-server/apps/docs-mcp"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[{"tool_name":"get-available-features","tool_description":"Returns canonical Sentry feature slugs for given libraries via an LLM agent reading docs.sentry.io","types":["read-only","untrusted-output","cost-impact"],"status":"IMPLEMENTED","component_name":"mcp-server/apps/docs-mcp"},{"tool_name":"get-docs","tool_description":"Returns a tailored Sentry integration guide synthesized by an LLM agent from live docs","types":["read-only","untrusted-output","cost-impact"],"status":"IMPLEMENTED","component_name":"mcp-server/apps/docs-mcp"},{"tool_name":"query-docs","tool_description":"Answers free-form Sentry docs questions using fetched docs.sentry.io pages with cited sources","types":["read-only","untrusted-output","cost-impact"],"status":"IMPLEMENTED","component_name":"mcp-server/apps/docs-mcp"},{"tool_name":"search-docs","tool_description":"Deterministic search of the public Sentry docs index","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/apps/docs-mcp"}],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":3,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{"read_only":4,"untrusted_output":4,"cost_impact":3},"worst_case_severity":"CRITICAL"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":47,"tier":"low","capability_band":"moderate","score_confidence":"medium","score_interval":{"low":23,"high":71},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL"]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli","component_path":"packages/cli/.cursor/skills/sentry-cli","metadata":{"manifest":"SKILL.md (symlink to packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md)"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"SKILL.md (symlink to packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md) is a documentation skill for the declared `sentry` CLI. No bundled scripts; commands shown match the stated purpose. The YARA credential-exfil hit (L159-255) covers ordinary CLI examples (sentry docs, status, schema, release, api); attestation unavailable on this page.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":2,"end_line":8,"snippet":"name: sentry-cli\nversion: 0.49.0-dev.0\ndescription: Guide for using the Sentry CLI to interact with Sentry from the command line...\nrequires:\n  bins: [\"sentry\"]\n  auth: true"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Guidance such as 'just run the command' concerns org/project auto-detection, not a bypass of host consent; the CLI prompts for login. No disclosure suppression or authority-override prose.","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":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":13,"end_line":13,"snippet":"> **Core rule for agents: just run the command.** The `sentry` CLI auto-detects your org and project..."}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The skill documents delegated credential handling: references/auth.md (lines 211-220) shows the sanctioned CLI printing the stored auth token, so credentials are read by delegation. No directive couples that read with a send to a third-party sink, and DSN detection from .env is performed by the declared CLI for its own function. Declared authentication is handled safely with no harvest-and-exfiltrate pairing.","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":[],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The component directory is symlinks to a SKILL.md and references folder; no bundled hooks, scripts, dotenv, git hooks or exec-enabling env assignments. (The sibling packages/cli/.cursor/hooks.json is a repo dev lint hook, not part of the skill.)","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":[],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Manifest declares only the `sentry` binary and auth requirement, matching its purpose; no wildcard allowed-tools, scopes or install hooks.","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":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":5,"end_line":7,"snippet":"requires:\n  bins: [\"sentry\"]\n  auth: true"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan found no zero-width, bidi, Tag-block or ANSI characters; no HTML comments or forged chat-template tokens.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":4,"end_line":4,"snippet":"description: Guide for using the Sentry CLI to interact with Sentry from the command line. Use when the user asks about viewing issues..."}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Instructions ship inside the bundle with a version in frontmatter; no live fetch of instructions, self-rewrite or launch entry. The documented curl installer for the CLI binary is user-run documentation, not an instruction fetch.","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 -- or the package its launch entry runs -- 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":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":3,"end_line":3,"snippet":"version: 0.49.0-dev.0"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Published from the getsentry GitHub org (origin https://github.com/getsentry/toolkit), the genuine Sentry owner; name sentry-cli is canonical.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":"packages/cli/.cursor/skills/sentry-cli/SKILL.md","start_line":2,"end_line":2,"snippet":"name: sentry-cli"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/.cursor/skills/sentry-cli"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":7,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/packages/cli/plugins/sentry-cli","component_path":"packages/cli/plugins/sentry-cli","metadata":{"manifest":"skills/sentry-cli/SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The skill is documentation for the declared `sentry` CLI and bundles only markdown references. It has no scripts, no hidden network clients and no credential reads. The commands it documents (issue/event/api/auth) match the stated purpose, and the `curl | bash` lines are the documented user install step for the CLI, not a runtime egress path.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":14,"snippet":"---\nname: sentry-cli\nversion: 0.49.0-dev.0\ndescription: Guide for using the Sentry CLI to interact with Sentry from the command line. ...\nrequires:\n  bins: [\"sentry\"]\n  auth: true\n---","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The Safety Rules section defers to user consent. The YARA hit on this file (Zeus_Metadata_Credential_Exfiltration_Directive_WideSpan, L159-255; the attestation was not available on this page) lands on CLI usage examples (docs, status, schema, release, api). None directs credential harvesting or consent suppression. Line 52 says the opposite: never store or log tokens.","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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":52,"snippet":"- Always confirm with the user before running destructive commands: `alert issues delete`, ... `project delete`, `release delete`, `token delete`, `trial start`\n- For mutations, verify the org/project context looks correct in the command output before proceeding with further changes\n- Never store or log authentication tokens — the CLI manages credentials automatically","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The bundled auth reference (references/auth.md, lines 211-220) directs the CLI to print a stored token, so credential handling is in scope for this rule. It is declared Sentry authentication functionality with in-band output. Nothing directs the agent to send credentials or conversation context to an external destination, and the skill tells the agent never to store or log tokens.","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":[],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The plugin directory ships only SKILL.md, markdown references and a plugin.json with name/version/description. There are no hooks, entry scripts, env assignments or host-config writes.","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":[],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The manifest declares only a required binary (sentry) and auth. There is no allowed-tools wildcard, no OAuth scope block and no postinstall. The declared reach matches the CLI skill's stated 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":"packages/cli/plugins/sentry-cli/.claude-plugin/plugin.json","start_line":1,"end_line":5,"snippet":"{\n  \"name\": \"sentry-cli\",\n  \"version\": \"0.49.0\",\n  \"description\": \"Skills for using the Sentry CLI to interact with Sentry from the command line\"\n}"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan of the skill and plugin files found no zero-width, bidi or Tag characters. There are no forged role tokens. The one 'Core rule for agents' line is ordinary usage guidance scoped to the CLI, not an override of host policy.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":14,"snippet":"> **Core rule for agents: just run the command.** The `sentry` CLI auto-detects your org and project ...","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","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 text ships inside the bundle. The CLI's agent-skill installer embeds the generated SKILL.md, so no network fetch of the skill's own instructions is needed. There is no launch entry and no unpinned package fetch. The `curl | bash` lines are the documented user install step for the CLI binary, not the skill's instructions.","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 -- or the package its launch entry runs -- 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":"packages/cli/plugins/sentry-cli/.claude-plugin/plugin.json","start_line":1,"end_line":5,"snippet":"{\n  \"name\": \"sentry-cli\",\n  \"version\": \"0.49.0\",\n  \"description\": \"Skills for using the Sentry CLI to interact with Sentry from the command line\"\n}"}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The name 'sentry-cli' is published from the getsentry org repo (origin https://github.com/getsentry/toolkit). The publisher is verifiably the brand owner.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":"packages/cli/plugins/sentry-cli/.claude-plugin/plugin.json","start_line":2,"end_line":2,"snippet":"  \"name\": \"sentry-cli\","}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":7,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli","component_path":"packages/cli/plugins/sentry-cli/skills/sentry-cli","metadata":{"manifest":"SKILL.md"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"SKILL.md and references/*.md document the sentry CLI and ship no scripts. The declared purpose matches the commands documented. The curl https://cli.sentry.dev/install | bash step is a disclosed installation step to the first-party host, and the manifest declares requires bins sentry and auth. The YARA credential-exfiltration match on SKILL.md (L159-255) was reviewed: it covers ordinary CLI usage examples, not a harvest-and-send directive. The YARA scanner_attestation was not available in this session.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":363,"snippet":"### Installation\n\n```bash\ncurl https://cli.sentry.dev/install -fsS | bash","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The Safety Rules section says to always confirm with the user before destructive commands. The 'just run the command' guidance concerns org/project discovery and does not skip consent for state-changing actions.","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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":50,"snippet":"- Always confirm with the user before running destructive commands: `alert issues delete`, `alert metrics delete`, `cli uninstall`, `dashboard widget delete`, `integration delete`, `issue merge`, `issue unlink`, `project delete`, `release delete`, `token delete`, `trial start`","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The skill documents `sentry auth token`, which prints the stored token to the user. It also documents CLI auth handling. It does not direct the agent to append credentials to any request or send them to a third party, and the Safety Rules say never to store or log tokens. The only egress is the CLI's calls to Sentry itself. The YARA Zeus_Metadata_Credential_Exfiltration_Directive_WideSpan match on SKILL.md (L159-255) was reviewed and is ordinary CLI usage examples with no harvest-plus-send directive. The scanner_attestation was not available in this session.","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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":52,"snippet":"- Never store or log authentication tokens — the CLI manages credentials automatically","vulnerability":{}},{"path":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":410,"snippet":"- `sentry auth token` — Print the stored authentication token","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The skill directory and its .claude-plugin/plugin.json contain only markdown and a name/version/description manifest. There are no hooks, entry scripts, dotenv or exec-enabling/endpoint env assignments. The curl installer in the docs is a user-run install step for the CLI, not a bundled load-time primitive.","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":[],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The manifest requires only the sentry binary and auth, which match the stated function of using the Sentry CLI. There is no allowed-tools wildcard, no OAuth scope block, and no postinstall or hook. plugin.json has only name, version and description.","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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":14,"snippet":"requires:\n  bins: [\"sentry\"]\n  auth: true","vulnerability":{}},{"path":"packages/cli/plugins/sentry-cli/.claude-plugin/plugin.json","start_line":2,"end_line":4,"snippet":"  \"name\": \"sentry-cli\",\n  \"version\": \"0.49.0\",\n  \"description\": \"Skills for using the Sentry CLI to interact with Sentry from the command line\""}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"A grep for zero-width, bidi and Tag-block codepoints across SKILL.md and references/ returned no matches. The only non-ASCII characters are typographic dashes. Directives such as 'just run the command' are visible, task-scoped usage guidance, not hidden or injected. The YARA instruction-override hit is on packages/cli/AGENTS.md, which is outside this component.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":14,"snippet":"> **Core rule for agents: just run the command.** The `sentry` CLI auto-detects your org and project (from `.sentryclirc`, DSNs in `.env`/source, and the directory name)","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"SKILL.md and references ship in the bundle with a recorded version. The skill has no .mcp.json or launch entry and does not self-modify. The documented install and upgrade examples (SKILL.md:359-365) are explicit, user-run commands for the first-party CLI installer, not a shipped launch entry or a silent mutation of the skill's own instructions. The SENTRY_VERSION pin selects the CLI release but does not pin or authenticate the installer script bytes. The YARA remote-bootstrap match is on packages/cli/install.ps1, which is outside this component.","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 -- or the package its launch entry runs -- 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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":365,"snippet":"curl https://cli.sentry.dev/install -fsS | SENTRY_VERSION=0.42.2 bash","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The skill is named for the Sentry CLI and is published from the getsentry GitHub org (origin https://github.com/getsentry/toolkit), the brand's genuine owner. Install URLs point to the first-party cli.sentry.dev, with no look-alike registry.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":"packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md","start_line":14,"end_line":14,"snippet":"name: sentry-cli","vulnerability":{}}],"affected_tools":null,"component_name":"skills-plugins/packages/cli/plugins/sentry-cli/skills/sentry-cli"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":7,"not_applicable":1},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":100,"tier":"high","capability_band":"minimal","score_confidence":"high","score_interval":{"low":96,"high":100},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"mcp-server","component_name":"mcp-server/packages/mcp-core","component_path":"packages/mcp-core","metadata":{"transports":["stdio"]}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-M0001","rule_version":"1.2.1","rule_name":"Tool Definition Integrity & Versioning (ETDI-style)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Tool definitions are compiled into the package (src/tools/catalog/*.ts, generated toolDefinitions.json) and registered in-process via server.registerTool. There is no remote tool manifest and no hot reload over HTTP. The set is fixed per server (listChanged: false). The package is not individually code-signed, but definitions are immutable once built.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents rug-pull tool poisoning.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/server.ts","start_line":333,"end_line":340,"snippet":"for (const { name, config, handler } of registrations) {\n    server.registerTool(name, config, handler);\n  }\n  // Registration enables this by default, but each server's tool catalog is fixed.\n  if (registrations.length > 0) {\n    server.server.registerCapabilities({ tools: { listChanged: false } });"}],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0002","rule_version":"1.2.0","rule_name":"Unrestricted Tool Permissions (Overprivileged Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Tools are Sentry-API wrappers with no fs, exec or arbitrary-network reach. Runtime availability is gated by grantedSkills, mode and project constraints in catalog-runtime/availability.ts; per-tool requiredScopes declarations and readOnly/destructive annotations are present in the catalog but are not directly checked in that availability path. The execute_sentry_tool dispatcher is annotated destructive, but it re-resolves the target tool through the same availability filter and errors on unavailable targets, so it does not widen privilege.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Limits blast radius and privilege escalation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/delete-alert-rule.ts","start_line":15,"end_line":42,"snippet":"name: \"delete_alert_rule\",\n  requiredScopes: [\"org:read\", \"project:read\", \"alerts:write\"],\n  annotations: {\n    readOnlyHint: false,\n    destructiveHint: true,\n    idempotentHint: true,\n    openWorldHint: true,\n  },"},{"path":"packages/mcp-core/src/tools/special/execute-tool.ts","start_line":165,"end_line":173,"snippet":"      const availableTools = getSearchableTools({\n        tools,\n        context,\n        experimentalMode: context.experimentalMode ?? false,\n        useDefaultSurfacePolicy: true,\n      });\n      const matchedTool = availableTools.find(({ key, tool }) => key === name || tool.name === name)?.tool ...\n        availableTools.find(\n          ({ key, tool }) => key === name || tool.name === name,"}],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0003","rule_version":"1.3.3","rule_name":"CORS / Origin / Host Validation (DNS Rebinding Defense)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The only transport in this package is stdio (src/transports/stdio.ts, StdioServerTransport). No HTTP/SSE listener exists, so there is no Origin/Host surface.","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":null,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0004","rule_version":"1.2.1","rule_name":"Binding to 0.0.0.0 Without Auth/TLS","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The component is stdio-only; it has no listen/bind call, Dockerfile or ingress.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"LAN/Internet exposure enables remote exploitation.","context_note":null,"within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0005","rule_version":"1.4.2","rule_name":"OAuth 2.1 + PKCE Correctness (MCP Authorization)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The component exposes only stdio (packages/mcp-core/src/transports/stdio.ts). There is no remote authorization surface and no OAuth flow in this package; the credential is supplied by the host process.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A remote MCP endpoint without OAuth 2.1 authorization can let anyone who reaches it, or who obtains a static key, call every tool it serves. A misconfigured OAuth flow enables token theft and confused-deputy attacks.","context_note":null,"within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0006","rule_version":"1.2.1","rule_name":"Tool Name Collision / Spoofing","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"Tool names are Sentry-domain identifiers. None shadows a named tool of a widely deployed server (filesystem, git, github). Generic names such as whoami or get_doc have no specific collision. Deprecated aliases are explicitly marked, and serverInfo.name is 'Sentry MCP'.","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":"packages/mcp-core/src/tools/catalog/index.ts","start_line":81,"end_line":86,"snippet":"  name: \"get_ai_conversation_details\",\n  title: \"Get AI Conversation Details (Deprecated)\",\n  includeInSkillDefinitions: false,\n  deprecated: { replacedBy: [\"get_agent_conversation_details\"] },"}],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0007","rule_version":"1.3.1","rule_name":"Prompt/Metadata Injection in Tool Descriptions","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"I reviewed the registered tool descriptions (e.g. execute_sentry_tool) and grepped src/tools for override directives, concealment phrases, ANSI escapes and bidi/zero-width codepoints. None were found. Descriptions are task-descriptive usage guidance. The YARA hit on tools/support/search-events/config.ts is an internal LLM-agent system prompt for query translation (field-verification guidance), not a registered tool description, and contains no credential-exfiltration directive. The YARA attestation was not available on this page.","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":"packages/mcp-core/src/tools/special/execute-tool.ts","start_line":96,"end_line":112,"snippet":"description: [\n      \"Execute an available Sentry MCP tool discovered through search_sentry_tools.\",\n      \"\",\n      \"Use this tool when you need to:\",\n      \"- Call a Sentry operation returned by search_sentry_tools\",\n      \"- Execute a tool by name using arguments that match its returned schema\",\n      ...\n      \"- Arguments are validated against the target tool's schema before execution.\","}],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0008","rule_version":"1.2.1","rule_name":"CORS Misuse: Wildcard with Credentials","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The package has no HTTP server and sets no CORS headers (stdio only).","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents credentialed cross-origin abuse.","context_note":null,"within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0009","rule_version":"1.3.1","rule_name":"Transport Security: TLS Everywhere (Remote)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The server transport is stdio only, so no remote MCP endpoint exists to secure. Outbound Sentry API calls are made through the host-configured Sentry host.","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":null,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0010","rule_version":"1.5.0","rule_name":"No Authentication or Authorization Checks (General)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"This rule concerns remote transports, and this package ships only a stdio adapter. The server is reached over stdio by the parent process, with no network listener, so the remote-listener authentication rule does not apply to this component.","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":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/permissions.ts","start_line":128,"end_line":151,"snippet":"requiredScopes: Scope[],\n ...\n  return requiredScopes.every((scope) => expandedScopes.has(scope));\n...\n  if (!requiredScopes || requiredScopes.length === 0) {\n  ...\n  return hasRequiredScopes(grantedScopes, requiredScopes);"}],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"},{"rule_id":"ATR-M0011","rule_version":"1.2.1","rule_name":"OAuth Proxy Per-Client Consent (Confused Deputy Defense)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"This package is not an OAuth proxy: it has no /authorize endpoint and no client registration. It is a stdio tool library that calls the Sentry API with the invoking user's credential, so no shared ambient credential is wielded for other callers.","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":[],"affected_tools":null,"component_name":"mcp-server/packages/mcp-core"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[{"tool_name":"add_issue_note","tool_description":"Add a note to a Sentry issue","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"add_team_to_project","tool_description":"Add a team to a project","types":["write","iam-admin"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"analyze_issue_with_seer","tool_description":"AI root cause analysis for an issue via Seer","types":["write","cost-impact"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_alert_rule","tool_description":"Create an alert rule","types":["write","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_dsn","tool_description":"Create a project client key (DSN)","types":["write","secrets-access"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_metric_monitor","tool_description":"Create a metric monitor","types":["write","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_monitor","tool_description":"Create a cron monitor","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_project","tool_description":"Create a project","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_team","tool_description":"Create a team","types":["write","iam-admin"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"create_uptime_monitor","tool_description":"Create an uptime monitor","types":["write","admin-config","network-broad"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"delete_alert_rule","tool_description":"Delete an alert rule","types":["destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"delete_metric_monitor","tool_description":"Delete a metric monitor","types":["destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"delete_monitor_environment","tool_description":"Delete a monitor environment","types":["destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"delete_uptime_monitor","tool_description":"Delete an uptime monitor","types":["destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"execute_sentry_tool","tool_description":"Execute any catalog tool discovered via search_sentry_tools (dispatcher; annotated destructive)","types":["write","destructive","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_alert_rules","tool_description":"List alert rules","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_dashboards","tool_description":"List dashboards","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_dropped_events","tool_description":"Find dropped events","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_dsns","tool_description":"List project client keys (DSNs)","types":["read-only","secrets-access"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_metric_monitors","tool_description":"List metric monitors","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_monitors","tool_description":"List cron monitors","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_organizations","tool_description":"List organizations","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_projects","tool_description":"List projects","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_releases","tool_description":"List releases","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_teams","tool_description":"List teams","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"find_uptime_monitors","tool_description":"List uptime monitors","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_agent_conversation_details","tool_description":"Get agent conversation details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_ai_conversation_details","tool_description":"Deprecated alias for get_agent_conversation_details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_alert_options","tool_description":"Get alert rule options","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_alert_rule","tool_description":"Get alert rule details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_dashboard_details","tool_description":"Get dashboard details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_doc","tool_description":"Fetch a Sentry documentation page","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_event_attachment","tool_description":"Download an event attachment","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_event_stacktrace","tool_description":"Get event stacktrace","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_issue_activity","tool_description":"Get issue activity","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_issue_breadcrumbs","tool_description":"Get issue breadcrumbs","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_issue_details","tool_description":"Get issue details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_issue_tag_values","tool_description":"Get issue tag values","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_issue_user_reports","tool_description":"Get issue user reports","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_latest_base_snapshot","tool_description":"Get the latest base snapshot for an app","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_metric_monitor_details","tool_description":"Get metric monitor details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_monitor_details","tool_description":"Get cron monitor details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_profile","tool_description":"Get a profile","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_profile_details","tool_description":"Get profile details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_release_details","tool_description":"Get release details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_replay_details","tool_description":"Get replay details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_sentry_mcp_info","tool_description":"Info about the Sentry MCP server","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_sentry_resource","tool_description":"Fetch a Sentry resource by URL or ID","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_snapshot","tool_description":"Get a preprod snapshot diff summary","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_snapshot_image","tool_description":"Get a snapshot image","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_span_details","tool_description":"Get span details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_trace_details","tool_description":"Get trace details","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"get_uptime_monitor_details","tool_description":"Get uptime monitor details","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"link_issue","tool_description":"Link an issue to an external tracker","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"onboarding_status_update","tool_description":"Update onboarding status","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"remove_team_from_project","tool_description":"Remove a team from a project","types":["destructive","iam-admin"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_agent_conversations","tool_description":"Search agent conversations","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_ai_conversations","tool_description":"Deprecated alias for search_agent_conversations","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_attributes","tool_description":"Search available telemetry attributes","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_docs","tool_description":"Search Sentry documentation","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_errors","tool_description":"Search error events","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_events","tool_description":"Search events","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_issue_events","tool_description":"Search events within an issue","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_issues","tool_description":"Search grouped issues","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_logs","tool_description":"Search logs","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_metrics","tool_description":"Search metrics","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_profiles","tool_description":"Search profiles","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_replays","tool_description":"Search replays","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_sentry_tools","tool_description":"Search available Sentry MCP catalog tools","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"search_traces","tool_description":"Search traces/spans","types":["read-only","untrusted-output"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"static_tool","tool_description":null,"types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"submit_feedback_to_sentry_team","tool_description":"Submit feedback about the MCP to the Sentry team","types":["write","social"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"unlink_issue","tool_description":"Unlink an external issue","types":["write","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_alert_rule","tool_description":"Update an alert rule","types":["write","admin-config","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_dsn","tool_description":"Update a project client key (DSN)","types":["write","secrets-access","admin-config","destructive","cost-impact"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_issue","tool_description":"Update issue status/assignment","types":["write","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_metric_monitor","tool_description":"Update a metric monitor","types":["write","admin-config","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_project","tool_description":"Update a project","types":["write","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"update_uptime_monitor","tool_description":"Update an uptime monitor","types":["write","admin-config","network-broad","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"},{"tool_name":"whoami","tool_description":"Identify the authenticated user","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server/packages/mcp-core"}],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":4,"not_applicable":7},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{"write":21,"iam_admin":3,"cost_impact":2,"admin_config":9,"secrets_access":3,"network_broad":2,"destructive":12,"read_only":54,"untrusted_output":25,"social":1},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":67,"tier":"medium","capability_band":"extreme","score_confidence":"low","score_interval":{"low":46,"high":88},"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/plugins/sentry-mcp","component_path":"plugins/sentry-mcp","metadata":{"manifest":"agents/sentry-mcp.md, .claude-plugin/plugin.json, .mcp.json"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Plugin bundles a prose agent definition and a .mcp.json pointing to Sentry's hosted MCP server; its behavior (searching/managing Sentry resources) matches its description. No bundled scripts or undeclared egress.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"plugins/sentry-mcp/.mcp.json","start_line":1,"end_line":6,"snippet":"{\n  \"sentry\": {\n    \"type\": \"http\",\n    \"url\": \"https://mcp.sentry.dev/mcp\"\n  }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The agent body is a workflow and tool-selection guide with no consent-suppression, disclosure-hiding or authority-override prose. It tells the agent to use the Sentry MCP tools instead of fetching Sentry URLs over raw HTTP, which delegates authentication to MCP and does not skip user approval. The bundle does not itself establish how the host gates MCP tool calls, so no consent gate is assumed as a repository control.","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":"plugins/sentry-mcp/agents/sentry-mcp.md","start_line":92,"end_line":93,"snippet":"1. Identify the user's intent and select the most appropriate tool by reading tool descriptions.\n2. Pass Sentry URLs unchanged to `issueUrl` or `url` parameters — NEVER try to fetch Sentry URLs via HTTP directly, always use the MCP tools which handle authentication."}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Agent instructions read no host credential stores or conversation context and direct no data to external sinks other than the declared Sentry MCP tools; no harvest step exists.","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":[],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Plugin ships a plugin.json, an agent markdown and a .mcp.json that declares a remote HTTP server URL (no command/args, no env assignments); no hooks, scripts, dotenv, endpoint-substituting env, or memory writes.","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":[],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"allowedTools is an explicit enumerated list of Sentry MCP tools with no wildcard, matching the agent's stated purpose of searching, triaging and managing Sentry resources. It includes create, update and delete tools (e.g. delete_alert_rule, update_project), but these operate on Sentry resources within the described management role rather than on the shell, filesystem or ambient host control. The Claude and Cursor configs in the bundle declare the same remote server. Host approval behavior is external to the bundle and is not relied upon. Hardening: consider omitting destructive tools from the default allow-list.","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":"plugins/sentry-mcp/agents/sentry-mcp.md","start_line":2,"end_line":12,"snippet":"name: sentry-mcp\ndescription: Sentry error tracking and performance monitoring agent. Use when the user asks about errors, exceptions, issues... Handles searching, analyzing, triaging, and managing Sentry resources including preprod snapshot inspection.\n  the user asks about errors, exceptions, issues, stack traces, performance,\n  traces, releases, snapshots, screenshots, visual regression, CI snapshot\n  failures, preprod checks, agent conversations, or provides a Sentry URL\n  (especially URLs containing sentry.io/preprod/snapshots/). Handles searching,\n  analyzing, triaging, and managing Sentry resources including preprod snapshot"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Codepoint scan of agent markdown found no concealment characters; no HTML comments, forged role tokens or host-directed injection.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"plugins/sentry-mcp/agents/sentry-mcp.md","start_line":88,"end_line":88,"snippet":"You are a Sentry expert. Investigate errors, analyze performance, and manage projects using the available MCP tools."}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The plugin's own agent instructions and manifest ship in the bundle. The launch entry is a remote HTTP URL (not an npx/uvx unpinned package fetch), and no self-updating or rewriting of the manifest exists. Tool definitions are served by the publisher's hosted MCP server, inherent to remote MCP and covered by ATR-M rules.","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 -- or the package its launch entry runs -- 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":"plugins/sentry-mcp/.mcp.json","start_line":1,"end_line":6,"snippet":"{\n  \"sentry\": {\n    \"type\": \"http\",\n    \"url\": \"https://mcp.sentry.dev/mcp\"\n  }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The repository origin is https://github.com/getsentry/toolkit, the genuine product owner, which establishes publisher authority independently of the manifest's self-asserted author. The Claude (sentry-mcp) and Cursor (sentry) plugin names use canonical spelling with no spoofing prefixes or replacement installers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":"plugins/sentry-mcp/.claude-plugin/plugin.json","start_line":1,"end_line":5,"snippet":"{\n  \"name\": \"sentry-mcp\",\n  \"description\": \"Sentry error tracking and performance monitoring via MCP\",\n  \"author\": { \"name\": \"Sentry\" }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":null,"tier":null,"capability_band":null,"score_confidence":null,"score_interval":null,"factors":[]}}},{"artifact_version":"2.0","component":{"component_type":"skill","component_name":"skills-plugins/plugins/sentry-mcp-experimental","component_path":"plugins/sentry-mcp-experimental","metadata":{"manifest":".claude-plugin/plugin.json"}},"scan_provenance":{"scanned_at":"2026-11-08T16:37:18.261Z","resolved_ref":"6c072bfecaf8e3acab4936f9b385169b09fc9213","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/getsentry/toolkit","languages":["TypeScript","JavaScript","PowerShell","Shell"],"transports_detected":["stdio","streamable_http"],"deployment_artifacts":["packages/mcp-cloudflare/wrangler.jsonc","packages/mcp-cloudflare/wrangler.canary.jsonc",".github/workflows/deploy.yml"],"provenance":{"publisher":"getsentry","publisher_verified":true,"signed_build":false,"dependency_cve_count":52,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-P0001","rule_version":"1.0.1","rule_name":"Capability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The plugin is a manifest, an agent markdown file and an MCP connection to the first-party Sentry endpoint. It bundles no scripts and has no undisclosed network, exec or credential behaviour. The agent's description matches the tools it is allowed.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"plugins/sentry-mcp-experimental/mcp.json","start_line":1,"end_line":8,"snippet":"{\n  \"mcpServers\": {\n    \"sentry\": {\n      \"type\": \"http\",\n      \"url\": \"https://mcp.sentry.dev/mcp?experimental=1\"\n    }\n  }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0002","rule_version":"1.0.1","rule_name":"Consent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted Instructions","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"The agent instructions are workflow guidance with no 'don't ask' or 'work silently' prose. The only imperative restricts the agent to the MCP tools rather than raw HTTP. Hardening note: allowedTools lists destructive tools such as delete_alert_rule; whether those auto-run depends on the host's permission model, and the prose does not suppress confirmation.","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":"plugins/sentry-mcp-experimental/agents/sentry-mcp.md","start_line":92,"end_line":92,"snippet":"2. Pass Sentry URLs unchanged to `issueUrl` or `url` parameters — NEVER try to fetch Sentry URLs via HTTP directly, always use the MCP tools which handle authentication."}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0003","rule_version":"1.2.1","rule_name":"Host-Credential and Conversation-Context Harvesting and Exfiltration","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The plugin contains no credential-store reads, env enumeration or conversation-context routing. Authentication is handled by the host's MCP connection.","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":[],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0004","rule_version":"1.2.1","rule_name":"Format-Native Out-of-Band Execution and Host Control-Plane Persistence","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The plugin ships only JSON manifests, an MCP remote URL and an agent markdown file. It has no hooks, scripts, exec-enabling env, or base-URL substitution. The remote MCP entry is a plain https URL to the first-party host.","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":[],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0005","rule_version":"1.0.1","rule_name":"Over-Declared / Over-Privileged Artifact Manifest Scope","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The agent's allowedTools is an explicit enumerated list of Sentry MCP tools (no '*' wildcard) and matches the documented purpose of 'searching, analyzing, triaging, and managing Sentry resources'. It includes mutating and delete tools, but these are intrinsic to the stated management function. There is no shell, filesystem or network tool class beyond the Sentry MCP.","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":"plugins/sentry-mcp-experimental/agents/sentry-mcp.md","start_line":10,"end_line":24,"snippet":"mcpServers:\n  - sentry\nallowedTools:\n  - add_issue_note\n  - add_team_to_project\n  - analyze_issue_with_seer\n  - create_alert_rule\n  ...\n  - delete_alert_rule"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0006","rule_version":"1.1.1","rule_name":"Injection and Concealed Directives in Skill/Plugin Manifest and Instruction Text","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"A codepoint scan found no zero-width, bidi or Tag characters in the plugin files. There are no forged role tokens or host-override directives. The agent body is task guidance.","vulnerability_details":null,"fix_guidance":null,"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":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"plugins/sentry-mcp-experimental/agents/sentry-mcp.md","start_line":88,"end_line":92,"snippet":"## Workflow\n\n1. Identify the user's intent and select the most appropriate tool by reading tool descriptions.\n2. Pass Sentry URLs unchanged to `issueUrl` or `url` parameters — NEVER try to fetch ..."}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0007","rule_version":"1.1.1","rule_name":"Skill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The agent instructions and the allowedTools list ship inside the bundle and are not live-fetched. The MCP entry is a documented first-party remote URL, not an unpinned npx/uvx launch package. The tools the remote server serves are the publisher's own backend and follow the host's normal MCP flow. This is a trust-in-publisher note, not a skill-instruction fetch.","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 -- or the package its launch entry runs -- 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":"plugins/sentry-mcp-experimental/.mcp.json","start_line":1,"end_line":6,"snippet":"{\n  \"sentry\": {\n    \"type\": \"http\",\n    \"url\": \"https://mcp.sentry.dev/mcp?experimental=1\"\n  }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"},{"rule_id":"ATR-P0008","rule_version":"1.0.1","rule_name":"Skill/Plugin Impersonation, Typosquatting & Publisher Spoofing","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The plugin is published from the getsentry org repo, with author 'Sentry' in the manifest, and no impersonation prose. The brand owner is the publisher.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Impersonation is the entry point of the agentic supply chain (OWASP Top 10 for Agentic Applications 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":"plugins/sentry-mcp-experimental/.claude-plugin/plugin.json","start_line":1,"end_line":5,"snippet":"{\n  \"name\": \"sentry-mcp-experimental\",\n  \"description\": \"Sentry MCP with experimental features enabled\",\n  \"author\": { \"name\": \"Sentry\" }\n}"}],"affected_tools":null,"component_name":"skills-plugins/plugins/sentry-mcp-experimental"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.1","rule_name":"Unrestricted Network Fetch / SSRF","finding":"UNKNOWN","severity":"HIGH","realized_severity":null,"confidence":"low","rationale":"SentryApiService.fetchImageByUrl does a bare fetch() of any https:// URL taken from Sentry snapshot API data (image_url / diff_image_url), with no host allowlist, private-IP filter, timeout or size cap. It is https-only and forwards no credentials. Content sniffing can accept image bytes even when the response lacks an image/* header. The hosted Wrangler config sets global_fetch_strictly_public, a platform mitigation for the Worker, but the stdio runtime has no such guarantee. The URL source is the upstream snapshot response, not the exposed imageIdentifier argument, and the repository does not establish that an attacker can write those URL fields, so a full source-to-sink flow is not proven. All other outbound calls go to a configured Sentry host, OAuth endpoints or fixed LLM providers.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5124,"snippet":"async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request("}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.3","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Secrets are read from env vars or runtime config. The committed .env.example files hold only empty or placeholder values. The OAuth client ID in oauth.ts is a documented public device-flow ID. The semgrep hit at sentryclirc.ts:367 is a log.debug that records only the config file name and source of the token, never the token value. The token is copied from the user's own .sentryclirc into the process env at runtime. Gitleaks hits are tests, fixtures and docs.","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":"packages/cli/src/lib/sentryclirc.ts","start_line":359,"end_line":372},{"path":"packages/cli/src/lib/oauth.ts","start_line":55,"end_line":64},{"path":"packages/cli/src/lib/oauth.ts","start_line":64,"end_line":65,"vulnerability":{}},{"path":"packages/cli/src/lib/sentryclirc.ts","start_line":367,"end_line":369,"vulnerability":{"severity":"WARNING"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.5.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"medium","rationale":"Process spawns in the CLI use argv-style spawn/execFileSync with fixed executables. The semgrep spawn-shell-true hits in packages/cli/src/lib/upgrade.ts use shell only on win32, with a fixed command (brew) or a semver-validated version (semverValid(version) === version). monitor run / local run execute the command the operator typed on their own CLI, which is the declared purpose and not an attacker-controlled path. The MCP server (mcp-core, Cloudflare Worker) has no child_process use. No eval or Function on external input was found. CI expressions are out of scope for this rule.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/cli/src/lib/upgrade.ts","start_line":299,"end_line":303,"snippet":"const proc = spawn(command, args, {\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      shell: process.platform === \"win32\",\n    });"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.1","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"LOW","confidence":"medium","rationale":"Rate limiting and inbound request caps do not bound outbound snapshot image downloads. get_snapshot_image calls fetchImageByUrl, which buffers the full response body into a blob and converts it to base64 with no response-size ceiling and no body-read timeout. fetchWithTimeout clears its abort timer once headers return, not after the body is consumed. A caller able to inspect snapshots can request full-resolution assets and force large buffering. This resource-consumption gap does not depend on proving who controls the image URL's host.","vulnerability_details":null,"fix_guidance":"In fetchImageByUrl (packages/mcp-core/src/api-client/client.ts), enforce a maximum response size by checking Content-Length and streaming the body with a byte cap. Keep the abort timer active until the body is fully read, and only then convert to base64.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity LOW (from MEDIUM: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":59,"end_line":65,"snippet":"      .enum([\"preview\", \"full\"])\n      .describe(\n        \"Return locally generated previews or original full-resolution bytes. Full-resolution images can substantially increase response size and token usage.\",\n      )\n      .default(\"preview\"),\n    regionUrl: ParamRegionUrl.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/get-snapshot-image.ts","start_line":87,"end_line":93,"snippet":"      params.snapshotId,\n      params.imageIdentifier,\n      params.imageResolution,\n      {\n        experimentalMode: context.experimentalMode ?? false,\n        availableToolNames: context.availableToolNames,\n        directToolNames: context.directToolNames,"},{"path":"packages/mcp-core/src/api-client/client.ts","start_line":5113,"end_line":5130,"snippet":"  async fetchImageByUrl(\n    imageUrl: string,\n  ): Promise<{ blob: Blob; contentType: string }> {\n    const response = imageUrl.startsWith(\"https://\")\n      ? await fetch(imageUrl)\n      : await this.request(\n          imageUrl.startsWith(\"/api/0\")\n            ? imageUrl.slice(\"/api/0\".length)\n            : imageUrl,\n        );\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch image: ${response.status} ${response.statusText}`,\n      );\n    }\n    const blob = await response.blob();\n    let contentType = response.headers.get(\"content-type\") ?? \"\";\n    if (!contentType.startsWith(\"image/\")) {"},{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":124,"end_line":132,"snippet":"    parts.push({\n      type: \"image\",\n      data: await blobToBase64(blob),\n      mimeType: contentType,\n    });\n    return;\n  }\n\n  const preview = await createPreviewOrNull(blob, contentType);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.1","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The core telemetry scrubber redacts Bearer and Sentry tokens (telem/sentry.ts), but it is not installed in the docs-mcp Worker. docs-mcp MCP instrumentation records complete inputs and outputs, including free-form documentation questions and project stack summaries, and its Sentry initialization enables default PII with no scrubbing hook. Sensitive context or tokens supplied in those strings can be persisted in telemetry. Intentional auth-token stdout commands are not the basis of this finding.","vulnerability_details":null,"fix_guidance":"In apps/docs-mcp, disable recording of full MCP inputs and outputs, or apply the redaction scrubber (for example a beforeSend hook) to telemetry before it is transmitted, and review the default-PII setting in apps/docs-mcp/src/index.ts.","why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"apps/docs-mcp/src/mcp.ts","start_line":152,"end_line":166,"snippet":"  const mcp = Sentry.wrapMcpServerWithSentry(\n    new McpServer(\n      {\n        description:\n          \"Generates tailored Sentry integration docs by reading docs.sentry.io.\",\n        name: \"sentry-docs\",\n        version: \"1.0.0\",\n      },\n      { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS },\n    ),\n    {\n      recordInputs: true,\n      recordOutputs: true,\n    },\n  );"},{"path":"apps/docs-mcp/src/mcp.ts","start_line":259,"end_line":266,"snippet":"      stackSummary: z\n        .string()\n        .optional()\n        .describe(\n          \"Natural-language description of the project's tech stack for this context. Used to discover relevant integration docs beyond the base platform guide.\",\n        ),\n    },\n    name: \"get-docs\","},{"path":"apps/docs-mcp/src/index.ts","start_line":116,"end_line":123,"snippet":"export default Sentry.withSentry(\n  (_env: Env) => ({\n    dsn: \"https://05c618aef6e0489b8c9d07e0d65664ce@o1.ingest.us.sentry.io/4511189954199552\",\n    sendDefaultPii: true,\n    tracesSampleRate: 1.0,\n  }),\n  app,\n);"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.3.1","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"The repo ships no Dockerfile, compose file or Kubernetes manifest. Deployment is to Cloudflare Workers (wrangler.jsonc) and as npm/standalone binaries.","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":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.3.1","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"medium","rationale":"A lockfile exists and installs are frozen, but 24 GitHub Actions steps use mutable tags (semgrep github-actions-mutable-action-tag). These include third-party dawidd6/action-download-artifact@v6 and the checkout, setup-node and github-script actions in eval.yml, which runs on main pushes and dispatch with checks:write. A moved upstream tag would change executable CI code with repository authority, and lockfile integrity hashes do not pin those actions. pnpm-workspace.yaml lists minimumReleaseAgeExclude entries but sets no minimumReleaseAge, trustPolicy or blockExoticSubdependencies, so no release-age or trust policy is in effect. Other workflows pin SHAs, so pinning is inconsistent. Trivy also matches older pinned dependencies against known advisories.","vulnerability_details":"A moved tag on an upstream action runs different code in CI jobs that hold repository tokens.","fix_guidance":"Pin every action to a full commit SHA, as deploy.yml and pr-risk-jev.yml already do.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[confirmed_exploitable|relief:none|ineligible:within_declared_contract] [readers_disagree:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. Located evidence satisfies the rule's required-evidence contract. The scan reported mitigation_present but the independent adjudication did not corroborate it; relief (-40%) requires both readers, so none is applied for that signal. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/eval.yml","start_line":1,"end_line":12,"snippet":"name: Eval\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n    paths:\n      - \"packages/mcp-core/src/tools/**\"\n      - \"packages/mcp-server-evals/**\"\n      - \"packages/mcp-server-mocks/**\"\n      - \".github/workflows/eval.yml\"\n  pull_request:"},{"path":".github/workflows/eval.yml","start_line":27,"end_line":30,"snippet":"- uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4"},{"path":".github/workflows/token-cost.yml","start_line":129,"end_line":131,"snippet":"- name: Download main branch token stats\n        if: github.event_name == 'pull_request'\n        uses: dawidd6/action-download-artifact@v6"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104846","pkg_name":"seroval","severity":"CRITICAL","installed_version":"1.5.6","fixed_version":"1.6.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111768","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111770","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-f897-62hf-6gxq","pkg_name":"@modelcontextprotocol/server","severity":"HIGH","installed_version":"2.0.0","fixed_version":"2.1.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102276","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.10, 3.0.7, 2.1.5, 1.1.19"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102278","pkg_name":"brace-expansion","severity":"HIGH","installed_version":"5.0.9","fixed_version":"5.0.11, 3.0.8, 2.1.6, 1.1.20"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73088","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-73089","pkg_name":"browserslist","severity":"HIGH","installed_version":"4.25.1","fixed_version":"4.28.7"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-35209","pkg_name":"defu","severity":"HIGH","installed_version":"6.1.4","fixed_version":"6.1.5"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-92708","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-mcm9-63f2-9j32","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r9w8-h9r3-54w4","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-x5rw-q4pp-hg5g","pkg_name":"devalue","severity":"HIGH","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48801","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-59887","pkg_name":"linkify-it","severity":"HIGH","installed_version":"5.0.0","fixed_version":"5.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4800","pkg_name":"lodash","severity":"HIGH","installed_version":"4.17.21","fixed_version":"4.18.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111758","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"14.3.3, 15.0.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111759","pkg_name":"markdown-it","severity":"HIGH","installed_version":"14.1.0","fixed_version":"15.0.2, 14.3.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-4926","pkg_name":"path-to-regexp","severity":"HIGH","installed_version":"8.3.0","fixed_version":"8.4.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-qwww-vcr4-c8h2","pkg_name":"react-router","severity":"HIGH","installed_version":"7.18.1","fixed_version":"7.18.2, 8.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-104845","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116580","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116581","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-116584","pkg_name":"seroval","severity":"HIGH","installed_version":"1.5.6","fixed_version":"1.6.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-93749","pkg_name":"source-map-js","severity":"HIGH","installed_version":"1.2.1","fixed_version":"1.2.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-44705","pkg_name":"tmp","severity":"HIGH","installed_version":"0.2.3","fixed_version":"0.2.6"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-27601","pkg_name":"underscore","severity":"HIGH","installed_version":"1.13.7","fixed_version":"1.13.8"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-111771","pkg_name":"@modelcontextprotocol/server","severity":"MEDIUM","installed_version":"2.0.0","fixed_version":"2.3.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-102277","pkg_name":"brace-expansion","severity":"MEDIUM","installed_version":"5.0.9","fixed_version":"5.0.12, 3.0.9, 2.1.7, 1.1.21"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-4q55-j62x-fr9h","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-hx4r-w6wj-j8fg","pkg_name":"devalue","severity":"MEDIUM","installed_version":"5.9.2","fixed_version":"5.9.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110180","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-110306","pkg_name":"fast-uri","severity":"MEDIUM","installed_version":"3.1.8","fixed_version":"2.4.8, 3.1.9, 4.2.3"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-118773","pkg_name":"glob","severity":"MEDIUM","installed_version":"13.0.6","fixed_version":"14.0.2"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2025-13465","pkg_name":"lodash","severity":"MEDIUM","installed_version":"4.17.21","fixed_version":"4.17.23"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-2327","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.1.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-48988","pkg_name":"markdown-it","severity":"MEDIUM","installed_version":"14.1.0","fixed_version":"14.2.0"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.3.0","rule_name":"Missing or Inadequate Input Validation","finding":"VULNERABLE","severity":"HIGH","realized_severity":"MEDIUM","confidence":"medium","rationale":"MCP tools declare typed zod input schemas, but the shared ParamSearchQuery and ParamCursor validators are type-only (query adds trim) with no length or pattern bound. find_projects exposes both directly and forwards them into listProjects API calls. Slug validators are stronger, but they do not cover these fields. A transport-wide body ceiling exists but is not a semantic limit on query or cursor values.","vulnerability_details":null,"fix_guidance":"In packages/mcp-core/src/schema.ts, add explicit maximum lengths (and a character pattern for cursors) to ParamSearchQuery and ParamCursor so find_projects rejects oversized or malformed values before calling listProjects.","why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":"[defense_in_depth|relief:-40%|ineligible:within_declared_contract] [judge_only:mitigation_present] within_declared_contract is ineligible for relief on ATR-C0008 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. A real mitigating control is present for this finding (mitigation_present, -40%), as assessed by the independent adjudication alone: the adjudication promoted this finding to VULNERABLE, so the scan never assessed it as VULNERABLE and has no answer to compare. Realized severity MEDIUM (from HIGH: 40% context relief -- mitigation_present -40%).","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"packages/mcp-core/src/schema.ts","start_line":56,"end_line":68,"snippet":"export const ParamSearchQuery = z\n  .string()\n  .trim()\n  .describe(\n    \"Search query to filter results by name or slug. Use this to narrow down results when there are many items.\",\n  );\n\nexport const ParamCursor = z\n  .string()\n  .describe(\n    \"Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.\",\n  );\n"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":43,"end_line":48,"snippet":"  inputSchema: {\n    organizationSlug: ParamOrganizationSlug,\n    regionUrl: ParamRegionUrl.nullable().default(null),\n    query: ParamSearchQuery.nullable().default(null),\n    cursor: ParamCursor.nullable().default(null),\n  },"},{"path":"packages/mcp-core/src/tools/catalog/find-projects.ts","start_line":69,"end_line":76,"snippet":"    const { projects, nextCursor } = await apiService.listProjects(\n      organizationSlug,\n      {\n        query: params.query ?? undefined,\n        limit: RESULT_LIMIT,\n        cursor: params.cursor ?? undefined,\n      },\n    );"}],"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":"medium","rationale":"Only JSON.parse of plain data is used (for example a base64 pagination cursor), with no code-reviving deserializers.","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":"packages/mcp-core/src/tools/catalog/find-organizations.ts","start_line":75,"end_line":75,"snippet":"    parsed = JSON.parse(atob(cursor.slice(ACCOUNTS_CURSOR_PREFIX.length)));"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.3.1","rule_name":"Lack of Output Encoding/Sanitization","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The server-rendered OAuth pages escape values through an escapeHtml helper, but that does not cover the CLI terminal surface. Sentry issue titles pass through markdown text escaping, which does not remove ESC or other control bytes. Interactive markdown output strips ANSI only in plain mode and writes the styled result directly to stdout. Crafted upstream issue text can therefore inject terminal control sequences that clear or rewrite the operator's display. This is terminal injection, not MCP JSON or OAuth XSS.","vulnerability_details":null,"fix_guidance":"Strip ESC and other disallowed terminal control bytes from untrusted values (issue titles and similar upstream text) before applying trusted ANSI styling in the CLI formatters, including the interactive rendering path.","why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":"[relief_ineligible|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), so whether the behaviour is within the tool's declared capability 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":"packages/cli/src/commands/issue/view.ts","start_line":219,"end_line":224,"snippet":"  const prefetched = await prefetchIssueDetails(issueArg, spans);\n  const { org: orgSlug, issue } = await resolveIssue({\n    issueArg,\n    cwd,\n    command: \"view\",\n  });"},{"path":"packages/cli/src/lib/formatters/human.ts","start_line":806,"end_line":810,"snippet":"export function formatIssueDetails(issue: SentryIssue): string {\n  const lines: string[] = [];\n\n  lines.push(`## ${issue.shortId}: ${text(issue.title ?? \"\")}`);\n  lines.push(\"\");"},{"path":"packages/toolkit-core/src/markdown.ts","start_line":99,"end_line":108,"snippet":"/** Puts a value on one line and backslash-escapes `syntax` and line-start syntax. */\nfunction escapeSyntax(value: string, syntax: RegExp): string {\n  return toSingleLine(value)\n    .trim()\n    .replace(syntax, (char) => `\\\\${char}`)\n    .replace(\n      LINE_START_SYNTAX,\n      (match) => `${match.slice(0, -1)}\\\\${match.slice(-1)}`,\n    );\n}"},{"path":"packages/cli/src/lib/formatters/markdown.ts","start_line":529,"end_line":539,"snippet":"export function renderMarkdown(md: string): string {\n  if (isPlainOutput()) {\n    // Parse and render to get structural formatting (headings, tables, lists),\n    // then strip ANSI codes. This produces human-readable output with aligned\n    // tables and proper heading emphasis, without ANSI escape sequences.\n    const tokens = marked.lexer(md ?? \"\");\n    return stripAnsi(renderBlocks(tokens)).trimEnd();\n  }\n  const tokens = marked.lexer(md ?? \"\");\n  return renderBlocks(tokens).trimEnd();\n}"},{"path":"packages/cli/src/lib/formatters/output.ts","start_line":442,"end_line":446,"snippet":"  const text = renderer.render(data);\n  if (text) {\n    const prefix = ctx.clearPrefix ?? \"\";\n    stdout.write(`${prefix}${text}\\n`);\n  }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.6.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The MCP server exposes only Sentry API tools and no filesystem tools. CLI path arguments are the local operator's own, in a local CLI context, with no caller-supplied path served over a remote surface.","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":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.1","rule_name":"Denial of Wallet / Cost Controls","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The hosted service has Cloudflare rate-limit bindings, and the embedded LLM agent calls use provider fallback and capped retries with backoff (callEmbeddedAgent.ts, fetch-utils.ts). The risk of unbounded billable calls is bounded.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents cost-based disruption.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/utils/rate-limiter.ts","start_line":1,"end_line":10,"snippet":"import type { RateLimit } from \"@cloudflare/workers-types\";\nexport const MCP_RATE_LIMIT_EXCEEDED_MESSAGE =\n  \"Rate limit exceeded. Please wait before trying again.\";"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.1","rule_name":"Tool Output Sanitization & Injection Containment","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"low","rationale":"MCP tools return third-party-authored Sentry data (snapshot metadata, issue and event text) interpolated into markdown with no untrusted envelope or control/role-marker stripping. A grep found no wrapper or sanitizer in mcp-core.","vulnerability_details":"formatImageMetadata embeds uploader-controlled display_name and description directly in tool text returned to the model.","fix_guidance":"Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","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":"[relief_ineligible|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0013 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals), so whether the behaviour is within the tool's declared capability does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":true,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"packages/mcp-core/src/tools/support/snapshots/handlers.ts","start_line":82,"end_line":93,"snippet":"function formatImageMetadata(img: SnapshotImageInfo): string[] {\n  const lines: string[] = [];\n  if (img.display_name) lines.push(`- **Display Name**: ${img.display_name}`);\n  if (img.group) lines.push(`- **Group**: ${img.group}`);\n  if (img.image_file_name) lines.push(`- **File**: \\`${img.image_file_name}\\``);\n  if (img.description) lines.push(`- **Description**: ${img.description}`);"}],"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":"medium","rationale":"Untrusted values reach scripts through env indirection (PR_NUMBER, SOURCE_RUN_ID, EXPECTED_SHA, PRODUCT). pr-risk-jev.yml runs under pull_request_target with pinned trusted action code and PR_NUMBER passed in env, and it never checks out or executes PR code. The workflow_run deploys require a successful same-repository push on main, compare the tested SHA to the current main head (an equality check, not an ancestry check) before checkout, pass the SHA as quoted env data, and use persist-credentials: false. The expression interpolations in token-cost.yml derive from the measurement job's numeric metrics and constant comparison symbols, not attacker-controlled event text, so they confer no privilege gain. cli-build.yml's run line interpolates a value from a literal matrix allowlist. No complete attacker-to-script chain was established.","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/deploy.yml","start_line":56,"end_line":67,"snippet":"- uses: actions/checkout@11d5960... # v4.4.0\n        with:\n          ref: ${{ github.event.workflow_run.head_sha }}\n          persist-credentials: false\n      - name: Require tested revision on main\n        env:\n          EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0015","rule_version":"1.1.0","rule_name":"Archive Extraction Path Escape (zip-slip / tar-slip)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Runtime ZIP handling exists: DIF unzipSync in lib/dif/zip.ts returns in-memory buffers with synthetic diagnostic paths, and the build ZIP listing in lib/build/index.ts never decompresses members. Neither writes to a destination derived from a member name, so there is no zip-slip filesystem sink. The installers download release binaries rather than extracting externally supplied archives.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A crafted archive can name members such as ../../.ssh/authorized_keys. Code that writes members by name without checking the destination writes attacker-chosen files anywhere the process can, including startup and credential locations.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0016","rule_version":"1.1.0","rule_name":"Authentication Bypass via Caller-Controlled Trust Flag","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"low","rationale":"Remote auth goes through the Cloudflare OAuth provider, or a Sentry-Bearer token that is passed upstream so Sentry itself verifies it. No request boolean grants a session or scope.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"A caller-toggleable \"trusted path\" grants unauthenticated or privileged access while a real credential check sits visibly in the code, so scanners that look for a missing check pass it and human reviewers see the check and stop reading. The result is full authentication bypass from an anonymous request.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"packages/mcp-cloudflare/src/server/lib/mcp-handler.ts","start_line":5,"end_line":12,"snippet":" * - OAuth requests extract auth props from ExecutionContext (set by OAuth provider)\n * - Sentry-Bearer requests pass an explicit upstream token directly\n * Direct Sentry-Bearer mode intentionally does not validate, store, refresh,\n * or revoke the upstream token. It only supplies that token to ServerContext."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.1","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"The product is the Sentry monitoring CLI and MCP server. The YARA hits were reviewed and are benign: the PowerShell installer validates versions, and its GitHub-download fallback explicitly warns that the download is not checksum-verified, which is a documented limitation and not malicious purpose. The project delete docs describe a documented --yes confirmation bypass flag for explicit administration, not covert attack tooling.","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":"packages/cli/src/commands/project/delete.ts","start_line":144,"end_line":148,"snippet":"\"Permanently delete a Sentry project. This action cannot be undone.\\n\\n\" +\n      \"Requires explicit target — auto-detection is disabled for safety.\",\n    examples: [\n      {\n        description: \"Delete a project (prompts for confirmation)\",\n        command: \"sentry project delete my-org/old-project\",\n      },\n      {\n        description: \"Delete without confirmation\",\n        command: \"sentry project delete my-org/old-project --yes\",","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[],"summary":{"high_level_risks":["docs-mcp (apps/docs-mcp/src/index.ts, access.ts, mcp.ts) exposes a remote HTTP MCP transport and /internal/docs with no real authentication: no OAuth 2.1/PKCE flow, only a hostname-based service-binding gate that trusts the platform to preserve the caller's internal URL rather than a verified credential, including on the localhost dev path.","docs-mcp has no Origin validation and uses a default permissive cors() configuration on /mcp and /internal/docs, leaving it open to cross-origin and DNS-rebinding style access.","Tool output returns third-party-authored content to the model without an untrusted envelope or sanitization; e.g. formatImageMetadata in packages/mcp-core/src/tools/support/snapshots/handlers.ts embeds uploader-controlled display_name and description directly in tool text (prompt injection).","Supply chain: GitHub workflows (.github/workflows/eval.yml, token-cost.yml) use mutable action tags rather than pinned commit SHAs; a moved upstream tag would run different code in CI jobs holding repository tokens.","Missing input validation: ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts have no maximum lengths or cursor character pattern, so find_projects passes oversized or malformed values to listProjects.","DoS exposure in image fetching: fetchImageByUrl (packages/mcp-core/src/api-client/client.ts) has no response size cap and the abort timer does not cover the full body read, affecting get-snapshot-image and snapshot handlers.","Logging hygiene: docs-mcp (apps/docs-mcp/src/mcp.ts, index.ts) records full MCP inputs/outputs and default PII in telemetry without redaction before transmission.","Terminal output injection: untrusted values such as issue titles are not stripped of ESC/control bytes before ANSI styling in CLI formatters (packages/cli/src/commands/issue/view.ts, lib/formatters/human.ts, packages/toolkit-core/src/markdown.ts)."],"top_remediations":["Require a verified credential (shared secret header or signed service-to-service token) in the docs-mcp access check for /mcp and /internal/docs, including the localhost path, instead of hostname alone; front with OAuth 2.1 (resource metadata, PKCE) if exposed beyond the service binding.","Add explicit Origin allowlist validation to docs-mcp /mcp and /internal/docs and replace default cors() with one restricted to trusted origins.","Wrap third-party content in a delimited untrusted block, strip control and role-marker sequences, and flag content tools as untrusted-output.","Pin every GitHub Action to a full commit SHA (as deploy.yml and pr-risk-jev.yml already do), including eval.yml and token-cost.yml.","Add max lengths and a cursor character pattern to ParamSearchQuery and ParamCursor in packages/mcp-core/src/schema.ts so find_projects rejects bad values early.","In fetchImageByUrl, enforce a max response size via Content-Length check and byte-capped streaming, and keep the abort timer active until the body is fully read before base64 conversion.","In apps/docs-mcp, disable recording of full MCP inputs/outputs or apply a redaction scrubber (e.g. beforeSend) before telemetry is sent, and review the default-PII setting in index.ts.","Strip ESC and other terminal control bytes from untrusted upstream text before applying trusted ANSI styling in CLI formatters, including the interactive rendering path."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":6,"not_applicable":2},"repo_findings_inherited_count":{"vulnerable":6,"secure":7,"not_applicable":3},"component_tools_count":{},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":27,"tier":"critical","capability_band":"extreme","score_confidence":"low","score_interval":{"low":1,"high":53},"factors":["ATR-M0003 HIGH","ATR-M0010 CRITICAL","ATR-C0005 MEDIUM","ATR-C0007 HIGH","ATR-C0010 MEDIUM","ATR-C0013 HIGH"]},"component_level":{"score":null,"tier":null,"capability_band":null,"score_confidence":null,"score_interval":null,"factors":[]}}}]}