From 0efac186ee08d0db5cec1bdd3b614f9bb0fecb7b Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 17 Mar 2026 03:28:23 +0530 Subject: [PATCH 1/9] feat(api): add vuln list handler for Security Hub endpoint Signed-off-by: Sypher845 --- pkg/api/types.go | 17 +++++ pkg/api/vulnerability_handler.go | 106 +++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/pkg/api/types.go b/pkg/api/types.go index 27cf95e8e..3728324e3 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -81,3 +81,20 @@ type GetMemberOptions struct { ID int64 ProjectNameOrID string } + +type ListVulnerabilityOptions struct { + CVEID string + CVSSScore string + Severity string + Repository string + ProjectName string + Package string + Tag string + Digest string + Exclude string + WithTag bool + Fixable bool + Page int64 + PageSize int64 + Q string +} diff --git a/pkg/api/vulnerability_handler.go b/pkg/api/vulnerability_handler.go index e759a30f7..068b62264 100644 --- a/pkg/api/vulnerability_handler.go +++ b/pkg/api/vulnerability_handler.go @@ -14,7 +14,12 @@ package api import ( + "fmt" + "slices" + "strings" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/securityhub" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" ) @@ -34,3 +39,104 @@ func GetVulnerabilitySummary(withDangerousArtifact, withDangerousCVE bool) (*sec return response, nil } + +func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVulnerabilitiesOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + var listFlags ListVulnerabilityOptions + if len(opts) > 0 { + listFlags = opts[0] + } + + q, err := buildVulnerabilityQuery(listFlags) + if err != nil { + return nil, err + } + + response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + Q: &q, + WithTag: &listFlags.WithTag, + }) + if err != nil { + return nil, err + } + + if listFlags.Fixable { + response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool { + return vul.FixedVersion == "" + }) + } + + if listFlags.Exclude != "" { + excludeMap := make(map[string]string) + for _, query := range strings.Split(listFlags.Exclude, ",") { + parts := strings.SplitN(strings.TrimSpace(query), "=", 2) + if len(parts) == 2 { + excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + + response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool { + if val, ok := excludeMap["cve_id"]; ok && vul.CVEID == val { + return true + } + if val, ok := excludeMap["severity"]; ok && strings.EqualFold(vul.Severity, val) { + return true + } + if val, ok := excludeMap["package"]; ok && vul.Package == val { + return true + } + if val, ok := excludeMap["repository_name"]; ok && vul.RepositoryName == val { + return true + } + if val, ok := excludeMap["digest"]; ok && vul.Digest == val { + return true + } + return false + }) + } + + return response, nil +} + +func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { + var queries []string + if opts.CVEID != "" { + queries = append(queries, fmt.Sprintf("cve_id=%s", opts.CVEID)) + } + if opts.CVSSScore != "" { + queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", opts.CVSSScore)) + } + if opts.Severity != "" { + queries = append(queries, fmt.Sprintf("severity=%s", opts.Severity)) + } + if opts.Repository != "" { + queries = append(queries, fmt.Sprintf("repository_name=%s", opts.Repository)) + } + if opts.ProjectName != "" { + projectId, err := GetProjectIDFromName(opts.ProjectName) + if err != nil { + return "", err + } + queries = append(queries, fmt.Sprintf("project_id=%d", projectId)) + } + if opts.Package != "" { + queries = append(queries, fmt.Sprintf("package=%s", opts.Package)) + } + if opts.Tag != "" { + queries = append(queries, fmt.Sprintf("tag=%s", opts.Tag)) + } + if opts.Digest != "" { + queries = append(queries, fmt.Sprintf("digest=%s", opts.Digest)) + } + if opts.Q != "" { + queries = append(queries, opts.Q) + } + + return strings.Join(queries, ","), nil +} From ef78343a0ebd83aecdac02ab3df32b5a84d24b5e Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 17 Mar 2026 03:29:28 +0530 Subject: [PATCH 2/9] feat(vuln): vuln list command and view Signed-off-by: Sypher845 --- cmd/harbor/root/vulnerability/cmd.go | 1 + cmd/harbor/root/vulnerability/list.go | 85 ++++++++++++++++++++++++++ pkg/api/vulnerability_handler.go | 1 + pkg/views/vulnerability/list/view.go | 86 +++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 cmd/harbor/root/vulnerability/list.go create mode 100644 pkg/views/vulnerability/list/view.go diff --git a/cmd/harbor/root/vulnerability/cmd.go b/cmd/harbor/root/vulnerability/cmd.go index d432c4a54..defcc4aa4 100644 --- a/cmd/harbor/root/vulnerability/cmd.go +++ b/cmd/harbor/root/vulnerability/cmd.go @@ -26,6 +26,7 @@ func Vulnerability() *cobra.Command { cmd.AddCommand( GetVulnerabilitySummaryCommand(), + ListVulnerabilitiesCommand(), ) return cmd diff --git a/cmd/harbor/root/vulnerability/list.go b/cmd/harbor/root/vulnerability/list.go new file mode 100644 index 000000000..3ce603776 --- /dev/null +++ b/cmd/harbor/root/vulnerability/list.go @@ -0,0 +1,85 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package vulnerability + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + vulnlist "github.com/goharbor/harbor-cli/pkg/views/vulnerability/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListVulnerabilitiesCommand() *cobra.Command { + var opts api.ListVulnerabilityOptions + + cmd := &cobra.Command{ + Use: "list", + Short: "List vulnerabilities in Security Hub", + Long: "List vulnerabilities from Harbor Security Hub", + Example: ` harbor vulnerability list`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.PageSize < 0 { + return fmt.Errorf("page size must be greater than or equal to 0") + } + + if opts.PageSize > 100 { + return fmt.Errorf("page size should be less than or equal to 100") + } + + response, err := api.ListVulnerabilities(opts) + if err != nil { + return fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) + } + + if len(response.Payload) == 0 { + log.Info("No vulnerabilities found") + return nil + } + + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + err = utils.PrintFormat(response.Payload, formatFlag) + if err != nil { + return err + } + } else { + vulnlist.ViewVulnerabilityList(response.Payload) + } + + return nil + }, + } + + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max]") + flags.StringVarP(&opts.CVEID, "cve-id", "", "", "Filter by exact CVE ID") + flags.StringVarP(&opts.CVSSScore, "cvss-score", "", "", "Filter by CVSS v3 score range (e.g. [7.0~10.0])") + flags.StringVarP(&opts.Severity, "severity", "", "", "Filter by severity level") + flags.StringVarP(&opts.Repository, "repository", "", "", "Filter by exact repository name") + flags.StringVarP(&opts.ProjectName, "project-name", "", "", "Filter by exact project name") + flags.StringVarP(&opts.Package, "package", "", "", "Filter by exact package name") + flags.StringVarP(&opts.Tag, "tag", "", "", "Filter by exact artifact tag") + flags.StringVarP(&opts.Digest, "digest", "", "", "Filter by exact artifact digest") + flags.StringVarP(&opts.Exclude, "exclude", "", "", "Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max])") + flags.BoolVarP(&opts.Fixable, "fixable", "", false, "Only show fixable vulnerabilities") + + return cmd +} diff --git a/pkg/api/vulnerability_handler.go b/pkg/api/vulnerability_handler.go index 068b62264..91009e376 100644 --- a/pkg/api/vulnerability_handler.go +++ b/pkg/api/vulnerability_handler.go @@ -56,6 +56,7 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul return nil, err } + listFlags.WithTag = true response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ Page: &listFlags.Page, PageSize: &listFlags.PageSize, diff --git a/pkg/views/vulnerability/list/view.go b/pkg/views/vulnerability/list/view.go new file mode 100644 index 000000000..bd74a92db --- /dev/null +++ b/pkg/views/vulnerability/list/view.go @@ -0,0 +1,86 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package list + +import ( + "fmt" + "strconv" + "strings" + + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/x/ansi" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "CVE ID", Width: tablelist.WidthL}, + {Title: "Description", Width: tablelist.WidthXXL}, + {Title: "Project", Width: tablelist.WidthM}, + {Title: "Repository", Width: tablelist.WidthM}, + {Title: "Digest", Width: tablelist.WidthM}, + {Title: "Tags", Width: tablelist.WidthS}, + {Title: "CVSSV3", Width: tablelist.WidthS}, + {Title: "Severity", Width: tablelist.WidthS}, + {Title: "Package", Width: tablelist.WidthM}, + {Title: "Version", Width: tablelist.WidthM}, + {Title: "Fixed", Width: tablelist.WidthM}, +} + +func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) { + if vulnerabilities == nil { + fmt.Println("No vulnerabilities found") + return + } + + var rows []table.Row + cveLinks := make(map[string]string) + for _, vulnerability := range vulnerabilities { + link := "" + if len(vulnerability.Links) > 0 { + link = vulnerability.Links[0] + } + + cveLinks[vulnerability.CVEID] = link + projectName := strconv.FormatInt(vulnerability.ProjectID, 10) + project, err := api.GetProject(projectName, true) + if err == nil && project.Payload != nil { + projectName = project.Payload.Name + } + rows = append(rows, table.Row{ + vulnerability.CVEID, + vulnerability.Desc, + projectName, + vulnerability.RepositoryName, + vulnerability.Digest, + strings.Join(vulnerability.Tags, ", "), + strconv.FormatFloat(float64(vulnerability.CvssV3Score), 'f', 1, 32), + vulnerability.Severity, + vulnerability.Package, + vulnerability.Version, + vulnerability.FixedVersion, + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + tableOutput := m.View() + + for cve, link := range cveLinks { + hyperlinkedID := ansi.SetHyperlink(link) + cve + ansi.ResetHyperlink() + tableOutput = strings.ReplaceAll(tableOutput, cve, hyperlinkedID) + } + + fmt.Println(tableOutput) +} From 8ef8542db76891a3050c81ef43479d5638a6044d Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 17 Mar 2026 04:10:50 +0530 Subject: [PATCH 3/9] docs: cli and man page docs for vuln list command Signed-off-by: Sypher845 --- doc/cli-docs/harbor-vulnerability-list.md | 55 +++++++++++ doc/cli-docs/harbor-vulnerability.md | 1 + doc/man-docs/man1/harbor-vulnerability-list.1 | 93 +++++++++++++++++++ doc/man-docs/man1/harbor-vulnerability.1 | 2 +- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 doc/cli-docs/harbor-vulnerability-list.md create mode 100644 doc/man-docs/man1/harbor-vulnerability-list.1 diff --git a/doc/cli-docs/harbor-vulnerability-list.md b/doc/cli-docs/harbor-vulnerability-list.md new file mode 100644 index 000000000..1ca59bdf0 --- /dev/null +++ b/doc/cli-docs/harbor-vulnerability-list.md @@ -0,0 +1,55 @@ +--- +title: harbor vulnerability list +weight: 90 +--- +## harbor vulnerability list + +### Description + +##### List vulnerabilities in Security Hub + +### Synopsis + +List vulnerabilities from Harbor Security Hub + +```sh +harbor vulnerability list [flags] +``` + +### Examples + +```sh + harbor vulnerability list +``` + +### Options + +```sh + --cve-id string Filter by exact CVE ID + --cvss-score string Filter by CVSS v3 score range (e.g. [7.0~10.0]) + --digest string Filter by exact artifact digest + --exclude string Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max]) + --fixable Only show fixable vulnerabilities + -h, --help help for list + --package string Filter by exact package name + --page int Page number (default 1) + --page-size int Size of per page (default 10) + --project-name string Filter by exact project name + -q, --query string Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max] + --repository string Filter by exact repository name + --severity string Filter by severity level + --tag string Filter by exact artifact tag +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor vulnerability](harbor-vulnerability.md) - Manage vulnerabilities in Security Hub + diff --git a/doc/cli-docs/harbor-vulnerability.md b/doc/cli-docs/harbor-vulnerability.md index 3ba923b4d..695b57392 100644 --- a/doc/cli-docs/harbor-vulnerability.md +++ b/doc/cli-docs/harbor-vulnerability.md @@ -35,5 +35,6 @@ List vulnerabilities and view vulnerability summary from Harbor Security Hub ### SEE ALSO * [harbor](harbor.md) - Official Harbor CLI +* [harbor vulnerability list](harbor-vulnerability-list.md) - List vulnerabilities in Security Hub * [harbor vulnerability summary](harbor-vulnerability-summary.md) - Get Security Hub vulnerability summary diff --git a/doc/man-docs/man1/harbor-vulnerability-list.1 b/doc/man-docs/man1/harbor-vulnerability-list.1 new file mode 100644 index 000000000..84afc2de6 --- /dev/null +++ b/doc/man-docs/man1/harbor-vulnerability-list.1 @@ -0,0 +1,93 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-vulnerability-list - List vulnerabilities in Security Hub + + +.SH SYNOPSIS +\fBharbor vulnerability list [flags]\fP + + +.SH DESCRIPTION +List vulnerabilities from Harbor Security Hub + + +.SH OPTIONS +\fB--cve-id\fP="" + Filter by exact CVE ID + +.PP +\fB--cvss-score\fP="" + Filter by CVSS v3 score range (e.g. [7.0~10.0]) + +.PP +\fB--digest\fP="" + Filter by exact artifact digest + +.PP +\fB--exclude\fP="" + Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max]) + +.PP +\fB--fixable\fP[=false] + Only show fixable vulnerabilities + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for list + +.PP +\fB--package\fP="" + Filter by exact package name + +.PP +\fB--page\fP=1 + Page number + +.PP +\fB--page-size\fP=10 + Size of per page + +.PP +\fB--project-name\fP="" + Filter by exact project name + +.PP +\fB-q\fP, \fB--query\fP="" + Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max] + +.PP +\fB--repository\fP="" + Filter by exact repository name + +.PP +\fB--severity\fP="" + Filter by severity level + +.PP +\fB--tag\fP="" + Filter by exact artifact tag + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor vulnerability list +.EE + + +.SH SEE ALSO +\fBharbor-vulnerability(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-vulnerability.1 b/doc/man-docs/man1/harbor-vulnerability.1 index 161ab03a4..d015c2a2d 100644 --- a/doc/man-docs/man1/harbor-vulnerability.1 +++ b/doc/man-docs/man1/harbor-vulnerability.1 @@ -38,4 +38,4 @@ List vulnerabilities and view vulnerability summary from Harbor Security Hub .SH SEE ALSO -\fBharbor(1)\fP, \fBharbor-vulnerability-summary(1)\fP \ No newline at end of file +\fBharbor(1)\fP, \fBharbor-vulnerability-list(1)\fP, \fBharbor-vulnerability-summary(1)\fP \ No newline at end of file From fe84b232b51330eec0f7f33580279970ad6ac9d2 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Fri, 20 Mar 2026 03:24:30 +0530 Subject: [PATCH 4/9] refactor: vuln filtering logic Signed-off-by: Sypher845 --- cmd/harbor/root/vulnerability/list.go | 41 ++++- doc/cli-docs/harbor-vulnerability-list.md | 2 +- doc/man-docs/man1/harbor-vulnerability-list.1 | 2 +- pkg/api/vulnerability_handler.go | 158 ++++++++++++++---- pkg/views/vulnerability/list/view.go | 32 ++-- 5 files changed, 181 insertions(+), 54 deletions(-) diff --git a/cmd/harbor/root/vulnerability/list.go b/cmd/harbor/root/vulnerability/list.go index 3ce603776..12dbc27ad 100644 --- a/cmd/harbor/root/vulnerability/list.go +++ b/cmd/harbor/root/vulnerability/list.go @@ -16,6 +16,7 @@ package vulnerability import ( "fmt" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/utils" vulnlist "github.com/goharbor/harbor-cli/pkg/views/vulnerability/list" @@ -42,24 +43,24 @@ func ListVulnerabilitiesCommand() *cobra.Command { return fmt.Errorf("page size should be less than or equal to 100") } - response, err := api.ListVulnerabilities(opts) + allVulnerabilities, hasNext, err := fetchVulnerabilities(opts) if err != nil { return fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) } - if len(response.Payload) == 0 { + if len(allVulnerabilities) == 0 { log.Info("No vulnerabilities found") return nil } formatFlag := viper.GetString("output-format") if formatFlag != "" { - err = utils.PrintFormat(response.Payload, formatFlag) + err = utils.PrintFormat(allVulnerabilities, formatFlag) if err != nil { return err } } else { - vulnlist.ViewVulnerabilityList(response.Payload) + vulnlist.ViewVulnerabilityList(allVulnerabilities, hasNext) } return nil @@ -71,7 +72,7 @@ func ListVulnerabilitiesCommand() *cobra.Command { flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") flags.StringVarP(&opts.Q, "query", "q", "", "Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max]") flags.StringVarP(&opts.CVEID, "cve-id", "", "", "Filter by exact CVE ID") - flags.StringVarP(&opts.CVSSScore, "cvss-score", "", "", "Filter by CVSS v3 score range (e.g. [7.0~10.0])") + flags.StringVarP(&opts.CVSSScore, "cvss-score", "", "", "Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0)") flags.StringVarP(&opts.Severity, "severity", "", "", "Filter by severity level") flags.StringVarP(&opts.Repository, "repository", "", "", "Filter by exact repository name") flags.StringVarP(&opts.ProjectName, "project-name", "", "", "Filter by exact project name") @@ -83,3 +84,33 @@ func ListVulnerabilitiesCommand() *cobra.Command { return cmd } + +func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.VulnerabilityItem, bool, error) { + var allVuln []*models.VulnerabilityItem + if opts.PageSize == 0 { + log.Debug("Page size is 0, will fetch all vulnerabilities") + opts.PageSize = 100 + opts.Page = 1 + for { + response, err := api.ListVulnerabilities(opts) + if err != nil { + return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) + } + if len(response.Payload) == 0 { + break + } + allVuln = append(allVuln, response.Payload...) + opts.Page++ + if opts.Page > 10 { + return allVuln, true, nil + } + } + } else { + response, err := api.ListVulnerabilities(opts) + if err != nil { + return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) + } + allVuln = append(allVuln, response.Payload...) + } + return allVuln, false, nil +} diff --git a/doc/cli-docs/harbor-vulnerability-list.md b/doc/cli-docs/harbor-vulnerability-list.md index 1ca59bdf0..b53ada2af 100644 --- a/doc/cli-docs/harbor-vulnerability-list.md +++ b/doc/cli-docs/harbor-vulnerability-list.md @@ -26,7 +26,7 @@ harbor vulnerability list [flags] ```sh --cve-id string Filter by exact CVE ID - --cvss-score string Filter by CVSS v3 score range (e.g. [7.0~10.0]) + --cvss-score string Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0) --digest string Filter by exact artifact digest --exclude string Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max]) --fixable Only show fixable vulnerabilities diff --git a/doc/man-docs/man1/harbor-vulnerability-list.1 b/doc/man-docs/man1/harbor-vulnerability-list.1 index 84afc2de6..82a1d3a93 100644 --- a/doc/man-docs/man1/harbor-vulnerability-list.1 +++ b/doc/man-docs/man1/harbor-vulnerability-list.1 @@ -19,7 +19,7 @@ List vulnerabilities from Harbor Security Hub .PP \fB--cvss-score\fP="" - Filter by CVSS v3 score range (e.g. [7.0~10.0]) + Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0) .PP \fB--digest\fP="" diff --git a/pkg/api/vulnerability_handler.go b/pkg/api/vulnerability_handler.go index 91009e376..41bb7a318 100644 --- a/pkg/api/vulnerability_handler.go +++ b/pkg/api/vulnerability_handler.go @@ -55,8 +55,55 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul if err != nil { return nil, err } - listFlags.WithTag = true + + if listFlags.Fixable || listFlags.Exclude != "" { + excludeMap := parseVulnerabilityExcludeMap(listFlags.Exclude) + var res []*models.VulnerabilityItem + start := int((listFlags.Page - 1) * listFlags.PageSize) + end := int(listFlags.Page * listFlags.PageSize) + count := 0 + for page := int64(1); count < end; page++ { + response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ + Page: &page, + PageSize: &listFlags.PageSize, + Q: &q, + WithTag: &listFlags.WithTag, + }) + if err != nil { + return nil, err + } + if len(response.Payload) == 0 { + break + } + + response.Payload = filterVulnerabilities( + response.Payload, + listFlags.Fixable, + listFlags.Exclude != "", + excludeMap, + ) + + for _, vul := range response.Payload { + if count >= start && count < end { + res = append(res, vul) + } + count++ + if count >= end { + break + } + } + } + if len(res) == 0 { + return &securityhub.ListVulnerabilitiesOK{ + Payload: nil, + }, nil + } + return &securityhub.ListVulnerabilitiesOK{ + Payload: res, + }, nil + } + response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ Page: &listFlags.Page, PageSize: &listFlags.PageSize, @@ -66,40 +113,10 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul if err != nil { return nil, err } - - if listFlags.Fixable { - response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool { - return vul.FixedVersion == "" - }) - } - - if listFlags.Exclude != "" { - excludeMap := make(map[string]string) - for _, query := range strings.Split(listFlags.Exclude, ",") { - parts := strings.SplitN(strings.TrimSpace(query), "=", 2) - if len(parts) == 2 { - excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - - response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool { - if val, ok := excludeMap["cve_id"]; ok && vul.CVEID == val { - return true - } - if val, ok := excludeMap["severity"]; ok && strings.EqualFold(vul.Severity, val) { - return true - } - if val, ok := excludeMap["package"]; ok && vul.Package == val { - return true - } - if val, ok := excludeMap["repository_name"]; ok && vul.RepositoryName == val { - return true - } - if val, ok := excludeMap["digest"]; ok && vul.Digest == val { - return true - } - return false - }) + if len(response.Payload) == 0 { + response.Payload = nil + response.XTotalCount = 0 + return response, nil } return response, nil @@ -111,9 +128,31 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { queries = append(queries, fmt.Sprintf("cve_id=%s", opts.CVEID)) } if opts.CVSSScore != "" { - queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", opts.CVSSScore)) + cvssRange := opts.CVSSScore + if strings.Contains(cvssRange, "~") { + parts := strings.Split(cvssRange, "~") + if len(parts) == 2 { + cvssRange = fmt.Sprintf("[%s~%s]", parts[0], parts[1]) + } else { + return "", fmt.Errorf("invalid cvss score range: %s. Expected format: min~max", cvssRange) + } + } else { + cvssRange = fmt.Sprintf("[%s~%s]", opts.CVSSScore, opts.CVSSScore) + } + queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", cvssRange)) } if opts.Severity != "" { + allowedSeverities := map[string]bool{ + "Critical": true, + "High": true, + "Medium": true, + "Low": true, + "n/a": true, + "None": true, + } + if !allowedSeverities[opts.Severity] { + return "", fmt.Errorf("invalid severity value: %s. Allowed values are: Low, Medium, High, Critical, n/a, None", opts.Severity) + } queries = append(queries, fmt.Sprintf("severity=%s", opts.Severity)) } if opts.Repository != "" { @@ -136,8 +175,55 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { queries = append(queries, fmt.Sprintf("digest=%s", opts.Digest)) } if opts.Q != "" { + // FIXME: Q parameter needs to be converted to standard query format + // This will be addressed in draft PR #731 queries = append(queries, opts.Q) } return strings.Join(queries, ","), nil } + +func parseVulnerabilityExcludeMap(exclude string) map[string]string { + excludeMap := make(map[string]string) + for _, query := range strings.Split(exclude, ",") { + parts := strings.SplitN(strings.TrimSpace(query), "=", 2) + if len(parts) == 2 { + excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + return excludeMap +} + +func filterVulnerabilities( + payload []*models.VulnerabilityItem, + fixable bool, + excludeProvided bool, + excludeMap map[string]string, +) []*models.VulnerabilityItem { + return slices.DeleteFunc(payload, func(vul *models.VulnerabilityItem) bool { + if fixable && vul.FixedVersion == "" { + return true + } + + if !excludeProvided { + return false + } + + if val, ok := excludeMap["cve_id"]; ok && vul.CVEID == val { + return true + } + if val, ok := excludeMap["severity"]; ok && vul.Severity == val { + return true + } + if val, ok := excludeMap["package"]; ok && vul.Package == val { + return true + } + if val, ok := excludeMap["repository_name"]; ok && vul.RepositoryName == val { + return true + } + if val, ok := excludeMap["digest"]; ok && vul.Digest == val { + return true + } + return false + }) +} diff --git a/pkg/views/vulnerability/list/view.go b/pkg/views/vulnerability/list/view.go index bd74a92db..59aff3f6b 100644 --- a/pkg/views/vulnerability/list/view.go +++ b/pkg/views/vulnerability/list/view.go @@ -21,17 +21,16 @@ import ( "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/x/ansi" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" - "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" ) var columns = []table.Column{ {Title: "CVE ID", Width: tablelist.WidthL}, {Title: "Description", Width: tablelist.WidthXXL}, - {Title: "Project", Width: tablelist.WidthM}, - {Title: "Repository", Width: tablelist.WidthM}, + {Title: "Repository", Width: tablelist.WidthL}, {Title: "Digest", Width: tablelist.WidthM}, - {Title: "Tags", Width: tablelist.WidthS}, + {Title: "Tags", Width: tablelist.WidthM}, {Title: "CVSSV3", Width: tablelist.WidthS}, {Title: "Severity", Width: tablelist.WidthS}, {Title: "Package", Width: tablelist.WidthM}, @@ -39,7 +38,7 @@ var columns = []table.Column{ {Title: "Fixed", Width: tablelist.WidthM}, } -func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) { +func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem, hasNext bool) { if vulnerabilities == nil { fmt.Println("No vulnerabilities found") return @@ -54,15 +53,9 @@ func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) { } cveLinks[vulnerability.CVEID] = link - projectName := strconv.FormatInt(vulnerability.ProjectID, 10) - project, err := api.GetProject(projectName, true) - if err == nil && project.Payload != nil { - projectName = project.Payload.Name - } rows = append(rows, table.Row{ vulnerability.CVEID, vulnerability.Desc, - projectName, vulnerability.RepositoryName, vulnerability.Digest, strings.Join(vulnerability.Tags, ", "), @@ -83,4 +76,21 @@ func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) { } fmt.Println(tableOutput) + if hasNext { + var hintURL string + if cfg, err := utils.GetCurrentHarborConfig(); err == nil { + for _, cred := range cfg.Credentials { + if cred.Name == cfg.CurrentCredentialName { + hintURL = strings.TrimRight(cred.ServerAddress, "/") + "/harbor/interrogation-services/security-hub" + break + } + } + } + + if hintURL != "" { + fmt.Printf("More results at: %s%s%s\n", ansi.SetHyperlink(hintURL), hintURL, ansi.ResetHyperlink()) + } else { + fmt.Println("More results available in the Harbor web console") + } + } } From faae44ba41310816acdf7da041c9d45ccabd5297 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Fri, 20 Mar 2026 17:53:22 +0530 Subject: [PATCH 5/9] refractor: add 'all' flag to fetch all vuln upto 1000 Signed-off-by: Sypher845 --- cmd/harbor/root/vulnerability/list.go | 13 +++++-------- doc/cli-docs/harbor-vulnerability-list.md | 1 + doc/man-docs/man1/harbor-vulnerability-list.1 | 4 ++++ pkg/api/types.go | 1 + 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cmd/harbor/root/vulnerability/list.go b/cmd/harbor/root/vulnerability/list.go index 12dbc27ad..c059d9cad 100644 --- a/cmd/harbor/root/vulnerability/list.go +++ b/cmd/harbor/root/vulnerability/list.go @@ -35,12 +35,8 @@ func ListVulnerabilitiesCommand() *cobra.Command { Example: ` harbor vulnerability list`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - if opts.PageSize < 0 { - return fmt.Errorf("page size must be greater than or equal to 0") - } - - if opts.PageSize > 100 { - return fmt.Errorf("page size should be less than or equal to 100") + if opts.PageSize <= 0 || opts.PageSize > 100 { + return fmt.Errorf("page size must be greater than 0 and less than or equal to 100") } allVulnerabilities, hasNext, err := fetchVulnerabilities(opts) @@ -81,14 +77,15 @@ func ListVulnerabilitiesCommand() *cobra.Command { flags.StringVarP(&opts.Digest, "digest", "", "", "Filter by exact artifact digest") flags.StringVarP(&opts.Exclude, "exclude", "", "", "Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max])") flags.BoolVarP(&opts.Fixable, "fixable", "", false, "Only show fixable vulnerabilities") + flags.BoolVarP(&opts.All, "all", "", false, "Show all vulnerabilities (up to 1000)") return cmd } func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.VulnerabilityItem, bool, error) { var allVuln []*models.VulnerabilityItem - if opts.PageSize == 0 { - log.Debug("Page size is 0, will fetch all vulnerabilities") + if opts.All { + log.Debug("Fetching all vulnerabilities") opts.PageSize = 100 opts.Page = 1 for { diff --git a/doc/cli-docs/harbor-vulnerability-list.md b/doc/cli-docs/harbor-vulnerability-list.md index b53ada2af..2a2cecad7 100644 --- a/doc/cli-docs/harbor-vulnerability-list.md +++ b/doc/cli-docs/harbor-vulnerability-list.md @@ -25,6 +25,7 @@ harbor vulnerability list [flags] ### Options ```sh + --all Show all vulnerabilities (up to 1000) --cve-id string Filter by exact CVE ID --cvss-score string Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0) --digest string Filter by exact artifact digest diff --git a/doc/man-docs/man1/harbor-vulnerability-list.1 b/doc/man-docs/man1/harbor-vulnerability-list.1 index 82a1d3a93..d2be8c091 100644 --- a/doc/man-docs/man1/harbor-vulnerability-list.1 +++ b/doc/man-docs/man1/harbor-vulnerability-list.1 @@ -14,6 +14,10 @@ List vulnerabilities from Harbor Security Hub .SH OPTIONS +\fB--all\fP[=false] + Show all vulnerabilities (up to 1000) + +.PP \fB--cve-id\fP="" Filter by exact CVE ID diff --git a/pkg/api/types.go b/pkg/api/types.go index 3728324e3..a4ea79385 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -94,6 +94,7 @@ type ListVulnerabilityOptions struct { Exclude string WithTag bool Fixable bool + All bool Page int64 PageSize int64 Q string From bab112df950d7b2cfa46af0f5e48bec9d17a0b54 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 24 Mar 2026 23:31:32 +0530 Subject: [PATCH 6/9] chore(vuln): remove raw --query flag Signed-off-by: Sypher845 --- cmd/harbor/root/vulnerability/list.go | 5 ++--- doc/cli-docs/harbor-vulnerability-list.md | 1 - doc/man-docs/man1/harbor-vulnerability-list.1 | 4 ---- pkg/api/types.go | 1 - pkg/api/vulnerability_handler.go | 5 ----- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/cmd/harbor/root/vulnerability/list.go b/cmd/harbor/root/vulnerability/list.go index c059d9cad..682ddf3a1 100644 --- a/cmd/harbor/root/vulnerability/list.go +++ b/cmd/harbor/root/vulnerability/list.go @@ -66,7 +66,6 @@ func ListVulnerabilitiesCommand() *cobra.Command { flags := cmd.Flags() flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") - flags.StringVarP(&opts.Q, "query", "q", "", "Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max]") flags.StringVarP(&opts.CVEID, "cve-id", "", "", "Filter by exact CVE ID") flags.StringVarP(&opts.CVSSScore, "cvss-score", "", "", "Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0)") flags.StringVarP(&opts.Severity, "severity", "", "", "Filter by severity level") @@ -91,7 +90,7 @@ func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.Vulnerab for { response, err := api.ListVulnerabilities(opts) if err != nil { - return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) + return nil, false, err } if len(response.Payload) == 0 { break @@ -105,7 +104,7 @@ func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.Vulnerab } else { response, err := api.ListVulnerabilities(opts) if err != nil { - return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err)) + return nil, false, err } allVuln = append(allVuln, response.Payload...) } diff --git a/doc/cli-docs/harbor-vulnerability-list.md b/doc/cli-docs/harbor-vulnerability-list.md index 2a2cecad7..e2189c1ae 100644 --- a/doc/cli-docs/harbor-vulnerability-list.md +++ b/doc/cli-docs/harbor-vulnerability-list.md @@ -36,7 +36,6 @@ harbor vulnerability list [flags] --page int Page number (default 1) --page-size int Size of per page (default 10) --project-name string Filter by exact project name - -q, --query string Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max] --repository string Filter by exact repository name --severity string Filter by severity level --tag string Filter by exact artifact tag diff --git a/doc/man-docs/man1/harbor-vulnerability-list.1 b/doc/man-docs/man1/harbor-vulnerability-list.1 index d2be8c091..43cc0c154 100644 --- a/doc/man-docs/man1/harbor-vulnerability-list.1 +++ b/doc/man-docs/man1/harbor-vulnerability-list.1 @@ -57,10 +57,6 @@ List vulnerabilities from Harbor Security Hub \fB--project-name\fP="" Filter by exact project name -.PP -\fB-q\fP, \fB--query\fP="" - Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max] - .PP \fB--repository\fP="" Filter by exact repository name diff --git a/pkg/api/types.go b/pkg/api/types.go index a4ea79385..d44fa28cf 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -97,5 +97,4 @@ type ListVulnerabilityOptions struct { All bool Page int64 PageSize int64 - Q string } diff --git a/pkg/api/vulnerability_handler.go b/pkg/api/vulnerability_handler.go index 41bb7a318..5d4924ccd 100644 --- a/pkg/api/vulnerability_handler.go +++ b/pkg/api/vulnerability_handler.go @@ -174,11 +174,6 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { if opts.Digest != "" { queries = append(queries, fmt.Sprintf("digest=%s", opts.Digest)) } - if opts.Q != "" { - // FIXME: Q parameter needs to be converted to standard query format - // This will be addressed in draft PR #731 - queries = append(queries, opts.Q) - } return strings.Join(queries, ","), nil } From 2d9a4cca2ad0d255292dddf61a3f19ef1f2bd360 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Fri, 27 Mar 2026 02:41:10 +0530 Subject: [PATCH 7/9] refactor(vuln): improve vulnerability list filtering flow Signed-off-by: Sypher845 --- cmd/harbor/root/vulnerability/list.go | 35 +++--- pkg/api/vulnerability_handler.go | 147 ++++++++++++++++---------- 2 files changed, 113 insertions(+), 69 deletions(-) diff --git a/cmd/harbor/root/vulnerability/list.go b/cmd/harbor/root/vulnerability/list.go index 682ddf3a1..e6a269932 100644 --- a/cmd/harbor/root/vulnerability/list.go +++ b/cmd/harbor/root/vulnerability/list.go @@ -35,6 +35,10 @@ func ListVulnerabilitiesCommand() *cobra.Command { Example: ` harbor vulnerability list`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if opts.Page < 1 { + return fmt.Errorf("page number must be greater than or equal to 1") + } + if opts.PageSize <= 0 || opts.PageSize > 100 { return fmt.Errorf("page size must be greater than 0 and less than or equal to 100") } @@ -82,31 +86,38 @@ func ListVulnerabilitiesCommand() *cobra.Command { } func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.VulnerabilityItem, bool, error) { - var allVuln []*models.VulnerabilityItem if opts.All { + var allVulnerabilities []*models.VulnerabilityItem + log.Debug("Fetching all vulnerabilities") opts.PageSize = 100 opts.Page = 1 + for { - response, err := api.ListVulnerabilities(opts) + vulnerabilities, err := api.ListVulnerabilities(opts) if err != nil { return nil, false, err } - if len(response.Payload) == 0 { + + if len(vulnerabilities) == 0 { break } - allVuln = append(allVuln, response.Payload...) + + allVulnerabilities = append(allVulnerabilities, vulnerabilities...) opts.Page++ + if opts.Page > 10 { - return allVuln, true, nil + return allVulnerabilities, true, nil } } - } else { - response, err := api.ListVulnerabilities(opts) - if err != nil { - return nil, false, err - } - allVuln = append(allVuln, response.Payload...) + + return allVulnerabilities, false, nil } - return allVuln, false, nil + + vulnerabilities, err := api.ListVulnerabilities(opts) + if err != nil { + return nil, false, err + } + + return vulnerabilities, false, nil } diff --git a/pkg/api/vulnerability_handler.go b/pkg/api/vulnerability_handler.go index 5d4924ccd..708cac231 100644 --- a/pkg/api/vulnerability_handler.go +++ b/pkg/api/vulnerability_handler.go @@ -14,10 +14,12 @@ package api import ( + "context" "fmt" "slices" "strings" + v2client "github.com/goharbor/go-client/pkg/sdk/v2.0/client" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/securityhub" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" @@ -40,7 +42,7 @@ func GetVulnerabilitySummary(withDangerousArtifact, withDangerousCVE bool) (*sec return response, nil } -func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVulnerabilitiesOK, error) { +func ListVulnerabilities(opts ...ListVulnerabilityOptions) ([]*models.VulnerabilityItem, error) { ctx, client, err := utils.ContextWithClient() if err != nil { return nil, err @@ -55,53 +57,26 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul if err != nil { return nil, err } + listFlags.WithTag = true if listFlags.Fixable || listFlags.Exclude != "" { excludeMap := parseVulnerabilityExcludeMap(listFlags.Exclude) - var res []*models.VulnerabilityItem - start := int((listFlags.Page - 1) * listFlags.PageSize) - end := int(listFlags.Page * listFlags.PageSize) - count := 0 - for page := int64(1); count < end; page++ { - response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ - Page: &page, - PageSize: &listFlags.PageSize, - Q: &q, - WithTag: &listFlags.WithTag, - }) - if err != nil { - return nil, err - } - if len(response.Payload) == 0 { - break - } - response.Payload = filterVulnerabilities( - response.Payload, - listFlags.Fixable, - listFlags.Exclude != "", - excludeMap, - ) - - for _, vul := range response.Payload { - if count >= start && count < end { - res = append(res, vul) - } - count++ - if count >= end { - break - } - } - } - if len(res) == 0 { - return &securityhub.ListVulnerabilitiesOK{ - Payload: nil, - }, nil + res, err := listFilteredVulnerabilities( + ctx, + client, + listFlags.Page, + listFlags.PageSize, + q, + listFlags.WithTag, + excludeMap, + listFlags.Fixable, + ) + if err != nil { + return nil, err } - return &securityhub.ListVulnerabilitiesOK{ - Payload: res, - }, nil + return res, nil } response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ @@ -114,12 +89,69 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul return nil, err } if len(response.Payload) == 0 { - response.Payload = nil - response.XTotalCount = 0 - return response, nil + return nil, nil } - return response, nil + return response.Payload, nil +} + +func listFilteredVulnerabilities( + ctx context.Context, + client *v2client.HarborAPI, + page int64, + pageSize int64, + query string, + withTag bool, + excludeMap map[string]string, + fixable bool, +) ([]*models.VulnerabilityItem, error) { + excludeProvided := len(excludeMap) > 0 + start := int((page - 1) * pageSize) + end := int(page * pageSize) + + filteredCount := 0 + var filteredVulnerabilities []*models.VulnerabilityItem + + for currentPage := int64(1); filteredCount < end; currentPage++ { + response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{ + Page: ¤tPage, + PageSize: &pageSize, + Q: &query, + WithTag: &withTag, + }) + if err != nil { + return nil, err + } + + payload := response.Payload + if len(payload) == 0 { + break + } + + payload = filterVulnerabilities( + payload, + fixable, + excludeProvided, + excludeMap, + ) + + for _, vul := range payload { + if filteredCount >= start && filteredCount < end { + filteredVulnerabilities = append(filteredVulnerabilities, vul) + } + + filteredCount++ + if filteredCount >= end { + break + } + } + } + + if len(filteredVulnerabilities) == 0 { + return nil, nil + } + + return filteredVulnerabilities, nil } func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { @@ -142,16 +174,8 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", cvssRange)) } if opts.Severity != "" { - allowedSeverities := map[string]bool{ - "Critical": true, - "High": true, - "Medium": true, - "Low": true, - "n/a": true, - "None": true, - } - if !allowedSeverities[opts.Severity] { - return "", fmt.Errorf("invalid severity value: %s. Allowed values are: Low, Medium, High, Critical, n/a, None", opts.Severity) + if err := validateVulnerabilitySeverity(opts.Severity); err != nil { + return "", err } queries = append(queries, fmt.Sprintf("severity=%s", opts.Severity)) } @@ -178,10 +202,19 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) { return strings.Join(queries, ","), nil } +func validateVulnerabilitySeverity(severity string) error { + switch severity { + case "Critical", "High", "Medium", "Low", "n/a", "None": + return nil + default: + return fmt.Errorf("invalid severity value: %s. Allowed values are: Low, Medium, High, Critical, n/a, None", severity) + } +} + func parseVulnerabilityExcludeMap(exclude string) map[string]string { excludeMap := make(map[string]string) for _, query := range strings.Split(exclude, ",") { - parts := strings.SplitN(strings.TrimSpace(query), "=", 2) + parts := strings.Split(strings.TrimSpace(query), "=") if len(parts) == 2 { excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } From b6560897353a2f4c0aec88c27fea334087027927 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 14 Apr 2026 21:54:23 +0530 Subject: [PATCH 8/9] feat: add CSV output format support using gocsv Signed-off-by: Sypher845 --- cmd/harbor/root/cmd.go | 2 +- go.mod | 3 ++- go.sum | 2 ++ pkg/utils/helper.go | 4 ++++ pkg/utils/helper_test.go | 14 ++++++++++++++ pkg/utils/utils.go | 12 ++++++++++++ 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 33509cbf8..039e39ea6 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -85,7 +85,7 @@ harbor help }, } - root.PersistentFlags().StringVarP(&output, "output-format", "o", "", "Output format. One of: json|yaml") + root.PersistentFlags().StringVarP(&output, "output-format", "o", "", "Output format. One of: json|yaml|csv") root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/harbor-cli/config.yaml)") root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") diff --git a/go.mod b/go.mod index f62a99455..a7012ebee 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.11.6 + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/sahilm/fuzzy v0.1.1 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 @@ -23,7 +25,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20241222104055-e1130b311607 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect diff --git a/go.sum b/go.sum index 72d34d58b..9ae1041e4 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/goharbor/go-client v0.213.1 h1:bohLwNog8uv8FKhIZ0SHiaDbYr3X/1hovgo5fqZWMdo= diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 3436d1fda..9255129c7 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -216,6 +216,10 @@ func PrintFormat[T any](resp T, format string) error { PrintPayloadInYAMLFormat(resp) return nil } + if format == "csv" { + PrintPayloadInCSVFormat(resp) + return nil + } return fmt.Errorf("unable to output in the specified '%s' format", format) } diff --git a/pkg/utils/helper_test.go b/pkg/utils/helper_test.go index b58ca0945..33c3aba4d 100644 --- a/pkg/utils/helper_test.go +++ b/pkg/utils/helper_test.go @@ -202,6 +202,20 @@ func TestPrintFormat(t *testing.T) { assert.NoError(t, err) assert.Contains(t, buf.String(), "foo: bar") + // CSV + buf.Reset() + r, w, _ = os.Pipe() + os.Stdout = w + err = utils.PrintFormat([]payload{obj}, "csv") + w.Close() + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("Failed to capture output: %v", err) + } + os.Stdout = oldOut + assert.NoError(t, err) + assert.Contains(t, buf.String(), "Foo") + assert.Contains(t, buf.String(), "bar") + // unsupported err = utils.PrintFormat(obj, "xml") assert.Error(t, err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index de1b83bc5..118b2a364 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -25,6 +25,7 @@ import ( "syscall" "github.com/charmbracelet/bubbles/table" + "github.com/gocarina/gocsv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/user" uview "github.com/goharbor/harbor-cli/pkg/views/user/select" log "github.com/sirupsen/logrus" @@ -63,6 +64,17 @@ func PrintPayloadInYAMLFormat(payload any) { fmt.Println(string(yamlStr)) } +func PrintPayloadInCSVFormat(payload any) { + if payload == nil { + return + } + + err := gocsv.Marshal(payload, os.Stdout) + if err != nil { + panic(err) + } +} + func ParseProjectRepo(projectRepo string) (project, repo string, err error) { split := strings.SplitN(projectRepo, "/", 2) // splits only at first slash if len(split) != 2 { From 5f678ca625c668c9b24e8a23a1f9f67bbc14869f Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 14 Apr 2026 21:57:12 +0530 Subject: [PATCH 9/9] doc(csv): docs for output-format csv Signed-off-by: Sypher845 --- doc/cli-docs/harbor-artifact-delete.md | 2 +- doc/cli-docs/harbor-artifact-label-add.md | 2 +- doc/cli-docs/harbor-artifact-label-delete.md | 2 +- doc/cli-docs/harbor-artifact-label-list.md | 2 +- doc/cli-docs/harbor-artifact-label.md | 2 +- doc/cli-docs/harbor-artifact-list.md | 2 +- doc/cli-docs/harbor-artifact-scan-start.md | 2 +- doc/cli-docs/harbor-artifact-scan-stop.md | 2 +- doc/cli-docs/harbor-artifact-scan.md | 2 +- doc/cli-docs/harbor-artifact-tags-create.md | 2 +- doc/cli-docs/harbor-artifact-tags-delete.md | 2 +- doc/cli-docs/harbor-artifact-tags-list.md | 2 +- doc/cli-docs/harbor-artifact-tags.md | 2 +- doc/cli-docs/harbor-artifact-view.md | 2 +- doc/cli-docs/harbor-artifact.md | 2 +- doc/cli-docs/harbor-config-apply.md | 2 +- doc/cli-docs/harbor-config-view.md | 2 +- doc/cli-docs/harbor-config.md | 2 +- doc/cli-docs/harbor-context-delete.md | 2 +- doc/cli-docs/harbor-context-get.md | 2 +- doc/cli-docs/harbor-context-list.md | 2 +- doc/cli-docs/harbor-context-switch.md | 2 +- doc/cli-docs/harbor-context-update.md | 2 +- doc/cli-docs/harbor-context.md | 2 +- doc/cli-docs/harbor-cve-allowlist-add.md | 2 +- doc/cli-docs/harbor-cve-allowlist-list.md | 2 +- doc/cli-docs/harbor-cve-allowlist.md | 2 +- doc/cli-docs/harbor-health.md | 2 +- doc/cli-docs/harbor-info.md | 2 +- doc/cli-docs/harbor-instance-create.md | 2 +- doc/cli-docs/harbor-instance-delete.md | 2 +- doc/cli-docs/harbor-instance-list.md | 2 +- doc/cli-docs/harbor-instance.md | 2 +- doc/cli-docs/harbor-label-create.md | 2 +- doc/cli-docs/harbor-label-delete.md | 2 +- doc/cli-docs/harbor-label-list.md | 2 +- doc/cli-docs/harbor-label-update.md | 2 +- doc/cli-docs/harbor-label.md | 2 +- doc/cli-docs/harbor-ldap-import.md | 2 +- doc/cli-docs/harbor-ldap-ping.md | 2 +- doc/cli-docs/harbor-ldap-search.md | 2 +- doc/cli-docs/harbor-ldap.md | 2 +- doc/cli-docs/harbor-login.md | 2 +- doc/cli-docs/harbor-logs.md | 2 +- doc/cli-docs/harbor-password.md | 2 +- doc/cli-docs/harbor-project-config-list.md | 2 +- doc/cli-docs/harbor-project-config-update.md | 2 +- doc/cli-docs/harbor-project-config.md | 2 +- doc/cli-docs/harbor-project-create.md | 2 +- doc/cli-docs/harbor-project-delete.md | 2 +- doc/cli-docs/harbor-project-list.md | 2 +- doc/cli-docs/harbor-project-logs.md | 2 +- doc/cli-docs/harbor-project-member-create.md | 2 +- doc/cli-docs/harbor-project-member-delete.md | 2 +- doc/cli-docs/harbor-project-member-list.md | 2 +- doc/cli-docs/harbor-project-member-update.md | 2 +- doc/cli-docs/harbor-project-member.md | 2 +- doc/cli-docs/harbor-project-robot-create.md | 2 +- doc/cli-docs/harbor-project-robot-delete.md | 2 +- doc/cli-docs/harbor-project-robot-list.md | 2 +- doc/cli-docs/harbor-project-robot-refresh.md | 2 +- doc/cli-docs/harbor-project-robot-update.md | 2 +- doc/cli-docs/harbor-project-robot-view.md | 2 +- doc/cli-docs/harbor-project-robot.md | 2 +- doc/cli-docs/harbor-project-search.md | 2 +- doc/cli-docs/harbor-project-view.md | 2 +- doc/cli-docs/harbor-project.md | 2 +- doc/cli-docs/harbor-quota-list.md | 2 +- doc/cli-docs/harbor-quota-update.md | 2 +- doc/cli-docs/harbor-quota-view.md | 2 +- doc/cli-docs/harbor-quota.md | 2 +- doc/cli-docs/harbor-registry-create.md | 2 +- doc/cli-docs/harbor-registry-delete.md | 2 +- doc/cli-docs/harbor-registry-list.md | 2 +- doc/cli-docs/harbor-registry-update.md | 2 +- doc/cli-docs/harbor-registry-view.md | 2 +- doc/cli-docs/harbor-registry.md | 2 +- doc/cli-docs/harbor-replication-executions-list.md | 2 +- doc/cli-docs/harbor-replication-executions-view.md | 2 +- doc/cli-docs/harbor-replication-executions.md | 2 +- doc/cli-docs/harbor-replication-log.md | 2 +- doc/cli-docs/harbor-replication-policies-create.md | 2 +- doc/cli-docs/harbor-replication-policies-delete.md | 2 +- doc/cli-docs/harbor-replication-policies-list.md | 2 +- doc/cli-docs/harbor-replication-policies-update.md | 2 +- doc/cli-docs/harbor-replication-policies-view.md | 2 +- doc/cli-docs/harbor-replication-policies.md | 2 +- doc/cli-docs/harbor-replication-start.md | 2 +- doc/cli-docs/harbor-replication-stop.md | 2 +- doc/cli-docs/harbor-replication.md | 2 +- doc/cli-docs/harbor-repo-delete.md | 2 +- doc/cli-docs/harbor-repo-list.md | 2 +- doc/cli-docs/harbor-repo-search.md | 2 +- doc/cli-docs/harbor-repo-view.md | 2 +- doc/cli-docs/harbor-repo.md | 2 +- doc/cli-docs/harbor-robot-create.md | 2 +- doc/cli-docs/harbor-robot-delete.md | 2 +- doc/cli-docs/harbor-robot-list.md | 2 +- doc/cli-docs/harbor-robot-refresh.md | 2 +- doc/cli-docs/harbor-robot-update.md | 2 +- doc/cli-docs/harbor-robot-view.md | 2 +- doc/cli-docs/harbor-robot.md | 2 +- doc/cli-docs/harbor-scan-all-metrics.md | 2 +- doc/cli-docs/harbor-scan-all-run.md | 2 +- doc/cli-docs/harbor-scan-all-stop.md | 2 +- doc/cli-docs/harbor-scan-all-update-schedule.md | 2 +- doc/cli-docs/harbor-scan-all-view-schedule.md | 2 +- doc/cli-docs/harbor-scan-all.md | 2 +- doc/cli-docs/harbor-scanner-create.md | 2 +- doc/cli-docs/harbor-scanner-delete.md | 2 +- doc/cli-docs/harbor-scanner-list.md | 2 +- doc/cli-docs/harbor-scanner-metadata.md | 2 +- doc/cli-docs/harbor-scanner-set-default.md | 2 +- doc/cli-docs/harbor-scanner-update.md | 2 +- doc/cli-docs/harbor-scanner-view.md | 2 +- doc/cli-docs/harbor-scanner.md | 2 +- doc/cli-docs/harbor-schedule-list.md | 2 +- doc/cli-docs/harbor-schedule.md | 2 +- doc/cli-docs/harbor-tag-immutable-create.md | 2 +- doc/cli-docs/harbor-tag-immutable-delete.md | 2 +- doc/cli-docs/harbor-tag-immutable-list.md | 2 +- doc/cli-docs/harbor-tag-immutable.md | 2 +- doc/cli-docs/harbor-tag.md | 2 +- doc/cli-docs/harbor-user-create.md | 2 +- doc/cli-docs/harbor-user-delete.md | 2 +- doc/cli-docs/harbor-user-elevate.md | 2 +- doc/cli-docs/harbor-user-list.md | 2 +- doc/cli-docs/harbor-user-password.md | 2 +- doc/cli-docs/harbor-user.md | 2 +- doc/cli-docs/harbor-version.md | 2 +- doc/cli-docs/harbor-vulnerability-list.md | 2 +- doc/cli-docs/harbor-vulnerability-summary.md | 2 +- doc/cli-docs/harbor-vulnerability.md | 2 +- doc/cli-docs/harbor-webhook-create.md | 2 +- doc/cli-docs/harbor-webhook-delete.md | 2 +- doc/cli-docs/harbor-webhook-edit.md | 2 +- doc/cli-docs/harbor-webhook-list.md | 2 +- doc/cli-docs/harbor-webhook.md | 2 +- doc/cli-docs/harbor.md | 2 +- doc/man-docs/man1/harbor-artifact-delete.1 | 2 +- doc/man-docs/man1/harbor-artifact-label-add.1 | 2 +- doc/man-docs/man1/harbor-artifact-label-delete.1 | 2 +- doc/man-docs/man1/harbor-artifact-label-list.1 | 2 +- doc/man-docs/man1/harbor-artifact-label.1 | 2 +- doc/man-docs/man1/harbor-artifact-list.1 | 2 +- doc/man-docs/man1/harbor-artifact-scan-start.1 | 2 +- doc/man-docs/man1/harbor-artifact-scan-stop.1 | 2 +- doc/man-docs/man1/harbor-artifact-scan.1 | 2 +- doc/man-docs/man1/harbor-artifact-tags-create.1 | 2 +- doc/man-docs/man1/harbor-artifact-tags-delete.1 | 2 +- doc/man-docs/man1/harbor-artifact-tags-list.1 | 2 +- doc/man-docs/man1/harbor-artifact-tags.1 | 2 +- doc/man-docs/man1/harbor-artifact-view.1 | 2 +- doc/man-docs/man1/harbor-artifact.1 | 2 +- doc/man-docs/man1/harbor-config-apply.1 | 2 +- doc/man-docs/man1/harbor-config-view.1 | 2 +- doc/man-docs/man1/harbor-config.1 | 2 +- doc/man-docs/man1/harbor-context-delete.1 | 2 +- doc/man-docs/man1/harbor-context-get.1 | 2 +- doc/man-docs/man1/harbor-context-list.1 | 2 +- doc/man-docs/man1/harbor-context-switch.1 | 2 +- doc/man-docs/man1/harbor-context-update.1 | 2 +- doc/man-docs/man1/harbor-context.1 | 2 +- doc/man-docs/man1/harbor-cve-allowlist-add.1 | 2 +- doc/man-docs/man1/harbor-cve-allowlist-list.1 | 2 +- doc/man-docs/man1/harbor-cve-allowlist.1 | 2 +- doc/man-docs/man1/harbor-health.1 | 2 +- doc/man-docs/man1/harbor-info.1 | 2 +- doc/man-docs/man1/harbor-instance-create.1 | 2 +- doc/man-docs/man1/harbor-instance-delete.1 | 2 +- doc/man-docs/man1/harbor-instance-list.1 | 2 +- doc/man-docs/man1/harbor-instance.1 | 2 +- doc/man-docs/man1/harbor-label-create.1 | 2 +- doc/man-docs/man1/harbor-label-delete.1 | 2 +- doc/man-docs/man1/harbor-label-list.1 | 2 +- doc/man-docs/man1/harbor-label-update.1 | 2 +- doc/man-docs/man1/harbor-label.1 | 2 +- doc/man-docs/man1/harbor-ldap-import.1 | 2 +- doc/man-docs/man1/harbor-ldap-ping.1 | 2 +- doc/man-docs/man1/harbor-ldap-search.1 | 2 +- doc/man-docs/man1/harbor-ldap.1 | 2 +- doc/man-docs/man1/harbor-login.1 | 2 +- doc/man-docs/man1/harbor-logs.1 | 2 +- doc/man-docs/man1/harbor-password.1 | 2 +- doc/man-docs/man1/harbor-project-config-list.1 | 2 +- doc/man-docs/man1/harbor-project-config-update.1 | 2 +- doc/man-docs/man1/harbor-project-config.1 | 2 +- doc/man-docs/man1/harbor-project-create.1 | 2 +- doc/man-docs/man1/harbor-project-delete.1 | 2 +- doc/man-docs/man1/harbor-project-list.1 | 2 +- doc/man-docs/man1/harbor-project-logs.1 | 2 +- doc/man-docs/man1/harbor-project-member-create.1 | 2 +- doc/man-docs/man1/harbor-project-member-delete.1 | 2 +- doc/man-docs/man1/harbor-project-member-list.1 | 2 +- doc/man-docs/man1/harbor-project-member-update.1 | 2 +- doc/man-docs/man1/harbor-project-member.1 | 2 +- doc/man-docs/man1/harbor-project-robot-create.1 | 2 +- doc/man-docs/man1/harbor-project-robot-delete.1 | 2 +- doc/man-docs/man1/harbor-project-robot-list.1 | 2 +- doc/man-docs/man1/harbor-project-robot-refresh.1 | 2 +- doc/man-docs/man1/harbor-project-robot-update.1 | 2 +- doc/man-docs/man1/harbor-project-robot-view.1 | 2 +- doc/man-docs/man1/harbor-project-robot.1 | 2 +- doc/man-docs/man1/harbor-project-search.1 | 2 +- doc/man-docs/man1/harbor-project-view.1 | 2 +- doc/man-docs/man1/harbor-project.1 | 2 +- doc/man-docs/man1/harbor-quota-list.1 | 2 +- doc/man-docs/man1/harbor-quota-update.1 | 2 +- doc/man-docs/man1/harbor-quota-view.1 | 2 +- doc/man-docs/man1/harbor-quota.1 | 2 +- doc/man-docs/man1/harbor-registry-create.1 | 2 +- doc/man-docs/man1/harbor-registry-delete.1 | 2 +- doc/man-docs/man1/harbor-registry-list.1 | 2 +- doc/man-docs/man1/harbor-registry-update.1 | 2 +- doc/man-docs/man1/harbor-registry-view.1 | 2 +- doc/man-docs/man1/harbor-registry.1 | 2 +- doc/man-docs/man1/harbor-replication-executions-list.1 | 2 +- doc/man-docs/man1/harbor-replication-executions-view.1 | 2 +- doc/man-docs/man1/harbor-replication-executions.1 | 2 +- doc/man-docs/man1/harbor-replication-log.1 | 2 +- doc/man-docs/man1/harbor-replication-policies-create.1 | 2 +- doc/man-docs/man1/harbor-replication-policies-delete.1 | 2 +- doc/man-docs/man1/harbor-replication-policies-list.1 | 2 +- doc/man-docs/man1/harbor-replication-policies-update.1 | 2 +- doc/man-docs/man1/harbor-replication-policies-view.1 | 2 +- doc/man-docs/man1/harbor-replication-policies.1 | 2 +- doc/man-docs/man1/harbor-replication-start.1 | 2 +- doc/man-docs/man1/harbor-replication-stop.1 | 2 +- doc/man-docs/man1/harbor-replication.1 | 2 +- doc/man-docs/man1/harbor-repo-delete.1 | 2 +- doc/man-docs/man1/harbor-repo-list.1 | 2 +- doc/man-docs/man1/harbor-repo-search.1 | 2 +- doc/man-docs/man1/harbor-repo-view.1 | 2 +- doc/man-docs/man1/harbor-repo.1 | 2 +- doc/man-docs/man1/harbor-robot-create.1 | 2 +- doc/man-docs/man1/harbor-robot-delete.1 | 2 +- doc/man-docs/man1/harbor-robot-list.1 | 2 +- doc/man-docs/man1/harbor-robot-refresh.1 | 2 +- doc/man-docs/man1/harbor-robot-update.1 | 2 +- doc/man-docs/man1/harbor-robot-view.1 | 2 +- doc/man-docs/man1/harbor-robot.1 | 2 +- doc/man-docs/man1/harbor-scan-all-metrics.1 | 2 +- doc/man-docs/man1/harbor-scan-all-run.1 | 2 +- doc/man-docs/man1/harbor-scan-all-stop.1 | 2 +- doc/man-docs/man1/harbor-scan-all-update-schedule.1 | 2 +- doc/man-docs/man1/harbor-scan-all-view-schedule.1 | 2 +- doc/man-docs/man1/harbor-scan-all.1 | 2 +- doc/man-docs/man1/harbor-scanner-create.1 | 2 +- doc/man-docs/man1/harbor-scanner-delete.1 | 2 +- doc/man-docs/man1/harbor-scanner-list.1 | 2 +- doc/man-docs/man1/harbor-scanner-metadata.1 | 2 +- doc/man-docs/man1/harbor-scanner-set-default.1 | 2 +- doc/man-docs/man1/harbor-scanner-update.1 | 2 +- doc/man-docs/man1/harbor-scanner-view.1 | 2 +- doc/man-docs/man1/harbor-scanner.1 | 2 +- doc/man-docs/man1/harbor-schedule-list.1 | 2 +- doc/man-docs/man1/harbor-schedule.1 | 2 +- doc/man-docs/man1/harbor-tag-immutable-create.1 | 2 +- doc/man-docs/man1/harbor-tag-immutable-delete.1 | 2 +- doc/man-docs/man1/harbor-tag-immutable-list.1 | 2 +- doc/man-docs/man1/harbor-tag-immutable.1 | 2 +- doc/man-docs/man1/harbor-tag.1 | 2 +- doc/man-docs/man1/harbor-user-create.1 | 2 +- doc/man-docs/man1/harbor-user-delete.1 | 2 +- doc/man-docs/man1/harbor-user-elevate.1 | 2 +- doc/man-docs/man1/harbor-user-list.1 | 2 +- doc/man-docs/man1/harbor-user-password.1 | 2 +- doc/man-docs/man1/harbor-user.1 | 2 +- doc/man-docs/man1/harbor-version.1 | 2 +- doc/man-docs/man1/harbor-vulnerability-list.1 | 2 +- doc/man-docs/man1/harbor-vulnerability-summary.1 | 2 +- doc/man-docs/man1/harbor-vulnerability.1 | 2 +- doc/man-docs/man1/harbor-webhook-create.1 | 2 +- doc/man-docs/man1/harbor-webhook-delete.1 | 2 +- doc/man-docs/man1/harbor-webhook-edit.1 | 2 +- doc/man-docs/man1/harbor-webhook-list.1 | 2 +- doc/man-docs/man1/harbor-webhook.1 | 2 +- doc/man-docs/man1/harbor.1 | 2 +- 278 files changed, 278 insertions(+), 278 deletions(-) diff --git a/doc/cli-docs/harbor-artifact-delete.md b/doc/cli-docs/harbor-artifact-delete.md index f79da59e6..72bfc679b 100644 --- a/doc/cli-docs/harbor-artifact-delete.md +++ b/doc/cli-docs/harbor-artifact-delete.md @@ -22,7 +22,7 @@ harbor artifact delete [flags] ```sh -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) - -o, --output-format string Output format. One of: json|yaml + -o, --output-format string Output format. One of: json|yaml|csv -v, --verbose verbose output ``` diff --git a/doc/cli-docs/harbor-artifact-label-add.md b/doc/cli-docs/harbor-artifact-label-add.md index 31c189a47..3354817e3 100644 --- a/doc/cli-docs/harbor-artifact-label-add.md +++ b/doc/cli-docs/harbor-artifact-label-add.md @@ -38,7 +38,7 @@ harbor artifact label add [flags] ```sh -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) - -o, --output-format string Output format. One of: json|yaml + -o, --output-format string Output format. One of: json|yaml|csv -v, --verbose verbose output ``` diff --git a/doc/cli-docs/harbor-artifact-label-delete.md b/doc/cli-docs/harbor-artifact-label-delete.md index 42d0008d2..0691ccc2c 100644 --- a/doc/cli-docs/harbor-artifact-label-delete.md +++ b/doc/cli-docs/harbor-artifact-label-delete.md @@ -40,7 +40,7 @@ harbor artifact label delete [flags] ```sh -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) - -o, --output-format string Output format. One of: json|yaml + -o, --output-format string Output format. One of: json|yaml|csv -v, --verbose verbose output ``` diff --git a/doc/cli-docs/harbor-artifact-label-list.md b/doc/cli-docs/harbor-artifact-label-list.md index 9254ee144..f68bdcbea 100644 --- a/doc/cli-docs/harbor-artifact-label-list.md +++ b/doc/cli-docs/harbor-artifact-label-list.md @@ -46,7 +46,7 @@ harbor artifact label list [flags] ```sh -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) - -o, --output-format string Output format. One of: json|yaml + -o, --output-format string Output format. One of: json|yaml|csv -v, --verbose verbose output ``` diff --git a/doc/cli-docs/harbor-artifact-label.md b/doc/cli-docs/harbor-artifact-label.md index 6a2666e9e..1b095bd18 100644 --- a/doc/cli-docs/harbor-artifact-label.md +++ b/doc/cli-docs/harbor-artifact-label.md @@ -30,7 +30,7 @@ harbor artifact label del //