Skip to content

fix(mcp): stop silently returning unfiltered findings - #8

Open
scrut-nick wants to merge 2 commits into
mainfrom
claude/axios-litellm-validation-0thkbr
Open

scrut-nick wants to merge 2 commits into
mainfrom
claude/axios-litellm-validation-0thkbr

Conversation

@scrut-nick

Copy link
Copy Markdown
Owner

Problem

The MCP finding tools forwarded repository, severity and status to the API as raw query parameters. No endpoint accepts a repository name, and severity/status are only honoured by some endpoints. The API ignores parameters it does not recognise rather than rejecting them, so each of those filters came back as a full, unfiltered page that reads as a filtered one.

Concretely, get_findings_for_repo("scrut-helpdesk-bot") returned findings from scrut-organizationService, scrut-trust-center, scrut-datasync-gcp and others — a global page presented under a per-repository tool name. There is no error and nothing in the response distinguishes it from a real result.

Filter support as it actually stands, read off the query parameters each generated client method in internal/api forwards:

filter endpoints honouring it
repository 0 of 7
severity 4 of 7 (not sca_dependencies, not secrets)
status 2 of 7

Fix

Filters are translated rather than forwarded, in a new internal/mcp/filters.go used by makeGetHandler, nullify_search_findings and get_findings_for_repo.

  • repository resolves to the numeric repository id the endpoints filter on — githubRepositoryId, or the Azure/Bitbucket equivalent selected from the configured VCS owner. GitLab has no repository-id parameter, so it returns a clear error instead.
  • The id comes from /sca/repositories and /sast/repositories, which are keyed by that numeric id. /context/repositories is deliberately not used: its id is a Nullify ULID, and passing one is rejected with invalid integer value '01HK…' type 'int64'. Non-numeric keys are skipped for the same reason.
  • Filters an endpoint cannot apply are never sent. Single-endpoint tools fail with a message naming the filter and endpoint; the fan-out tools report them per finding type as unapplied_filters, so a caller can tell which results are narrowed and which are not.

Resolved names are cached per API host. Those listings cap limit at 10, so a cache miss costs one request per ten repositories; every name seen on the way is cached, so the cost is paid at most once per process for any repository appearing before the target.

Verification

go build ./..., go test ./... and golangci-lint run ./... (0 issues) all pass.

Unit tests cover provider selection, paging until a match, the cache suppressing repeat requests, fall-through from the SCA listing to the SAST one, ULID keys being skipped, per-endpoint severity/status support, and a guard that every finding type has a support entry.

Checked against a live instance:

  • scrut-trust-center → githubRepositoryId=480701645; the filtered request returns 25/25 findings from that repository alone.
  • scrut-datasync-kolide → 1003604517. This one is ULID-keyed in /context/repositories, so the obvious implementation would have returned HTTP 400 here.
  • severity on /sca/dependencies/findings is reported as unapplied rather than silently dropped.

Notes

The same defect exists upstream in Nullify-Platform/cli — tools_composite.go and tools_unified.go are unmodified from there, so this is worth sending upstream too.

Unrelated and not addressed here: scripts/generate/main.go:generateCobraUse() names commands from HTTP method plus the last non-parameter path segment, so /sca/containers/findings and /sca/dependencies/findings both become list-findings under the same sca parent and one shadows the other. That makes /sca/dependencies/findings unreachable from the CLI. Fixing it means regenerating from the OpenAPI spec, which is not in this repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R27reGQuYtybuiX9RScNUG


Generated by Claude Code

claude added 2 commits August 21, 2026 00:34
The MCP finding tools forwarded `repository`, `severity` and `status` to the
API as raw query parameters. No endpoint accepts a repository *name*, and
`severity`/`status` are only honoured by some: the API ignores parameters it
does not recognise rather than rejecting them, so every one of those filters
came back as a full, unfiltered page that read as a filtered one.
`get_findings_for_repo("scrut-helpdesk-bot")` returned findings from
scrut-organizationService, scrut-trust-center and others.

Translate filters instead of forwarding them:

- `repository` resolves to the numeric repository id the endpoints filter on
  (`githubRepositoryId` and the Azure/Bitbucket equivalents). The id comes from
  the /sca/repositories and /sast/repositories listings, which are keyed by it.
  /context/repositories is not usable here: its id is a Nullify ULID, and the
  API rejects one as `invalid integer value ... type 'int64'`. Non-numeric keys
  are skipped for the same reason. Resolved names are cached per host, since
  those listings cap `limit` at 10 and a miss costs a request per ten repos.
- Filters an endpoint cannot apply are never sent. Single-endpoint tools fail
  with a message naming the filter; the fan-out tools report them per type as
  `unapplied_filters`, so a caller can tell which results are narrowed.

Verified against a live instance: scrut-trust-center resolves to 480701645 and
scrut-datasync-kolide to 1003604517 (the latter is ULID-keyed in
/context/repositories and would previously have 400'd), and severity on
/sca/dependencies/findings is reported unapplied rather than dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R27reGQuYtybuiX9RScNUG
SubImage models NACLs and route tables but its collection is incomplete
(subnet-to-NACL edges resolve for 2 of 7 subnets we checked, and NACL rule
nodes carry no port range), and it models no WAF at all. An AWS-backed server
gives us ground truth for those questions.

Wires https://aws-mcp.us-east-1.api.aws/mcp through mcp-proxy-for-aws, behind
the existing with-op-env.sh secret plumbing.

Two things this had to design around:

- Cloud-session sandboxes already export AWS_ACCESS_KEY_ID and
  AWS_SECRET_ACCESS_KEY for their own object storage, and with-op-env.sh
  leaves already-set vars untouched by design. Credentials named AWS_* would
  never have been fetched from 1Password, and the server would have signed
  every call as the sandbox - querying the wrong account and reporting the
  answer as ours. They travel as SCRUT_AWS_* and are mapped onto AWS_* after
  the lookup. The launcher exits non-zero rather than fall back to the
  ambient credentials.

- --read-only is kept on, which means the server currently answers
  documentation questions only. Measured against the live endpoint it exposes
  6 of 9 tools; the two that reach the AWS API (aws___call_aws,
  aws___run_script) are annotated destructiveHint=true and are dropped, so it
  cannot yet describe a NACL. Removing the flag is a deliberate follow-up that
  needs a strictly read-only IAM principal behind SCRUT_AWS_*, since
  aws___run_script runs arbitrary boto3.

Inert until SCRUT_AWS_ACCESS_KEY_ID / SCRUT_AWS_SECRET_ACCESS_KEY exist in the
1Password item; the server fails to start and the others are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R27reGQuYtybuiX9RScNUG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants