From 3596b24b41bc833d6c93a61b03b6a453ebceee43 Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Fri, 5 Feb 2021 12:07:01 -0800 Subject: [PATCH 1/9] add state cve report command --- cmd/state/internal/cmdtree/cmdtree.go | 7 +- cmd/state/internal/cmdtree/cve.go | 24 ++++ internal/runners/cve/cve.go | 22 ++++ internal/runners/cve/report.go | 173 +++++++++++++++++++++++++ pkg/platform/api/mediator/model/cve.go | 6 +- 5 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 internal/runners/cve/report.go diff --git a/cmd/state/internal/cmdtree/cmdtree.go b/cmd/state/internal/cmdtree/cmdtree.go index 85bc49e4c1..67016bb3d3 100644 --- a/cmd/state/internal/cmdtree/cmdtree.go +++ b/cmd/state/internal/cmdtree/cmdtree.go @@ -26,6 +26,11 @@ func New(prime *primer.Values, args ...string) *CmdTree { newLogoutCommand(prime), ) + cveCmd := newCveCommand(prime) + cveCmd.AddChildren( + newReportCommand(prime), + ) + exportCmd := newExportCommand(prime) exportCmd.AddChildren( newRecipeCommand(prime), @@ -115,7 +120,7 @@ func New(prime *primer.Values, args ...string) *CmdTree { newActivateCommand(prime), newInitCommand(prime), newPushCommand(prime), - newCveCommand(prime), + cveCmd, projectsCmd, authCmd, exportCmd, diff --git a/cmd/state/internal/cmdtree/cve.go b/cmd/state/internal/cmdtree/cve.go index 4a041ae7fa..184f017b9b 100644 --- a/cmd/state/internal/cmdtree/cve.go +++ b/cmd/state/internal/cmdtree/cve.go @@ -25,3 +25,27 @@ func newCveCommand(prime *primer.Values) *captain.Command { cmd.SetGroup(PlatformGroup) return cmd } + +func newReportCommand(prime *primer.Values) *captain.Command { + report := cve.NewReport(prime) + params := cve.ReportParams{} + + return captain.NewCommand( + "report", + locale.Tl("cve_report_title", "Print a vulnerability report"), + locale.Tl("cve_report_cmd_description", "Print a vulnerability report"), + prime.Output(), + prime.Config(), + []*captain.Flag{}, + []*captain.Argument{ + { + Name: locale.Tl("cve_report_namespace_arg", "organization/project"), + Description: locale.Tl("cve_report_namespace_arg_description", "The project for which the report is created"), + Value: ¶ms.Namespace, + }, + }, + func(_ *captain.Command, _ []string) error { + return report.Run(¶ms) + }, + ) +} diff --git a/internal/runners/cve/cve.go b/internal/runners/cve/cve.go index 362a8a28c4..8c01baecba 100644 --- a/internal/runners/cve/cve.go +++ b/internal/runners/cve/cve.go @@ -37,11 +37,23 @@ type ByPackageOutput struct { CveCount int `json:"cve_count" locale:"state_cve_package_count,Count"` } +type DetailedByPackageOutput struct { + Name string `json:"name"` + Version string `json:"version"` + Details []medmodel.Vulnerability `json:"cves"` +} + type ProjectInfo struct { Project string `locale:"project,Project"` CommitID string `locale:"commit_id,Commit ID"` } +type ReportInfo struct { + Project string `locale:"project,Project"` + CommitID string `locale:"commit_id,Commit ID"` + Date string `locale:"generated_on,Generated on"` +} + type primeable interface { primer.Projecter primer.Auther @@ -120,6 +132,14 @@ type SeverityCountOutput struct { Severity string `locale:"severity,Severity"` } +func (od *outputDataPrinter) printFooter() { + od.output.Print("") + od.output.Print([]string{ + locale.Tl("cve_hint_report", "To view a detailed report for this runtime, run [ACTIONABLE]state cve report[/RESET]"), + locale.Tl("cve_hint_specific_report", "For a specific runtime, run [ACTIONABLE]state cve report [orgName/projectName][/RESET]"), + }) +} + func (od *outputDataPrinter) MarshalOutput(format output.Format) interface{} { if format != output.PlainFormatName { return od.data @@ -135,6 +155,7 @@ func (od *outputDataPrinter) MarshalOutput(format output.Format) interface{} { if len(od.data.Histogram) == 0 { od.output.Print("") od.output.Print(fmt.Sprintf("[SUCCESS]✔ %s[/RESET]", locale.Tl("no_cves", "No CVEs detected!"))) + od.printFooter() return output.Suppress } @@ -162,5 +183,6 @@ func (od *outputDataPrinter) MarshalOutput(format output.Format) interface{} { od.output.Print(output.Heading(fmt.Sprintf("%d Affected Packages", len(od.data.Packages)))) od.output.Print(od.data.Packages) + od.printFooter() return output.Suppress } diff --git a/internal/runners/cve/report.go b/internal/runners/cve/report.go new file mode 100644 index 0000000000..67cde6b471 --- /dev/null +++ b/internal/runners/cve/report.go @@ -0,0 +1,173 @@ +package cve + +import ( + "fmt" + "strconv" + "time" + + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/output" + medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/platform/model" + "github.com/ActiveState/cli/pkg/project" +) + +type Report struct { + proj *project.Project + auth *authentication.Auth + out output.Outputer +} + +type ReportParams struct { + Namespace *project.Namespaced +} + +func NewReport(prime primeable) *Report { + return &Report{prime.Project(), prime.Auth(), prime.Output()} +} + +type reportData struct { + Project string `json:"project"` + CommitID string `json:"commitID"` + Date time.Time `json:"generated_on"` + Histogram []medmodel.SeverityCount `json:"vulnerability_histogram"` + Packages []DetailedByPackageOutput `json:"packages"` +} + +type reportDataPrinter struct { + output output.Outputer + data *reportData +} + +func (r *Report) Run(params *ReportParams) error { + ns := params.Namespace + if ns == nil { + if r.proj == nil { + return locale.NewInputError("err_no_project") + } + ns = r.proj.Namespace() + } + + if !r.auth.Authenticated() { + return errs.AddTips( + locale.NewError("cve_needs_authentication"), + locale.T("auth_tip"), + ) + } + + resp, err := model.FetchProjectVulnerabilities(r.auth, ns.Owner, ns.Project) + if err != nil { + return locale.WrapError(err, "cve_mediator_resp", "Failed to retrieve vulnerability information") + } + + var packageVulnerabilities []DetailedByPackageOutput + visited := make(map[string]struct{}) + for _, v := range resp.Project.Commit.Ingredients { + if len(v.Vulnerabilities) == 0 { + continue + } + + // Remove this block with story https://www.pivotaltracker.com/story/show/176508772 + // filter double entries + if _, ok := visited[v.Name]; ok { + continue + } + visited[v.Name] = struct{}{} + + cves := make(map[string][]medmodel.Vulnerability) + for _, ve := range v.Vulnerabilities { + if _, ok := cves[ve.Version]; !ok { + cves[ve.Version] = []medmodel.Vulnerability{} + } + cves[ve.Version] = append(cves[ve.Version], ve) + } + + for ver, vuls := range cves { + packageVulnerabilities = append(packageVulnerabilities, DetailedByPackageOutput{ + v.Name, ver, vuls, + }) + } + } + + reportOutput := &reportData{ + Project: resp.Project.Name, + CommitID: resp.Project.Commit.CommitID, + Date: time.Now(), + + Histogram: resp.Project.Commit.VulnerabilityHistogram, + Packages: packageVulnerabilities, + } + + rdp := &reportDataPrinter{ + r.out, + reportOutput, + } + + r.out.Print(rdp) + + return nil +} + +func (od *reportDataPrinter) MarshalOutput(format output.Format) interface{} { + if format != output.PlainFormatName { + return od.data + } + ri := &ReportInfo{ + fmt.Sprintf("[ACTIONABLE]%s[/RESET]", od.data.Project), + od.data.CommitID, + od.data.Date.Format("01/02/06"), + } + od.output.Print(struct { + *ReportInfo `opts:"verticalTable"` + }{ri}) + + if len(od.data.Histogram) == 0 { + od.output.Print("") + od.output.Print(fmt.Sprintf("[SUCCESS]✔ %s[/RESET]", locale.Tl("no_cves", "No CVEs detected!"))) + + return output.Suppress + } + + hist := make([]*SeverityCountOutput, 0, len(od.data.Histogram)) + totalCount := 0 + for _, h := range od.data.Histogram { + totalCount += h.Count + var ho *SeverityCountOutput + if h.Severity == "CRITICAL" { + ho = &SeverityCountOutput{ + fmt.Sprintf("[ERROR]%d[/RESET]", h.Count), + fmt.Sprintf("[ERROR]%s[/RESET]", h.Severity), + } + } else { + ho = &SeverityCountOutput{ + fmt.Sprintf("%d", h.Count), + h.Severity, + } + } + hist = append(hist, ho) + } + od.output.Print(output.Heading(fmt.Sprintf("%d Vulnerabilities", totalCount))) + od.output.Print(hist) + + od.output.Print(output.Heading(fmt.Sprintf("%d Affected Packages", len(od.data.Packages)))) + for _, ap := range od.data.Packages { + od.output.Print(fmt.Sprintf("[NOTICE]%s %s[/RESET]", ap.Name, ap.Version)) + od.output.Print(locale.Tl("report_package_vulnerabilities", "{{.V0}} Vulnerabilities", strconv.Itoa(len(ap.Details)))) + for i, d := range ap.Details { + bar := "├─" + if i == len(ap.Details)-1 { + bar = "└─" + } + severity := d.Severity + if severity == "CRITICAL" { + severity = fmt.Sprintf("[ERROR]%s[/RESET]", severity) + } + od.output.Print(fmt.Sprintf(" %s %-12s [ACTIONABLE]%s[/RESET]", bar, severity, d.CveId)) + } + od.output.Print("") + } + + return output.Suppress +} diff --git a/pkg/platform/api/mediator/model/cve.go b/pkg/platform/api/mediator/model/cve.go index 9902578b90..bc3e0164ab 100644 --- a/pkg/platform/api/mediator/model/cve.go +++ b/pkg/platform/api/mediator/model/cve.go @@ -28,8 +28,10 @@ type IngredientVulnerability struct { } type Vulnerability struct { - Version string `json:"ingredient_version"` - Severity string `json:"severity"` + Version string `json:"ingredient_version"` + Severity string `json:"severity"` + CveId string `json:"cve_id"` + AltIds []string `json:"alt_ids"` } type Organization struct { From 09774276970c0afe4b369a422606ab817c501658 Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 09:46:17 -0800 Subject: [PATCH 2/9] cleanup --- cmd/state/internal/cmdtree/cve.go | 15 +++-- internal/runners/cve/cve.go | 16 +----- internal/runners/cve/report.go | 78 ++++++++++++++++++-------- locale/en-us.yaml | 4 ++ pkg/platform/api/mediator/model/cve.go | 31 +++++++++- 5 files changed, 100 insertions(+), 44 deletions(-) diff --git a/cmd/state/internal/cmdtree/cve.go b/cmd/state/internal/cmdtree/cve.go index 184f017b9b..fd8b800adf 100644 --- a/cmd/state/internal/cmdtree/cve.go +++ b/cmd/state/internal/cmdtree/cve.go @@ -5,6 +5,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/primer" "github.com/ActiveState/cli/internal/runners/cve" + "github.com/ActiveState/cli/pkg/project" ) func newCveCommand(prime *primer.Values) *captain.Command { @@ -12,8 +13,8 @@ func newCveCommand(prime *primer.Values) *captain.Command { cmd := captain.NewCommand( "cve", - locale.Tl("cve_title", "CVE Summary"), - locale.Tl("cve_description", "Show a summary of CVE vulnerabilities"), + locale.Tl("cve_title", "Vulnerability Summary"), + locale.Tl("cve_description", "Show a summary of project vulnerabilities"), prime.Output(), prime.Config(), []*captain.Flag{}, @@ -28,12 +29,14 @@ func newCveCommand(prime *primer.Values) *captain.Command { func newReportCommand(prime *primer.Values) *captain.Command { report := cve.NewReport(prime) - params := cve.ReportParams{} + params := cve.ReportParams{ + Namespace: &project.Namespaced{}, + } return captain.NewCommand( "report", - locale.Tl("cve_report_title", "Print a vulnerability report"), - locale.Tl("cve_report_cmd_description", "Print a vulnerability report"), + locale.Tl("cve_report_title", "Vulnerability Report"), + locale.Tl("cve_report_cmd_description", "Show a detailed report of project vulnerabilities"), prime.Output(), prime.Config(), []*captain.Flag{}, @@ -41,7 +44,7 @@ func newReportCommand(prime *primer.Values) *captain.Command { { Name: locale.Tl("cve_report_namespace_arg", "organization/project"), Description: locale.Tl("cve_report_namespace_arg_description", "The project for which the report is created"), - Value: ¶ms.Namespace, + Value: params.Namespace, }, }, func(_ *captain.Command, _ []string) error { diff --git a/internal/runners/cve/cve.go b/internal/runners/cve/cve.go index 8c01baecba..6a51ac681b 100644 --- a/internal/runners/cve/cve.go +++ b/internal/runners/cve/cve.go @@ -37,23 +37,11 @@ type ByPackageOutput struct { CveCount int `json:"cve_count" locale:"state_cve_package_count,Count"` } -type DetailedByPackageOutput struct { - Name string `json:"name"` - Version string `json:"version"` - Details []medmodel.Vulnerability `json:"cves"` -} - type ProjectInfo struct { Project string `locale:"project,Project"` CommitID string `locale:"commit_id,Commit ID"` } -type ReportInfo struct { - Project string `locale:"project,Project"` - CommitID string `locale:"commit_id,Commit ID"` - Date string `locale:"generated_on,Generated on"` -} - type primeable interface { primer.Projecter primer.Auther @@ -71,8 +59,8 @@ func (c *Cve) Run() error { if !c.auth.Authenticated() { return errs.AddTips( - locale.NewError("cve_needs_authentication", "You need to be authenticated in order to access vulnerability information about your project."), - locale.Tl("auth_tip", "Run `state auth` to authenticate."), + locale.NewError("cve_needs_authentication"), + locale.T("auth_tip"), ) } diff --git a/internal/runners/cve/report.go b/internal/runners/cve/report.go index 67cde6b471..96c703374c 100644 --- a/internal/runners/cve/report.go +++ b/internal/runners/cve/report.go @@ -2,6 +2,7 @@ package cve import ( "fmt" + "sort" "strconv" "time" @@ -20,14 +21,26 @@ type Report struct { out output.Outputer } -type ReportParams struct { - Namespace *project.Namespaced +type ReportInfo struct { + Project string `locale:"project,Project"` + CommitID string `locale:"commit_id,Commit ID"` + Date string `locale:"generated_on,Generated on"` } func NewReport(prime primeable) *Report { return &Report{prime.Project(), prime.Auth(), prime.Output()} } +type DetailedByPackageOutput struct { + Name string `json:"name"` + Version string `json:"version"` + Details []medmodel.Vulnerability `json:"cves"` +} + +type ReportParams struct { + Namespace *project.Namespaced +} + type reportData struct { Project string `json:"project"` CommitID string `json:"commitID"` @@ -92,7 +105,7 @@ func (r *Report) Run(params *ReportParams) error { } reportOutput := &reportData{ - Project: resp.Project.Name, + Project: ns.String(), CommitID: resp.Project.Commit.CommitID, Date: time.Now(), @@ -110,29 +123,29 @@ func (r *Report) Run(params *ReportParams) error { return nil } -func (od *reportDataPrinter) MarshalOutput(format output.Format) interface{} { +func (rd *reportDataPrinter) MarshalOutput(format output.Format) interface{} { if format != output.PlainFormatName { - return od.data + return rd.data } ri := &ReportInfo{ - fmt.Sprintf("[ACTIONABLE]%s[/RESET]", od.data.Project), - od.data.CommitID, - od.data.Date.Format("01/02/06"), + fmt.Sprintf("[ACTIONABLE]%s[/RESET]", rd.data.Project), + rd.data.CommitID, + rd.data.Date.Format("01/02/06"), } - od.output.Print(struct { + rd.output.Print(struct { *ReportInfo `opts:"verticalTable"` }{ri}) - if len(od.data.Histogram) == 0 { - od.output.Print("") - od.output.Print(fmt.Sprintf("[SUCCESS]✔ %s[/RESET]", locale.Tl("no_cves", "No CVEs detected!"))) + if len(rd.data.Histogram) == 0 { + rd.output.Print("") + rd.output.Print(fmt.Sprintf("[SUCCESS]✔ %s[/RESET]", locale.Tl("no_cves", "No CVEs detected!"))) return output.Suppress } - hist := make([]*SeverityCountOutput, 0, len(od.data.Histogram)) + hist := make([]*SeverityCountOutput, 0, len(rd.data.Histogram)) totalCount := 0 - for _, h := range od.data.Histogram { + for _, h := range rd.data.Histogram { totalCount += h.Count var ho *SeverityCountOutput if h.Severity == "CRITICAL" { @@ -148,13 +161,28 @@ func (od *reportDataPrinter) MarshalOutput(format output.Format) interface{} { } hist = append(hist, ho) } - od.output.Print(output.Heading(fmt.Sprintf("%d Vulnerabilities", totalCount))) - od.output.Print(hist) + rd.output.Print(output.Heading(fmt.Sprintf("%d Vulnerabilities", totalCount))) + rd.output.Print(hist) + + rd.output.Print(output.Heading(fmt.Sprintf("%d Affected Packages", len(rd.data.Packages)))) + for _, ap := range rd.data.Packages { + rd.output.Print(fmt.Sprintf("[NOTICE]%s %s[/RESET]", ap.Name, ap.Version)) + rd.output.Print(locale.Tl("report_package_vulnerabilities", "{{.V0}} Vulnerabilities", strconv.Itoa(len(ap.Details)))) + + sort.SliceStable(ap.Details, func(i, j int) bool { + sevI := ap.Details[i].Severity + sevJ := ap.Details[j].Severity + si := medmodel.ParseSeverityIndex(sevI) + sj := medmodel.ParseSeverityIndex(sevJ) + if si < sj { + return true + } + if si == sj { + return sevI < sevJ + } + return false + }) - od.output.Print(output.Heading(fmt.Sprintf("%d Affected Packages", len(od.data.Packages)))) - for _, ap := range od.data.Packages { - od.output.Print(fmt.Sprintf("[NOTICE]%s %s[/RESET]", ap.Name, ap.Version)) - od.output.Print(locale.Tl("report_package_vulnerabilities", "{{.V0}} Vulnerabilities", strconv.Itoa(len(ap.Details)))) for i, d := range ap.Details { bar := "├─" if i == len(ap.Details)-1 { @@ -162,12 +190,16 @@ func (od *reportDataPrinter) MarshalOutput(format output.Format) interface{} { } severity := d.Severity if severity == "CRITICAL" { - severity = fmt.Sprintf("[ERROR]%s[/RESET]", severity) + severity = fmt.Sprintf("[ERROR]%-10s[/RESET]", severity) } - od.output.Print(fmt.Sprintf(" %s %-12s [ACTIONABLE]%s[/RESET]", bar, severity, d.CveId)) + rd.output.Print(fmt.Sprintf(" %s %-10s [ACTIONABLE]%s[/RESET]", bar, severity, d.CveId)) } - od.output.Print("") + rd.output.Print("") } + rd.output.Print("") + rd.output.Print([]string{ + locale.Tl("cve_report_hint_cve", "To view a specific CVE, run [ACTIONABLE]state cve open [cve-id][/RESET]."), + }) return output.Suppress } diff --git a/locale/en-us.yaml b/locale/en-us.yaml index 40a72b6291..68b4862c0e 100644 --- a/locale/en-us.yaml +++ b/locale/en-us.yaml @@ -1633,3 +1633,7 @@ err_lock_version_invalid: other: "The locked version '{{.V0}}' could not be verified, are you sure it's valid?" config_get_error: other: Failed to read configuration. +cve_needs_authentication: + other: You need to be authenticated in order to access vulnerability information about your project. +auth_tip: + other: Run `state auth` to authenticate. diff --git a/pkg/platform/api/mediator/model/cve.go b/pkg/platform/api/mediator/model/cve.go index bc3e0164ab..d013be87a4 100644 --- a/pkg/platform/api/mediator/model/cve.go +++ b/pkg/platform/api/mediator/model/cve.go @@ -1,6 +1,35 @@ package model -import "github.com/go-openapi/strfmt" +import ( + "strings" + + "github.com/go-openapi/strfmt" +) + +type Severity int + +const ( + Critical Severity = iota + High + Moderate + Low + Unknown +) + +func ParseSeverityIndex(severity string) Severity { + switch strings.ToUpper(severity) { + case "CRITICAL": + return Critical + case "HIGH": + return High + case "MODERATE": + return Moderate + case "LOW": + return Low + default: + return Unknown + } +} type ProjectVulnerabilities struct { Project struct { From 47828fe72be794a80f3ef4f03b5a44a4ce3c8eed Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 10:01:25 -0800 Subject: [PATCH 3/9] add integration tests --- test/integration/cve_int_test.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/integration/cve_int_test.go b/test/integration/cve_int_test.go index 6866b4f843..537c2e501c 100644 --- a/test/integration/cve_int_test.go +++ b/test/integration/cve_int_test.go @@ -26,16 +26,38 @@ func (suite *CveIntegrationTestSuite) TestCveSummary() { cp.Expect("ActivePython-3.7") cp.Expect("0b87e7a4-dc62-46fd-825b-9c35a53fe0a2") - cp.Expect("37 Vulnerabilities") + cp.Expect("Vulnerabilities") cp.Expect("6") cp.Expect("CRITICAL") - cp.Expect("10 Affected Packages") + cp.Expect("13 Affected Packages") cp.Expect("tensorflow") cp.Expect("1.12.0") cp.Expect("18") cp.ExpectExitCode(0) } +func (suite *CveIntegrationTestSuite) TestCveReport() { + suite.OnlyRunForTags(tagsuite.Cve) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.LoginAsPersistentUser() + + cp := ts.Spawn("cve", "report", "ActiveState/ActivePython-3.7") + cp.Expect("Commit ID") + + cp.Expect("Vulnerabilities") + cp.Expect("6") + cp.Expect("CRITICAL") + cp.Expect("13 Affected Packages") + cp.Expect("tensorflow") + cp.Expect("1.12.0") + cp.Expect("CRITICAL") + cp.Expect("CVE-2019-16778") + cp.ExpectExitCode(0) +} + func (suite *CveIntegrationTestSuite) TestCveNoVulnerabilities() { suite.OnlyRunForTags(tagsuite.Cve) @@ -48,7 +70,10 @@ func (suite *CveIntegrationTestSuite) TestCveNoVulnerabilities() { cp := ts.Spawn("cve") cp.Expect("No CVEs detected") + cp.ExpectExitCode(0) + cp := ts.Spawn("cve", "report") + cp.Expect("No CVEs detected") cp.ExpectExitCode(0) } From 18535428bb0c0e1fb15f04f0f0730df214f414cb Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 10:01:37 -0800 Subject: [PATCH 4/9] fix default namespace intialization --- internal/runners/cve/report.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/runners/cve/report.go b/internal/runners/cve/report.go index 96c703374c..d755287feb 100644 --- a/internal/runners/cve/report.go +++ b/internal/runners/cve/report.go @@ -8,6 +8,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" + "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" "github.com/ActiveState/cli/pkg/platform/authentication" @@ -56,7 +57,7 @@ type reportDataPrinter struct { func (r *Report) Run(params *ReportParams) error { ns := params.Namespace - if ns == nil { + if !ns.IsValid() { if r.proj == nil { return locale.NewInputError("err_no_project") } @@ -70,6 +71,7 @@ func (r *Report) Run(params *ReportParams) error { ) } + logging.Debug("Fetching vulnerabilities for %s", ns.String()) resp, err := model.FetchProjectVulnerabilities(r.auth, ns.Owner, ns.Project) if err != nil { return locale.WrapError(err, "cve_mediator_resp", "Failed to retrieve vulnerability information") From 09c95ee717b848a4a635f2152900bbf239177260 Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 10:07:41 -0800 Subject: [PATCH 5/9] fix int test --- test/integration/cve_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/cve_int_test.go b/test/integration/cve_int_test.go index 537c2e501c..100ff3a38e 100644 --- a/test/integration/cve_int_test.go +++ b/test/integration/cve_int_test.go @@ -72,7 +72,7 @@ func (suite *CveIntegrationTestSuite) TestCveNoVulnerabilities() { cp.Expect("No CVEs detected") cp.ExpectExitCode(0) - cp := ts.Spawn("cve", "report") + cp = ts.Spawn("cve", "report") cp.Expect("No CVEs detected") cp.ExpectExitCode(0) } From 794c4e6db8939bae9feb7bef8baba10871cd64a2 Mon Sep 17 00:00:00 2001 From: Martin Drohmann Date: Mon, 8 Feb 2021 13:46:49 -0800 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Mike Drakos --- cmd/state/internal/cmdtree/cve.go | 2 +- internal/runners/cve/cve.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/state/internal/cmdtree/cve.go b/cmd/state/internal/cmdtree/cve.go index fd8b800adf..896f7f3c8a 100644 --- a/cmd/state/internal/cmdtree/cve.go +++ b/cmd/state/internal/cmdtree/cve.go @@ -42,7 +42,7 @@ func newReportCommand(prime *primer.Values) *captain.Command { []*captain.Flag{}, []*captain.Argument{ { - Name: locale.Tl("cve_report_namespace_arg", "organization/project"), + Name: locale.Tl("cve_report_namespace_arg", "Organization/Project"), Description: locale.Tl("cve_report_namespace_arg_description", "The project for which the report is created"), Value: params.Namespace, }, diff --git a/internal/runners/cve/cve.go b/internal/runners/cve/cve.go index 6a51ac681b..c50cc63af3 100644 --- a/internal/runners/cve/cve.go +++ b/internal/runners/cve/cve.go @@ -124,7 +124,7 @@ func (od *outputDataPrinter) printFooter() { od.output.Print("") od.output.Print([]string{ locale.Tl("cve_hint_report", "To view a detailed report for this runtime, run [ACTIONABLE]state cve report[/RESET]"), - locale.Tl("cve_hint_specific_report", "For a specific runtime, run [ACTIONABLE]state cve report [orgName/projectName][/RESET]"), + locale.Tl("cve_hint_specific_report", "For a specific runtime, run [ACTIONABLE]state cve report [Organization/Project][/RESET]"), }) } From b8170cd06393720df8645cc72436c7915bdb90d5 Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 13:52:16 -0800 Subject: [PATCH 7/9] use forked project in int test --- test/integration/cve_int_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/cve_int_test.go b/test/integration/cve_int_test.go index 100ff3a38e..655eb7663c 100644 --- a/test/integration/cve_int_test.go +++ b/test/integration/cve_int_test.go @@ -20,7 +20,7 @@ func (suite *CveIntegrationTestSuite) TestCveSummary() { ts.LoginAsPersistentUser() - ts.PrepareActiveStateYAML(`project: https://platform.activestate.com/ActiveState/ActivePython-3.7`) + ts.PrepareActiveStateYAML(`project: https://platform.activestate.com/ActiveState-CLI/VulnerablePython-3.7`) cp := ts.Spawn("cve") cp.Expect("ActivePython-3.7") @@ -44,7 +44,7 @@ func (suite *CveIntegrationTestSuite) TestCveReport() { ts.LoginAsPersistentUser() - cp := ts.Spawn("cve", "report", "ActiveState/ActivePython-3.7") + cp := ts.Spawn("cve", "report", "ActiveState-CLI/VulnerablePython-3.7") cp.Expect("Commit ID") cp.Expect("Vulnerabilities") From 59ea441ec10997955dafdf9f9c0197ea7ddb2a11 Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 15:07:07 -0800 Subject: [PATCH 8/9] move package vulnerability extraction to model --- internal/runners/cve/cve.go | 33 +++++-------------------- internal/runners/cve/report.go | 45 +++++----------------------------- pkg/platform/model/cve.go | 40 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 66 deletions(-) diff --git a/internal/runners/cve/cve.go b/internal/runners/cve/cve.go index c50cc63af3..73075e482d 100644 --- a/internal/runners/cve/cve.go +++ b/internal/runners/cve/cve.go @@ -69,33 +69,12 @@ func (c *Cve) Run() error { return locale.WrapError(err, "cve_mediator_resp", "Failed to retrieve vulnerability information") } - var packageVulnerabilities []ByPackageOutput - visited := make(map[string]struct{}) - for _, v := range resp.Project.Commit.Ingredients { - if len(v.Vulnerabilities) == 0 { - continue - } - - // Remove this block with story https://www.pivotaltracker.com/story/show/176508772 - // filter double entries - if _, ok := visited[v.Name]; ok { - continue - } - visited[v.Name] = struct{}{} - - countByVersion := make(map[string]int) - for _, ve := range v.Vulnerabilities { - if _, ok := countByVersion[ve.Version]; !ok { - countByVersion[ve.Version] = 0 - } - countByVersion[ve.Version]++ - } - - for ver, count := range countByVersion { - packageVulnerabilities = append(packageVulnerabilities, ByPackageOutput{ - v.Name, ver, count, - }) - } + details := model.ExtractPackageVulnerabilities(resp.Project.Commit.Ingredients) + packageVulnerabilities := make([]ByPackageOutput, 0, len(details)) + for _, v := range details { + packageVulnerabilities = append(packageVulnerabilities, ByPackageOutput{ + v.Name, v.Version, len(v.Details), + }) } cveOutput := &outputData{ diff --git a/internal/runners/cve/report.go b/internal/runners/cve/report.go index d755287feb..bb07bae4f9 100644 --- a/internal/runners/cve/report.go +++ b/internal/runners/cve/report.go @@ -32,22 +32,16 @@ func NewReport(prime primeable) *Report { return &Report{prime.Project(), prime.Auth(), prime.Output()} } -type DetailedByPackageOutput struct { - Name string `json:"name"` - Version string `json:"version"` - Details []medmodel.Vulnerability `json:"cves"` -} - type ReportParams struct { Namespace *project.Namespaced } type reportData struct { - Project string `json:"project"` - CommitID string `json:"commitID"` - Date time.Time `json:"generated_on"` - Histogram []medmodel.SeverityCount `json:"vulnerability_histogram"` - Packages []DetailedByPackageOutput `json:"packages"` + Project string `json:"project"` + CommitID string `json:"commitID"` + Date time.Time `json:"generated_on"` + Histogram []medmodel.SeverityCount `json:"vulnerability_histogram"` + Packages []model.PackageVulnerability `json:"packages"` } type reportDataPrinter struct { @@ -77,34 +71,7 @@ func (r *Report) Run(params *ReportParams) error { return locale.WrapError(err, "cve_mediator_resp", "Failed to retrieve vulnerability information") } - var packageVulnerabilities []DetailedByPackageOutput - visited := make(map[string]struct{}) - for _, v := range resp.Project.Commit.Ingredients { - if len(v.Vulnerabilities) == 0 { - continue - } - - // Remove this block with story https://www.pivotaltracker.com/story/show/176508772 - // filter double entries - if _, ok := visited[v.Name]; ok { - continue - } - visited[v.Name] = struct{}{} - - cves := make(map[string][]medmodel.Vulnerability) - for _, ve := range v.Vulnerabilities { - if _, ok := cves[ve.Version]; !ok { - cves[ve.Version] = []medmodel.Vulnerability{} - } - cves[ve.Version] = append(cves[ve.Version], ve) - } - - for ver, vuls := range cves { - packageVulnerabilities = append(packageVulnerabilities, DetailedByPackageOutput{ - v.Name, ver, vuls, - }) - } - } + packageVulnerabilities := model.ExtractPackageVulnerabilities(resp.Project.Commit.Ingredients) reportOutput := &reportData{ Project: ns.String(), diff --git a/pkg/platform/model/cve.go b/pkg/platform/model/cve.go index e563de2b65..7424de3737 100644 --- a/pkg/platform/model/cve.go +++ b/pkg/platform/model/cve.go @@ -9,6 +9,13 @@ import ( "github.com/ActiveState/cli/pkg/platform/authentication" ) +type Vulnerability model.Vulnerability +type PackageVulnerability struct { + Name string `json:"name"` + Version string `json:"version"` + Details []Vulnerability `json:"cves"` +} + // FetchProjectVulnerabilities returns the vulnerability information of a project func FetchProjectVulnerabilities(auth *authentication.Auth, org, project string) (*model.ProjectVulnerabilities, error) { // This should be removed by https://www.pivotaltracker.com/story/show/176508740 @@ -33,3 +40,36 @@ func FetchProjectVulnerabilities(auth *authentication.Auth, org, project string) return &resp, nil } + +func ExtractPackageVulnerabilities(ingredients []model.IngredientVulnerability) []PackageVulnerability { + var packageVulnerabilities []PackageVulnerability + visited := make(map[string]struct{}) + for _, v := range ingredients { + if len(v.Vulnerabilities) == 0 { + continue + } + + // Remove this block with story https://www.pivotaltracker.com/story/show/176508772 + // filter double entries + if _, ok := visited[v.Name]; ok { + continue + } + visited[v.Name] = struct{}{} + + cves := make(map[string][]Vulnerability) + for _, ve := range v.Vulnerabilities { + if _, ok := cves[ve.Version]; !ok { + cves[ve.Version] = []Vulnerability{} + } + cves[ve.Version] = append(cves[ve.Version], Vulnerability(ve)) + } + + for ver, vuls := range cves { + packageVulnerabilities = append(packageVulnerabilities, PackageVulnerability{ + v.Name, ver, vuls, + }) + } + } + + return packageVulnerabilities +} From 5c0458381013abc8032a0565a74a121092dbf80f Mon Sep 17 00:00:00 2001 From: Martin C Drohmann Date: Mon, 8 Feb 2021 15:39:13 -0800 Subject: [PATCH 9/9] fix one more int test --- test/integration/cve_int_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/cve_int_test.go b/test/integration/cve_int_test.go index 655eb7663c..757623170a 100644 --- a/test/integration/cve_int_test.go +++ b/test/integration/cve_int_test.go @@ -23,7 +23,7 @@ func (suite *CveIntegrationTestSuite) TestCveSummary() { ts.PrepareActiveStateYAML(`project: https://platform.activestate.com/ActiveState-CLI/VulnerablePython-3.7`) cp := ts.Spawn("cve") - cp.Expect("ActivePython-3.7") + cp.Expect("VulnerablePython-3.7") cp.Expect("0b87e7a4-dc62-46fd-825b-9c35a53fe0a2") cp.Expect("Vulnerabilities")