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..f0fcfa95 100644 --- a/internal/naisapi/gql/generated.go +++ b/internal/naisapi/gql/generated.go @@ -364,6 +364,302 @@ 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"` + Suppression FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression `json:"suppression"` +} + +// GetPackage returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Package, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetPackage() string { + 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: +// 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 +5654,44 @@ 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, +} + +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: @@ -7552,6 +7886,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 +8439,60 @@ 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 + suppression { + state + } + } + 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..fbe61e1c --- /dev/null +++ b/internal/vulnerability/command/command.go @@ -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"), + } +} diff --git a/internal/vulnerability/command/find.go b/internal/vulnerability/command/find.go new file mode 100644 index 00000000..8e73a531 --- /dev/null +++ b/internal/vulnerability/command/find.go @@ -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 + }, + } +} diff --git a/internal/vulnerability/command/flag/flag.go b/internal/vulnerability/command/flag/flag.go new file mode 100644 index 00000000..476b8305 --- /dev/null +++ b/internal/vulnerability/command/flag/flag.go @@ -0,0 +1,12 @@ +package flag + +import ( + alpha "github.com/nais/cli/internal/alpha/command/flag" +) + +type ( + Vulnerability struct{ *alpha.Alpha } + Find struct { + *Vulnerability + } +) diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go new file mode 100644 index 00000000..2962719f --- /dev/null +++ b/internal/vulnerability/vulnerability.go @@ -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 +} 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! +} +