Repository navigation
fix(mcp): stop silently returning unfiltered findings - #8
Open
scrut-nick wants to merge 2 commits into
Open
scrut-nick wants to merge 2 commits into
scrut-nick wants to merge 2 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The MCP finding tools forwarded
repository,severityandstatusto the API as raw query parameters. No endpoint accepts a repository name, andseverity/statusare 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 fromscrut-organizationService,scrut-trust-center,scrut-datasync-gcpand 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/apiforwards:repositoryseveritystatusFix
Filters are translated rather than forwarded, in a new
internal/mcp/filters.goused bymakeGetHandler,nullify_search_findingsandget_findings_for_repo.repositoryresolves 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./sca/repositoriesand/sast/repositories, which are keyed by that numeric id./context/repositoriesis deliberately not used: itsidis a Nullify ULID, and passing one is rejected withinvalid integer value '01HK…' type 'int64'. Non-numeric keys are skipped for the same reason.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
limitat 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 ./...andgolangci-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.severityon/sca/dependencies/findingsis reported as unapplied rather than silently dropped.Notes
The same defect exists upstream in
Nullify-Platform/cli—tools_composite.goandtools_unified.goare 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/findingsand/sca/dependencies/findingsboth becomelist-findingsunder the samescaparent and one shadows the other. That makes/sca/dependencies/findingsunreachable 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