Skip to content
Open
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
1 change: 1 addition & 0 deletions genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ operations:
- internal/member/**/*.go
- internal/naisapi/**/*.go
- internal/app/**/*.go
- internal/vulnerability/**/*.go
bindings:
Slug:
type: string
Expand Down
2 changes: 2 additions & 0 deletions internal/alpha/command/alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
naisapi "github.com/nais/cli/internal/naisapi/command"
opensearch "github.com/nais/cli/internal/opensearch/command"
valkey "github.com/nais/cli/internal/valkey/command"
vulnerability "github.com/nais/cli/internal/vulnerability/command"
"github.com/nais/naistrix"
)

Expand All @@ -26,6 +27,7 @@ func Alpha(parentFlags *flags.GlobalFlags) *naistrix.Command {
opensearch.OpenSearch(flags),
log.Log(flags),
krakend.Krakend(flags),
vulnerability.Vulnerability(flags),
},
}
}
396 changes: 396 additions & 0 deletions internal/naisapi/gql/generated.go

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions internal/vulnerability/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package command

import (
"fmt"

alpha "github.com/nais/cli/internal/alpha/command/flag"
"github.com/nais/cli/internal/vulnerability"
"github.com/nais/cli/internal/vulnerability/command/flag"
"github.com/nais/naistrix"
)

func Vulnerability(parentFlags *alpha.Alpha) *naistrix.Command {
flags := &flag.Vulnerability{Alpha: parentFlags}
return &naistrix.Command{
Name: "vulnerability",
Aliases: []string{"vuln"},
Title: "Interact with vulnerabilities.",
StickyFlags: flags,
SubCommands: []*naistrix.Command{
find(flags),
},
}
}

var defaultArgs = []naistrix.Argument{
{Name: "cve_id"},
}

func validateArgs(args *naistrix.Arguments) error {
if args.Get("cve_id") == "" {
return fmt.Errorf("cve_id cannot be empty")
}
return nil
}

func metadataFromArgs(args *naistrix.Arguments) vulnerability.Metadata {
return vulnerability.Metadata{
CveId: args.Get("cve_id"),
}
}
63 changes: 63 additions & 0 deletions internal/vulnerability/command/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package command

import (
"context"
"fmt"

"github.com/nais/cli/internal/vulnerability"
"github.com/nais/cli/internal/vulnerability/command/flag"
"github.com/nais/naistrix"
"github.com/pterm/pterm"
)

func find(parentFlags *flag.Vulnerability) *naistrix.Command {
flags := &flag.Find{Vulnerability: parentFlags}
return &naistrix.Command{
Name: "find",
Title: "Find vulnerabilities or workloads with vulnerabilities.",
Args: defaultArgs,
Flags: flags,
ValidateFunc: func(ctx context.Context, args *naistrix.Arguments) error {
return validateArgs(args)
},
AutoCompleteFunc: func(ctx context.Context, args *naistrix.Arguments, toComplete string) (completions []string, activeHelp string) {
// Need a sort of fuzzy search here?
return nil, ""
},
Examples: []naistrix.Example{
{
Description: "Find workloads affected by CVE-2023-1234.",
Command: "CVE-2023-1234",
},
},
RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error {
metadata := metadataFromArgs(args)
worklodsForCve, err := vulnerability.FindWorkloadsForCve(ctx, metadata)
if err != nil {
return fmt.Errorf("finding workloads for CVE %s: %w", metadata.CveId, err)
}
pterm.DefaultSection.Println(fmt.Sprintf("%d Workloads affected by %s", len(worklodsForCve.Workloads.GetNodes()), metadata.CveId))
if len(worklodsForCve.Workloads.Nodes) == 0 {
pterm.Info.Println("No workloads found affected by this vulnerability.")
return nil
}
err = pterm.DefaultTable.
WithHasHeader().
WithHeaderRowSeparator("-").
WithData(vulnerability.FormatDetails(worklodsForCve, flags.IsVerbose())).
Render()
if err != nil {
return fmt.Errorf("rendering table: %w", err)
}
err = pterm.DefaultTable.
WithHasHeader().
WithHeaderRowSeparator("-").
WithData(vulnerability.FormatWorkloadsForCve(worklodsForCve)).
Render()
if err != nil {
return fmt.Errorf("rendering workloads for CVE %s: %w", metadata.CveId, err)
}
return nil
},
}
}
12 changes: 12 additions & 0 deletions internal/vulnerability/command/flag/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package flag

import (
alpha "github.com/nais/cli/internal/alpha/command/flag"
)

type (
Vulnerability struct{ *alpha.Alpha }
Find struct {
*Vulnerability
}
)
97 changes: 97 additions & 0 deletions internal/vulnerability/vulnerability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package vulnerability

import (
"context"
"fmt"

"github.com/nais/cli/internal/naisapi"
"github.com/nais/cli/internal/naisapi/gql"
)

type Metadata struct {
// CVE identifier, e.g. "CVE-2023-12345"
CveId string
}

type WorkloadVulnerability = gql.FindWorkloadsForCveCveCVE

func FindWorkloadsForCve(ctx context.Context, metadata Metadata) (*WorkloadVulnerability, error) {
_ = `# @genqlient
query FindWorkloadsForCve($identifier: String!) {
cve(identifier: $identifier) {
id
identifier
severity
title
description
detailsLink
cvssScore
workloads {
nodes {
vulnerability {
package
suppression {
state
}
}
workload {
__typename
name
}
}
}
}
}
`

client, err := naisapi.GraphqlClient(ctx)
if err != nil {
return nil, err
}

resp, err := gql.FindWorkloadsForCve(ctx, client, metadata.CveId)
if err != nil {
return nil, err
}

return &resp.Cve, nil
}

func FormatDetails(w *WorkloadVulnerability, verbose bool) [][]string {
rows := [][]string{
{"Field", "Value"},
{"CVE Id", w.Identifier},
{"Title", w.Title},
{"Severity", string(w.Severity)},
{"CVSS Score", fmt.Sprintf("%.1f", w.CvssScore)},
{"Details Link", w.DetailsLink},
}

if verbose {
rows = append(rows, []string{"Description", w.Description})
}

return rows
}

func FormatWorkloadsForCve(w *WorkloadVulnerability) [][]string {
rows := [][]string{
{"Workload Name", "Workload Type", "Vulnerable Package", "Suppress State"},
}

for _, node := range w.Workloads.Nodes {
suppressState := "N/A"
if node.Vulnerability.Suppression.State != "" {
suppressState = string(node.Vulnerability.Suppression.State)
}

rows = append(rows, []string{
node.Workload.GetName(),
node.Workload.GetTypename(),
node.Vulnerability.Package,
suppressState,
})
}

return rows
}
Loading