From 6d3aaa8b40a2484d70b42af045e52cca5e43bbd1 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:00:28 +0100 Subject: [PATCH 1/7] wip: add alpha vulnerability cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * find workloads from CVE Co-authored-by: Tommy Trøen --- genqlient.yaml | 1 + internal/alpha/command/alpha.go | 2 + internal/naisapi/gql/generated.go | 356 ++++++++++++++++++++ internal/vulnerability/command/command.go | 20 ++ internal/vulnerability/command/find.go | 19 ++ internal/vulnerability/command/flag/flag.go | 10 + internal/vulnerability/vulnerability.go | 49 +++ schema.graphql | 219 +++++++++++- 8 files changed, 669 insertions(+), 7 deletions(-) create mode 100644 internal/vulnerability/command/command.go create mode 100644 internal/vulnerability/command/find.go create mode 100644 internal/vulnerability/command/flag/flag.go create mode 100644 internal/vulnerability/vulnerability.go diff --git a/genqlient.yaml b/genqlient.yaml index 8477b834..a73ef3ea 100644 --- a/genqlient.yaml +++ b/genqlient.yaml @@ -7,6 +7,7 @@ operations: - internal/member/**/*.go - internal/naisapi/**/*.go - internal/app/**/*.go + - internal/vulnerability/**/*.go bindings: Slug: type: string diff --git a/internal/alpha/command/alpha.go b/internal/alpha/command/alpha.go index 3433e04a..8f4e54b2 100644 --- a/internal/alpha/command/alpha.go +++ b/internal/alpha/command/alpha.go @@ -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" ) @@ -26,6 +27,7 @@ func Alpha(parentFlags *flags.GlobalFlags) *naistrix.Command { opensearch.OpenSearch(flags), log.Log(flags), krakend.Krakend(flags), + vulnerability.Vulnerability(flags), }, } } diff --git a/internal/naisapi/gql/generated.go b/internal/naisapi/gql/generated.go index 4505c38f..2a47f19a 100644 --- a/internal/naisapi/gql/generated.go +++ b/internal/naisapi/gql/generated.go @@ -364,6 +364,285 @@ func (v *EnvironmentsResponse) GetEnvironments() EnvironmentsEnvironmentsEnviron return v.Environments } +// FindWorkloadsForCveCveCVE includes the requested fields of the GraphQL type CVE. +type FindWorkloadsForCveCveCVE struct { + // The globally unique ID of the CVE. + Id string `json:"id"` + // The unique identifier of the CVE. E.g. CVE-****-****. + Identifier string `json:"identifier"` + // Severity of the CVE. + Severity ImageVulnerabilitySeverity `json:"severity"` + // Title of the CVE + Title string `json:"title"` + // Description of the CVE. + Description string `json:"description"` + // Link to the CVE details. + DetailsLink string `json:"detailsLink"` + // CVSS score of the CVE. + CvssScore float64 `json:"cvssScore"` + // Affected workloads + Workloads FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection `json:"workloads"` +} + +// GetId returns FindWorkloadsForCveCveCVE.Id, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetId() string { return v.Id } + +// GetIdentifier returns FindWorkloadsForCveCveCVE.Identifier, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetIdentifier() string { return v.Identifier } + +// GetSeverity returns FindWorkloadsForCveCveCVE.Severity, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetSeverity() ImageVulnerabilitySeverity { return v.Severity } + +// GetTitle returns FindWorkloadsForCveCveCVE.Title, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetTitle() string { return v.Title } + +// GetDescription returns FindWorkloadsForCveCveCVE.Description, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetDescription() string { return v.Description } + +// GetDetailsLink returns FindWorkloadsForCveCveCVE.DetailsLink, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetDetailsLink() string { return v.DetailsLink } + +// GetCvssScore returns FindWorkloadsForCveCveCVE.CvssScore, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetCvssScore() float64 { return v.CvssScore } + +// GetWorkloads returns FindWorkloadsForCveCveCVE.Workloads, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetWorkloads() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection { + return v.Workloads +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection includes the requested fields of the GraphQL type WorkloadWithVulnerabilityConnection. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection struct { + // List of nodes. + Nodes []FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability `json:"nodes"` +} + +// GetNodes returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection.Nodes, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection) GetNodes() []FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability { + return v.Nodes +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability includes the requested fields of the GraphQL type WorkloadWithVulnerability. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability struct { + Vulnerability FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability `json:"vulnerability"` + Workload FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload `json:"-"` +} + +// GetVulnerability returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability.Vulnerability, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability) GetVulnerability() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability { + return v.Vulnerability +} + +// GetWorkload returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability.Workload, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability) GetWorkload() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload { + return v.Workload +} + +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability) UnmarshalJSON(b []byte) error { + + if string(b) == "null" { + return nil + } + + var firstPass struct { + *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability + Workload json.RawMessage `json:"workload"` + graphql.NoUnmarshalJSON + } + firstPass.FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + dst := &v.Workload + src := firstPass.Workload + if len(src) != 0 && string(src) != "null" { + err = __unmarshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload( + src, dst) + if err != nil { + return fmt.Errorf( + "unable to unmarshal FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability.Workload: %w", err) + } + } + } + return nil +} + +type __premarshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability struct { + Vulnerability FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability `json:"vulnerability"` + + Workload json.RawMessage `json:"workload"` +} + +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability) MarshalJSON() ([]byte, error) { + premarshaled, err := v.__premarshalJSON() + if err != nil { + return nil, err + } + return json.Marshal(premarshaled) +} + +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability) __premarshalJSON() (*__premarshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability, error) { + var retval __premarshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability + + retval.Vulnerability = v.Vulnerability + { + + dst := &retval.Workload + src := v.Workload + var err error + *dst, err = __marshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload( + &src) + if err != nil { + return nil, fmt.Errorf( + "unable to marshal FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability.Workload: %w", err) + } + } + return &retval, nil +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability includes the requested fields of the GraphQL type ImageVulnerability. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability struct { + // Package name of the vulnerability. + Package string `json:"package"` +} + +// GetPackage returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Package, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetPackage() string { + return v.Package +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload includes the requested fields of the GraphQL interface Workload. +// +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload is implemented by the following types: +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob +// The GraphQL type's documentation follows. +// +// Interface for workloads. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload interface { + implementsGraphQLInterfaceFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() string + // GetName returns the interface-field "name" from its implementation. + // The GraphQL interface field's documentation follows. + // + // Interface for workloads. + GetName() string +} + +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication) implementsGraphQLInterfaceFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload() { +} +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob) implementsGraphQLInterfaceFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload() { +} + +func __unmarshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload(b []byte, v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload) error { + if string(b) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(b, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Application": + *v = new(FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication) + return json.Unmarshal(b, *v) + case "Job": + *v = new(FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob) + return json.Unmarshal(b, *v) + case "": + return fmt.Errorf( + "response was missing Workload.__typename") + default: + return fmt.Errorf( + `unexpected concrete type for FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload: "%v"`, tn.TypeName) + } +} + +func __marshalFindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload(v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload) ([]byte, error) { + + var typename string + switch v := (*v).(type) { + case *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication: + typename = "Application" + + result := struct { + TypeName string `json:"__typename"` + *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication + }{typename, v} + return json.Marshal(result) + case *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob: + typename = "Job" + + result := struct { + TypeName string `json:"__typename"` + *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob + }{typename, v} + return json.Marshal(result) + case nil: + return []byte("null"), nil + default: + return nil, fmt.Errorf( + `unexpected concrete type for FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload: "%T"`, v) + } +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication includes the requested fields of the GraphQL type Application. +// The GraphQL type's documentation follows. +// +// An application lets you run one or more instances of a container image on the [Nais platform](https://nais.io/). +// +// Learn more about how to create and configure your applications in the [Nais documentation](https://docs.nais.io/workloads/application/). +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication struct { + Typename string `json:"__typename"` + // Interface for workloads. + Name string `json:"name"` +} + +// GetTypename returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication.Typename, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication) GetTypename() string { + return v.Typename +} + +// GetName returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication.Name, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadApplication) GetName() string { + return v.Name +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob includes the requested fields of the GraphQL type Job. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob struct { + Typename string `json:"__typename"` + // Interface for workloads. + Name string `json:"name"` +} + +// GetTypename returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob.Typename, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob) GetTypename() string { + return v.Typename +} + +// GetName returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob.Name, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkloadJob) GetName() string { + return v.Name +} + +// FindWorkloadsForCveResponse is returned by FindWorkloadsForCve on success. +type FindWorkloadsForCveResponse struct { + // Get a specific CVE by its identifier. + Cve FindWorkloadsForCveCveCVE `json:"cve"` +} + +// GetCve returns FindWorkloadsForCveResponse.Cve, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveResponse) GetCve() FindWorkloadsForCveCveCVE { return v.Cve } + // GetAllIssuesResponse is returned by GetAllIssues on success. type GetAllIssuesResponse struct { // Get a team by its slug. @@ -5358,6 +5637,24 @@ func (v *GetValkeyTeamEnvironmentValkeyAccessValkeyAccessConnectionEdgesValkeyAc return v.Slug } +type ImageVulnerabilitySeverity string + +const ( + ImageVulnerabilitySeverityLow ImageVulnerabilitySeverity = "LOW" + ImageVulnerabilitySeverityMedium ImageVulnerabilitySeverity = "MEDIUM" + ImageVulnerabilitySeverityHigh ImageVulnerabilitySeverity = "HIGH" + ImageVulnerabilitySeverityCritical ImageVulnerabilitySeverity = "CRITICAL" + ImageVulnerabilitySeverityUnassigned ImageVulnerabilitySeverity = "UNASSIGNED" +) + +var AllImageVulnerabilitySeverity = []ImageVulnerabilitySeverity{ + ImageVulnerabilitySeverityLow, + ImageVulnerabilitySeverityMedium, + ImageVulnerabilitySeverityHigh, + ImageVulnerabilitySeverityCritical, + ImageVulnerabilitySeverityUnassigned, +} + // IsAdminMeAuthenticatedUser includes the requested fields of the GraphQL interface AuthenticatedUser. // // IsAdminMeAuthenticatedUser is implemented by the following types: @@ -7552,6 +7849,14 @@ func (v *__DeleteValkeyInput) GetEnvironmentName() string { return v.Environment // GetTeamSlug returns __DeleteValkeyInput.TeamSlug, and is useful for accessing the field via an interface. func (v *__DeleteValkeyInput) GetTeamSlug() string { return v.TeamSlug } +// __FindWorkloadsForCveInput is used internally by genqlient +type __FindWorkloadsForCveInput struct { + Identifier string `json:"identifier"` +} + +// GetIdentifier returns __FindWorkloadsForCveInput.Identifier, and is useful for accessing the field via an interface. +func (v *__FindWorkloadsForCveInput) GetIdentifier() string { return v.Identifier } + // __GetAllIssuesInput is used internally by genqlient type __GetAllIssuesInput struct { TeamSlug string `json:"teamSlug"` @@ -8097,6 +8402,57 @@ func Environments( return data_, err_ } +// The query executed by FindWorkloadsForCve. +const FindWorkloadsForCve_Operation = ` +query FindWorkloadsForCve ($identifier: String!) { + cve(identifier: $identifier) { + id + identifier + severity + title + description + detailsLink + cvssScore + workloads { + nodes { + vulnerability { + package + } + workload { + __typename + name + } + } + } + } +} +` + +func FindWorkloadsForCve( + ctx_ context.Context, + client_ graphql.Client, + identifier string, +) (data_ *FindWorkloadsForCveResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "FindWorkloadsForCve", + Query: FindWorkloadsForCve_Operation, + Variables: &__FindWorkloadsForCveInput{ + Identifier: identifier, + }, + } + + data_ = &FindWorkloadsForCveResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The query executed by GetAllIssues. const GetAllIssues_Operation = ` query GetAllIssues ($teamSlug: Slug!, $filter: IssueFilter) { diff --git a/internal/vulnerability/command/command.go b/internal/vulnerability/command/command.go new file mode 100644 index 00000000..efa445bc --- /dev/null +++ b/internal/vulnerability/command/command.go @@ -0,0 +1,20 @@ +package command + +import ( + alpha "github.com/nais/cli/internal/alpha/command/flag" + "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), + }, + } +} diff --git a/internal/vulnerability/command/find.go b/internal/vulnerability/command/find.go new file mode 100644 index 00000000..d4d8a13b --- /dev/null +++ b/internal/vulnerability/command/find.go @@ -0,0 +1,19 @@ +package command + +import ( + "context" + + "github.com/nais/cli/internal/vulnerability/command/flag" + "github.com/nais/naistrix" +) + +func find(flag *flag.Vulnerability) *naistrix.Command { + return &naistrix.Command{ + Name: "find", + Title: "Find vulnerabilities or workloads with vulnerabilities.", + RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { + out.Println("Find vulnerabilities command is not yet implemented.") + return nil + }, + } +} diff --git a/internal/vulnerability/command/flag/flag.go b/internal/vulnerability/command/flag/flag.go new file mode 100644 index 00000000..68b5a5ed --- /dev/null +++ b/internal/vulnerability/command/flag/flag.go @@ -0,0 +1,10 @@ +package flag + +import alpha "github.com/nais/cli/internal/alpha/command/flag" + +type Vulnerability struct { + *alpha.Alpha + Environment Env `name:"environment" short:"e" usage:"Filter by environment."` +} + +type Env string diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go new file mode 100644 index 00000000..c0e08731 --- /dev/null +++ b/internal/vulnerability/vulnerability.go @@ -0,0 +1,49 @@ +package vulnerability + +import ( + "context" + + "github.com/nais/cli/internal/naisapi" + "github.com/nais/cli/internal/naisapi/gql" +) + +type WorkloadVulnerability = *gql.FindWorkloadsForCveCveCVE + +func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerability, error) { + _ = `# @genqlient + query FindWorkloadsForCve($identifier: String!) { + cve(identifier: $identifier) { + id + identifier + severity + title + description + detailsLink + cvssScore + workloads { + nodes { + vulnerability { + package + } + workload { + __typename + name + } + } + } + } + } + ` + + client, err := naisapi.GraphqlClient(ctx) + if err != nil { + return nil, err + } + + resp, err := gql.FindWorkloadsForCve(ctx, client, cveId) + if err != nil { + return nil, err + } + + return resp.Cve, nil +} diff --git a/schema.graphql b/schema.graphql index c7e989b5..81563ef5 100644 --- a/schema.graphql +++ b/schema.graphql @@ -179,6 +179,10 @@ Service account token was deleted. """ SERVICE_ACCOUNT_TOKEN_DELETED """ +A user was granted access to a postgres cluster +""" + POSTGRES_GRANT_ACCESS +""" Team was created. """ TEAM_CREATED @@ -352,6 +356,10 @@ All activity log entries related to secrets will use this resource type. SECRET SERVICE_ACCOUNT """ +All activity log entries related to postgres clusters will use this resource type. +""" + POSTGRES +""" All activity log entries related to teams will use this resource type. """ TEAM @@ -1264,6 +1272,58 @@ The threshold that must be met for the scaling to trigger. threshold: Int! } +type CVE implements Node{ +""" +The globally unique ID of the CVE. +""" + id: ID! +""" +The unique identifier of the CVE. E.g. CVE-****-****. +""" + identifier: String! +""" +Severity of the CVE. +""" + severity: ImageVulnerabilitySeverity! +""" +Title of the CVE +""" + title: String! +""" +Description of the CVE. +""" + description: String! +""" +Link to the CVE details. +""" + detailsLink: String! +""" +CVSS score of the CVE. +""" + cvssScore: Float +""" +Affected workloads +""" + workloads( +""" +Get the first n items in the connection. This can be used in combination with the after parameter. +""" + first: Int +""" +Get items after this cursor. +""" + after: Cursor +""" +Get the last n items in the connection. This can be used in combination with the before parameter. +""" + last: Int +""" +Get items before this cursor. +""" + before: Cursor + ): WorkloadWithVulnerabilityConnection! +} + input ChangeDeploymentKeyInput { teamSlug: Slug! } @@ -1868,6 +1928,10 @@ The deployment. node: Deployment! } +input DeploymentFilter { + from: Time +} + """ Deployment key type. """ @@ -2264,6 +2328,18 @@ The `Float` scalar type represents signed double-precision fractional values as """ scalar Float +input GrantPostgresAccessInput { + clusterName: String! + teamSlug: Slug! + environmentName: String! + grantee: String! + duration: String! +} + +type GrantPostgresAccessPayload { + error: String +} + """ The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID. """ @@ -2311,6 +2387,10 @@ Timestamp of when the vulnerability got its current severity. Link to the vulnerability details. """ vulnerabilityDetailsLink: String! +""" +CVSS score of the vulnerability. +""" + cvssScore: Float } type ImageVulnerabilityConnection { @@ -3714,6 +3794,12 @@ Start maintenance updates for an OpenSearch instance. input: StartOpenSearchMaintenanceInput! ): StartOpenSearchMaintenancePayload """ +Grant access to this postgres cluster +""" + grantPostgresAccess( + input: GrantPostgresAccessInput! + ): GrantPostgresAccessPayload! +""" Create a new Nais team The user creating the team will be granted team ownership, unless the user is a service account, in which case the @@ -4335,6 +4421,58 @@ interface Persistence { teamEnvironment: TeamEnvironment! } +type Postgres implements Persistence & Node{ + id: ID! + name: String! + team: Team! + environment: TeamEnvironment! @deprecated(reason: "Use the `teamEnvironment` field instead.") + teamEnvironment: TeamEnvironment! +} + +type PostgresGrantAccessActivityLogEntry implements ActivityLogEntry & Node{ +""" +ID of the entry. +""" + id: ID! +""" +The identity of the actor who performed the action. The value is either the name of a service account, or the email address of a user. +""" + actor: String! +""" +Creation time of the entry. +""" + createdAt: Time! +""" +Message that summarizes the entry. +""" + message: String! +""" +Type of the resource that was affected by the action. +""" + resourceType: ActivityLogEntryResourceType! +""" +Name of the resource that was affected by the action. +""" + resourceName: String! +""" +The team slug that the entry belongs to. +""" + teamSlug: Slug! +""" +The environment name that the entry belongs to. +""" + environmentName: String +""" +Data associated with the update. +""" + data: PostgresGrantAccessActivityLogEntryData! +} + +type PostgresGrantAccessActivityLogEntryData { + grantee: String! + until: Time! +} + type Price { value: Float! } @@ -4453,6 +4591,31 @@ End month of the period, inclusive. to: Date! ): CostMonthlySummary! """ +Get a list of deployments. +""" + deployments( +""" +Get the first n items in the connection. This can be used in combination with the after parameter. +""" + first: Int +""" +Get items after this cursor. +""" + after: Cursor +""" +Get the last n items in the connection. This can be used in combination with the before parameter. +""" + last: Int +""" +Get items before this cursor. +""" + before: Cursor +""" +Filter options for the deployments returned from the connection. +""" + filter: DeploymentFilter + ): DeploymentConnection! +""" Get a list of environments. """ environments( @@ -4667,6 +4830,12 @@ Get the mean time to fix history for all teams. vulnerabilityFixHistory( from: Date! ): VulnerabilityFixHistory! +""" +Get a specific CVE by its identifier. +""" + cve( + identifier: String! + ): CVE! } """ @@ -5450,7 +5619,7 @@ Search filter for filtering search results. """ Types that can be searched for. """ -union SearchNode =Team | Application | BigQueryDataset | Bucket | Job | KafkaTopic | OpenSearch | SqlInstance | Valkey +union SearchNode =Team | Application | BigQueryDataset | Bucket | Job | KafkaTopic | OpenSearch | Postgres | SqlInstance | Valkey """ Search node connection. @@ -5501,6 +5670,7 @@ Search for applications. JOB KAFKA_TOPIC OPENSEARCH + POSTGRES SQL_INSTANCE VALKEY } @@ -5708,6 +5878,10 @@ Input for filtering the secrets of a team. input SecretFilter { """ Input for filtering the secrets of a team. +""" + name: String +""" +Input for filtering the secrets of a team. """ inUse: Boolean } @@ -7187,6 +7361,12 @@ Get vulnerability summary from given date until today. """ from: Date! ): ImageVulnerabilityHistory! +""" +Get the mean time to fix history for a team. +""" + vulnerabilityFixHistory( + from: Date! + ): VulnerabilityFixHistory! vulnerabilitySummary( filter: TeamVulnerabilitySummaryFilter ): TeamVulnerabilitySummary! @@ -7220,12 +7400,6 @@ Ordering options for items returned from the connection. orderBy: VulnerabilitySummaryOrder ): WorkloadVulnerabilitySummaryConnection! """ -Get the mean time to fix history for a team. -""" - vulnerabilityFixHistory( - from: Date! - ): VulnerabilityFixHistory! -""" Nais workloads owned by the team. """ workloads( @@ -10117,4 +10291,35 @@ The workload vulnerability summary. node: WorkloadVulnerabilitySummary! } +type WorkloadWithVulnerability { + vulnerability: ImageVulnerability! + workload: Workload! +} + +type WorkloadWithVulnerabilityConnection { +""" +Information to aid in pagination. +""" + pageInfo: PageInfo! +""" +List of edges. +""" + edges: [WorkloadWithVulnerabilityEdge!]! +""" +List of nodes. +""" + nodes: [WorkloadWithVulnerability!]! +} + +type WorkloadWithVulnerabilityEdge { +""" +A cursor for use in pagination. +""" + cursor: Cursor! +""" +The vulnerability. +""" + node: WorkloadWithVulnerability! +} + From e90f59fc82d72297fcc9a008f0e8c536e30b9098 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:33:52 +0100 Subject: [PATCH 2/7] feat(vulnerability): implement find command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CVE for workloads Co-authored-by: Tommy Trøen --- internal/vulnerability/command/command.go | 20 ++++++++++ internal/vulnerability/command/find.go | 46 ++++++++++++++++++++++- internal/vulnerability/vulnerability.go | 36 +++++++++++++++++- 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/internal/vulnerability/command/command.go b/internal/vulnerability/command/command.go index efa445bc..d11bc01a 100644 --- a/internal/vulnerability/command/command.go +++ b/internal/vulnerability/command/command.go @@ -1,6 +1,8 @@ package command import ( + "fmt" + alpha "github.com/nais/cli/internal/alpha/command/flag" "github.com/nais/cli/internal/vulnerability/command/flag" "github.com/nais/naistrix" @@ -18,3 +20,21 @@ func Vulnerability(parentFlags *alpha.Alpha) *naistrix.Command { }, } } + +var defaultArgs = []naistrix.Argument{ + {Name: "cve-id"}, +} + +func validateArgs(args *naistrix.Arguments) error { + if args.Len() != 1 { + return fmt.Errorf("expected 1 argument, got %d", args.Len()) + } + if args.Get("cve-id") == "" { + return fmt.Errorf("name cannot be empty") + } + return nil +} + +func cveIDFromArgs(args *naistrix.Arguments) string { + return args.Get("cve-id") +} diff --git a/internal/vulnerability/command/find.go b/internal/vulnerability/command/find.go index d4d8a13b..19210faa 100644 --- a/internal/vulnerability/command/find.go +++ b/internal/vulnerability/command/find.go @@ -2,17 +2,59 @@ 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(flag *flag.Vulnerability) *naistrix.Command { +func find(parentFlags *flag.Vulnerability) *naistrix.Command { return &naistrix.Command{ Name: "find", Title: "Find vulnerabilities or workloads with vulnerabilities.", + Args: defaultArgs, + 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 { - out.Println("Find vulnerabilities command is not yet implemented.") + cveId := cveIDFromArgs(args) + worklodsForCve, err := vulnerability.FindWorkloadsForCve(ctx, cveId) + if err != nil { + return fmt.Errorf("finding workloads for CVE %s: %w", cveId, err) + } + pterm.DefaultSection.Println(fmt.Sprintf("Workloads affected by %s", 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)). + 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", cveId, err) + } return nil }, } diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go index c0e08731..b581b8f4 100644 --- a/internal/vulnerability/vulnerability.go +++ b/internal/vulnerability/vulnerability.go @@ -2,12 +2,18 @@ package vulnerability import ( "context" + "fmt" "github.com/nais/cli/internal/naisapi" "github.com/nais/cli/internal/naisapi/gql" ) -type WorkloadVulnerability = *gql.FindWorkloadsForCveCveCVE +type Metadata struct { + // CVE identifier, e.g. "CVE-2023-12345" + CveId string +} + +type WorkloadVulnerability = gql.FindWorkloadsForCveCveCVE func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerability, error) { _ = `# @genqlient @@ -45,5 +51,31 @@ func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerabil return nil, err } - return resp.Cve, nil + return &resp.Cve, nil +} + +func FormatDetails(w *WorkloadVulnerability) [][]string { + return [][]string{ + {"Field", "Value"}, + {"CVE ID", w.Identifier}, + {"Title", w.Title}, + {"Description", w.Description}, + {"Severity", string(w.Severity)}, + {"CVSS Score", fmt.Sprintf("%.1f", w.CvssScore)}, + {"Details Link", w.DetailsLink}, + } +} + +func FormatWorkloadsForCve(w *WorkloadVulnerability) [][]string { + rows := [][]string{ + {"Workload Type", "Workload Name", "Vulnerable Package"}, + } + for _, node := range w.Workloads.Nodes { + rows = append(rows, []string{ + node.Workload.GetTypename(), + node.Workload.GetName(), + node.Vulnerability.Package, + }) + } + return rows } From e42fe5cbeabd15bf859e0f18096e083285b5575c Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:42:51 +0100 Subject: [PATCH 3/7] fix(vulnerability): improve output formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tommy Trøen --- internal/vulnerability/command/find.go | 2 +- internal/vulnerability/vulnerability.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/vulnerability/command/find.go b/internal/vulnerability/command/find.go index 19210faa..ff0fb722 100644 --- a/internal/vulnerability/command/find.go +++ b/internal/vulnerability/command/find.go @@ -34,7 +34,7 @@ func find(parentFlags *flag.Vulnerability) *naistrix.Command { if err != nil { return fmt.Errorf("finding workloads for CVE %s: %w", cveId, err) } - pterm.DefaultSection.Println(fmt.Sprintf("Workloads affected by %s", cveId)) + pterm.DefaultSection.Println(fmt.Sprintf("%d Workloads affected by %s", len(worklodsForCve.Workloads.GetNodes()), cveId)) if len(worklodsForCve.Workloads.Nodes) == 0 { pterm.Info.Println("No workloads found affected by this vulnerability.") return nil diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go index b581b8f4..64eb44ed 100644 --- a/internal/vulnerability/vulnerability.go +++ b/internal/vulnerability/vulnerability.go @@ -58,22 +58,22 @@ func FormatDetails(w *WorkloadVulnerability) [][]string { return [][]string{ {"Field", "Value"}, {"CVE ID", w.Identifier}, - {"Title", w.Title}, - {"Description", w.Description}, {"Severity", string(w.Severity)}, {"CVSS Score", fmt.Sprintf("%.1f", w.CvssScore)}, + {"Title", w.Title}, + {"Description", w.Description}, {"Details Link", w.DetailsLink}, } } func FormatWorkloadsForCve(w *WorkloadVulnerability) [][]string { rows := [][]string{ - {"Workload Type", "Workload Name", "Vulnerable Package"}, + {"Workload Name", "Workload Type", "Vulnerable Package"}, } for _, node := range w.Workloads.Nodes { rows = append(rows, []string{ - node.Workload.GetTypename(), node.Workload.GetName(), + node.Workload.GetTypename(), node.Vulnerability.Package, }) } From ccd541a0c93db4ea2efb466ad2c788fe907071fc Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:09:34 +0100 Subject: [PATCH 4/7] fix(vulnerability): enhance find command to include suppression state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tommy Trøen --- internal/naisapi/gql/generated.go | 42 ++++++++++++++++++++- internal/vulnerability/command/command.go | 7 +++- internal/vulnerability/command/find.go | 14 ++++--- internal/vulnerability/command/flag/flag.go | 12 +++--- internal/vulnerability/vulnerability.go | 28 ++++++++++---- 5 files changed, 81 insertions(+), 22 deletions(-) diff --git a/internal/naisapi/gql/generated.go b/internal/naisapi/gql/generated.go index 2a47f19a..f0fcfa95 100644 --- a/internal/naisapi/gql/generated.go +++ b/internal/naisapi/gql/generated.go @@ -506,7 +506,8 @@ func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNo // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability includes the requested fields of the GraphQL type ImageVulnerability. type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability struct { // Package name of the vulnerability. - Package string `json:"package"` + Package string `json:"package"` + Suppression FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression `json:"suppression"` } // GetPackage returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Package, and is useful for accessing the field via an interface. @@ -514,6 +515,22 @@ func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNo return v.Package } +// GetSuppression returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Suppression, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetSuppression() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression { + return v.Suppression +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression includes the requested fields of the GraphQL type ImageVulnerabilitySuppression. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression struct { + // Suppression state of the vulnerability. + State ImageVulnerabilitySuppressionState `json:"state"` +} + +// GetState returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression.State, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression) GetState() ImageVulnerabilitySuppressionState { + return v.State +} + // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload includes the requested fields of the GraphQL interface Workload. // // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload is implemented by the following types: @@ -5655,6 +5672,26 @@ var AllImageVulnerabilitySeverity = []ImageVulnerabilitySeverity{ ImageVulnerabilitySeverityUnassigned, } +type ImageVulnerabilitySuppressionState string + +const ( + // Vulnerability is in triage. + ImageVulnerabilitySuppressionStateInTriage ImageVulnerabilitySuppressionState = "IN_TRIAGE" + // Vulnerability is resolved. + ImageVulnerabilitySuppressionStateResolved ImageVulnerabilitySuppressionState = "RESOLVED" + // Vulnerability is marked as false positive. + ImageVulnerabilitySuppressionStateFalsePositive ImageVulnerabilitySuppressionState = "FALSE_POSITIVE" + // Vulnerability is marked as not affected. + ImageVulnerabilitySuppressionStateNotAffected ImageVulnerabilitySuppressionState = "NOT_AFFECTED" +) + +var AllImageVulnerabilitySuppressionState = []ImageVulnerabilitySuppressionState{ + ImageVulnerabilitySuppressionStateInTriage, + ImageVulnerabilitySuppressionStateResolved, + ImageVulnerabilitySuppressionStateFalsePositive, + ImageVulnerabilitySuppressionStateNotAffected, +} + // IsAdminMeAuthenticatedUser includes the requested fields of the GraphQL interface AuthenticatedUser. // // IsAdminMeAuthenticatedUser is implemented by the following types: @@ -8417,6 +8454,9 @@ query FindWorkloadsForCve ($identifier: String!) { nodes { vulnerability { package + suppression { + state + } } workload { __typename diff --git a/internal/vulnerability/command/command.go b/internal/vulnerability/command/command.go index d11bc01a..dcf38603 100644 --- a/internal/vulnerability/command/command.go +++ b/internal/vulnerability/command/command.go @@ -4,6 +4,7 @@ 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" ) @@ -35,6 +36,8 @@ func validateArgs(args *naistrix.Arguments) error { return nil } -func cveIDFromArgs(args *naistrix.Arguments) string { - return args.Get("cve-id") +func metadataFromArgs(args *naistrix.Arguments) vulnerability.Metadata { + return vulnerability.Metadata{ + CveId: args.Get("cve-id"), + } } diff --git a/internal/vulnerability/command/find.go b/internal/vulnerability/command/find.go index ff0fb722..8e73a531 100644 --- a/internal/vulnerability/command/find.go +++ b/internal/vulnerability/command/find.go @@ -11,10 +11,12 @@ import ( ) 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) }, @@ -29,12 +31,12 @@ func find(parentFlags *flag.Vulnerability) *naistrix.Command { }, }, RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error { - cveId := cveIDFromArgs(args) - worklodsForCve, err := vulnerability.FindWorkloadsForCve(ctx, cveId) + metadata := metadataFromArgs(args) + worklodsForCve, err := vulnerability.FindWorkloadsForCve(ctx, metadata) if err != nil { - return fmt.Errorf("finding workloads for CVE %s: %w", cveId, err) + 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()), cveId)) + 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 @@ -42,7 +44,7 @@ func find(parentFlags *flag.Vulnerability) *naistrix.Command { err = pterm.DefaultTable. WithHasHeader(). WithHeaderRowSeparator("-"). - WithData(vulnerability.FormatDetails(worklodsForCve)). + WithData(vulnerability.FormatDetails(worklodsForCve, flags.IsVerbose())). Render() if err != nil { return fmt.Errorf("rendering table: %w", err) @@ -53,7 +55,7 @@ func find(parentFlags *flag.Vulnerability) *naistrix.Command { WithData(vulnerability.FormatWorkloadsForCve(worklodsForCve)). Render() if err != nil { - return fmt.Errorf("rendering workloads for CVE %s: %w", cveId, err) + return fmt.Errorf("rendering workloads for CVE %s: %w", metadata.CveId, err) } return nil }, diff --git a/internal/vulnerability/command/flag/flag.go b/internal/vulnerability/command/flag/flag.go index 68b5a5ed..7f62a2a2 100644 --- a/internal/vulnerability/command/flag/flag.go +++ b/internal/vulnerability/command/flag/flag.go @@ -1,10 +1,10 @@ package flag -import alpha "github.com/nais/cli/internal/alpha/command/flag" +import ( + alpha "github.com/nais/cli/internal/alpha/command/flag" +) -type Vulnerability struct { - *alpha.Alpha - Environment Env `name:"environment" short:"e" usage:"Filter by environment."` +type Vulnerability struct{ *alpha.Alpha } +type Find struct { + *Vulnerability } - -type Env string diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go index 64eb44ed..d068adc5 100644 --- a/internal/vulnerability/vulnerability.go +++ b/internal/vulnerability/vulnerability.go @@ -15,7 +15,7 @@ type Metadata struct { type WorkloadVulnerability = gql.FindWorkloadsForCveCveCVE -func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerability, error) { +func FindWorkloadsForCve(ctx context.Context, metadata Metadata) (*WorkloadVulnerability, error) { _ = `# @genqlient query FindWorkloadsForCve($identifier: String!) { cve(identifier: $identifier) { @@ -30,6 +30,9 @@ func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerabil nodes { vulnerability { package + suppression { + state + } } workload { __typename @@ -46,7 +49,7 @@ func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerabil return nil, err } - resp, err := gql.FindWorkloadsForCve(ctx, client, cveId) + resp, err := gql.FindWorkloadsForCve(ctx, client, metadata.CveId) if err != nil { return nil, err } @@ -54,21 +57,27 @@ func FindWorkloadsForCve(ctx context.Context, cveId string) (*WorkloadVulnerabil return &resp.Cve, nil } -func FormatDetails(w *WorkloadVulnerability) [][]string { - return [][]string{ +func FormatDetails(w *WorkloadVulnerability, verbose bool) [][]string { + var rows [][]string + rows = append(rows, [][]string{ {"Field", "Value"}, {"CVE ID", w.Identifier}, + {"Title", w.Title}, {"Severity", string(w.Severity)}, {"CVSS Score", fmt.Sprintf("%.1f", w.CvssScore)}, - {"Title", w.Title}, - {"Description", w.Description}, {"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"}, + {"Workload Name", "Workload Type", "Vulnerable Package", "Suppress State"}, } for _, node := range w.Workloads.Nodes { rows = append(rows, []string{ @@ -76,6 +85,11 @@ func FormatWorkloadsForCve(w *WorkloadVulnerability) [][]string { node.Workload.GetTypename(), node.Vulnerability.Package, }) + if node.Vulnerability.Suppression.State != "" { + rows[len(rows)-1] = append(rows[len(rows)-1], string(node.Vulnerability.Suppression.State)) + } else { + rows[len(rows)-1] = append(rows[len(rows)-1], "N/A") + } } return rows } From 402b4ebcf3779df09ca06173fd3577cad96ff5e2 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:32:06 +0100 Subject: [PATCH 5/7] fix(vulnerability): streamline output formatting in FormatDetails function --- internal/vulnerability/vulnerability.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go index d068adc5..2962719f 100644 --- a/internal/vulnerability/vulnerability.go +++ b/internal/vulnerability/vulnerability.go @@ -58,15 +58,14 @@ func FindWorkloadsForCve(ctx context.Context, metadata Metadata) (*WorkloadVulne } func FormatDetails(w *WorkloadVulnerability, verbose bool) [][]string { - var rows [][]string - rows = append(rows, [][]string{ + rows := [][]string{ {"Field", "Value"}, - {"CVE ID", w.Identifier}, + {"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}) @@ -79,17 +78,20 @@ 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, }) - if node.Vulnerability.Suppression.State != "" { - rows[len(rows)-1] = append(rows[len(rows)-1], string(node.Vulnerability.Suppression.State)) - } else { - rows[len(rows)-1] = append(rows[len(rows)-1], "N/A") - } } + return rows } From 523a7800c9ad6585e54e48daf6ec493d18e472f8 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:37:19 +0100 Subject: [PATCH 6/7] refactor: reorganize type definitions for clarity --- internal/vulnerability/command/flag/flag.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/vulnerability/command/flag/flag.go b/internal/vulnerability/command/flag/flag.go index 7f62a2a2..476b8305 100644 --- a/internal/vulnerability/command/flag/flag.go +++ b/internal/vulnerability/command/flag/flag.go @@ -4,7 +4,9 @@ import ( alpha "github.com/nais/cli/internal/alpha/command/flag" ) -type Vulnerability struct{ *alpha.Alpha } -type Find struct { - *Vulnerability -} +type ( + Vulnerability struct{ *alpha.Alpha } + Find struct { + *Vulnerability + } +) From dc58eb42a69e812fe20e787328dbaf42d44c0058 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:13:46 +0100 Subject: [PATCH 7/7] fix: update argument name from 'cve-id' to 'cve_id' for consistency --- internal/vulnerability/command/command.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/vulnerability/command/command.go b/internal/vulnerability/command/command.go index dcf38603..fbe61e1c 100644 --- a/internal/vulnerability/command/command.go +++ b/internal/vulnerability/command/command.go @@ -23,21 +23,18 @@ func Vulnerability(parentFlags *alpha.Alpha) *naistrix.Command { } var defaultArgs = []naistrix.Argument{ - {Name: "cve-id"}, + {Name: "cve_id"}, } func validateArgs(args *naistrix.Arguments) error { - if args.Len() != 1 { - return fmt.Errorf("expected 1 argument, got %d", args.Len()) - } - if args.Get("cve-id") == "" { - return fmt.Errorf("name cannot be empty") + 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"), + CveId: args.Get("cve_id"), } }