Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge state security report into state security. #3007

Merged
merged 1 commit into from
Jan 10, 2024
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
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
Loading