diff --git a/scanner/keppel/client/generated.go b/scanner/keppel/client/generated.go new file mode 100644 index 0000000..7af7b31 --- /dev/null +++ b/scanner/keppel/client/generated.go @@ -0,0 +1,641 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package client + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// AddComponentVersionToIssueResponse is returned by AddComponentVersionToIssue on success. +type AddComponentVersionToIssueResponse struct { + AddComponentVersionToIssue *Issue `json:"addComponentVersionToIssue"` +} + +// GetAddComponentVersionToIssue returns AddComponentVersionToIssueResponse.AddComponentVersionToIssue, and is useful for accessing the field via an interface. +func (v *AddComponentVersionToIssueResponse) GetAddComponentVersionToIssue() *Issue { + return v.AddComponentVersionToIssue +} + +// Component includes the requested fields of the GraphQL type Component. +type Component struct { + Id string `json:"id"` + Name string `json:"name"` + Type ComponentTypeValues `json:"type"` +} + +// GetId returns Component.Id, and is useful for accessing the field via an interface. +func (v *Component) GetId() string { return v.Id } + +// GetName returns Component.Name, and is useful for accessing the field via an interface. +func (v *Component) GetName() string { return v.Name } + +// GetType returns Component.Type, and is useful for accessing the field via an interface. +func (v *Component) GetType() ComponentTypeValues { return v.Type } + +// ComponentConnection includes the requested fields of the GraphQL type ComponentConnection. +type ComponentConnection struct { + Edges []*ComponentConnectionEdgesComponentEdge `json:"edges"` +} + +// GetEdges returns ComponentConnection.Edges, and is useful for accessing the field via an interface. +func (v *ComponentConnection) GetEdges() []*ComponentConnectionEdgesComponentEdge { return v.Edges } + +// ComponentConnectionEdgesComponentEdge includes the requested fields of the GraphQL type ComponentEdge. +type ComponentConnectionEdgesComponentEdge struct { + Node *Component `json:"node"` +} + +// GetNode returns ComponentConnectionEdgesComponentEdge.Node, and is useful for accessing the field via an interface. +func (v *ComponentConnectionEdgesComponentEdge) GetNode() *Component { return v.Node } + +type ComponentFilter struct { + ComponentName []string `json:"componentName"` +} + +// GetComponentName returns ComponentFilter.ComponentName, and is useful for accessing the field via an interface. +func (v *ComponentFilter) GetComponentName() []string { return v.ComponentName } + +type ComponentInput struct { + Name string `json:"name"` + Type ComponentTypeValues `json:"type"` +} + +// GetName returns ComponentInput.Name, and is useful for accessing the field via an interface. +func (v *ComponentInput) GetName() string { return v.Name } + +// GetType returns ComponentInput.Type, and is useful for accessing the field via an interface. +func (v *ComponentInput) GetType() ComponentTypeValues { return v.Type } + +type ComponentTypeValues string + +const ( + ComponentTypeValuesContainerimage ComponentTypeValues = "containerImage" + ComponentTypeValuesVirtualmachineimage ComponentTypeValues = "virtualMachineImage" + ComponentTypeValuesRepository ComponentTypeValues = "repository" +) + +// ComponentVersion includes the requested fields of the GraphQL type ComponentVersion. +type ComponentVersion struct { + Id string `json:"id"` + Version string `json:"version"` + ComponentId string `json:"componentId"` +} + +// GetId returns ComponentVersion.Id, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetId() string { return v.Id } + +// GetVersion returns ComponentVersion.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetVersion() string { return v.Version } + +// GetComponentId returns ComponentVersion.ComponentId, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetComponentId() string { return v.ComponentId } + +// ComponentVersionConnection includes the requested fields of the GraphQL type ComponentVersionConnection. +type ComponentVersionConnection struct { + Edges []*ComponentVersionConnectionEdgesComponentVersionEdge `json:"edges"` +} + +// GetEdges returns ComponentVersionConnection.Edges, and is useful for accessing the field via an interface. +func (v *ComponentVersionConnection) GetEdges() []*ComponentVersionConnectionEdgesComponentVersionEdge { + return v.Edges +} + +// ComponentVersionConnectionEdgesComponentVersionEdge includes the requested fields of the GraphQL type ComponentVersionEdge. +type ComponentVersionConnectionEdgesComponentVersionEdge struct { + Node *ComponentVersion `json:"node"` +} + +// GetNode returns ComponentVersionConnectionEdgesComponentVersionEdge.Node, and is useful for accessing the field via an interface. +func (v *ComponentVersionConnectionEdgesComponentVersionEdge) GetNode() *ComponentVersion { + return v.Node +} + +type ComponentVersionFilter struct { + IssueId []string `json:"issueId"` + Version []string `json:"version"` +} + +// GetIssueId returns ComponentVersionFilter.IssueId, and is useful for accessing the field via an interface. +func (v *ComponentVersionFilter) GetIssueId() []string { return v.IssueId } + +// GetVersion returns ComponentVersionFilter.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersionFilter) GetVersion() []string { return v.Version } + +type ComponentVersionInput struct { + Version string `json:"version"` + ComponentId string `json:"componentId"` +} + +// GetVersion returns ComponentVersionInput.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersionInput) GetVersion() string { return v.Version } + +// GetComponentId returns ComponentVersionInput.ComponentId, and is useful for accessing the field via an interface. +func (v *ComponentVersionInput) GetComponentId() string { return v.ComponentId } + +// CreateComponentResponse is returned by CreateComponent on success. +type CreateComponentResponse struct { + CreateComponent *Component `json:"createComponent"` +} + +// GetCreateComponent returns CreateComponentResponse.CreateComponent, and is useful for accessing the field via an interface. +func (v *CreateComponentResponse) GetCreateComponent() *Component { return v.CreateComponent } + +// CreateComponentVersionResponse is returned by CreateComponentVersion on success. +type CreateComponentVersionResponse struct { + CreateComponentVersion *ComponentVersion `json:"createComponentVersion"` +} + +// GetCreateComponentVersion returns CreateComponentVersionResponse.CreateComponentVersion, and is useful for accessing the field via an interface. +func (v *CreateComponentVersionResponse) GetCreateComponentVersion() *ComponentVersion { + return v.CreateComponentVersion +} + +// CreateIssueResponse is returned by CreateIssue on success. +type CreateIssueResponse struct { + CreateIssue *Issue `json:"createIssue"` +} + +// GetCreateIssue returns CreateIssueResponse.CreateIssue, and is useful for accessing the field via an interface. +func (v *CreateIssueResponse) GetCreateIssue() *Issue { return v.CreateIssue } + +// Issue includes the requested fields of the GraphQL type Issue. +type Issue struct { + Id string `json:"id"` + PrimaryName string `json:"primaryName"` + Description string `json:"description"` + Type IssueTypes `json:"type"` +} + +// GetId returns Issue.Id, and is useful for accessing the field via an interface. +func (v *Issue) GetId() string { return v.Id } + +// GetPrimaryName returns Issue.PrimaryName, and is useful for accessing the field via an interface. +func (v *Issue) GetPrimaryName() string { return v.PrimaryName } + +// GetDescription returns Issue.Description, and is useful for accessing the field via an interface. +func (v *Issue) GetDescription() string { return v.Description } + +// GetType returns Issue.Type, and is useful for accessing the field via an interface. +func (v *Issue) GetType() IssueTypes { return v.Type } + +// IssueConnection includes the requested fields of the GraphQL type IssueConnection. +type IssueConnection struct { + Edges []*IssueConnectionEdgesIssueEdge `json:"edges"` +} + +// GetEdges returns IssueConnection.Edges, and is useful for accessing the field via an interface. +func (v *IssueConnection) GetEdges() []*IssueConnectionEdgesIssueEdge { return v.Edges } + +// IssueConnectionEdgesIssueEdge includes the requested fields of the GraphQL type IssueEdge. +type IssueConnectionEdgesIssueEdge struct { + Node *Issue `json:"node"` +} + +// GetNode returns IssueConnectionEdgesIssueEdge.Node, and is useful for accessing the field via an interface. +func (v *IssueConnectionEdgesIssueEdge) GetNode() *Issue { return v.Node } + +type IssueFilter struct { + AffectedService []string `json:"affectedService"` + PrimaryName []string `json:"primaryName"` + IssueMatchStatus []IssueMatchStatusValues `json:"issueMatchStatus"` + IssueType []IssueTypes `json:"issueType"` + ComponentVersionId []string `json:"componentVersionId"` + Search []string `json:"search"` +} + +// GetAffectedService returns IssueFilter.AffectedService, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetAffectedService() []string { return v.AffectedService } + +// GetPrimaryName returns IssueFilter.PrimaryName, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetPrimaryName() []string { return v.PrimaryName } + +// GetIssueMatchStatus returns IssueFilter.IssueMatchStatus, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetIssueMatchStatus() []IssueMatchStatusValues { return v.IssueMatchStatus } + +// GetIssueType returns IssueFilter.IssueType, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetIssueType() []IssueTypes { return v.IssueType } + +// GetComponentVersionId returns IssueFilter.ComponentVersionId, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetComponentVersionId() []string { return v.ComponentVersionId } + +// GetSearch returns IssueFilter.Search, and is useful for accessing the field via an interface. +func (v *IssueFilter) GetSearch() []string { return v.Search } + +type IssueInput struct { + PrimaryName string `json:"primaryName"` + Description string `json:"description"` + Type IssueTypes `json:"type"` +} + +// GetPrimaryName returns IssueInput.PrimaryName, and is useful for accessing the field via an interface. +func (v *IssueInput) GetPrimaryName() string { return v.PrimaryName } + +// GetDescription returns IssueInput.Description, and is useful for accessing the field via an interface. +func (v *IssueInput) GetDescription() string { return v.Description } + +// GetType returns IssueInput.Type, and is useful for accessing the field via an interface. +func (v *IssueInput) GetType() IssueTypes { return v.Type } + +type IssueMatchStatusValues string + +const ( + IssueMatchStatusValuesNew IssueMatchStatusValues = "new" + IssueMatchStatusValuesRiskAccepted IssueMatchStatusValues = "risk_accepted" + IssueMatchStatusValuesFalsePositive IssueMatchStatusValues = "false_positive" + IssueMatchStatusValuesMitigated IssueMatchStatusValues = "mitigated" +) + +type IssueTypes string + +const ( + IssueTypesVulnerability IssueTypes = "Vulnerability" + IssueTypesPolicyviolation IssueTypes = "PolicyViolation" + IssueTypesSecurityevent IssueTypes = "SecurityEvent" +) + +// ListComponentVersionsResponse is returned by ListComponentVersions on success. +type ListComponentVersionsResponse struct { + ComponentVersions *ComponentVersionConnection `json:"ComponentVersions"` +} + +// GetComponentVersions returns ListComponentVersionsResponse.ComponentVersions, and is useful for accessing the field via an interface. +func (v *ListComponentVersionsResponse) GetComponentVersions() *ComponentVersionConnection { + return v.ComponentVersions +} + +// ListComponentsResponse is returned by ListComponents on success. +type ListComponentsResponse struct { + Components *ComponentConnection `json:"Components"` +} + +// GetComponents returns ListComponentsResponse.Components, and is useful for accessing the field via an interface. +func (v *ListComponentsResponse) GetComponents() *ComponentConnection { return v.Components } + +// ListIssuesResponse is returned by ListIssues on success. +type ListIssuesResponse struct { + Issues *IssueConnection `json:"Issues"` +} + +// GetIssues returns ListIssuesResponse.Issues, and is useful for accessing the field via an interface. +func (v *ListIssuesResponse) GetIssues() *IssueConnection { return v.Issues } + +// __AddComponentVersionToIssueInput is used internally by genqlient +type __AddComponentVersionToIssueInput struct { + IssueId string `json:"issueId"` + ComponentVersionId string `json:"componentVersionId"` +} + +// GetIssueId returns __AddComponentVersionToIssueInput.IssueId, and is useful for accessing the field via an interface. +func (v *__AddComponentVersionToIssueInput) GetIssueId() string { return v.IssueId } + +// GetComponentVersionId returns __AddComponentVersionToIssueInput.ComponentVersionId, and is useful for accessing the field via an interface. +func (v *__AddComponentVersionToIssueInput) GetComponentVersionId() string { + return v.ComponentVersionId +} + +// __CreateComponentInput is used internally by genqlient +type __CreateComponentInput struct { + Input *ComponentInput `json:"input,omitempty"` +} + +// GetInput returns __CreateComponentInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateComponentInput) GetInput() *ComponentInput { return v.Input } + +// __CreateComponentVersionInput is used internally by genqlient +type __CreateComponentVersionInput struct { + Input *ComponentVersionInput `json:"input,omitempty"` +} + +// GetInput returns __CreateComponentVersionInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateComponentVersionInput) GetInput() *ComponentVersionInput { return v.Input } + +// __CreateIssueInput is used internally by genqlient +type __CreateIssueInput struct { + Input *IssueInput `json:"input,omitempty"` +} + +// GetInput returns __CreateIssueInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateIssueInput) GetInput() *IssueInput { return v.Input } + +// __ListComponentVersionsInput is used internally by genqlient +type __ListComponentVersionsInput struct { + Filter *ComponentVersionFilter `json:"filter,omitempty"` + First int `json:"first"` +} + +// GetFilter returns __ListComponentVersionsInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListComponentVersionsInput) GetFilter() *ComponentVersionFilter { return v.Filter } + +// GetFirst returns __ListComponentVersionsInput.First, and is useful for accessing the field via an interface. +func (v *__ListComponentVersionsInput) GetFirst() int { return v.First } + +// __ListComponentsInput is used internally by genqlient +type __ListComponentsInput struct { + Filter *ComponentFilter `json:"filter,omitempty"` + First int `json:"first"` +} + +// GetFilter returns __ListComponentsInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListComponentsInput) GetFilter() *ComponentFilter { return v.Filter } + +// GetFirst returns __ListComponentsInput.First, and is useful for accessing the field via an interface. +func (v *__ListComponentsInput) GetFirst() int { return v.First } + +// __ListIssuesInput is used internally by genqlient +type __ListIssuesInput struct { + Filter *IssueFilter `json:"filter,omitempty"` + First int `json:"first"` +} + +// GetFilter returns __ListIssuesInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListIssuesInput) GetFilter() *IssueFilter { return v.Filter } + +// GetFirst returns __ListIssuesInput.First, and is useful for accessing the field via an interface. +func (v *__ListIssuesInput) GetFirst() int { return v.First } + +// The query or mutation executed by AddComponentVersionToIssue. +const AddComponentVersionToIssue_Operation = ` +mutation AddComponentVersionToIssue ($issueId: ID!, $componentVersionId: ID!) { + addComponentVersionToIssue(issueId: $issueId, componentVersionId: $componentVersionId) { + id + primaryName + description + type + } +} +` + +func AddComponentVersionToIssue( + ctx_ context.Context, + client_ graphql.Client, + issueId string, + componentVersionId string, +) (*AddComponentVersionToIssueResponse, error) { + req_ := &graphql.Request{ + OpName: "AddComponentVersionToIssue", + Query: AddComponentVersionToIssue_Operation, + Variables: &__AddComponentVersionToIssueInput{ + IssueId: issueId, + ComponentVersionId: componentVersionId, + }, + } + var err_ error + + var data_ AddComponentVersionToIssueResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by CreateComponent. +const CreateComponent_Operation = ` +mutation CreateComponent ($input: ComponentInput!) { + createComponent(input: $input) { + id + name + type + } +} +` + +func CreateComponent( + ctx_ context.Context, + client_ graphql.Client, + input *ComponentInput, +) (*CreateComponentResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateComponent", + Query: CreateComponent_Operation, + Variables: &__CreateComponentInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateComponentResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by CreateComponentVersion. +const CreateComponentVersion_Operation = ` +mutation CreateComponentVersion ($input: ComponentVersionInput!) { + createComponentVersion(input: $input) { + id + version + componentId + } +} +` + +func CreateComponentVersion( + ctx_ context.Context, + client_ graphql.Client, + input *ComponentVersionInput, +) (*CreateComponentVersionResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateComponentVersion", + Query: CreateComponentVersion_Operation, + Variables: &__CreateComponentVersionInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateComponentVersionResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by CreateIssue. +const CreateIssue_Operation = ` +mutation CreateIssue ($input: IssueInput!) { + createIssue(input: $input) { + id + primaryName + description + type + } +} +` + +func CreateIssue( + ctx_ context.Context, + client_ graphql.Client, + input *IssueInput, +) (*CreateIssueResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateIssue", + Query: CreateIssue_Operation, + Variables: &__CreateIssueInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateIssueResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListComponentVersions. +const ListComponentVersions_Operation = ` +query ListComponentVersions ($filter: ComponentVersionFilter, $first: Int) { + ComponentVersions(filter: $filter, first: $first) { + edges { + node { + id + version + componentId + } + } + } +} +` + +func ListComponentVersions( + ctx_ context.Context, + client_ graphql.Client, + filter *ComponentVersionFilter, + first int, +) (*ListComponentVersionsResponse, error) { + req_ := &graphql.Request{ + OpName: "ListComponentVersions", + Query: ListComponentVersions_Operation, + Variables: &__ListComponentVersionsInput{ + Filter: filter, + First: first, + }, + } + var err_ error + + var data_ ListComponentVersionsResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListComponents. +const ListComponents_Operation = ` +query ListComponents ($filter: ComponentFilter, $first: Int) { + Components(filter: $filter, first: $first) { + edges { + node { + id + name + type + } + } + } +} +` + +func ListComponents( + ctx_ context.Context, + client_ graphql.Client, + filter *ComponentFilter, + first int, +) (*ListComponentsResponse, error) { + req_ := &graphql.Request{ + OpName: "ListComponents", + Query: ListComponents_Operation, + Variables: &__ListComponentsInput{ + Filter: filter, + First: first, + }, + } + var err_ error + + var data_ ListComponentsResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListIssues. +const ListIssues_Operation = ` +query ListIssues ($filter: IssueFilter, $first: Int) { + Issues(filter: $filter, first: $first) { + edges { + node { + id + primaryName + description + type + } + } + } +} +` + +func ListIssues( + ctx_ context.Context, + client_ graphql.Client, + filter *IssueFilter, + first int, +) (*ListIssuesResponse, error) { + req_ := &graphql.Request{ + OpName: "ListIssues", + Query: ListIssues_Operation, + Variables: &__ListIssuesInput{ + Filter: filter, + First: first, + }, + } + var err_ error + + var data_ ListIssuesResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} diff --git a/scanner/keppel/client/genqlient.yaml b/scanner/keppel/client/genqlient.yaml new file mode 100644 index 0000000..dcdf2bc --- /dev/null +++ b/scanner/keppel/client/genqlient.yaml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: ../../../internal/api/graphql/graph/schema/*.graphqls +operations: +- ./query/*.graphql +generated: generated.go +package: client +use_struct_references: true \ No newline at end of file diff --git a/scanner/keppel/client/query/addComponentVersionToIssue.graphql b/scanner/keppel/client/query/addComponentVersionToIssue.graphql new file mode 100644 index 0000000..2946b6e --- /dev/null +++ b/scanner/keppel/client/query/addComponentVersionToIssue.graphql @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation AddComponentVersionToIssue($issueId: ID!, $componentVersionId: ID!) { + # @genqlient(typename: "Issue") + addComponentVersionToIssue ( + issueId: $issueId, + componentVersionId: $componentVersionId + ) { + id + primaryName + description + type + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/createComponent.graphql b/scanner/keppel/client/query/createComponent.graphql new file mode 100644 index 0000000..33849ab --- /dev/null +++ b/scanner/keppel/client/query/createComponent.graphql @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateComponent($input: ComponentInput!) { + # @genqlient(typename: "Component") + createComponent ( + input: $input + ) { + id + name + type + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/createComponentVersion.graphql b/scanner/keppel/client/query/createComponentVersion.graphql new file mode 100644 index 0000000..215cd0a --- /dev/null +++ b/scanner/keppel/client/query/createComponentVersion.graphql @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateComponentVersion($input: ComponentVersionInput!) { + # @genqlient(typename: "ComponentVersion") + createComponentVersion ( + input: $input + ) { + id + version + componentId + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/createIssue.graphql b/scanner/keppel/client/query/createIssue.graphql new file mode 100644 index 0000000..15ca109 --- /dev/null +++ b/scanner/keppel/client/query/createIssue.graphql @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateIssue($input: IssueInput!) { + # @genqlient(typename: "Issue") + createIssue ( + input: $input + ) { + id + primaryName + description + type + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/listComponentVersions.graphql b/scanner/keppel/client/query/listComponentVersions.graphql new file mode 100644 index 0000000..ac1fdb0 --- /dev/null +++ b/scanner/keppel/client/query/listComponentVersions.graphql @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListComponentVersions($filter: ComponentVersionFilter, $first: Int) { + # @genqlient(typename: "ComponentVersionConnection") + ComponentVersions ( + filter: $filter, + first: $first, + ) { + edges { + # @genqlient(typename: "ComponentVersion") + node { + id + version + componentId + } + } + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/listComponents.graphql b/scanner/keppel/client/query/listComponents.graphql new file mode 100644 index 0000000..8e4063e --- /dev/null +++ b/scanner/keppel/client/query/listComponents.graphql @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListComponents($filter: ComponentFilter, $first: Int) { + # @genqlient(typename: "ComponentConnection") + Components ( + filter: $filter, + first: $first, + ) { + edges { + # @genqlient(typename: "Component") + node { + id + name + type + } + } + } +} \ No newline at end of file diff --git a/scanner/keppel/client/query/listIssues.graphql b/scanner/keppel/client/query/listIssues.graphql new file mode 100644 index 0000000..d5027a6 --- /dev/null +++ b/scanner/keppel/client/query/listIssues.graphql @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListIssues($filter: IssueFilter, $first: Int) { + # @genqlient(typename: "IssueConnection") + Issues ( + filter: $filter, + first: $first, + ) { + edges { + # @genqlient(typename: "Issue") + node { + id + primaryName + description + type + } + } + } +} \ No newline at end of file diff --git a/scanner/keppel/go.mod b/scanner/keppel/go.mod index 9b8b884..6de6d7c 100644 --- a/scanner/keppel/go.mod +++ b/scanner/keppel/go.mod @@ -1,3 +1,35 @@ module github.com/cloudoperators/heureka/scanners/keppel -go 1.22.6 \ No newline at end of file +go 1.22.6 + +require ( + github.com/aquasecurity/trivy v0.54.1 + github.com/gophercloud/gophercloud v1.14.0 + github.com/gophercloud/gophercloud/v2 v2.1.0 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/pkg/errors v0.9.1 +) + +require ( + github.com/Khan/genqlient v0.7.0 // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/alexflint/go-arg v1.4.2 // indirect + github.com/alexflint/go-scalar v1.0.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.11 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +require ( + github.com/aquasecurity/trivy-db v0.0.0-20240718084044-d23a6ca8ba04 // indirect + github.com/google/go-containerregistry v0.20.1 // indirect + github.com/machinebox/graphql v0.2.2 + github.com/package-url/packageurl-go v0.1.3 // indirect + github.com/samber/lo v1.46.0 // indirect + github.com/sirupsen/logrus v1.9.3 + golang.org/x/text v0.16.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect +) diff --git a/scanner/keppel/go.sum b/scanner/keppel/go.sum new file mode 100644 index 0000000..10f2702 --- /dev/null +++ b/scanner/keppel/go.sum @@ -0,0 +1,65 @@ +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= +github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= +github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= +github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= +github.com/aquasecurity/trivy v0.54.1 h1:/uNCF06PfdC69v5n3Zh4fXVf0xmXBml0c/ergf066SQ= +github.com/aquasecurity/trivy v0.54.1/go.mod h1:i5S54WUtOEN9egFF0AHsxq6XT7QD11n9pSmIXhMJV0g= +github.com/aquasecurity/trivy-db v0.0.0-20240718084044-d23a6ca8ba04 h1:6/T8sFdNVG/AwOGoK6X55h7hF7LYqK8bsuPz8iEz8jM= +github.com/aquasecurity/trivy-db v0.0.0-20240718084044-d23a6ca8ba04/go.mod h1:0T6oy2t1Iedt+yi3Ml5cpOYp5FZT4MI1/mx+3p+PIs8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/google/go-containerregistry v0.20.1 h1:eTgx9QNYugV4DN5mz4U8hiAGTi1ybXn0TPi4Smd8du0= +github.com/google/go-containerregistry v0.20.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8= +github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud/v2 v2.1.0 h1:91p6c+uMckXyx39nSIYjDirDBnPVFQq0q1njLNPX+NY= +github.com/gophercloud/gophercloud/v2 v2.1.0/go.mod h1:f2hMRC7Kakbv5vM7wSGHrIPZh6JZR60GVHryJlF/K44= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= +github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= +github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= +github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scanner/keppel/main.go b/scanner/keppel/main.go index fc9c09c..e7e03f7 100644 --- a/scanner/keppel/main.go +++ b/scanner/keppel/main.go @@ -4,9 +4,148 @@ package main import ( - "fmt" + "os" + "sync" + + "github.com/cloudoperators/heureka/scanners/keppel/client" + "github.com/cloudoperators/heureka/scanners/keppel/models" + "github.com/cloudoperators/heureka/scanners/keppel/processor" + "github.com/cloudoperators/heureka/scanners/keppel/scanner" + "github.com/kelseyhightower/envconfig" + log "github.com/sirupsen/logrus" ) +type Config struct { + LogLevel string `envconfig:"LOG_LEVEL" default:"debug" required:"true" json:"-"` +} + +func init() { + // Log as JSON instead of the default ASCII formatter. + log.SetFormatter(&log.JSONFormatter{}) + + // Output to stdout instead of the default stderr + // Can be any io.Writer, see below for File example + log.SetOutput(os.Stdout) + + var cfg Config + err := envconfig.Process("heureka", &cfg) + if err != nil { + log.WithError(err).Fatal("Error while reading env config") + } + + level, err := log.ParseLevel(cfg.LogLevel) + + if err != nil { + log.WithError(err).Fatal("Error while parsing log level") + } + + // Only log the warning severity or above. + log.SetLevel(level) +} + func main() { - fmt.Println("This is the Keppel scanner") + var wg sync.WaitGroup + var scannerCfg scanner.Config + err := envconfig.Process("heureka", &scannerCfg) + if err != nil { + log.WithError(err).Fatal("Error while reading env config for scanner") + } + + var processorCfg processor.Config + err = envconfig.Process("heureka", &processorCfg) + if err != nil { + log.WithError(err).Fatal("Error while reading env config for processor") + } + + keppelScanner := scanner.NewScanner(scannerCfg) + keppelProcessor := processor.NewProcessor(processorCfg) + + err = keppelScanner.Setup() + if err != nil { + log.WithError(err).Fatal("Error during scanner setup") + } + accounts, err := keppelScanner.ListAccounts() + if err != nil { + log.WithError(err).Fatal("Error during ListAccounts") + } + + wg.Add(len(accounts)) + + for _, account := range accounts { + go HandleAccount(account, keppelScanner, keppelProcessor, &wg) + } + + wg.Wait() +} + +func HandleAccount(account models.Account, keppelScanner *scanner.Scanner, keppelProcessor *processor.Processor, wg *sync.WaitGroup) error { + defer wg.Done() + repositories, err := keppelScanner.ListRepositories(account.Name) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + }).WithError(err).Error("Error during ProcessRepository") + return err + } + + for _, repository := range repositories { + HandleRepository(account, repository, keppelScanner, keppelProcessor) + } + + return nil +} + +func HandleRepository(account models.Account, repository models.Repository, keppelScanner *scanner.Scanner, keppelProcessor *processor.Processor) { + component, err := keppelProcessor.ProcessRepository(repository) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during ProcessRepository") + component, err = keppelProcessor.GetComponent(repository.Name) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during GetComponent") + } + } + + manifests, err := keppelScanner.ListManifests(account.Name, repository.Name) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during ListManifests") + return + } + for _, manifest := range manifests { + HandleManifest(account, repository, manifest, component, keppelScanner, keppelProcessor) + } +} + +func HandleManifest(account models.Account, repository models.Repository, manifest models.Manifest, component *client.Component, keppelScanner *scanner.Scanner, keppelProcessor *processor.Processor) { + componentVersion, err := keppelProcessor.ProcessManifest(manifest, component.Id) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during ProcessManifest") + componentVersion, err = keppelProcessor.GetComponentVersion(manifest.Digest) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during GetComponentVersion") + } + } + trivyReport, err := keppelScanner.GetTrivyReport(account.Name, repository.Name, manifest.Digest) + if err != nil { + log.WithFields(log.Fields{ + "account:": account.Name, + "repository": repository.Name, + }).WithError(err).Error("Error during GetTrivyReport") + return + } + keppelProcessor.ProcessReport(*trivyReport, componentVersion.Id) } diff --git a/scanner/keppel/models/models.go b/scanner/keppel/models/models.go new file mode 100644 index 0000000..0c70ebf --- /dev/null +++ b/scanner/keppel/models/models.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package models + +import ( + "time" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + stypes "github.com/aquasecurity/trivy/pkg/module/serialize" +) + +// For more attributes check +// https://github.com/sapcc/keppel/blob/master/internal/models/account.go +type Account struct { + Name string + AuthTenantID string `json:"auth_tenant_id"` +} + +type AccountResponse struct { + Accounts []Account +} + +type RepositoryResponse struct { + Repositories []Repository +} + +type Repository struct { + Name string `json:"name"` + ManifestCount uint64 `json:"manifest_count"` + TagCount uint64 `json:"tag_count"` + SizeBytes uint64 `json:"size_bytes,omitempty"` + PushedAt int64 `json:"pushed_at,omitempty"` +} + +type Manifest struct { + Digest string + MediaType string `json:"media_type"` + SizeBytes uint64 `json:"size_bytes"` + PushedAt uint64 `json:"pushed_at"` + LastPulledAt uint64 `json:"last_pulled_at"` + VulnerabilityStatus string `json:"vulnerability_status"` + Labels Labels + MaxLayerCreatedAt int64 `json:"max_layer_created_at"` + MinLayerCreatedAt int64 `json:"min_layer_created_at"` +} + +type ManifestResponse struct { + Manifests []Manifest +} + +type Labels struct { + Maintainer string + Maintainers string + SourceRepository string `json:"source_repository"` +} + +type TrivyReport struct { + SchemaVersion int `json:",omitempty"` + CreatedAt time.Time `json:",omitempty"` + ArtifactName string `json:",omitempty"` + ArtifactType string `json:",omitempty"` // generic replacement for original type `artifact.Type` + Metadata TrivyMetadata `json:",omitempty"` // generic replacement for original type `types.Metadata` + Results stypes.Results `json:",omitempty"` // compatible replacement for original type `types.Results` +} + +type TrivyMetadata struct { + Size int64 `json:",omitempty"` + OS *ftypes.OS `json:",omitempty"` + + // Container image + ImageID string `json:",omitempty"` + DiffIDs []string `json:",omitempty"` + RepoTags []string `json:",omitempty"` + RepoDigests []string `json:",omitempty"` + ImageConfig map[string]any `json:",omitempty"` +} diff --git a/scanner/keppel/processor/config.go b/scanner/keppel/processor/config.go new file mode 100644 index 0000000..aa3b146 --- /dev/null +++ b/scanner/keppel/processor/config.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor + +type Config struct { + HeurekaUrl string `envconfig:"HEUREKA_URL" required:"true" json:"-"` +} diff --git a/scanner/keppel/processor/processor.go b/scanner/keppel/processor/processor.go new file mode 100644 index 0000000..4dd4cc1 --- /dev/null +++ b/scanner/keppel/processor/processor.go @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor + +import ( + "context" + "net/http" + + "github.com/Khan/genqlient/graphql" + "github.com/cloudoperators/heureka/scanners/keppel/client" + "github.com/cloudoperators/heureka/scanners/keppel/models" + log "github.com/sirupsen/logrus" +) + +type Processor struct { + Client *graphql.Client +} + +func NewProcessor(cfg Config) *Processor { + httpClient := http.Client{} + gClient := graphql.NewClient(cfg.HeurekaUrl, &httpClient) + return &Processor{ + Client: &gClient, + } +} + +func (p *Processor) ProcessRepository(repository models.Repository) (*client.Component, error) { + r, err := client.CreateComponent(context.Background(), *p.Client, &client.ComponentInput{ + Name: repository.Name, + Type: client.ComponentTypeValuesContainerimage, + }) + + if err != nil { + return nil, err + } + + component := r.GetCreateComponent() + + log.WithFields(log.Fields{ + "componentId": component.Id, + "component": component, + }).Info("Component created") + + return component, nil +} + +func (p *Processor) ProcessManifest(manifest models.Manifest, componentId string) (*client.ComponentVersion, error) { + r, err := client.CreateComponentVersion(context.Background(), *p.Client, &client.ComponentVersionInput{ + Version: manifest.Digest, + ComponentId: componentId, + }) + + if err != nil { + return nil, err + } + + componentVersion := r.GetCreateComponentVersion() + + log.WithFields(log.Fields{ + "componentVersionId": componentVersion.Id, + "componentVersion": componentVersion, + }).Info("ComponentVersion created") + + return componentVersion, nil +} + +func (p *Processor) ProcessReport(report models.TrivyReport, componentVersionId string) { + for _, result := range report.Results { + for _, vulnerability := range result.Vulnerabilities { + issue, err := p.GetIssue(vulnerability.VulnerabilityID) + if err != nil { + log.WithFields(log.Fields{ + "vulnerabilityID": vulnerability.VulnerabilityID, + "issueID": issue.Id, + "issuePrimaryName": issue.PrimaryName, + "componentVersionID": componentVersionId, + "report": report.ArtifactName, + }).WithError(err).Error("Error while getting issue") + continue + } + if issue == nil { + log.WithFields(log.Fields{ + "vulnerabilityID": vulnerability.VulnerabilityID, + }).Warning("Issue not found") + continue + // use this for inserting issues, necessary to test without nvd scanner + // i, err := p.CreateIssue(vulnerability.VulnerabilityID, vulnerability.Description) + // if err != nil { + // fmt.Println(err) + // continue + // } + // issue = &i + } + _, err = client.AddComponentVersionToIssue(context.Background(), *p.Client, issue.Id, componentVersionId) + + if err != nil { + log.WithFields(log.Fields{ + "issueId": issue.Id, + "componentVersionId": componentVersionId, + }).WithError(err).Error("Could not add component version to issue") + } else { + log.WithFields(log.Fields{ + "issueId": issue.Id, + "componentVersionId": componentVersionId, + }).Info("Added issue to componentVersion") + } + } + + } +} + +func (p *Processor) GetComponent(name string) (*client.Component, error) { + r, err := client.ListComponents(context.Background(), *p.Client, &client.ComponentFilter{ + ComponentName: []string{name}, + }, 1) + + if err != nil { + return nil, err + } + + var component *client.Component + if len(r.Components.Edges) > 0 { + component = r.Components.Edges[0].GetNode() + } + + return component, nil +} + +func (p *Processor) GetComponentVersion(version string) (*client.ComponentVersion, error) { + r, err := client.ListComponentVersions(context.Background(), *p.Client, &client.ComponentVersionFilter{ + Version: []string{version}, + }, 1) + + if err != nil { + return nil, err + } + + var componentVersion *client.ComponentVersion + if len(r.ComponentVersions.Edges) > 0 { + componentVersion = r.ComponentVersions.Edges[0].GetNode() + } + + return componentVersion, nil +} + +func (p *Processor) GetIssue(primaryName string) (*client.Issue, error) { + r, err := client.ListIssues(context.Background(), *p.Client, &client.IssueFilter{ + PrimaryName: []string{primaryName}, + }, 1) + + if err != nil { + return nil, err + } + + var issue *client.Issue + if len(r.Issues.Edges) > 0 { + issue = r.Issues.Edges[0].GetNode() + } + + return issue, nil +} + +func (p *Processor) CreateIssue(primaryName string, description string) (*client.Issue, error) { + r, err := client.CreateIssue(context.Background(), *p.Client, &client.IssueInput{ + PrimaryName: primaryName, + Description: description, + Type: "Vulnerability", + }) + + if err != nil { + return nil, err + } + + issue := r.GetCreateIssue() + + log.WithFields(log.Fields{ + "issueId": issue.Id, + "issue": issue, + }).Info("Issue created") + + return issue, nil +} diff --git a/scanner/keppel/scanner/config.go b/scanner/keppel/scanner/config.go new file mode 100644 index 0000000..1975322 --- /dev/null +++ b/scanner/keppel/scanner/config.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +type Config struct { + KeppelBaseUrl string `envconfig:"KEPPEL_BASE_URL" required:"true" json:"-"` + KeppelUsername string `envconfig:"KEPPEL_USERNAME" required:"true" json:"-"` + KeppelPassword string `envconfig:"KEPPEL_PASSWORD" required:"true" json:"-"` + Domain string `envconfig:"KEPPEL_DOMAIN" required:"true" json:"-"` + Project string `envconfig:"KEPPEL_PROJECT" required:"true" json:"-"` + IdentityEndpoint string `envconfig:"IDENTITY_ENDPOINT" required:"true" json:"-"` +} diff --git a/scanner/keppel/scanner/scanner.go b/scanner/keppel/scanner/scanner.go new file mode 100644 index 0000000..afe528b --- /dev/null +++ b/scanner/keppel/scanner/scanner.go @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/cloudoperators/heureka/scanners/keppel/models" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + log "github.com/sirupsen/logrus" +) + +type Scanner struct { + KeppelBaseUrl string + IdentityEndpoint string + Username string + Password string + AuthToken string + Domain string + Project string +} + +func NewScanner(cfg Config) *Scanner { + return &Scanner{ + KeppelBaseUrl: cfg.KeppelBaseUrl, + Username: cfg.KeppelUsername, + Password: cfg.KeppelPassword, + Domain: cfg.Domain, + Project: cfg.Project, + IdentityEndpoint: cfg.IdentityEndpoint, + } +} + +func (s *Scanner) Setup() error { + token, err := s.CreateAuthToken() + if err != nil { + return fmt.Errorf("failed to create auth token: %w", err) + } + s.AuthToken = token + return nil +} + +func (s *Scanner) CreateAuthToken() (string, error) { + provider, err := s.newAuthenticatedProviderClient() + if err != nil { + return "", err + } + endpointOpts := gophercloud.EndpointOpts{} + + iClient, err := openstack.NewIdentityV3(provider, endpointOpts) + if err != nil { + return "", fmt.Errorf("failed to create identity v3 client: %w", err) + } + + return iClient.Token(), nil +} + +func (s *Scanner) newAuthenticatedProviderClient() (*gophercloud.ProviderClient, error) { + + opts := &tokens.AuthOptions{ + IdentityEndpoint: s.IdentityEndpoint, + Username: s.Username, + Password: s.Password, + DomainName: s.Domain, + AllowReauth: true, + Scope: tokens.Scope{ + ProjectName: s.Project, + DomainName: s.Domain, + }, + } + + provider, err := openstack.NewClient(opts.IdentityEndpoint) + if err != nil { + log.WithFields(log.Fields{ + "identityEndpoint": opts.IdentityEndpoint, + "domain": s.Domain, + "project": s.Project, + }).WithError(err) + return nil, err + } + + err = openstack.AuthenticateV3(provider, opts, gophercloud.EndpointOpts{}) + return provider, err +} + +func (s *Scanner) ListAccounts() ([]models.Account, error) { + url := fmt.Sprintf("%s/keppel/v1/accounts", s.KeppelBaseUrl) + body, err := s.sendRequest(url, s.AuthToken) + if err != nil { + log.WithFields(log.Fields{ + "url": url, + }).WithError(err).Error("Error during request in ListAccounts") + return nil, err + } + + var accountResponse models.AccountResponse + if err = json.Unmarshal(body, &accountResponse); err != nil { + log.WithFields(log.Fields{ + "url": url, + "body": body, + }).WithError(err).Error("Error during unmarshal in ListAccounts") + return nil, err + } + + return accountResponse.Accounts, nil +} + +func (s *Scanner) ListRepositories(account string) ([]models.Repository, error) { + url := fmt.Sprintf("%s/keppel/v1/accounts/%s/repositories", s.KeppelBaseUrl, account) + body, err := s.sendRequest(url, s.AuthToken) + if err != nil { + log.WithFields(log.Fields{ + "url": url, + }).WithError(err).Error("Error during request in ListRepositories") + return nil, err + } + + var repositoryResponse models.RepositoryResponse + if err = json.Unmarshal(body, &repositoryResponse); err != nil { + log.WithFields(log.Fields{ + "url": url, + "body": body, + }).WithError(err).Error("Error during unmarshal in ListRepositories") + return nil, err + } + + return repositoryResponse.Repositories, nil +} + +func (s *Scanner) ListManifests(account string, repository string) ([]models.Manifest, error) { + url := fmt.Sprintf("%s/keppel/v1/accounts/%s/repositories/%s/_manifests", s.KeppelBaseUrl, account, repository) + body, err := s.sendRequest(url, s.AuthToken) + if err != nil { + log.WithFields(log.Fields{ + "url": url, + }).WithError(err).Error("Error during request in ListManifests") + return nil, err + } + + var manifestResponse models.ManifestResponse + if err = json.Unmarshal(body, &manifestResponse); err != nil { + log.WithFields(log.Fields{ + "url": url, + "body": body, + }).WithError(err).Error("Error during unmarshal in ListManifests") + return nil, err + } + + return manifestResponse.Manifests, nil +} + +func (s *Scanner) GetTrivyReport(account string, repository string, manifest string) (*models.TrivyReport, error) { + url := fmt.Sprintf("%s/keppel/v1/accounts/%s/repositories/%s/_manifests/%s/trivy_report", s.KeppelBaseUrl, account, repository, manifest) + body, err := s.sendRequest(url, s.AuthToken) + if err != nil { + log.WithFields(log.Fields{ + "url": url}). + WithError(err).Error("Error during GetTrivyReport") + return nil, err + } + + var trivyReport models.TrivyReport + if err = json.Unmarshal(body, &trivyReport); err != nil { + log.WithFields(log.Fields{ + "url": url, + "body": body, + }).WithError(err).Error("Error during unmarshal in GetTrivyReport") + return nil, err + } + + return &trivyReport, nil + +} + +func (s *Scanner) sendRequest(url string, token string) ([]byte, error) { + client := new(http.Client) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header = http.Header{ + "X-Auth-Token": []string{token}, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.WithFields(log.Fields{ + "url": url, + "body": body, + }).WithError(err).Error("Error during reading response body") + return nil, err + } + + return body, nil +}