Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cmd/state/internal/cmdtree/cmdtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -115,7 +120,7 @@ func New(prime *primer.Values, args ...string) *CmdTree {
newActivateCommand(prime),
newInitCommand(prime),
newPushCommand(prime),
newCveCommand(prime),
cveCmd,
projectsCmd,
authCmd,
exportCmd,
Expand Down
31 changes: 29 additions & 2 deletions cmd/state/internal/cmdtree/cve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ 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 {
runner := cve.NewCve(prime)

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{},
Expand All @@ -25,3 +26,29 @@ 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{
Namespace: &project.Namespaced{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why initialize an empty namespace? Shouldn't this be nil if it wasn't provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we did in other places to set the namespace as an argument, by calling its Set() method as required by the ArgMarshaler interface.

}

return captain.NewCommand(
"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{},
[]*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: params.Namespace,
},
},
func(_ *captain.Command, _ []string) error {
return report.Run(&params)
},
)
}
47 changes: 18 additions & 29 deletions internal/runners/cve/cve.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,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"),
)
}

Expand All @@ -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{
Expand All @@ -120,6 +99,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 [Organization/Project][/RESET]"),
})
}

func (od *outputDataPrinter) MarshalOutput(format output.Format) interface{} {
if format != output.PlainFormatName {
return od.data
Expand All @@ -135,6 +122,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
}

Expand Down Expand Up @@ -162,5 +150,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
}
174 changes: 174 additions & 0 deletions internal/runners/cve/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package cve

import (
"fmt"
"sort"
"strconv"
"time"

"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"
"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 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 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 []model.PackageVulnerability `json:"packages"`
}

type reportDataPrinter struct {
output output.Outputer
data *reportData
}

func (r *Report) Run(params *ReportParams) error {
ns := params.Namespace
if !ns.IsValid() {
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"),
)
}

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")
}

packageVulnerabilities := model.ExtractPackageVulnerabilities(resp.Project.Commit.Ingredients)

reportOutput := &reportData{
Project: ns.String(),
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 (rd *reportDataPrinter) MarshalOutput(format output.Format) interface{} {
if format != output.PlainFormatName {
return rd.data
}
ri := &ReportInfo{
fmt.Sprintf("[ACTIONABLE]%s[/RESET]", rd.data.Project),
rd.data.CommitID,
rd.data.Date.Format("01/02/06"),
}
rd.output.Print(struct {
*ReportInfo `opts:"verticalTable"`
}{ri})

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(rd.data.Histogram))
totalCount := 0
for _, h := range rd.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)
}
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
})

for i, d := range ap.Details {
bar := "├─"
if i == len(ap.Details)-1 {
bar = "└─"
}
severity := d.Severity
if severity == "CRITICAL" {
severity = fmt.Sprintf("[ERROR]%-10s[/RESET]", severity)
}
rd.output.Print(fmt.Sprintf(" %s %-10s [ACTIONABLE]%s[/RESET]", bar, severity, d.CveId))
}
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
}
4 changes: 4 additions & 0 deletions locale/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
37 changes: 34 additions & 3 deletions pkg/platform/api/mediator/model/cve.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -28,8 +57,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 {
Expand Down
Loading