diff --git a/docs/sca.md b/docs/sca.md index 93781ec..a48168e 100644 --- a/docs/sca.md +++ b/docs/sca.md @@ -13,7 +13,7 @@ credentials are required. | `sca list` | List SCA projects known to Dependency-Track | | `sca get ` | Project overview: risk score, severity counts, last BOM import | | `sca components ` | Dependencies with outdated / direct / severity filters | -| `sca findings ` | Flat vulnerability listing (CVE-level) with severity filter + truncation | +| `sca findings ` | Flat vulnerability listing (CVE-level), unpaginated, server-cap 1000 rows | All commands accept `-o, --output` with `table` (default) or `json`. @@ -118,8 +118,10 @@ commons-text 1.9 1.13.1 yes Apache-2.0 5.5 0/1 2 components, page 1 of 1 (page-size 50) ``` -Combined filters are **AND** — server-side `--only-outdated` / `--only-direct` -are forwarded to Dep-Track, and `--severity` narrows client-side afterwards: +Combined filters are **AND** — `--only-outdated`, `--only-direct`, and +`--severity` are all applied server-side. The portal auto-pages across the +full project (up to a safety cap) to evaluate the severity filter before +paginating; if the cap is reached, the response carries `truncated=true`: ```bash # Outdated direct dependencies with at least one HIGH or CRITICAL finding @@ -150,10 +152,14 @@ Filter by upstream vulnerability source (e.g. `NVD`, `GITHUB`, `OSV`): krci sca findings payments-api --source=NVD ``` -Very large projects are capped server-side at 1000 rows with a footer hint: +Very large projects are capped server-side at 1000 rows. `--source` is the +only flag that narrows the upstream query; `--severity` filters client-side +after the cap and cannot recover findings beyond row 1000. To audit a +truncated project for severities, drop `--severity` and post-filter the JSON, +or scope by `--branch` first: ``` -(findings truncated to 1000 rows — narrow the query via --severity or --source) +(findings truncated to 1000 rows server-side — only --source narrows the upstream query; --severity filters client-side and cannot recover capped rows) ``` Script-friendly CVE extraction: diff --git a/internal/portal/sca.go b/internal/portal/sca.go index ae23234..cb3e24f 100644 --- a/internal/portal/sca.go +++ b/internal/portal/sca.go @@ -211,13 +211,13 @@ func scaBranchNotFoundErr(err error, body []byte, codebase, branch string) error bodyLower := strings.ToLower(string(body)) switch { case branch != "": - return &scaNotFoundError{msg: fmt.Sprintf("codebase %s not found", codebase)} + return &scaNotFoundError{msg: fmt.Sprintf("project %s not found", codebase)} case strings.Contains(bodyLower, "default_branch_missing"): return &scaNotFoundError{msg: fmt.Sprintf( - "codebase %s has no spec.defaultBranch configured — pass --branch explicitly", codebase)} + "project %s has no spec.defaultBranch configured — pass --branch explicitly", codebase)} default: return &scaNotFoundError{msg: fmt.Sprintf( - "codebase %s not found — use 'krci sca list --search=%s' to find projects known to Dep-Track", + "project %s not found — use 'krci sca list --search=%s' to find projects known to Dep-Track", codebase, codebase)} } } diff --git a/internal/portal/sca_test.go b/internal/portal/sca_test.go index 36791b3..5434c8d 100644 --- a/internal/portal/sca_test.go +++ b/internal/portal/sca_test.go @@ -224,7 +224,7 @@ func TestSCAService_Get_404_CodebaseNotFound_WithExplicitBranch(t *testing.T) { if !errors.Is(err, ErrNotFound) { t.Errorf("want wrap of ErrNotFound, got %v", err) } - if !strings.Contains(err.Error(), "codebase nope not found") { + if !strings.Contains(err.Error(), "project nope not found") { t.Errorf("unexpected message: %v", err) } } diff --git a/pkg/cmd/project/deployments/deployments.go b/pkg/cmd/project/deployments/deployments.go index aa7f7ac..6994102 100644 --- a/pkg/cmd/project/deployments/deployments.go +++ b/pkg/cmd/project/deployments/deployments.go @@ -45,7 +45,7 @@ project is registered (CDPipeline.spec.applications) but no Application exists yet are emitted with "-" placeholders (table) or null values (JSON). Rows are sorted by deployment ascending, then by Stage.spec.order ascending.`, - Args: cmdutil.ExactArgs(1, "a project (codebase) name", + Args: cmdutil.ExactArgs(1, "a project name", "to see available projects: krci project list"), Example: ` # Default krci project deployments my-app diff --git a/pkg/cmd/project/project.go b/pkg/cmd/project/project.go index 04d9ca5..c1efd3e 100644 --- a/pkg/cmd/project/project.go +++ b/pkg/cmd/project/project.go @@ -14,7 +14,7 @@ import ( func NewCmdProject(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "project", - Short: "Manage projects (Codebases)", + Short: "Manage projects", Aliases: []string{"proj"}, } diff --git a/pkg/cmd/sca/components/components.go b/pkg/cmd/sca/components/components.go index 57f617c..f7b1543 100644 --- a/pkg/cmd/sca/components/components.go +++ b/pkg/cmd/sca/components/components.go @@ -39,9 +39,9 @@ func NewCmdComponents(f *cmdutil.Factory, runF func(*ComponentsOptions) error) * } cmd := &cobra.Command{ - Use: "components ", + Use: "components ", Short: "List dependencies (components) for an SCA project", - Args: cmdutil.ExactArgs(1, "a KubeRocketCI codebase name", + Args: cmdutil.ExactArgs(1, "a KubeRocketCI project name", "to see available projects: krci sca list"), Example: ` # Default branch, first 50 dependencies krci sca components payments-api @@ -83,7 +83,8 @@ func NewCmdComponents(f *cmdutil.Factory, runF func(*ComponentsOptions) error) * cmd.Flags().StringSliceVar(&opts.Severity, "severity", nil, scainternal.SeverityFlagUsage+ " Applied server-side across all dependencies of the project.") - cmd.Flags().BoolVar(&opts.OnlyOutdated, "only-outdated", false, "Only components marked outdated by Dependency-Track") + cmd.Flags().BoolVar(&opts.OnlyOutdated, "only-outdated", false, + "Only components Dep-Track marks outdated (newer version exists; independent of vulnerability status)") cmd.Flags().BoolVar(&opts.OnlyDirect, "only-direct", false, "Only direct (non-transitive) dependencies") cmd.Flags().IntVar(&opts.Page, "page", 1, "Page index (1-based)") cmd.Flags().IntVar(&opts.PageSize, "page-size", defaultComponentsPageSize, "Page size (max 500)") diff --git a/pkg/cmd/sca/findings/findings.go b/pkg/cmd/sca/findings/findings.go index 9b14239..58c23c9 100644 --- a/pkg/cmd/sca/findings/findings.go +++ b/pkg/cmd/sca/findings/findings.go @@ -41,9 +41,16 @@ func NewCmdFindings(f *cmdutil.Factory, runF func(*FindingsOptions) error) *cobr } cmd := &cobra.Command{ - Use: "findings ", - Short: "List Dep-Track vulnerability findings for a codebase", - Args: cmdutil.ExactArgs(1, "a KubeRocketCI codebase name", + Use: "findings ", + Short: "List Dep-Track vulnerability findings for a project", + Long: fmt.Sprintf( + "List Dep-Track vulnerability findings for a project.\n\n"+ + "Unpaginated: the portal returns a single response capped at %d rows\n"+ + "server-side. Only --source narrows the upstream query; --severity is\n"+ + "applied client-side after the cap, so it cannot recover findings beyond\n"+ + "row %d.", + scainternal.FindingsServerCap, scainternal.FindingsServerCap), + Args: cmdutil.ExactArgs(1, "a KubeRocketCI project name", "to see available projects: krci sca list"), Example: ` # All unsuppressed findings, default branch krci sca findings payments-api @@ -111,7 +118,10 @@ func findingsRun(ctx context.Context, opts *FindingsOptions) error { return scainternal.HandleError(opts.IO, opts.OutputFormat, err) } - // Client-side inclusive severity filter (server does not narrow by severity). + // Client-side inclusive severity filter — server does not narrow by + // severity. Truncated stays as-returned by the server: hiding it after a + // client-side filter would silently drop findings that fell off the cap + // before we ever saw them. if inclusive := scainternal.ExpandSeverityFlag(opts.Severity); len(inclusive) > 0 { filtered := make([]portal.SCAFinding, 0, len(result.Items)) for _, f := range result.Items { @@ -120,11 +130,6 @@ func findingsRun(ctx context.Context, opts *FindingsOptions) error { } } result.Items = filtered - // Once the user has narrowed by --severity, the upstream "1000-row cap" - // hint is misleading: they already supplied the only follow-up flag we - // would have suggested. Clear the flag so neither the table footer nor - // the JSON envelope keeps advertising it. - result.Truncated = false } return scainternal.Render(opts.IO, opts.OutputFormat, result, func(w io.Writer, isTTY bool) error { @@ -174,8 +179,10 @@ func renderTable(w io.Writer, isTTY bool, codebase string, result *portal.SCAFin if _, err := fmt.Fprintln(w); err != nil { return err } - if _, err := fmt.Fprintln(w, - "(findings truncated to 1000 rows — narrow the query via --severity or --source)"); err != nil { + if _, err := fmt.Fprintf(w, + "(findings truncated to %d rows server-side — only --source narrows the upstream query;"+ + " --severity filters client-side and cannot recover capped rows)\n", + scainternal.FindingsServerCap); err != nil { return err } } diff --git a/pkg/cmd/sca/get/get.go b/pkg/cmd/sca/get/get.go index 44ef688..22b87a5 100644 --- a/pkg/cmd/sca/get/get.go +++ b/pkg/cmd/sca/get/get.go @@ -35,9 +35,9 @@ func NewCmdGet(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command } cmd := &cobra.Command{ - Use: "get ", - Short: "Show a Dep-Track project's overview for a codebase", - Args: cmdutil.ExactArgs(1, "a KubeRocketCI codebase name", + Use: "get ", + Short: "Show SCA scan overview for a project", + Args: cmdutil.ExactArgs(1, "a KubeRocketCI project name", "to see available projects: krci sca list"), Example: ` # Uses Codebase.spec.defaultBranch when --branch is omitted krci sca get payments-api @@ -118,7 +118,7 @@ func printDetail(w io.Writer, codebase, requestedBranch string, d *portal.SCAPro } pairs := []kvPair{ - {label: "Codebase", value: codebase}, + {label: "Project", value: codebase}, {label: "Branch", value: project.Version}, {label: "Classifier", value: scainternal.OrDash(project.Classifier)}, {label: "Active", value: formatActive(project.Active, styled)}, diff --git a/pkg/cmd/sca/get/get_test.go b/pkg/cmd/sca/get/get_test.go index 09f1bd5..f1f3f38 100644 --- a/pkg/cmd/sca/get/get_test.go +++ b/pkg/cmd/sca/get/get_test.go @@ -87,7 +87,7 @@ func TestGet_BranchFlagUsageStringIsVerbatim(t *testing.T) { if !strings.Contains(flag.Usage, "Dep-Track project 'version'") { t.Errorf("--branch usage must reference Dep-Track version field: %q", flag.Usage) } - if !strings.Contains(flag.Usage, "krci sca list --search=") { + if !strings.Contains(flag.Usage, "krci sca list --search=") { t.Errorf("--branch usage must hint discovery path: %q", flag.Usage) } } @@ -140,7 +140,7 @@ func TestPrintDetail_HappyPath(t *testing.T) { t.Fatalf("printDetail: %v", err) } out := buf.String() - wants := []string{"svc @ main", "Codebase", "Branch", "Classifier", "Risk score", + wants := []string{"svc @ main", "Project", "Branch", "Classifier", "Risk score", "Vulnerabilities", "Critical", "Components", "Total"} for _, w := range wants { if !strings.Contains(out, w) { diff --git a/pkg/cmd/sca/internal/validate.go b/pkg/cmd/sca/internal/validate.go index 5f633a0..b2ae17e 100644 --- a/pkg/cmd/sca/internal/validate.go +++ b/pkg/cmd/sca/internal/validate.go @@ -18,13 +18,19 @@ import ( // verb. Matches the OpenAPI ceiling. const MaxPageSize = 500 +// FindingsServerCap is the maximum number of rows the portal returns from +// the unpaginated `sca findings` endpoint. Mirrors the server-side ceiling +// documented in the OpenAPI spec; the help text and truncation footer must +// stay in sync with it. +const FindingsServerCap = 1000 + // BranchFlagUsage is the verbatim help text for `--branch` across every // per-codebase sca verb. Spec Requirement "Codebase + Branch Addressing" // mandates this exact string so users always see the same Dep-Track `version` // field explanation. const BranchFlagUsage = "branch name (maps to the Dep-Track project 'version' field). " + - "Defaults to the codebase's spec.defaultBranch. " + - "Run 'krci sca list --search=' to discover all recorded versions." + "Defaults to the project's spec.defaultBranch. " + + "Run 'krci sca list --search=' to discover all recorded versions." // SeverityFlagUsage is the verbatim help text for `--severity` across the // verbs that expose it. Spec design §D4 mandates this exact string: it @@ -38,15 +44,15 @@ const SeverityFlagUsage = "minimum severity to include (inclusive). " + // names by platform convention follow DNS-1123. func ValidateCodebaseKey(codebase string) error { if codebase == "" { - return fmt.Errorf(" must not be empty") + return fmt.Errorf(" must not be empty") } if len(codebase) > cmdutil.DNS1123SubdomainMaxLength { - return fmt.Errorf(" must be at most %d characters", cmdutil.DNS1123SubdomainMaxLength) + return fmt.Errorf(" must be at most %d characters", cmdutil.DNS1123SubdomainMaxLength) } if !cmdutil.IsValidDNS1123Label(codebase) { - return fmt.Errorf(" must be a valid DNS-1123 name") + return fmt.Errorf(" must be a valid DNS-1123 name") } return nil