Skip to content

Commit

Permalink
Merge state security report into state security.
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchell-as committed Jan 10, 2024
1 parent 4c8e6b3 commit 9b271e2
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 329 deletions.
41 changes: 24 additions & 17 deletions cmd/state/internal/cmdtree/cve.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,22 @@ import (

func newCveCommand(prime *primer.Values) *captain.Command {
runner := cve.NewCve(prime)
params := cve.Params{Namespace: &project.Namespaced{}}

cmd := captain.NewCommand(
"security",
locale.Tl("cve_title", "Vulnerability Summary"),
locale.Tl("cve_description", "Show a summary of project vulnerabilities"),
locale.T("cve_title"),
locale.T("cve_description"),
prime,
[]*captain.Flag{},
[]*captain.Argument{},
func(_ *captain.Command, _ []string) error {
return runner.Run()
[]*captain.Argument{
{
Name: locale.T("cve_namespace_arg"),
Description: locale.T("cve_namespace_arg_description"),
Value: params.Namespace,
},
}, func(_ *captain.Command, _ []string) error {
return runner.Run(&params)
},
)
cmd.SetGroup(PlatformGroup)
Expand All @@ -29,29 +35,30 @@ func newCveCommand(prime *primer.Values) *captain.Command {
return cmd
}

// newReportCommand is a hidden, legacy alias of the parent command
func newReportCommand(prime *primer.Values) *captain.Command {
report := cve.NewReport(prime)
params := cve.ReportParams{
Namespace: &project.Namespaced{},
}
report := cve.NewCve(prime)
params := cve.Params{Namespace: &project.Namespaced{}}

return captain.NewCommand(
cmd := captain.NewCommand(
"report",
locale.Tl("cve_report_title", "Vulnerability Report"),
locale.Tl("cve_report_cmd_description", "Show a detailed report of project vulnerabilities"),
locale.T("cve_title"),
locale.T("cve_description"),
prime,
[]*captain.Flag{},
[]*captain.Argument{
{
Name: locale.Tl("cve_report_namespace_arg", "org/project"),
Description: locale.Tl("cve_report_namespace_arg_description", "The project for which the report is created"),
Name: locale.T("cve_namespace_arg"),
Description: locale.T("cve_namespace_arg_description"),
Value: params.Namespace,
},
},
func(_ *captain.Command, _ []string) error {
}, func(_ *captain.Command, _ []string) error {
return report.Run(&params)
},
).SetSupportsStructuredOutput()
)
cmd.SetSupportsStructuredOutput()
cmd.SetHidden(true)
return cmd
}

func newOpenCommand(prime *primer.Values) *captain.Command {
Expand Down
8 changes: 8 additions & 0 deletions internal/locale/locales/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,14 @@ err_read_projectfile:
other: The activestate.yaml at {{.V0}} could not be read.
err_auth_fail_totp:
other: A two-factor authentication code is required.
cve_title:
other: Vulnerability Summary
cve_description:
other: Show a summary of project vulnerabilities
cve_namespace_arg:
other: org/project
cve_namespace_arg_description:
other: The project for which the report is created
cve_needs_authentication:
other: You need to be authenticated in order to access vulnerability information about your project.
auth_tip:
Expand Down
204 changes: 129 additions & 75 deletions internal/runners/cve/cve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package cve

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

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/locale"
Expand All @@ -14,121 +17,139 @@ import (
"github.com/ActiveState/cli/pkg/project"
)

type primeable interface {
primer.Projecter
primer.Auther
primer.Outputer
}

type Cve struct {
proj *project.Project
auth *authentication.Auth
out output.Outputer
}

type outputData struct {
Project string `json:"project"`
CommitID string `json:"commitID"`
Histogram []medmodel.SeverityCount `json:"vulnerability_histogram"`
Packages []ByPackageOutput `json:"packages"`
type CveInfo struct {
Project string `locale:"project,Project"`
CommitID string `locale:"commit_id,Commit ID"`
Date string `locale:"generated_on,Generated on"`
}

type cveOutput struct {
output output.Outputer
data *outputData
func NewCve(prime primeable) *Cve {
return &Cve{prime.Project(), prime.Auth(), prime.Output()}
}

type ByPackageOutput struct {
Name string `json:"name" locale:"state_cve_package_name,Name"`
Version string `json:"version" locale:"state_cve_package_version,Version"`
CveCount int `json:"cve_count" locale:"state_cve_package_count,Count"`
type Params struct {
Namespace *project.Namespaced
}

type ProjectInfo struct {
Project string `locale:"project,Project"`
CommitID string `locale:"commit_id,Commit ID"`
type cveData 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 primeable interface {
primer.Projecter
primer.Auther
primer.Outputer
}

func NewCve(prime *primer.Values) *Cve {
return &Cve{prime.Project(), prime.Auth(), prime.Output()}
type cveOutput struct {
output output.Outputer
data *cveData
}

func (c *Cve) Run() error {
if c.proj == nil {
return locale.NewError("cve_no_project", "No project found at the current directory.")
func (r *Cve) Run(params *Params) error {
if !params.Namespace.IsValid() && r.proj == nil {
return locale.NewInputError("err_no_project")
}
c.out.Notice(locale.Tr("operating_message", c.proj.NamespaceString(), c.proj.Dir()))

if !c.auth.Authenticated() {
if !r.auth.Authenticated() {
return errs.AddTips(
locale.NewError("cve_needs_authentication"),
locale.T("auth_tip"),
)
}

commitID, err := commitmediator.Get(c.proj)
if err != nil {
return errs.Wrap(err, "Could not get local commit")
}

resp, err := model.FetchCommitVulnerabilities(c.auth, commitID.String())
vulnerabilities, err := r.fetchVulnerabilities(*params.Namespace)
if err != nil {
return locale.WrapError(err, "cve_mediator_resp", "Failed to retrieve vulnerability information")
}

details := model.ExtractPackageVulnerabilities(resp.Sources)
packageVulnerabilities := make([]ByPackageOutput, 0, len(details))
for _, v := range details {
packageVulnerabilities = append(packageVulnerabilities, ByPackageOutput{
v.Name, v.Version, len(v.Details),
})
packageVulnerabilities := model.ExtractPackageVulnerabilities(vulnerabilities.Sources)

ns := params.Namespace
if !ns.IsValid() {
ns = r.proj.Namespace()
}

c.out.Print(&cveOutput{
c.out,
&outputData{
Project: c.proj.Name(),
CommitID: resp.CommitID,
Histogram: resp.VulnerabilityHistogram,
r.out.Print(&cveOutput{
r.out,
&cveData{
Project: ns.String(),
CommitID: vulnerabilities.CommitID,
Date: time.Now(),
Histogram: vulnerabilities.VulnerabilityHistogram,
Packages: packageVulnerabilities,
},
})

return nil
}

func (r *Cve) fetchVulnerabilities(namespaceOverride project.Namespaced) (*medmodel.CommitVulnerabilities, error) {
if namespaceOverride.IsValid() && namespaceOverride.CommitID == nil {
resp, err := model.FetchProjectVulnerabilities(r.auth, namespaceOverride.Owner, namespaceOverride.Project)
if err != nil {
return nil, errs.Wrap(err, "Failed to fetch vulnerability information for project %s", namespaceOverride.String())
}
return resp.Commit, nil
}

// fetch by commit ID
var commitID string
if namespaceOverride.IsValid() {
commitID = namespaceOverride.CommitID.String()
} else {
var err error
commitUUID, err := commitmediator.Get(r.proj)
if err != nil {
return nil, errs.Wrap(err, "Unable to get local commit")
}
commitID = commitUUID.String()
}
resp, err := model.FetchCommitVulnerabilities(r.auth, commitID)
if err != nil {
return nil, errs.Wrap(err, "Failed to fetch vulnerability information for commit %s", commitID)
}
return resp, nil
}

type SeverityCountOutput struct {
Count string `locale:"count,Count" json:"count"`
Severity string `locale:"severity,Severity" json:"severity"`
}

func (od *cveOutput) printFooter() {
od.output.Print("")
od.output.Print([]string{
locale.Tl("cve_hint_report", "To view a detailed report for this runtime, run [ACTIONABLE]state security report[/RESET]"),
locale.Tl("cve_hint_specific_report", "For a specific runtime, run [ACTIONABLE]state security report <org/project>[/RESET]"),
})
}

func (od *cveOutput) MarshalOutput(format output.Format) interface{} {
pi := &ProjectInfo{
od.data.Project,
od.data.CommitID,
func (rd *cveOutput) MarshalOutput(format output.Format) interface{} {
if format != output.PlainFormatName {
return rd.data
}
ri := &CveInfo{
fmt.Sprintf("[ACTIONABLE]%s[/RESET]", rd.data.Project),
rd.data.CommitID,
rd.data.Date.Format("01/02/06"),
}
od.output.Print(struct {
*ProjectInfo `opts:"verticalTable"`
}{pi})

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()
rd.output.Print(struct {
*CveInfo `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(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" {
Expand All @@ -144,16 +165,49 @@ func (od *cveOutput) MarshalOutput(format output.Format) interface{} {
}
hist = append(hist, ho)
}
od.output.Print(output.Title(fmt.Sprintf("%d Vulnerabilities", totalCount)))
od.output.Print(hist)
rd.output.Print(output.Title(fmt.Sprintf("%d Vulnerabilities", totalCount)))
rd.output.Print(hist)

rd.output.Print(output.Title(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("cve_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.Title(fmt.Sprintf("%d Affected Packages", len(od.data.Packages))))
od.output.Print(od.data.Packages)
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("")
}

od.printFooter()
rd.output.Print("")
rd.output.Print([]string{
locale.Tl("cve_hint_cve", "To view a specific CVE, run [ACTIONABLE]state security open [cve-id][/RESET]."),
})
return output.Suppress
}

func (od *cveOutput) MarshalStructured(format output.Format) interface{} {
return od.data
func (rd *cveOutput) MarshalStructured(format output.Format) interface{} {
return rd.data
}
Loading

0 comments on commit 9b271e2

Please sign in to comment.