Skip to content

Commit a744965

Browse files
committed
refactor: vuln filtering logic
Signed-off-by: Sypher845 <suyashpatil845@gmail.com>
1 parent 8eb543c commit a744965

5 files changed

Lines changed: 181 additions & 54 deletions

File tree

cmd/harbor/root/vulnerability/list.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package vulnerability
1616
import (
1717
"fmt"
1818

19+
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
1920
"github.com/goharbor/harbor-cli/pkg/api"
2021
"github.com/goharbor/harbor-cli/pkg/utils"
2122
vulnlist "github.com/goharbor/harbor-cli/pkg/views/vulnerability/list"
@@ -42,24 +43,24 @@ func ListVulnerabilitiesCommand() *cobra.Command {
4243
return fmt.Errorf("page size should be less than or equal to 100")
4344
}
4445

45-
response, err := api.ListVulnerabilities(opts)
46+
allVulnerabilities, hasNext, err := fetchVulnerabilities(opts)
4647
if err != nil {
4748
return fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err))
4849
}
4950

50-
if len(response.Payload) == 0 {
51+
if len(allVulnerabilities) == 0 {
5152
log.Info("No vulnerabilities found")
5253
return nil
5354
}
5455

5556
formatFlag := viper.GetString("output-format")
5657
if formatFlag != "" {
57-
err = utils.PrintFormat(response.Payload, formatFlag)
58+
err = utils.PrintFormat(allVulnerabilities, formatFlag)
5859
if err != nil {
5960
return err
6061
}
6162
} else {
62-
vulnlist.ViewVulnerabilityList(response.Payload)
63+
vulnlist.ViewVulnerabilityList(allVulnerabilities, hasNext)
6364
}
6465

6566
return nil
@@ -71,7 +72,7 @@ func ListVulnerabilitiesCommand() *cobra.Command {
7172
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
7273
flags.StringVarP(&opts.Q, "query", "q", "", "Filter vulnerabilities with a ',' separated query string like exact k=v and range k=[min~max]")
7374
flags.StringVarP(&opts.CVEID, "cve-id", "", "", "Filter by exact CVE ID")
74-
flags.StringVarP(&opts.CVSSScore, "cvss-score", "", "", "Filter by CVSS v3 score range (e.g. [7.0~10.0])")
75+
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)")
7576
flags.StringVarP(&opts.Severity, "severity", "", "", "Filter by severity level")
7677
flags.StringVarP(&opts.Repository, "repository", "", "", "Filter by exact repository name")
7778
flags.StringVarP(&opts.ProjectName, "project-name", "", "", "Filter by exact project name")
@@ -83,3 +84,33 @@ func ListVulnerabilitiesCommand() *cobra.Command {
8384

8485
return cmd
8586
}
87+
88+
func fetchVulnerabilities(opts api.ListVulnerabilityOptions) ([]*models.VulnerabilityItem, bool, error) {
89+
var allVuln []*models.VulnerabilityItem
90+
if opts.PageSize == 0 {
91+
log.Debug("Page size is 0, will fetch all vulnerabilities")
92+
opts.PageSize = 100
93+
opts.Page = 1
94+
for {
95+
response, err := api.ListVulnerabilities(opts)
96+
if err != nil {
97+
return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err))
98+
}
99+
if len(response.Payload) == 0 {
100+
break
101+
}
102+
allVuln = append(allVuln, response.Payload...)
103+
opts.Page++
104+
if opts.Page > 10 {
105+
return allVuln, true, nil
106+
}
107+
}
108+
} else {
109+
response, err := api.ListVulnerabilities(opts)
110+
if err != nil {
111+
return nil, false, fmt.Errorf("failed to list vulnerabilities: %v", utils.ParseHarborErrorMsg(err))
112+
}
113+
allVuln = append(allVuln, response.Payload...)
114+
}
115+
return allVuln, false, nil
116+
}

doc/cli-docs/harbor-vulnerability-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ harbor vulnerability list [flags]
2626

