From f79cfc066d0311155fc1334f128e2e7cc381aab9 Mon Sep 17 00:00:00 2001 From: Jonathan Lam Date: Tue, 19 May 2026 13:44:29 +1000 Subject: [PATCH] fix(findings): fix severity casing, type mapping, and pagination for unified endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three additional issues discovered via live testing against the API: 1. Severity values must be uppercase (CRITICAL, HIGH, MEDIUM, LOW) — the unified endpoint rejects lowercase values silently, returning 0 results. 2. The unified endpoint uses PascalCase finding type names (Code, Dependencies, Containers, Secrets, Pentest, BugHunting, Cloud) rather than the CLI's internal slugs (sast, sca_dependencies, cspm, etc.). Added findingTypeToAPI mapping in both CLI and MCP. 3. Pagination uses page numbers, not scroll IDs — the API returns hasMoreData=true but scrollId=null. The loop now falls back to incrementing the page field when scrollId is absent, and correctly fetches all pages up to the requested limit. --- cmd/cli/cmd/findings.go | 22 +++++++++++++++++----- cmd/cli/cmd/scanners.go | 12 ++++++++++++ internal/mcp/tools_unified.go | 31 +++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/cmd/cli/cmd/findings.go b/cmd/cli/cmd/findings.go index 414b11d..11268bc 100644 --- a/cmd/cli/cmd/findings.go +++ b/cmd/cli/cmd/findings.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/nullify-platform/cli/internal/auth" "github.com/nullify-platform/cli/internal/client" @@ -77,10 +78,12 @@ Results are paginated automatically up to --limit total findings.`, Total int `json:"total"` HasMoreData bool `json:"hasMoreData"` ScrollID *string `json:"scrollId"` + Page int `json:"page"` } allFindings := make([]json.RawMessage, 0) var scrollID string + nextPage := 1 var lastTotal int for { @@ -95,15 +98,20 @@ Results are paginated automatically up to --limit total findings.`, query := map[string]any{ "pageSize": pageSize, + "page": nextPage, } if repo != "" { query["repository"] = []string{repo} } if severity != "" { - query["severity"] = []string{severity} + query["severity"] = []string{strings.ToUpper(severity)} } if findingType != "" { - query["type"] = []string{findingType} + if apiType, ok := findingTypeToAPI[findingType]; ok { + query["type"] = []string{apiType} + } else { + query["type"] = []string{findingType} + } } if scrollID != "" { query["scrollId"] = scrollID @@ -161,13 +169,17 @@ Results are paginated automatically up to --limit total findings.`, lastTotal = resp.Total if debug { - fmt.Fprintf(os.Stderr, "[debug] page fetched: %d findings, hasMoreData=%v, total=%d\n", len(resp.Findings), resp.HasMoreData, resp.Total) + fmt.Fprintf(os.Stderr, "[debug] page %d fetched: %d findings, hasMoreData=%v, total=%d\n", nextPage, len(resp.Findings), resp.HasMoreData, resp.Total) } - if !resp.HasMoreData || resp.ScrollID == nil || *resp.ScrollID == "" { + if !resp.HasMoreData || len(resp.Findings) == 0 { break } - scrollID = *resp.ScrollID + if resp.ScrollID != nil && *resp.ScrollID != "" { + scrollID = *resp.ScrollID + } else { + nextPage = resp.Page + 1 + } } result := findingsOutput{ diff --git a/cmd/cli/cmd/scanners.go b/cmd/cli/cmd/scanners.go index f8b53b6..2ced7c4 100644 --- a/cmd/cli/cmd/scanners.go +++ b/cmd/cli/cmd/scanners.go @@ -1,5 +1,17 @@ package cmd +// findingTypeToAPI maps CLI type slugs to the API's FindingType values. +// The unified /admin/findings endpoint uses PascalCase type names. +var findingTypeToAPI = map[string]string{ + "sast": "Code", + "sca_dependencies": "Dependencies", + "sca_containers": "Containers", + "secrets": "Secrets", + "pentest": "Pentest", + "bughunt": "BugHunting", + "cspm": "Cloud", +} + // scannerEndpoint represents a scanner type and its API path. type scannerEndpoint struct { name string diff --git a/internal/mcp/tools_unified.go b/internal/mcp/tools_unified.go index 84b2b40..87e3f20 100644 --- a/internal/mcp/tools_unified.go +++ b/internal/mcp/tools_unified.go @@ -14,6 +14,18 @@ import ( "github.com/mark3labs/mcp-go/server" ) +// findingTypeToAPI maps CLI type slugs to the API's FindingType values +// used by the unified /admin/findings endpoint. +var findingTypeToAPI = map[string]string{ + "sast": "Code", + "sca_dependencies": "Dependencies", + "sca_containers": "Containers", + "secrets": "Secrets", + "pentest": "Pentest", + "bughunt": "BugHunting", + "cspm": "Cloud", +} + type findingTypeConfig struct { basePath string triage bool @@ -93,6 +105,7 @@ func registerUnifiedTools(s *server.MCPServer, c *client.NullifyClient, queryPar Total int `json:"total"` HasMoreData bool `json:"hasMoreData"` ScrollID *string `json:"scrollId"` + Page int `json:"page"` } type searchOutput struct { @@ -102,6 +115,7 @@ func registerUnifiedTools(s *server.MCPServer, c *client.NullifyClient, queryPar allFindings := make([]json.RawMessage, 0) var scrollID string + nextPage := 1 var lastTotal int for { @@ -116,18 +130,23 @@ func registerUnifiedTools(s *server.MCPServer, c *client.NullifyClient, queryPar query := map[string]any{ "pageSize": pageSize, + "page": nextPage, } if repository != "" { query["repository"] = []string{repository} } if severity != "" { - query["severity"] = []string{severity} + query["severity"] = []string{strings.ToUpper(severity)} } if typeName != "" { if _, err := resolveFindingType(typeName); err != nil { return toolError(err), nil } - query["type"] = []string{typeName} + if apiType, ok := findingTypeToAPI[typeName]; ok { + query["type"] = []string{apiType} + } else { + query["type"] = []string{typeName} + } } if scrollID != "" { query["scrollId"] = scrollID @@ -168,10 +187,14 @@ func registerUnifiedTools(s *server.MCPServer, c *client.NullifyClient, queryPar allFindings = append(allFindings, resp.Findings...) lastTotal = resp.Total - if !resp.HasMoreData || resp.ScrollID == nil || *resp.ScrollID == "" { + if !resp.HasMoreData || len(resp.Findings) == 0 { break } - scrollID = *resp.ScrollID + if resp.ScrollID != nil && *resp.ScrollID != "" { + scrollID = *resp.ScrollID + } else { + nextPage = resp.Page + 1 + } } out, _ := json.MarshalIndent(searchOutput{Findings: allFindings, Total: lastTotal}, "", " ")