diff --git a/internal/naisapi/gql/generated.go b/internal/naisapi/gql/generated.go index ce32ab42..ddf5ba7d 100644 --- a/internal/naisapi/gql/generated.go +++ b/internal/naisapi/gql/generated.go @@ -827,6 +827,52 @@ func (v *ApplicationStatusTeamEnvironmentApplicationInstanceGroupsInstanceGroupI return v.LastExitCode } +// Filter active CVEs by their calculated risk assessment. +type CVEFilter struct { + // Filter active CVEs by their calculated risk assessment. + Priority *CVEPriority `json:"priority"` +} + +// GetPriority returns CVEFilter.Priority, and is useful for accessing the field via an interface. +func (v *CVEFilter) GetPriority() *CVEPriority { return v.Priority } + +// Ordering options when fetching CVEs. +type CVEOrder struct { + // Ordering options when fetching CVEs. + Field CVEOrderField `json:"field"` + // Ordering options when fetching CVEs. + Direction OrderDirection `json:"direction"` +} + +// GetField returns CVEOrder.Field, and is useful for accessing the field via an interface. +func (v *CVEOrder) GetField() CVEOrderField { return v.Field } + +// GetDirection returns CVEOrder.Direction, and is useful for accessing the field via an interface. +func (v *CVEOrder) GetDirection() OrderDirection { return v.Direction } + +type CVEOrderField string + +const ( + // Order by CVE identifier. + CVEOrderFieldIdentifier CVEOrderField = "IDENTIFIER" + // Order by CVE severity. + CVEOrderFieldSeverity CVEOrderField = "SEVERITY" + // Order by CVSS score. + CVEOrderFieldCvssScore CVEOrderField = "CVSS_SCORE" + // Order by number of affected workloads. + CVEOrderFieldAffectedWorkloadsCount CVEOrderField = "AFFECTED_WORKLOADS_COUNT" + // Order by CVE priority derived from threat intelligence signals. + CVEOrderFieldPriority CVEOrderField = "PRIORITY" +) + +var AllCVEOrderField = []CVEOrderField{ + CVEOrderFieldIdentifier, + CVEOrderFieldSeverity, + CVEOrderFieldCvssScore, + CVEOrderFieldAffectedWorkloadsCount, + CVEOrderFieldPriority, +} + // Operational priority levels for vulnerabilities and CVEs. type CVEPriority string @@ -848,6 +894,15 @@ var AllCVEPriority = []CVEPriority{ CVEPriorityMonitor, } +// Filter for workloads affected by a CVE. When teamSlugs is provided only workloads belonging to those teams are returned. +type CVEWorkloadsFilter struct { + // Filter for workloads affected by a CVE. When teamSlugs is provided only workloads belonging to those teams are returned. + TeamSlugs []string `json:"teamSlugs"` +} + +// GetTeamSlugs returns CVEWorkloadsFilter.TeamSlugs, and is useful for accessing the field via an interface. +func (v *CVEWorkloadsFilter) GetTeamSlugs() []string { return v.TeamSlugs } + // Input for filtering the configs of a team. type ConfigFilter struct { // Input for filtering the configs of a team. @@ -1843,8 +1898,8 @@ type FindWorkloadsForCveCveCVE struct { Description string `json:"description"` // Link to the CVE details. DetailsLink string `json:"detailsLink"` - // CVSS score of the CVE. - CvssScore *float64 `json:"cvssScore"` + // Risk and threat intelligence signals used to prioritize the CVE. + RiskAssessment FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment `json:"riskAssessment"` // Affected workloads Workloads FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection `json:"workloads"` } @@ -1867,20 +1922,78 @@ func (v *FindWorkloadsForCveCveCVE) GetDescription() string { return v.Descripti // 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 } +// GetRiskAssessment returns FindWorkloadsForCveCveCVE.RiskAssessment, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVE) GetRiskAssessment() FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment { + return v.RiskAssessment +} // GetWorkloads returns FindWorkloadsForCveCveCVE.Workloads, and is useful for accessing the field via an interface. func (v *FindWorkloadsForCveCveCVE) GetWorkloads() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection { return v.Workloads } +// FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment includes the requested fields of the GraphQL type VulnerabilityRiskAssessment. +// The GraphQL type's documentation follows. +// +// Threat intelligence and scoring signals used to prioritize a vulnerability. +type FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment struct { + // Operational priority derived from the available risk signals. + Priority CVEPriority `json:"priority"` + // CVSS score of the vulnerability. + CvssScore *float64 `json:"cvssScore"` + // EPSS score, representing the estimated probability of exploitation. + EpssScore *float64 `json:"epssScore"` + // EPSS percentile of the vulnerability as a percentage (0-100). + EpssPercentile *float64 `json:"epssPercentile"` + // Whether the vulnerability has a CISA Known Exploited Vulnerabilities (KEV) entry. + HasKevEntry bool `json:"hasKevEntry"` + // Whether the vulnerability is known to be used in ransomware attacks. + KnownRansomwareUse bool `json:"knownRansomwareUse"` +} + +// GetPriority returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.Priority, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetPriority() CVEPriority { + return v.Priority +} + +// GetCvssScore returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.CvssScore, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetCvssScore() *float64 { + return v.CvssScore +} + +// GetEpssScore returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.EpssScore, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetEpssScore() *float64 { + return v.EpssScore +} + +// GetEpssPercentile returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.EpssPercentile, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetEpssPercentile() *float64 { + return v.EpssPercentile +} + +// GetHasKevEntry returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.HasKevEntry, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetHasKevEntry() bool { + return v.HasKevEntry +} + +// GetKnownRansomwareUse returns FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment.KnownRansomwareUse, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVERiskAssessmentVulnerabilityRiskAssessment) GetKnownRansomwareUse() bool { + return v.KnownRansomwareUse +} + // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection includes the requested fields of the GraphQL type WorkloadWithVulnerabilityConnection. type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection struct { + // Information to aid in pagination. + PageInfo FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo `json:"pageInfo"` // List of nodes. Nodes []FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability `json:"nodes"` } +// GetPageInfo returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection.PageInfo, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection) GetPageInfo() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo { + return v.PageInfo +} + // GetNodes returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection.Nodes, and is useful for accessing the field via an interface. func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnection) GetNodes() []FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerability { return v.Nodes @@ -1975,25 +2088,76 @@ func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNo // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability includes the requested fields of the GraphQL type ImageVulnerability. type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability struct { + // The globally unique ID of the image vulnerability node. + Id string `json:"id"` // Package name of the vulnerability. - Package string `json:"package"` + Package string `json:"package"` + // Risk and threat intelligence signals used to prioritize the image vulnerability. + RiskAssessment FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment `json:"riskAssessment"` + // Package version information used to remediate the image vulnerability. + Remediation FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation `json:"remediation"` Suppression *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression `json:"suppression"` } +// GetId returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Id, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetId() string { + return v.Id +} + // GetPackage returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Package, and is useful for accessing the field via an interface. func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetPackage() string { return v.Package } +// GetRiskAssessment returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.RiskAssessment, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetRiskAssessment() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment { + return v.RiskAssessment +} + +// GetRemediation returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Remediation, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetRemediation() FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation { + return v.Remediation +} + // GetSuppression returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability.Suppression, and is useful for accessing the field via an interface. func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerability) GetSuppression() *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression { return v.Suppression } +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation includes the requested fields of the GraphQL type VulnerabilityRemediation. +// The GraphQL type's documentation follows. +// +// Package version information that helps remediate an image vulnerability. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation struct { + // First known package version that contains a fix. + FixVersion *string `json:"fixVersion"` +} + +// GetFixVersion returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation.FixVersion, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRemediation) GetFixVersion() *string { + return v.FixVersion +} + +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment includes the requested fields of the GraphQL type VulnerabilityRiskAssessment. +// The GraphQL type's documentation follows. +// +// Threat intelligence and scoring signals used to prioritize a vulnerability. +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment struct { + // Operational priority derived from the available risk signals. + Priority CVEPriority `json:"priority"` +} + +// GetPriority returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment.Priority, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilityRiskAssessment) GetPriority() CVEPriority { + return v.Priority +} + // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression includes the requested fields of the GraphQL type ImageVulnerabilitySuppression. type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression struct { // Suppression state of the vulnerability. State ImageVulnerabilitySuppressionState `json:"state"` + // The reason for the suppression of the vulnerability. + Reason string `json:"reason"` } // GetState returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression.State, and is useful for accessing the field via an interface. @@ -2001,6 +2165,11 @@ func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNo return v.State } +// GetReason returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression.Reason, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityVulnerabilityImageVulnerabilitySuppression) GetReason() string { + return v.Reason +} + // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload includes the requested fields of the GraphQL interface Workload. // // FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNodesWorkloadWithVulnerabilityWorkload is implemented by the following types: @@ -2204,6 +2373,29 @@ func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionNo return v.Name } +// FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo includes the requested fields of the GraphQL type PageInfo. +// The GraphQL type's documentation follows. +// +// # This type is used for paginating the connection +// +// Learn more about how we have implemented pagination in the [GraphQL Best Practices documentation](https://graphql.org/learn/pagination/). +type FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo struct { + // Whether or not there exists a next page in the connection. + HasNextPage bool `json:"hasNextPage"` + // The cursor for the last item in the edges. This cursor is used when paginating forwards. + EndCursor *string `json:"endCursor"` +} + +// GetHasNextPage returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo) GetHasNextPage() bool { + return v.HasNextPage +} + +// GetEndCursor returns FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface. +func (v *FindWorkloadsForCveCveCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo) GetEndCursor() *string { + return v.EndCursor +} + // FindWorkloadsForCveResponse is returned by FindWorkloadsForCve on success. type FindWorkloadsForCveResponse struct { // Get a specific CVE by its identifier. @@ -34729,6 +34921,168 @@ func (v *ListCVEsTeamWorkloadsWorkloadConnectionNodesWorkloadTeamEnvironmentEnvi return v.Name } +// ListCatalogCVEsCvesCVEConnection includes the requested fields of the GraphQL type CVEConnection. +type ListCatalogCVEsCvesCVEConnection struct { + // Information to aid in pagination. + PageInfo ListCatalogCVEsCvesCVEConnectionPageInfo `json:"pageInfo"` + // List of nodes. + Nodes []ListCatalogCVEsCvesCVEConnectionNodesCVE `json:"nodes"` +} + +// GetPageInfo returns ListCatalogCVEsCvesCVEConnection.PageInfo, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnection) GetPageInfo() ListCatalogCVEsCvesCVEConnectionPageInfo { + return v.PageInfo +} + +// GetNodes returns ListCatalogCVEsCvesCVEConnection.Nodes, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnection) GetNodes() []ListCatalogCVEsCvesCVEConnectionNodesCVE { + return v.Nodes +} + +// ListCatalogCVEsCvesCVEConnectionNodesCVE includes the requested fields of the GraphQL type CVE. +type ListCatalogCVEsCvesCVEConnectionNodesCVE struct { + // The unique identifier of the CVE. E.g. CVE-****-****. + Identifier string `json:"identifier"` + // Title of the CVE + Title string `json:"title"` + // Severity of the CVE. + Severity ImageVulnerabilitySeverity `json:"severity"` + // Link to the CVE details. + DetailsLink string `json:"detailsLink"` + // Risk and threat intelligence signals used to prioritize the CVE. + RiskAssessment ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment `json:"riskAssessment"` + // Affected workloads + Workloads ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection `json:"workloads"` +} + +// GetIdentifier returns ListCatalogCVEsCvesCVEConnectionNodesCVE.Identifier, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetIdentifier() string { return v.Identifier } + +// GetTitle returns ListCatalogCVEsCvesCVEConnectionNodesCVE.Title, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetTitle() string { return v.Title } + +// GetSeverity returns ListCatalogCVEsCvesCVEConnectionNodesCVE.Severity, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetSeverity() ImageVulnerabilitySeverity { + return v.Severity +} + +// GetDetailsLink returns ListCatalogCVEsCvesCVEConnectionNodesCVE.DetailsLink, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetDetailsLink() string { return v.DetailsLink } + +// GetRiskAssessment returns ListCatalogCVEsCvesCVEConnectionNodesCVE.RiskAssessment, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetRiskAssessment() ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment { + return v.RiskAssessment +} + +// GetWorkloads returns ListCatalogCVEsCvesCVEConnectionNodesCVE.Workloads, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVE) GetWorkloads() ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection { + return v.Workloads +} + +// ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment includes the requested fields of the GraphQL type VulnerabilityRiskAssessment. +// The GraphQL type's documentation follows. +// +// Threat intelligence and scoring signals used to prioritize a vulnerability. +type ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment struct { + // Operational priority derived from the available risk signals. + Priority CVEPriority `json:"priority"` + // CVSS score of the vulnerability. + CvssScore *float64 `json:"cvssScore"` + // EPSS score, representing the estimated probability of exploitation. + EpssScore *float64 `json:"epssScore"` + // EPSS percentile of the vulnerability as a percentage (0-100). + EpssPercentile *float64 `json:"epssPercentile"` + // Whether the vulnerability has a CISA Known Exploited Vulnerabilities (KEV) entry. + HasKevEntry bool `json:"hasKevEntry"` + // Whether the vulnerability is known to be used in ransomware attacks. + KnownRansomwareUse bool `json:"knownRansomwareUse"` +} + +// GetPriority returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.Priority, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetPriority() CVEPriority { + return v.Priority +} + +// GetCvssScore returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.CvssScore, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetCvssScore() *float64 { + return v.CvssScore +} + +// GetEpssScore returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.EpssScore, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetEpssScore() *float64 { + return v.EpssScore +} + +// GetEpssPercentile returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.EpssPercentile, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetEpssPercentile() *float64 { + return v.EpssPercentile +} + +// GetHasKevEntry returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.HasKevEntry, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetHasKevEntry() bool { + return v.HasKevEntry +} + +// GetKnownRansomwareUse returns ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment.KnownRansomwareUse, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVERiskAssessmentVulnerabilityRiskAssessment) GetKnownRansomwareUse() bool { + return v.KnownRansomwareUse +} + +// ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection includes the requested fields of the GraphQL type WorkloadWithVulnerabilityConnection. +type ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection struct { + // Information to aid in pagination. + PageInfo ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo `json:"pageInfo"` +} + +// GetPageInfo returns ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection.PageInfo, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnection) GetPageInfo() ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo { + return v.PageInfo +} + +// ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo includes the requested fields of the GraphQL type PageInfo. +// The GraphQL type's documentation follows. +// +// # This type is used for paginating the connection +// +// Learn more about how we have implemented pagination in the [GraphQL Best Practices documentation](https://graphql.org/learn/pagination/). +type ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo struct { + // The total amount of items in the connection. + TotalCount int `json:"totalCount"` +} + +// GetTotalCount returns ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo.TotalCount, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionNodesCVEWorkloadsWorkloadWithVulnerabilityConnectionPageInfo) GetTotalCount() int { + return v.TotalCount +} + +// ListCatalogCVEsCvesCVEConnectionPageInfo includes the requested fields of the GraphQL type PageInfo. +// The GraphQL type's documentation follows. +// +// # This type is used for paginating the connection +// +// Learn more about how we have implemented pagination in the [GraphQL Best Practices documentation](https://graphql.org/learn/pagination/). +type ListCatalogCVEsCvesCVEConnectionPageInfo struct { + // Whether or not there exists a next page in the connection. + HasNextPage bool `json:"hasNextPage"` + // The cursor for the last item in the edges. This cursor is used when paginating forwards. + EndCursor *string `json:"endCursor"` +} + +// GetHasNextPage returns ListCatalogCVEsCvesCVEConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionPageInfo) GetHasNextPage() bool { return v.HasNextPage } + +// GetEndCursor returns ListCatalogCVEsCvesCVEConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsCvesCVEConnectionPageInfo) GetEndCursor() *string { return v.EndCursor } + +// ListCatalogCVEsResponse is returned by ListCatalogCVEs on success. +type ListCatalogCVEsResponse struct { + // List active CVEs for workloads in all environments. + Cves ListCatalogCVEsCvesCVEConnection `json:"cves"` +} + +// GetCves returns ListCatalogCVEsResponse.Cves, and is useful for accessing the field via an interface. +func (v *ListCatalogCVEsResponse) GetCves() ListCatalogCVEsCvesCVEConnection { return v.Cves } + // ListWorkloadVulnerabilitySummariesResponse is returned by ListWorkloadVulnerabilitySummaries on success. type ListWorkloadVulnerabilitySummariesResponse struct { // Get a team by its slug. @@ -37622,12 +37976,20 @@ func (v *__EnvironmentOIDCIssuerInput) GetName() string { return v.Name } // __FindWorkloadsForCveInput is used internally by genqlient type __FindWorkloadsForCveInput struct { - Identifier string `json:"identifier"` + Identifier string `json:"identifier"` + After *string `json:"after"` + Filter *CVEWorkloadsFilter `json:"filter"` } // GetIdentifier returns __FindWorkloadsForCveInput.Identifier, and is useful for accessing the field via an interface. func (v *__FindWorkloadsForCveInput) GetIdentifier() string { return v.Identifier } +// GetAfter returns __FindWorkloadsForCveInput.After, and is useful for accessing the field via an interface. +func (v *__FindWorkloadsForCveInput) GetAfter() *string { return v.After } + +// GetFilter returns __FindWorkloadsForCveInput.Filter, and is useful for accessing the field via an interface. +func (v *__FindWorkloadsForCveInput) GetFilter() *CVEWorkloadsFilter { return v.Filter } + // __GetActivePostgresBranchInput is used internally by genqlient type __GetActivePostgresBranchInput struct { Team string `json:"team"` @@ -38308,6 +38670,26 @@ func (v *__ListCVEsInput) GetTeam() string { return v.Team } // GetFilter returns __ListCVEsInput.Filter, and is useful for accessing the field via an interface. func (v *__ListCVEsInput) GetFilter() *TeamWorkloadsFilter { return v.Filter } +// __ListCatalogCVEsInput is used internally by genqlient +type __ListCatalogCVEsInput struct { + First int `json:"first"` + After *string `json:"after"` + Filter *CVEFilter `json:"filter"` + Order *CVEOrder `json:"order"` +} + +// GetFirst returns __ListCatalogCVEsInput.First, and is useful for accessing the field via an interface. +func (v *__ListCatalogCVEsInput) GetFirst() int { return v.First } + +// GetAfter returns __ListCatalogCVEsInput.After, and is useful for accessing the field via an interface. +func (v *__ListCatalogCVEsInput) GetAfter() *string { return v.After } + +// GetFilter returns __ListCatalogCVEsInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListCatalogCVEsInput) GetFilter() *CVEFilter { return v.Filter } + +// GetOrder returns __ListCatalogCVEsInput.Order, and is useful for accessing the field via an interface. +func (v *__ListCatalogCVEsInput) GetOrder() *CVEOrder { return v.Order } + // __ListWorkloadVulnerabilitySummariesInput is used internally by genqlient type __ListWorkloadVulnerabilitySummariesInput struct { Team string `json:"team"` @@ -39671,7 +40053,7 @@ func Environments( // The query executed by FindWorkloadsForCve. const FindWorkloadsForCve_Operation = ` -query FindWorkloadsForCve ($identifier: String!) { +query FindWorkloadsForCve ($identifier: String!, $after: Cursor, $filter: CVEWorkloadsFilter) { cve(identifier: $identifier) { id identifier @@ -39679,13 +40061,32 @@ query FindWorkloadsForCve ($identifier: String!) { title description detailsLink - cvssScore - workloads(first: 1000) { + riskAssessment { + priority + cvssScore + epssScore + epssPercentile + hasKevEntry + knownRansomwareUse + } + workloads(first: 100, after: $after, filter: $filter) { + pageInfo { + hasNextPage + endCursor + } nodes { vulnerability { + id package + riskAssessment { + priority + } + remediation { + fixVersion + } suppression { state + reason } } workload { @@ -39710,12 +40111,16 @@ func FindWorkloadsForCve( ctx_ context.Context, client_ graphql.Client, identifier string, + after *string, + filter *CVEWorkloadsFilter, ) (data_ *FindWorkloadsForCveResponse, err_ error) { req_ := &graphql.Request{ OpName: "FindWorkloadsForCve", Query: FindWorkloadsForCve_Operation, Variables: &__FindWorkloadsForCveInput{ Identifier: identifier, + After: after, + Filter: filter, }, } @@ -42188,6 +42593,68 @@ func ListCVEs( return data_, err_ } +// The query executed by ListCatalogCVEs. +const ListCatalogCVEs_Operation = ` +query ListCatalogCVEs ($first: Int!, $after: Cursor, $filter: CVEFilter, $order: CVEOrder) { + cves(first: $first, after: $after, filter: $filter, orderBy: $order) { + pageInfo { + hasNextPage + endCursor + } + nodes { + identifier + title + severity + detailsLink + riskAssessment { + priority + cvssScore + epssScore + epssPercentile + hasKevEntry + knownRansomwareUse + } + workloads(first: 1) { + pageInfo { + totalCount + } + } + } + } +} +` + +func ListCatalogCVEs( + ctx_ context.Context, + client_ graphql.Client, + first int, + after *string, + filter *CVEFilter, + order *CVEOrder, +) (data_ *ListCatalogCVEsResponse, err_ error) { + req_ := &graphql.Request{ + OpName: "ListCatalogCVEs", + Query: ListCatalogCVEs_Operation, + Variables: &__ListCatalogCVEsInput{ + First: first, + After: after, + Filter: filter, + Order: order, + }, + } + + data_ = &ListCatalogCVEsResponse{} + resp_ := &graphql.Response{Data: data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return data_, err_ +} + // The query executed by ListWorkloadVulnerabilitySummaries. const ListWorkloadVulnerabilitySummaries_Operation = ` query ListWorkloadVulnerabilitySummaries ($team: Slug!, $filter: TeamVulnerabilitySummaryFilter) { diff --git a/internal/vulnerability/catalog.go b/internal/vulnerability/catalog.go new file mode 100644 index 00000000..a8aa2215 --- /dev/null +++ b/internal/vulnerability/catalog.go @@ -0,0 +1,109 @@ +package vulnerability + +import ( + "context" + "fmt" + + "github.com/Khan/genqlient/graphql" + "github.com/nais/cli/internal/naisapi" + "github.com/nais/cli/internal/naisapi/gql" +) + +type CatalogCVE struct { + Identifier string `json:"identifier"` + Title string `json:"title"` + Severity string `json:"severity"` + Priority string `json:"priority"` + CVSSScore *float64 `json:"cvss_score"` + EPSSScore *float64 `json:"epss_score"` + EPSSPercentile *float64 `json:"epss_percentile"` + HasKEVEntry bool `json:"has_kev_entry"` + KnownRansomwareUse bool `json:"known_ransomware_use"` + AffectedWorkloads int `json:"affected_workloads"` + DetailsLink string `json:"details_link"` +} + +func ListCatalogCVEs(ctx context.Context, priority *gql.CVEPriority, order gql.CVEOrder, limit int) ([]CatalogCVE, error) { + _ = `# @genqlient + query ListCatalogCVEs($first: Int!, $after: Cursor, $filter: CVEFilter, $order: CVEOrder) { + cves(first: $first, after: $after, filter: $filter, orderBy: $order) { + pageInfo { hasNextPage endCursor } + nodes { + identifier + title + severity + detailsLink + riskAssessment { + priority + # @genqlient(pointer: true) + cvssScore + # @genqlient(pointer: true) + epssScore + # @genqlient(pointer: true) + epssPercentile + hasKevEntry + knownRansomwareUse + } + workloads(first: 1) { pageInfo { totalCount } } + } + } + } + ` + + if limit < 0 { + return nil, fmt.Errorf("CVE limit must be zero or greater") + } + client, err := naisapi.GraphqlClient(ctx) + if err != nil { + return nil, err + } + return listCatalogCVEs(ctx, client, priority, order, limit) +} + +func listCatalogCVEs(ctx context.Context, client graphql.Client, priority *gql.CVEPriority, order gql.CVEOrder, limit int) ([]CatalogCVE, error) { + if limit < 0 { + return nil, fmt.Errorf("CVE limit must be zero or greater") + } + var filter *gql.CVEFilter + if priority != nil { + filter = &gql.CVEFilter{Priority: priority} + } + ret := make([]CatalogCVE, 0) + var after *string + for { + first := 100 + if limit > 0 && limit-len(ret) < first { + first = limit - len(ret) + } + resp, err := gql.ListCatalogCVEs(ctx, client, first, after, filter, &order) + if err != nil { + return nil, fmt.Errorf("listing CVEs: %w", err) + } + for _, cve := range resp.Cves.Nodes { + ret = append(ret, CatalogCVE{ + Identifier: cve.Identifier, + Title: cve.Title, + Severity: string(cve.Severity), + Priority: string(cve.RiskAssessment.Priority), + CVSSScore: cve.RiskAssessment.CvssScore, + EPSSScore: cve.RiskAssessment.EpssScore, + EPSSPercentile: cve.RiskAssessment.EpssPercentile, + HasKEVEntry: cve.RiskAssessment.HasKevEntry, + KnownRansomwareUse: cve.RiskAssessment.KnownRansomwareUse, + AffectedWorkloads: cve.Workloads.PageInfo.TotalCount, + DetailsLink: cve.DetailsLink, + }) + } + if limit > 0 && len(ret) >= limit { + return ret, nil + } + page := resp.Cves.PageInfo + if !page.HasNextPage { + return ret, nil + } + if page.EndCursor == nil || *page.EndCursor == "" || (after != nil && *page.EndCursor == *after) { + return nil, fmt.Errorf("listing CVEs: missing next cursor") + } + after = page.EndCursor + } +} diff --git a/internal/vulnerability/catalog_test.go b/internal/vulnerability/catalog_test.go new file mode 100644 index 00000000..06d159f3 --- /dev/null +++ b/internal/vulnerability/catalog_test.go @@ -0,0 +1,132 @@ +package vulnerability + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/Khan/genqlient/graphql" + "github.com/nais/cli/internal/naisapi/gql" +) + +type catalogClientFunc func(context.Context, *graphql.Request, *graphql.Response) error + +func (f catalogClientFunc) MakeRequest(ctx context.Context, req *graphql.Request, resp *graphql.Response) error { + return f(ctx, req, resp) +} + +func TestListCatalogCVEsPaginatesWithFilterAndOrder(t *testing.T) { + requests := 0 + client := catalogClientFunc(func(_ context.Context, req *graphql.Request, resp *graphql.Response) error { + var vars struct { + After *string `json:"after"` + Filter struct { + Priority string `json:"priority"` + } `json:"filter"` + Order struct { + Field string `json:"field"` + Direction string `json:"direction"` + } `json:"order"` + } + data, err := json.Marshal(req.Variables) + if err != nil { + return err + } + if err := json.Unmarshal(data, &vars); err != nil { + return err + } + if vars.Filter.Priority != "HIGH" || vars.Order.Field != "AFFECTED_WORKLOADS_COUNT" || vars.Order.Direction != "DESC" { + t.Errorf("variables = %+v; want HIGH sorted by affected workload count descending", vars) + } + if requests == 1 && (vars.After == nil || *vars.After != "next") { + t.Errorf("after = %v; want next", vars.After) + } + requests++ + data, err = json.Marshal(map[string]any{"cves": map[string]any{ + "pageInfo": map[string]any{"hasNextPage": requests == 1, "endCursor": "next"}, + "nodes": []any{map[string]any{ + "identifier": fmt.Sprintf("CVE-2026-%04d", requests), + "severity": "HIGH", "title": "test", "detailsLink": "https://example.org/cve", + "riskAssessment": map[string]any{"priority": "HIGH", "hasKevEntry": requests == 1, "knownRansomwareUse": false}, + "workloads": map[string]any{"pageInfo": map[string]any{"totalCount": requests + 2}}, + }}, + }}) + if err != nil { + return err + } + return json.Unmarshal(data, resp.Data) + }) + priority := gql.CVEPriorityHigh + cves, err := listCatalogCVEs(context.Background(), client, &priority, gql.CVEOrder{ + Field: gql.CVEOrderFieldAffectedWorkloadsCount, Direction: gql.OrderDirectionDesc, + }, 0) + if err != nil { + t.Fatal(err) + } + if requests != 2 || len(cves) != 2 || cves[0].AffectedWorkloads != 3 || !cves[0].HasKEVEntry || + cves[1].AffectedWorkloads != 4 || cves[1].Identifier != "CVE-2026-0002" { + t.Errorf("requests = %d, CVEs = %+v; want two pages with counts and signals", requests, cves) + } +} + +func TestListCatalogCVEsRejectsMissingCursor(t *testing.T) { + client := catalogClientFunc(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error { + data, err := json.Marshal(map[string]any{"cves": map[string]any{ + "pageInfo": map[string]any{"hasNextPage": true}, "nodes": []any{}, + }}) + if err != nil { + return err + } + return json.Unmarshal(data, resp.Data) + }) + cves, err := listCatalogCVEs(context.Background(), client, nil, gql.CVEOrder{ + Field: gql.CVEOrderFieldPriority, Direction: gql.OrderDirectionAsc, + }, 0) + if err == nil || cves != nil { + t.Errorf("got CVEs %v, error %v; want a cursor error and no partial result", cves, err) + } +} + +func TestListCatalogCVEsStopsAtLimit(t *testing.T) { + requests := 0 + client := catalogClientFunc(func(_ context.Context, req *graphql.Request, resp *graphql.Response) error { + var vars struct { + First int `json:"first"` + After *string `json:"after"` + } + body, err := json.Marshal(req.Variables) + if err != nil { + return err + } + if err := json.Unmarshal(body, &vars); err != nil { + return err + } + if requests >= 2 || (requests == 0 && (vars.First != 100 || vars.After != nil)) || + (requests == 1 && (vars.First != 1 || vars.After == nil || *vars.After != "next")) { + t.Errorf("unexpected catalog page %d: %+v", requests, vars) + } + nodes := make([]map[string]any, vars.First) + for i := range nodes { + nodes[i] = map[string]any{ + "identifier": fmt.Sprintf("CVE-2026-%04d", requests*100+i+1), + "riskAssessment": map[string]any{"priority": "HIGH"}, + "workloads": map[string]any{"pageInfo": map[string]any{"totalCount": 1}}, + } + } + requests++ + body, err = json.Marshal(map[string]any{"cves": map[string]any{ + "pageInfo": map[string]any{"hasNextPage": true, "endCursor": "next"}, "nodes": nodes, + }}) + if err != nil { + return err + } + return json.Unmarshal(body, resp.Data) + }) + cves, err := listCatalogCVEs(context.Background(), client, nil, gql.CVEOrder{ + Field: gql.CVEOrderFieldPriority, Direction: gql.OrderDirectionAsc, + }, 101) + if err != nil || requests != 2 || len(cves) != 101 || cves[100].Identifier != "CVE-2026-0101" { + t.Fatalf("got %d CVEs in %d requests, error %v; want 101 CVEs in two requests", len(cves), requests, err) + } +} diff --git a/internal/vulnerability/command/command.go b/internal/vulnerability/command/command.go index d657d05f..8f21818d 100644 --- a/internal/vulnerability/command/command.go +++ b/internal/vulnerability/command/command.go @@ -21,6 +21,7 @@ func Vulnerabilities(parentFlags *flags.GlobalFlags) *naistrix.Command { summary(f), list(f), search(f), + cves(f), }, } } diff --git a/internal/vulnerability/command/completion_test.go b/internal/vulnerability/command/completion_test.go new file mode 100644 index 00000000..1bc48080 --- /dev/null +++ b/internal/vulnerability/command/completion_test.go @@ -0,0 +1,47 @@ +package command_test + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/nais/cli/internal/application" + "github.com/nais/naistrix" +) + +func TestCVEFlagCompletion(t *testing.T) { + for _, tt := range []struct { + flag, want string + }{ + {"priority", "HIGH"}, + {"sort", "AFFECTED_WORKLOADS_COUNT"}, + {"direction", "DESC"}, + } { + t.Run(tt.flag, func(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + t.Setenv("APPDATA", t.TempDir()) + t.Setenv("NAIS_CONFIG", "") + config := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(config, []byte("{}\n"), 0o600); err != nil { + t.Fatal(err) + } + var output bytes.Buffer + app, _, err := application.New(&output) + if err != nil { + t.Fatal(err) + } + if err := app.Run(naistrix.RunWithArgs([]string{"--config", config, "--no-colors", "__complete", "vulns", "cves", "--" + tt.flag, ""})); err != nil { + t.Fatal(err) + } + for line := range strings.SplitSeq(output.String(), "\n") { + if strings.SplitN(line, "\t", 2)[0] == tt.want { + return + } + } + t.Errorf("--%s completion missing %q: %s", tt.flag, tt.want, output.String()) + }) + } +} diff --git a/internal/vulnerability/command/cves.go b/internal/vulnerability/command/cves.go new file mode 100644 index 00000000..f9e7501c --- /dev/null +++ b/internal/vulnerability/command/cves.go @@ -0,0 +1,88 @@ +package command + +import ( + "context" + "fmt" + "strings" + + "github.com/nais/cli/internal/naisapi/gql" + "github.com/nais/cli/internal/vulnerability" + "github.com/nais/cli/internal/vulnerability/command/flag" + "github.com/nais/naistrix" + "github.com/nais/naistrix/output" +) + +func cves(parent *flag.Vulnerabilities) *naistrix.Command { + f := &flag.CVEs{Vulnerabilities: parent, Output: "table", Limit: 100, Sort: "PRIORITY", Direction: "ASC"} + return &naistrix.Command{ + Name: "cves", + Title: "List CVEs affecting workloads across all teams.", + Description: "List up to 100 global CVEs by default with risk signals and affected-workload counts. Use --limit 0 for all CVEs, or 'vulns search -A ' for affected workload details.", + Flags: f, + ValidateFlagsFunc: func(context.Context) error { + if f.Limit < 0 { + return fmt.Errorf("--limit must be zero or greater") + } + return nil + }, + Examples: []naistrix.CommandExample{ + {Description: "List high-priority CVEs by affected workload count.", Command: "--priority HIGH --sort AFFECTED_WORKLOADS_COUNT --direction DESC"}, + {Description: "Export the full CVE catalog.", Command: "--limit 0 -o json"}, + }, + RunFunc: func(ctx context.Context, _ *naistrix.Arguments, out *naistrix.OutputWriter) error { + var priority *gql.CVEPriority + if f.Priority != "" { + p := gql.CVEPriority(strings.ToUpper(string(f.Priority))) + switch p { + case gql.CVEPriorityHigh, gql.CVEPriorityElevated, gql.CVEPriorityMonitor: + priority = &p + default: + return fmt.Errorf("invalid priority %q: choose HIGH, ELEVATED, or MONITOR (URGENT requires workload context)", f.Priority) + } + } + + order := gql.CVEOrder{Field: gql.CVEOrderField(strings.ToUpper(string(f.Sort))), Direction: gql.OrderDirection(strings.ToUpper(string(f.Direction)))} + switch order.Field { + case gql.CVEOrderFieldIdentifier, gql.CVEOrderFieldSeverity, gql.CVEOrderFieldCvssScore, + gql.CVEOrderFieldAffectedWorkloadsCount, gql.CVEOrderFieldPriority: + default: + return fmt.Errorf("invalid sort %q: choose IDENTIFIER, SEVERITY, CVSS_SCORE, AFFECTED_WORKLOADS_COUNT, or PRIORITY", f.Sort) + } + if order.Direction != gql.OrderDirectionAsc && order.Direction != gql.OrderDirectionDesc { + return fmt.Errorf("invalid direction %q: choose ASC or DESC", f.Direction) + } + + catalog, err := vulnerability.ListCatalogCVEs(ctx, priority, order, f.Limit) + if err != nil { + return err + } + if f.Output == "json" { + return out.JSON(output.JSONWithPrettyOutput()).Render(catalog) + } + if len(catalog) == 0 { + out.Println("No CVEs found.") + return nil + } + type row struct { + CVE output.Link `json:"cve"` + Title string `json:"title"` + Severity string `json:"severity"` + Priority string `json:"priority"` + CVSSScore any `json:"cvss_score" heading:"CVSS Score"` + KEV bool `json:"kev" heading:"KEV"` + KnownRansomware bool `json:"known_ransomware" heading:"Ransomware"` + AffectedWorkloads int `json:"affected_workloads" heading:"Workloads"` + } + rows := make([]row, 0, len(catalog)) + for _, cve := range catalog { + rows = append(rows, row{ + CVE: output.Link{Name: cve.Identifier, URL: cve.DetailsLink}, Title: cve.Title, + Severity: cve.Severity, Priority: cve.Priority, CVSSScore: cvssDisplay(cve.CVSSScore), + KEV: cve.HasKEVEntry, KnownRansomware: cve.KnownRansomwareUse, + AffectedWorkloads: cve.AffectedWorkloads, + }) + } + return out.Table().Render(rows) + }, + } +} diff --git a/internal/vulnerability/command/flag/flag.go b/internal/vulnerability/command/flag/flag.go index 88c892df..f79591c7 100644 --- a/internal/vulnerability/command/flag/flag.go +++ b/internal/vulnerability/command/flag/flag.go @@ -34,8 +34,35 @@ type ( *List Output Output `name:"output" short:"o" usage:"Format output (table or json)."` } + + CVEs struct { + *Vulnerabilities + Output Output `name:"output" short:"o" usage:"Format output (table or json)."` + Limit int `name:"limit" short:"l" usage:"Maximum number of CVEs to fetch (0 = all)."` + Priority CVEPriority `name:"priority" usage:"Filter by global CVE priority (HIGH, ELEVATED, MONITOR)."` + Sort CVESort `name:"sort" usage:"Sort by IDENTIFIER, SEVERITY, CVSS_SCORE, AFFECTED_WORKLOADS_COUNT, or PRIORITY."` + Direction SortDirection `name:"direction" usage:"Sort direction (ASC or DESC)."` + } ) +type CVEPriority string + +func (*CVEPriority) AutoComplete(context.Context, *naistrix.Arguments, string, any) ([]string, string) { + return []string{"HIGH", "ELEVATED", "MONITOR"}, "Global CVE priorities." +} + +type CVESort string + +func (*CVESort) AutoComplete(context.Context, *naistrix.Arguments, string, any) ([]string, string) { + return []string{"IDENTIFIER", "SEVERITY", "CVSS_SCORE", "AFFECTED_WORKLOADS_COUNT", "PRIORITY"}, "CVE sort fields." +} + +type SortDirection string + +func (*SortDirection) AutoComplete(context.Context, *naistrix.Arguments, string, any) ([]string, string) { + return []string{"ASC", "DESC"}, "Sort directions." +} + type Output string var _ naistrix.FlagAutoCompleter = (*Output)(nil) diff --git a/internal/vulnerability/command/search.go b/internal/vulnerability/command/search.go index cff7f382..184fbb67 100644 --- a/internal/vulnerability/command/search.go +++ b/internal/vulnerability/command/search.go @@ -69,7 +69,11 @@ func runCVESearch(ctx context.Context, args *naistrix.Arguments, out *naistrix.O {Field: "CVE Id", Value: output.Link{Name: report.Identifier, URL: report.DetailsLink}}, {Field: "Title", Value: report.Title}, {Field: "Severity", Value: report.Severity}, + {Field: "Priority", Value: report.Priority}, {Field: "CVSS Score", Value: cvssDisplay(report.CvssScore)}, + {Field: "EPSS Percentile", Value: cvssDisplay(report.EpssPercentile)}, + {Field: "Known Exploited (KEV)", Value: report.HasKevEntry}, + {Field: "Known Ransomware Use", Value: report.KnownRansomwareUse}, } if err := out.Table().Render(detailRows); err != nil { @@ -94,6 +98,8 @@ func runCVESearch(ctx context.Context, args *naistrix.Arguments, out *naistrix.O Type string `json:"type"` Environment string `json:"environment"` Package string `json:"package"` + Priority string `json:"priority"` + FixVersion string `json:"fix_version" heading:"Fix version"` Suppression string `json:"suppression"` } @@ -108,6 +114,8 @@ func runCVESearch(ctx context.Context, args *naistrix.Arguments, out *naistrix.O Type: w.WorkloadType, Environment: w.Environment, Package: w.Package, + Priority: w.Priority, + FixVersion: searchFixVersion(w.FixVersion), Suppression: w.SuppressionState, }) } @@ -118,3 +126,10 @@ func runCVESearch(ctx context.Context, args *naistrix.Arguments, out *naistrix.O return nil } + +func searchFixVersion(version *string) string { + if version == nil || *version == "" { + return "N/A" + } + return *version +} diff --git a/internal/vulnerability/search_test.go b/internal/vulnerability/search_test.go new file mode 100644 index 00000000..bb434d12 --- /dev/null +++ b/internal/vulnerability/search_test.go @@ -0,0 +1,77 @@ +package vulnerability + +import ( + "context" + "encoding/json" + "testing" + + "github.com/Khan/genqlient/graphql" +) + +type searchClientFunc func(context.Context, *graphql.Request, *graphql.Response) error + +func (f searchClientFunc) MakeRequest(ctx context.Context, req *graphql.Request, resp *graphql.Response) error { + return f(ctx, req, resp) +} + +func TestFindWorkloadsForCvePagesBeforeEnvironmentFiltering(t *testing.T) { + requests := 0 + client := searchClientFunc(func(_ context.Context, req *graphql.Request, resp *graphql.Response) error { + var vars struct { + After *string `json:"after"` + Filter struct { + TeamSlugs []string `json:"teamSlugs"` + } `json:"filter"` + } + data, err := json.Marshal(req.Variables) + if err != nil { + return err + } + if err := json.Unmarshal(data, &vars); err != nil { + return err + } + if len(vars.Filter.TeamSlugs) != 1 || vars.Filter.TeamSlugs[0] != "team-a" { + t.Errorf("filter = %+v; want team-a", vars.Filter) + } + if requests == 1 && (vars.After == nil || *vars.After != "next") { + t.Errorf("after = %v; want next", vars.After) + } + requests++ + env := "dev" + if requests == 2 { + env = "prod" + } + data, err = json.Marshal(map[string]any{"cve": map[string]any{ + "id": "CVE_1", "identifier": "CVE-2026-0001", "severity": "HIGH", + "title": "Test", "description": "Test", "detailsLink": "https://example.org/cve", + "riskAssessment": map[string]any{"priority": "HIGH", "hasKevEntry": true, "knownRansomwareUse": false}, + "workloads": map[string]any{ + "pageInfo": map[string]any{"hasNextPage": requests == 1, "endCursor": "next"}, + "nodes": []any{map[string]any{ + "vulnerability": map[string]any{ + "id": "VULN_1", "package": "pkg", + "riskAssessment": map[string]any{"priority": "HIGH"}, + "remediation": map[string]any{"fixVersion": "2.0"}, + }, + "workload": map[string]any{ + "__typename": "Application", "name": "app", + "team": map[string]any{"slug": "team-a"}, + "teamEnvironment": map[string]any{"environment": map[string]any{"name": env}}, + }, + }}, + }, + }}) + if err != nil { + return err + } + return json.Unmarshal(data, resp.Data) + }) + report, err := findWorkloadsForCve(context.Background(), client, CVEMetadata{CveID: "CVE-2026-0001"}, "team-a", "prod", false) + if err != nil { + t.Fatal(err) + } + if requests != 2 || len(report.Workloads) != 1 || report.Workloads[0].Environment != "prod" || + report.Workloads[0].VulnerabilityID != "VULN_1" || report.Workloads[0].FixVersion == nil || *report.Workloads[0].FixVersion != "2.0" { + t.Errorf("requests = %d, report = %+v; want prod finding after two pages", requests, report) + } +} diff --git a/internal/vulnerability/vulnerability.go b/internal/vulnerability/vulnerability.go index e91e05b8..e946ff6e 100644 --- a/internal/vulnerability/vulnerability.go +++ b/internal/vulnerability/vulnerability.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/Khan/genqlient/graphql" "github.com/nais/cli/internal/naisapi" "github.com/nais/cli/internal/naisapi/gql" "k8s.io/utils/ptr" @@ -58,22 +59,31 @@ type CVEEntry struct { } type CVEWorkload struct { - Team string `json:"team"` - WorkloadName string `json:"workload_name"` - WorkloadType string `json:"workload_type"` - Environment string `json:"environment"` - Package string `json:"package"` - SuppressionState string `json:"suppression_state"` + Team string `json:"team"` + WorkloadName string `json:"workload_name"` + WorkloadType string `json:"workload_type"` + Environment string `json:"environment"` + Package string `json:"package"` + VulnerabilityID string `json:"vulnerability_id"` + Priority string `json:"priority"` + FixVersion *string `json:"fix_version"` + SuppressionState string `json:"suppression_state"` + SuppressionReason string `json:"suppression_reason"` } type CVEReport struct { - Identifier string `json:"identifier"` - Severity string `json:"severity"` - Title string `json:"title"` - Description string `json:"description"` - DetailsLink string `json:"details_link"` - CvssScore *float64 `json:"cvss_score"` - Workloads []CVEWorkload `json:"workloads"` + Identifier string `json:"identifier"` + Severity string `json:"severity"` + Title string `json:"title"` + Description string `json:"description"` + DetailsLink string `json:"details_link"` + CvssScore *float64 `json:"cvss_score"` + EpssScore *float64 `json:"epss_score"` + Priority string `json:"priority"` + EpssPercentile *float64 `json:"epss_percentile"` + HasKevEntry bool `json:"has_kev_entry"` + KnownRansomwareUse bool `json:"known_ransomware_use"` + Workloads []CVEWorkload `json:"workloads"` } func GetTeamSummary(ctx context.Context, team, environment string) (*TeamSummary, error) { @@ -302,7 +312,7 @@ func ListCVEs(ctx context.Context, team string, environment string) ([]CVEEntry, func FindWorkloadsForCve(ctx context.Context, metadata CVEMetadata, team string, environment string, allTeams bool) (*CVEReport, error) { _ = `# @genqlient - query FindWorkloadsForCve($identifier: String!) { + query FindWorkloadsForCve($identifier: String!, $after: Cursor, $filter: CVEWorkloadsFilter) { cve(identifier: $identifier) { id identifier @@ -310,15 +320,29 @@ func FindWorkloadsForCve(ctx context.Context, metadata CVEMetadata, team string, title description detailsLink - # @genqlient(pointer: true) - cvssScore - workloads(first: 1000) { + riskAssessment { + priority + # @genqlient(pointer: true) + cvssScore + # @genqlient(pointer: true) + epssScore + # @genqlient(pointer: true) + epssPercentile + hasKevEntry + knownRansomwareUse + } + workloads(first: 100, after: $after, filter: $filter) { + pageInfo { hasNextPage endCursor } nodes { vulnerability { + id package + riskAssessment { priority } + remediation { fixVersion } # @genqlient(pointer: true) suppression { state + reason } } workload { @@ -343,52 +367,97 @@ func FindWorkloadsForCve(ctx context.Context, metadata CVEMetadata, team string, if err != nil { return nil, err } + return findWorkloadsForCve(ctx, client, metadata, team, environment, allTeams) +} - resp, err := gql.FindWorkloadsForCve(ctx, client, metadata.CveID) - if err != nil { - return nil, err +func ConsoleResourceURL(host, team, environment, workloadType, workloadName string) string { + if environment == "" { + return "" + } + + resourceType := strings.ToLower(workloadType) + switch resourceType { + case "application": + resourceType = "app" + case "job": + resourceType = "job" + default: + return "" } - var cvssScore *float64 - if resp.Cve.CvssScore != nil { - score := *resp.Cve.CvssScore - cvssScore = &score + return fmt.Sprintf("https://%s/team/%s/%s/%s/%s", host, team, environment, resourceType, workloadName) +} + +func findWorkloadsForCve(ctx context.Context, client graphql.Client, metadata CVEMetadata, team, environment string, allTeams bool) (*CVEReport, error) { + var filter *gql.CVEWorkloadsFilter + if !allTeams { + filter = &gql.CVEWorkloadsFilter{TeamSlugs: []string{team}} + } + resp, err := gql.FindWorkloadsForCve(ctx, client, metadata.CveID, nil, filter) + if err != nil { + return nil, err } ret := &CVEReport{ - Identifier: resp.Cve.Identifier, - Severity: string(resp.Cve.Severity), - Title: resp.Cve.Title, - Description: resp.Cve.Description, - DetailsLink: resp.Cve.DetailsLink, - CvssScore: cvssScore, - Workloads: make([]CVEWorkload, 0, len(resp.Cve.Workloads.Nodes)), + Identifier: resp.Cve.Identifier, + Severity: string(resp.Cve.Severity), + Title: resp.Cve.Title, + Description: resp.Cve.Description, + DetailsLink: resp.Cve.DetailsLink, + CvssScore: resp.Cve.RiskAssessment.CvssScore, + EpssScore: resp.Cve.RiskAssessment.EpssScore, + Priority: string(resp.Cve.RiskAssessment.Priority), + EpssPercentile: resp.Cve.RiskAssessment.EpssPercentile, + HasKevEntry: resp.Cve.RiskAssessment.HasKevEntry, + KnownRansomwareUse: resp.Cve.RiskAssessment.KnownRansomwareUse, + Workloads: make([]CVEWorkload, 0, len(resp.Cve.Workloads.Nodes)), } - for _, node := range resp.Cve.Workloads.Nodes { - workloadTeam := node.Workload.GetTeam().Slug - if !allTeams && workloadTeam != team { - continue - } + var previousCursor *string + for { + for _, node := range resp.Cve.Workloads.Nodes { + workloadTeam := node.Workload.GetTeam().Slug + if !allTeams && workloadTeam != team { + continue + } - env := node.Workload.GetTeamEnvironment().Environment.Name - if len(environment) > 0 && environment != env { - continue - } + env := node.Workload.GetTeamEnvironment().Environment.Name + if len(environment) > 0 && environment != env { + continue + } - suppressionState := "N/A" - if node.Vulnerability.Suppression != nil && node.Vulnerability.Suppression.State != "" { - suppressionState = string(node.Vulnerability.Suppression.State) - } + suppressionState := "N/A" + suppressionReason := "" + if node.Vulnerability.Suppression != nil && node.Vulnerability.Suppression.State != "" { + suppressionState = string(node.Vulnerability.Suppression.State) + suppressionReason = node.Vulnerability.Suppression.Reason + } - ret.Workloads = append(ret.Workloads, CVEWorkload{ - Team: workloadTeam, - WorkloadName: node.Workload.GetName(), - WorkloadType: ptr.Deref(node.Workload.GetTypename(), ""), - Environment: env, - Package: node.Vulnerability.Package, - SuppressionState: suppressionState, - }) + ret.Workloads = append(ret.Workloads, CVEWorkload{ + Team: workloadTeam, + WorkloadName: node.Workload.GetName(), + WorkloadType: ptr.Deref(node.Workload.GetTypename(), ""), + Environment: env, + Package: node.Vulnerability.Package, + VulnerabilityID: node.Vulnerability.Id, + Priority: string(node.Vulnerability.RiskAssessment.Priority), + FixVersion: node.Vulnerability.Remediation.FixVersion, + SuppressionState: suppressionState, + SuppressionReason: suppressionReason, + }) + } + page := resp.Cve.Workloads.PageInfo + if !page.HasNextPage { + break + } + if page.EndCursor == nil || *page.EndCursor == "" || (previousCursor != nil && *page.EndCursor == *previousCursor) { + return nil, fmt.Errorf("listing workloads affected by %q: missing next cursor", metadata.CveID) + } + previousCursor = page.EndCursor + resp, err = gql.FindWorkloadsForCve(ctx, client, metadata.CveID, page.EndCursor, filter) + if err != nil { + return nil, fmt.Errorf("listing workloads affected by %q: %w", metadata.CveID, err) + } } sort.Slice(ret.Workloads, func(i, j int) bool { @@ -400,21 +469,3 @@ func FindWorkloadsForCve(ctx context.Context, metadata CVEMetadata, team string, return ret, nil } - -func ConsoleResourceURL(host, team, environment, workloadType, workloadName string) string { - if environment == "" { - return "" - } - - resourceType := strings.ToLower(workloadType) - switch resourceType { - case "application": - resourceType = "app" - case "job": - resourceType = "job" - default: - return "" - } - - return fmt.Sprintf("https://%s/team/%s/%s/%s/%s", host, team, environment, resourceType, workloadName) -}