2727
```sh
2828
--cve-id string Filter by exact CVE ID
29-
--cvss-score string Filter by CVSS v3 score range (e.g. [7.0~10.0])
29+
--cvss-score string Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0)
3030
--digest string Filter by exact artifact digest
3131
--exclude string Exclude vulnerabilities using a ',' separated query string (e.g., k=v or k=[min~max])
3232
--fixable Only show fixable vulnerabilities

doc/man-docs/man1/harbor-vulnerability-list.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ List vulnerabilities from Harbor Security Hub
1919

2020
.PP
2121
\fB--cvss-score\fP=""
22-
Filter by CVSS v3 score range (e.g. [7.0~10.0])
22+
Filter by CVSS v3 score range (e.g. 7.0~10.0) or exact score (e.g. 7.0)
2323

2424
.PP
2525
\fB--digest\fP=""

pkg/api/vulnerability_handler.go

Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,55 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul
5555
if err != nil {
5656
return nil, err
5757
}
58-
5958
listFlags.WithTag = true
59+
60+
if listFlags.Fixable || listFlags.Exclude != "" {
61+
excludeMap := parseVulnerabilityExcludeMap(listFlags.Exclude)
62+
var res []*models.VulnerabilityItem
63+
start := int((listFlags.Page - 1) * listFlags.PageSize)
64+
end := int(listFlags.Page * listFlags.PageSize)
65+
count := 0
66+
for page := int64(1); count < end; page++ {
67+
response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{
68+
Page: &page,
69+
PageSize: &listFlags.PageSize,
70+
Q: &q,
71+
WithTag: &listFlags.WithTag,
72+
})
73+
if err != nil {
74+
return nil, err
75+
}
76+
if len(response.Payload) == 0 {
77+
break
78+
}
79+
80+
response.Payload = filterVulnerabilities(
81+
response.Payload,
82+
listFlags.Fixable,
83+
listFlags.Exclude != "",
84+
excludeMap,
85+
)
86+
87+
for _, vul := range response.Payload {
88+
if count >= start && count < end {
89+
res = append(res, vul)
90+
}
91+
count++
92+
if count >= end {
93+
break
94+
}
95+
}
96+
}
97+
if len(res) == 0 {
98+
return &securityhub.ListVulnerabilitiesOK{
99+
Payload: nil,
100+
}, nil
101+
}
102+
return &securityhub.ListVulnerabilitiesOK{
103+
Payload: res,
104+
}, nil
105+
}
106+
60107
response, err := client.Securityhub.ListVulnerabilities(ctx, &securityhub.ListVulnerabilitiesParams{
61108
Page: &listFlags.Page,
62109
PageSize: &listFlags.PageSize,
@@ -66,40 +113,10 @@ func ListVulnerabilities(opts ...ListVulnerabilityOptions) (*securityhub.ListVul
66113
if err != nil {
67114
return nil, err
68115
}
69-
70-
if listFlags.Fixable {
71-
response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool {
72-
return vul.FixedVersion == ""
73-
})
74-
}
75-
76-
if listFlags.Exclude != "" {
77-
excludeMap := make(map[string]string)
78-
for _, query := range strings.Split(listFlags.Exclude, ",") {
79-
parts := strings.SplitN(strings.TrimSpace(query), "=", 2)
80-
if len(parts) == 2 {
81-
excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
82-
}
83-
}
84-
85-
response.Payload = slices.DeleteFunc(response.Payload, func(vul *models.VulnerabilityItem) bool {
86-
if val, ok := excludeMap["cve_id"]; ok && vul.CVEID == val {
87-
return true
88-
}
89-
if val, ok := excludeMap["severity"]; ok && strings.EqualFold(vul.Severity, val) {
90-
return true
91-
}
92-
if val, ok := excludeMap["package"]; ok && vul.Package == val {
93-
return true
94-
}
95-
if val, ok := excludeMap["repository_name"]; ok && vul.RepositoryName == val {
96-
return true
97-
}
98-
if val, ok := excludeMap["digest"]; ok && vul.Digest == val {
99-
return true
100-
}
101-
return false
102-
})
116+
if len(response.Payload) == 0 {
117+
response.Payload = nil
118+
response.XTotalCount = 0
119+
return response, nil
103120
}
104121

105122
return response, nil
@@ -111,9 +128,31 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) {
111128
queries = append(queries, fmt.Sprintf("cve_id=%s", opts.CVEID))
112129
}
113130
if opts.CVSSScore != "" {
114-
queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", opts.CVSSScore))
131+
cvssRange := opts.CVSSScore
132+
if strings.Contains(cvssRange, "~") {
133+
parts := strings.Split(cvssRange, "~")
134+
if len(parts) == 2 {
135+
cvssRange = fmt.Sprintf("[%s~%s]", parts[0], parts[1])
136+
} else {
137+
return "", fmt.Errorf("invalid cvss score range: %s. Expected format: min~max", cvssRange)
138+
}
139+
} else {
140+
cvssRange = fmt.Sprintf("[%s~%s]", opts.CVSSScore, opts.CVSSScore)
141+
}
142+
queries = append(queries, fmt.Sprintf("cvss_score_v3=%s", cvssRange))
115143
}
116144
if opts.Severity != "" {
145+
allowedSeverities := map[string]bool{
146+
"Critical": true,
147+
"High": true,
148+
"Medium": true,
149+
"Low": true,
150+
"n/a": true,
151+
"None": true,
152+
}
153+
if !allowedSeverities[opts.Severity] {
154+
return "", fmt.Errorf("invalid severity value: %s. Allowed values are: Low, Medium, High, Critical, n/a, None", opts.Severity)
155+
}
117156
queries = append(queries, fmt.Sprintf("severity=%s", opts.Severity))
118157
}
119158
if opts.Repository != "" {
@@ -136,8 +175,55 @@ func buildVulnerabilityQuery(opts ListVulnerabilityOptions) (string, error) {
136175
queries = append(queries, fmt.Sprintf("digest=%s", opts.Digest))
137176
}
138177
if opts.Q != "" {
178+
// FIXME: Q parameter needs to be converted to standard query format
179+
// This will be addressed in draft PR #731
139180
queries = append(queries, opts.Q)
140181
}
141182

142183
return strings.Join(queries, ","), nil
143184
}
185+
186+
func parseVulnerabilityExcludeMap(exclude string) map[string]string {
187+
excludeMap := make(map[string]string)
188+
for _, query := range strings.Split(exclude, ",") {
189+
parts := strings.SplitN(strings.TrimSpace(query), "=", 2)
190+
if len(parts) == 2 {
191+
excludeMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
192+
}
193+
}
194+
return excludeMap
195+
}
196+
197+
func filterVulnerabilities(
198+
payload []*models.VulnerabilityItem,
199+
fixable bool,
200+
excludeProvided bool,
201+
excludeMap map[string]string,
202+
) []*models.VulnerabilityItem {
203+
return slices.DeleteFunc(payload, func(vul *models.VulnerabilityItem) bool {
204+
if fixable && vul.FixedVersion == "" {
205+
return true
206+
}
207+
208+
if !excludeProvided {
209+
return false
210+
}
211+
212+
if val, ok := excludeMap["cve_id"]; ok && vul.CVEID == val {
213+
return true
214+
}
215+
if val, ok := excludeMap["severity"]; ok && vul.Severity == val {
216+
return true
217+
}
218+
if val, ok := excludeMap["package"]; ok && vul.Package == val {
219+
return true
220+
}
221+
if val, ok := excludeMap["repository_name"]; ok && vul.RepositoryName == val {
222+
return true
223+
}
224+
if val, ok := excludeMap["digest"]; ok && vul.Digest == val {
225+
return true
226+
}
227+
return false
228+
})
229+
}

pkg/views/vulnerability/list/view.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,24 @@ import (
2121
"github.com/charmbracelet/bubbles/table"
2222
"github.com/charmbracelet/x/ansi"
2323
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
24-
"github.com/goharbor/harbor-cli/pkg/api"
24+
"github.com/goharbor/harbor-cli/pkg/utils"
2525
"github.com/goharbor/harbor-cli/pkg/views/base/tablelist"
2626
)
2727

2828
var columns = []table.Column{
2929
{Title: "CVE ID", Width: tablelist.WidthL},
3030
{Title: "Description", Width: tablelist.WidthXXL},
31-
{Title: "Project", Width: tablelist.WidthM},
32-
{Title: "Repository", Width: tablelist.WidthM},
31+
{Title: "Repository", Width: tablelist.WidthL},
3332
{Title: "Digest", Width: tablelist.WidthM},
34-
{Title: "Tags", Width: tablelist.WidthS},
33+
{Title: "Tags", Width: tablelist.WidthM},
3534
{Title: "CVSSV3", Width: tablelist.WidthS},
3635
{Title: "Severity", Width: tablelist.WidthS},
3736
{Title: "Package", Width: tablelist.WidthM},
3837
{Title: "Version", Width: tablelist.WidthM},
3938
{Title: "Fixed", Width: tablelist.WidthM},
4039
}
4140

42-
func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) {
41+
func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem, hasNext bool) {
4342
if vulnerabilities == nil {
4443
fmt.Println("No vulnerabilities found")
4544
return
@@ -54,15 +53,9 @@ func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) {
5453
}
5554

5655
cveLinks[vulnerability.CVEID] = link
57-
projectName := strconv.FormatInt(vulnerability.ProjectID, 10)
58-
project, err := api.GetProject(projectName, true)
59-
if err == nil && project.Payload != nil {
60-
projectName = project.Payload.Name
61-
}
6256
rows = append(rows, table.Row{
6357
vulnerability.CVEID,
6458
vulnerability.Desc,
65-
projectName,
6659
vulnerability.RepositoryName,
6760
vulnerability.Digest,
6861
strings.Join(vulnerability.Tags, ", "),
@@ -83,4 +76,21 @@ func ViewVulnerabilityList(vulnerabilities []*models.VulnerabilityItem) {
8376
}
8477

8578
fmt.Println(tableOutput)
79+
if hasNext {
80+
var hintURL string
81+
if cfg, err := utils.GetCurrentHarborConfig(); err == nil {
82+
for _, cred := range cfg.Credentials {
83+
if cred.Name == cfg.CurrentCredentialName {
84+
hintURL = strings.TrimRight(cred.ServerAddress, "/") + "/harbor/interrogation-services/security-hub"
85+
break
86+
}
87+
}
88+
}
89+
90+
if hintURL != "" {
91+
fmt.Printf("More results at: %s%s%s\n", ansi.SetHyperlink(hintURL), hintURL, ansi.ResetHyperlink())
92+
} else {
93+
fmt.Println("More results available in the Harbor web console")
94+
}
95+
}
8696
}

0 commit comments

Comments
 (0)