diff --git a/cmd/dev/app/image/list_tags.go b/cmd/dev/app/image/list_tags.go index 6aaa05818e..e391b7503b 100644 --- a/cmd/dev/app/image/list_tags.go +++ b/cmd/dev/app/image/list_tags.go @@ -37,6 +37,7 @@ func CmdListTags() *cobra.Command { } listCmd.Flags().StringP("base-url", "b", "", "base URL for the OCI registry") + listCmd.Flags().StringP("owner", "o", "", "owner of the artifact") listCmd.Flags().StringP("container", "c", "", "container name to list tags for") //nolint:goconst // let's not use a const for this one listCmd.Flags().StringP("token", "t", "", "token to authenticate to the provider."+ @@ -57,6 +58,7 @@ func runCmdListTags(cmd *cobra.Command, _ []string) error { // get the provider baseURL := cmd.Flag("base-url") + owner := cmd.Flag("owner") contname := cmd.Flag("container") if baseURL.Value.String() == "" { @@ -66,8 +68,10 @@ func runCmdListTags(cmd *cobra.Command, _ []string) error { return fmt.Errorf("container name is required") } + regWithOwner := fmt.Sprintf("%s/%s", owner.Value.String(), baseURL.Value.String()) + cred := credentials.NewOAuth2TokenCredential(viper.GetString("auth.token")) - prov := oci.New(cred, baseURL.Value.String()) + prov := oci.New(cred, baseURL.Value.String(), regWithOwner) // get the containers containers, err := prov.ListTags(ctx, contname.Value.String()) diff --git a/go.mod b/go.mod index c0f6620744..30be14f308 100644 --- a/go.mod +++ b/go.mod @@ -254,7 +254,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.0 github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect diff --git a/internal/engine/ingester/artifact/artifact.go b/internal/engine/ingester/artifact/artifact.go index bc350453a5..cdccc06c4b 100644 --- a/internal/engine/ingester/artifact/artifact.go +++ b/internal/engine/ingester/artifact/artifact.go @@ -19,8 +19,6 @@ package artifact import ( "context" "fmt" - "net/url" - "sort" "strings" "time" @@ -50,7 +48,7 @@ const ( // Ingest is the engine for a rule type that uses artifact data ingest // Implements enginer.ingester.Ingester type Ingest struct { - ghCli provifv1.GitHub + prov provifv1.Provider // artifactVerifier is the verifier for sigstore. It's only used in the Ingest method // but we store it in the Ingest structure to allow tests to set a custom artifactVerifier @@ -74,9 +72,9 @@ type verifiedAttestation struct { } // NewArtifactDataIngest creates a new artifact rule data ingest engine -func NewArtifactDataIngest(ghCli provifv1.GitHub) (*Ingest, error) { +func NewArtifactDataIngest(prov provifv1.Provider) (*Ingest, error) { return &Ingest{ - ghCli: ghCli, + prov: prov, }, nil } @@ -124,24 +122,23 @@ func (i *Ingest) getApplicableArtifactVersions( artifact *pb.Artifact, cfg *ingesterConfig, ) ([]map[string]any, error) { - // Make sure the artifact type matches - if newArtifactIngestType(artifact.Type) != cfg.Type { - return nil, evalerrors.NewErrEvaluationSkipSilently("artifact type mismatch") + if err := validateConfiguration(artifact, cfg); err != nil { + return nil, err } - // If a name is specified, make sure it matches - if cfg.Name != "" && cfg.Name != artifact.Name { - return nil, evalerrors.NewErrEvaluationSkipSilently("artifact name mismatch") + vers, err := getVersioner(i.prov, artifact) + if err != nil { + return nil, err } - // Get all artifact versions filtering out those that don't apply to this rule - versions, err := getAndFilterArtifactVersions(ctx, cfg, i.ghCli, artifact) + // Get all artifact checksums filtering out those that don't apply to this rule + checksums, err := getAndFilterArtifactVersions(ctx, cfg, vers, artifact) if err != nil { return nil, err } // Get the provenance info for all artifact versions that apply to this rule - verificationResults, err := i.getVerificationResult(ctx, cfg, artifact, versions) + verificationResults, err := i.getVerificationResult(ctx, cfg, artifact, checksums) if err != nil { return nil, err } @@ -160,11 +157,32 @@ func (i *Ingest) getApplicableArtifactVersions( return result, nil } +func validateConfiguration( + artifact *pb.Artifact, + cfg *ingesterConfig, +) error { + // Make sure the artifact type matches + if newArtifactIngestType(artifact.Type) != cfg.Type { + return evalerrors.NewErrEvaluationSkipSilently("artifact type mismatch") + } + + if cfg.Type != artifactTypeContainer { + return evalerrors.NewErrEvaluationSkipSilently("only container artifacts are supported at the moment") + } + + // If a name is specified, make sure it matches + if cfg.Name != "" && cfg.Name != artifact.Name { + return evalerrors.NewErrEvaluationSkipSilently("artifact name mismatch") + } + + return nil +} + func (i *Ingest) getVerificationResult( ctx context.Context, cfg *ingesterConfig, artifact *pb.Artifact, - versions []string, + checksums []string, ) ([]verification, error) { var versionResults []verification // Get the verifier for sigstore @@ -174,13 +192,13 @@ func (i *Ingest) getVerificationResult( } // Loop through all artifact versions that apply to this rule and get the provenance info for each - for _, artifactVersion := range versions { + for _, artifactChecksum := range checksums { // Try getting provenance info for the artifact version results, err := artifactVerifier.Verify(ctx, verifyif.ArtifactTypeContainer, - artifact.Owner, artifact.Name, artifactVersion) + artifact.Owner, artifact.Name, artifactChecksum) if err != nil { // We consider err != nil as a fatal error, so we'll fail the rule evaluation here - artifactName := container.BuildImageRef("", artifact.Owner, artifact.Name, artifactVersion) + artifactName := container.BuildImageRef("", artifact.Owner, artifact.Name, artifactChecksum) zerolog.Ctx(ctx).Debug().Err(err).Str("name", artifactName).Msg("failed getting signature information") return nil, fmt.Errorf("failed getting signature information: %w", err) } @@ -188,7 +206,7 @@ func (i *Ingest) getVerificationResult( for _, res := range results { // Log a debug message in case we failed to find or verify any signature information for the artifact version if !res.IsSigned || !res.IsVerified { - artifactName := container.BuildImageRef("", artifact.Owner, artifact.Name, artifactVersion) + artifactName := container.BuildImageRef("", artifact.Owner, artifact.Name, artifactChecksum) zerolog.Ctx(ctx).Debug().Str("name", artifactName).Msg("failed to find or verify signature information") } @@ -230,10 +248,31 @@ func getVerifier(i *Ingest, cfg *ingesterConfig) (verifyif.ArtifactVerifier, err return i.artifactVerifier, nil } + verifieropts := []container.AuthMethod{} + if i.prov.CanImplement(pb.ProviderType_PROVIDER_TYPE_GITHUB) { + ghcli, err := provifv1.As[provifv1.GitHub](i.prov) + if err != nil { + return nil, fmt.Errorf("unable to get github provider from provider configuration") + } + verifieropts = append(verifieropts, container.WithGitHubClient(ghcli)) + } else if i.prov.CanImplement(pb.ProviderType_PROVIDER_TYPE_OCI) { + ocicli, err := provifv1.As[provifv1.OCI](i.prov) + if err != nil { + return nil, fmt.Errorf("unable to get oci provider from provider configuration") + } + cauthn, err := ocicli.GetAuthenticator() + if err != nil { + return nil, fmt.Errorf("unable to get oci authenticator: %w", err) + } + verifieropts = append(verifieropts, container.WithRegistry(ocicli.GetRegistry()), + container.WithAuthenticator(cauthn)) + } + artifactVerifier, err := verifier.NewVerifier( verifier.VerifierSigstore, cfg.Sigstore, - container.WithGitHubClient(i.ghCli)) + verifieropts..., + ) if err != nil { return nil, fmt.Errorf("error getting sigstore verifier: %w", err) } @@ -241,10 +280,13 @@ func getVerifier(i *Ingest, cfg *ingesterConfig) (verifyif.ArtifactVerifier, err return artifactVerifier, nil } +// getAndFilterArtifactVersions fetches the available versions and filters the +// ones that apply to the rule. Note that this returns the checksums of the +// applicable artifact versions. func getAndFilterArtifactVersions( ctx context.Context, cfg *ingesterConfig, - ghCli provifv1.GitHub, + vers versioner, artifact *pb.Artifact, ) ([]string, error) { var res []string @@ -256,23 +298,21 @@ func getAndFilterArtifactVersions( } // Fetch all available versions of the artifact - artifactName := url.QueryEscape(artifact.GetName()) - upstreamVersions, err := ghCli.GetPackageVersions( - ctx, artifact.Owner, artifact.GetTypeLower(), artifactName, - ) + upstreamVersions, err := vers.GetVersions(ctx) if err != nil { return nil, fmt.Errorf("error retrieving artifact versions: %w", err) } + name := artifact.GetName() + // Loop through all and filter out the versions that don't apply to this rule for _, version := range upstreamVersions { - tags := version.Metadata.Container.Tags - sort.Strings(tags) - // Decide if the artifact version should be skipped or not - err = isSkippable(verifyif.ArtifactTypeContainer, version.CreatedAt.Time, map[string]interface{}{"tags": tags}, filter) + tags := version.GetTags() + tagsopt := map[string]interface{}{"tags": tags} + err = isSkippable(version.GetCreatedAt().AsTime(), tagsopt, filter) if err != nil { - zerolog.Ctx(ctx).Debug().Str("name", *version.Name).Strs("tags", tags).Str( + zerolog.Ctx(ctx).Debug().Str("name", name).Strs("tags", tags).Str( "reason", err.Error(), ).Msg("skipping artifact version") @@ -280,8 +320,8 @@ func getAndFilterArtifactVersions( } // If the artifact version is applicable to this rule, add it to the list - zerolog.Ctx(ctx).Debug().Str("name", *version.Name).Strs("tags", tags).Msg("artifact version matched") - res = append(res, *version.Name) + zerolog.Ctx(ctx).Debug().Str("name", name).Strs("tags", tags).Msg("artifact version matched") + res = append(res, version.Sha) } // If no applicable artifact versions were found for this rule, we can go ahead and fail the rule evaluation here @@ -299,33 +339,29 @@ var ( ) // isSkippable determines if an artifact should be skipped +// Note this is only applicable to container artifacts. // TODO - this should be refactored as well, for now just a forklift from reconciler -func isSkippable(artifactType verifyif.ArtifactType, createdAt time.Time, opts map[string]interface{}, filter tagMatcher) error { - switch artifactType { - case verifyif.ArtifactTypeContainer: - // if the artifact is older than the retention period, skip it - if createdAt.Before(ArtifactTypeContainerRetentionPeriod) { - return fmt.Errorf("artifact is older than retention period - %s", ArtifactTypeContainerRetentionPeriod) - } - tags, ok := opts["tags"].([]string) - if !ok { - return nil - } else if len(tags) == 0 { - // if the artifact has no tags, skip it - return fmt.Errorf("artifact has no tags") - } - // if the artifact has a .sig tag it's a signature, skip it - if verifier.GetSignatureTag(tags) != "" { - return fmt.Errorf("artifact is a signature") - } - // if the artifact tags don't match the tag matcher, skip it - if !filter.MatchTag(tags...) { - return fmt.Errorf("artifact tags does not match") - } - return nil - default: +func isSkippable(createdAt time.Time, opts map[string]interface{}, filter tagMatcher) error { + // if the artifact is older than the retention period, skip it + if createdAt.Before(ArtifactTypeContainerRetentionPeriod) { + return fmt.Errorf("artifact is older than retention period - %s", ArtifactTypeContainerRetentionPeriod) + } + tags, ok := opts["tags"].([]string) + if !ok { return nil + } else if len(tags) == 0 { + // if the artifact has no tags, skip it + return fmt.Errorf("artifact has no tags") + } + // if the artifact has a .sig tag it's a signature, skip it + if verifier.GetSignatureTag(tags) != "" { + return fmt.Errorf("artifact is a signature") } + // if the artifact tags don't match the tag matcher, skip it + if !filter.MatchTag(tags...) { + return fmt.Errorf("artifact tags does not match") + } + return nil } func branchFromRef(ref string) string { diff --git a/internal/engine/ingester/artifact/artifact_test.go b/internal/engine/ingester/artifact/artifact_test.go index 31a520d780..7a9b2a6af1 100644 --- a/internal/engine/ingester/artifact/artifact_test.go +++ b/internal/engine/ingester/artifact/artifact_test.go @@ -569,7 +569,7 @@ func TestArtifactIngestMatching(t *testing.T) { ing, err := NewArtifactDataIngest(prov) require.NoError(t, err, "expected no error") - ing.ghCli = mockGhClient + ing.prov = mockGhClient ing.artifactVerifier = mockVerifier tt.mockSetup(mockGhClient, mockVerifier) diff --git a/internal/engine/ingester/artifact/versioner.go b/internal/engine/ingester/artifact/versioner.go new file mode 100644 index 0000000000..8ec89c7db7 --- /dev/null +++ b/internal/engine/ingester/artifact/versioner.go @@ -0,0 +1,144 @@ +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Package rule provides the CLI subcommand for managing rules + +// Package artifact provides the artifact ingestion engine +package artifact + +import ( + "context" + "fmt" + "net/url" + "sort" + "time" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "google.golang.org/protobuf/types/known/timestamppb" + + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + provifv1 "github.com/stacklok/minder/pkg/providers/v1" +) + +type versioner interface { + // Gets the available versions of a given artifact + GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error) +} + +func getVersioner(prov provifv1.Provider, a *minderv1.Artifact) (versioner, error) { + ghprov, err := provifv1.As[provifv1.GitHub](prov) + if err == nil { + return &githubVersioner{ + ghCli: ghprov, + artifact: a, + }, nil + } + + ociprov, err := provifv1.As[provifv1.OCI](prov) + if err == nil { + return &ociVersioner{ + ocicli: ociprov, + artifact: a, + }, nil + } + + return nil, fmt.Errorf("Unable to get version lister from provider implementation") +} + +type githubVersioner struct { + ghCli provifv1.GitHub + artifact *minderv1.Artifact +} + +// in case of the GitHub provider, a package version may be +// linked to multiple tags +func (gv *githubVersioner) GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error) { + artifactName := url.QueryEscape(gv.artifact.GetName()) + upstreamVersions, err := gv.ghCli.GetPackageVersions( + ctx, gv.artifact.GetOwner(), gv.artifact.GetTypeLower(), artifactName, + ) + if err != nil { + return nil, fmt.Errorf("error retrieving artifact versions: %w", err) + } + + out := make([]*minderv1.ArtifactVersion, 0, len(upstreamVersions)) + for _, uv := range upstreamVersions { + tags := uv.Metadata.Container.Tags + sort.Strings(tags) + + // only the tags and creation time is relevant to us. + out = append(out, &minderv1.ArtifactVersion{ + Tags: tags, + // NOTE: GitHub's name is actually a SHA. This is misleading... + // but it is what it is. We'll use it as the SHA for now. + Sha: *uv.Name, + CreatedAt: timestamppb.New(uv.CreatedAt.Time), + }) + } + + return out, nil +} + +type ociVersioner struct { + ocicli provifv1.OCI + artifact *minderv1.Artifact +} + +func (ov *ociVersioner) GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error) { + tags, err := ov.ocicli.ListTags(ctx, ov.artifact.GetName()) + if err != nil { + return nil, fmt.Errorf("error retrieving artifact versions: %w", err) + } + + out := make([]*minderv1.ArtifactVersion, 0, len(tags)) + for _, t := range tags { + // TODO: We probably should try to surface errors while returning a subset + // of manifests. + man, err := ov.ocicli.GetManifest(ctx, ov.artifact.GetName(), t) + if err != nil { + return nil, err + } + + // NOTE/FIXME: This is going to be a hassle as not a lot of + // container images have the needed annotations. We'd need + // go down to a specific image configuration (e.g. for _some_ + // architecture) to actually verify the creation date... + // Anybody has other ideas? + strcreated, ok := man.Annotations[v1.AnnotationCreated] + var createdAt time.Time + if ok { + // TODO: Verify if this is correct + createdAt, err = time.Parse(time.RFC3339, strcreated) + if err != nil { + return nil, fmt.Errorf("unable to get creation time for tag %s: %w", t, err) + } + } else { + // FIXME: This is a hack + createdAt = time.Now() + } + + // TODO: Consider caching + digest, err := ov.ocicli.GetDigest(ctx, ov.artifact.GetName(), t) + if err != nil { + return nil, fmt.Errorf("unable to get digest") + } + + out = append(out, &minderv1.ArtifactVersion{ + Tags: []string{t}, + Sha: digest, + CreatedAt: timestamppb.New(createdAt), + }) + } + + return out, nil +} diff --git a/internal/engine/ingester/ingester.go b/internal/engine/ingester/ingester.go index 617f3d2097..725ca593e1 100644 --- a/internal/engine/ingester/ingester.go +++ b/internal/engine/ingester/ingester.go @@ -63,11 +63,7 @@ func NewRuleDataIngest(rt *pb.RuleType, provider provinfv1.Provider) (engif.Inge if rt.Def.Ingest.GetArtifact() == nil { return nil, fmt.Errorf("rule type engine missing artifact configuration") } - client, err := provinfv1.As[provinfv1.GitHub](provider) - if err != nil { - return nil, errors.New("provider does not implement github trait") - } - return artifact.NewArtifactDataIngest(client) + return artifact.NewArtifactDataIngest(provider) case git.GitRuleDataIngestType: client, err := provinfv1.As[provinfv1.Git](provider) diff --git a/internal/providers/dockerhub/dockerhub.go b/internal/providers/dockerhub/dockerhub.go index d104f2285c..2de21e5814 100644 --- a/internal/providers/dockerhub/dockerhub.go +++ b/internal/providers/dockerhub/dockerhub.go @@ -22,10 +22,12 @@ import ( "fmt" "net/http" "net/url" + "path" "golang.org/x/oauth2" "github.com/stacklok/minder/internal/db" + "github.com/stacklok/minder/internal/providers/oci" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" provifv1 "github.com/stacklok/minder/pkg/providers/v1" ) @@ -33,6 +35,10 @@ import ( // DockerHub is the string that represents the DockerHub provider const DockerHub = "dockerhub" +const ( + dockerioBaseURL = "docker.io" +) + // Implements is the list of provider types that the DockerHub provider implements var Implements = []db.ProviderType{ db.ProviderTypeImageLister, @@ -46,6 +52,7 @@ var AuthorizationFlows = []db.AuthorizationFlow{ // dockerHubImageLister is the struct that contains the Docker Hub specific operations type dockerHubImageLister struct { + *oci.OCI cred provifv1.OAuth2TokenCredential cli *http.Client namespace string @@ -68,7 +75,9 @@ func New(cred provifv1.OAuth2TokenCredential, cfg *minderv1.DockerHubProviderCon ns := cfg.GetNamespace() t := u.JoinPath(ns) + o := oci.New(cred, dockerioBaseURL, path.Join(dockerioBaseURL, cfg.GetNamespace())) return &dockerHubImageLister{ + OCI: o, namespace: ns, cred: cred, cli: cli, @@ -102,7 +111,8 @@ func (d *dockerHubImageLister) GetNamespaceURL() string { // CanImplement returns true if the provider can implement the specified trait func (_ *dockerHubImageLister) CanImplement(trait minderv1.ProviderType) bool { - return trait == minderv1.ProviderType_PROVIDER_TYPE_IMAGE_LISTER + return trait == minderv1.ProviderType_PROVIDER_TYPE_IMAGE_LISTER || + trait == minderv1.ProviderType_PROVIDER_TYPE_OCI } // ListImages lists the containers in the Docker Hub diff --git a/internal/providers/github/mock/github.go b/internal/providers/github/mock/github.go index deb02893b3..15b48ab9e6 100644 --- a/internal/providers/github/mock/github.go +++ b/internal/providers/github/mock/github.go @@ -15,9 +15,11 @@ import ( reflect "reflect" git "github.com/go-git/go-git/v5" + authn "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" github "github.com/google/go-github/v61/github" - v1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" - v10 "github.com/stacklok/minder/pkg/providers/v1" + v10 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + v11 "github.com/stacklok/minder/pkg/providers/v1" gomock "go.uber.org/mock/gomock" ) @@ -45,7 +47,7 @@ func (m *MockProvider) EXPECT() *MockProviderMockRecorder { } // CanImplement mocks base method. -func (m *MockProvider) CanImplement(trait v1.ProviderType) bool { +func (m *MockProvider) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -82,7 +84,7 @@ func (m *MockGit) EXPECT() *MockGitMockRecorder { } // CanImplement mocks base method. -func (m *MockGit) CanImplement(trait v1.ProviderType) bool { +func (m *MockGit) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -134,7 +136,7 @@ func (m *MockREST) EXPECT() *MockRESTMockRecorder { } // CanImplement mocks base method. -func (m *MockREST) CanImplement(trait v1.ProviderType) bool { +func (m *MockREST) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -215,7 +217,7 @@ func (m *MockRepoLister) EXPECT() *MockRepoListerMockRecorder { } // CanImplement mocks base method. -func (m *MockRepoLister) CanImplement(trait v1.ProviderType) bool { +func (m *MockRepoLister) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -229,10 +231,10 @@ func (mr *MockRepoListerMockRecorder) CanImplement(trait any) *gomock.Call { } // ListAllRepositories mocks base method. -func (m *MockRepoLister) ListAllRepositories(arg0 context.Context) ([]*v1.Repository, error) { +func (m *MockRepoLister) ListAllRepositories(arg0 context.Context) ([]*v10.Repository, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAllRepositories", arg0) - ret0, _ := ret[0].([]*v1.Repository) + ret0, _ := ret[0].([]*v10.Repository) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -281,7 +283,7 @@ func (mr *MockGitHubMockRecorder) AddAuthToPushOptions(ctx, options any) *gomock } // CanImplement mocks base method. -func (m *MockGitHub) CanImplement(trait v1.ProviderType) bool { +func (m *MockGitHub) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -503,10 +505,10 @@ func (mr *MockGitHubMockRecorder) GetBranchProtection(arg0, arg1, arg2, arg3 any } // GetCredential mocks base method. -func (m *MockGitHub) GetCredential() v10.GitHubCredential { +func (m *MockGitHub) GetCredential() v11.GitHubCredential { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCredential") - ret0, _ := ret[0].(v10.GitHubCredential) + ret0, _ := ret[0].(v11.GitHubCredential) return ret0 } @@ -695,10 +697,10 @@ func (mr *MockGitHubMockRecorder) IsOrg() *gomock.Call { } // ListAllRepositories mocks base method. -func (m *MockGitHub) ListAllRepositories(arg0 context.Context) ([]*v1.Repository, error) { +func (m *MockGitHub) ListAllRepositories(arg0 context.Context) ([]*v10.Repository, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAllRepositories", arg0) - ret0, _ := ret[0].([]*v1.Repository) + ret0, _ := ret[0].([]*v10.Repository) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -912,7 +914,7 @@ func (m *MockImageLister) EXPECT() *MockImageListerMockRecorder { } // CanImplement mocks base method. -func (m *MockImageLister) CanImplement(trait v1.ProviderType) bool { +func (m *MockImageLister) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -978,7 +980,7 @@ func (m *MockOCI) EXPECT() *MockOCIMockRecorder { } // CanImplement mocks base method. -func (m *MockOCI) CanImplement(trait v1.ProviderType) bool { +func (m *MockOCI) CanImplement(trait v10.ProviderType) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CanImplement", trait) ret0, _ := ret[0].(bool) @@ -991,6 +993,21 @@ func (mr *MockOCIMockRecorder) CanImplement(trait any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanImplement", reflect.TypeOf((*MockOCI)(nil).CanImplement), trait) } +// GetAuthenticator mocks base method. +func (m *MockOCI) GetAuthenticator() (authn.Authenticator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthenticator") + ret0, _ := ret[0].(authn.Authenticator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthenticator indicates an expected call of GetAuthenticator. +func (mr *MockOCIMockRecorder) GetAuthenticator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthenticator", reflect.TypeOf((*MockOCI)(nil).GetAuthenticator)) +} + // GetDigest mocks base method. func (m *MockOCI) GetDigest(ctx context.Context, name, tag string) (string, error) { m.ctrl.T.Helper() @@ -1007,10 +1024,10 @@ func (mr *MockOCIMockRecorder) GetDigest(ctx, name, tag any) *gomock.Call { } // GetManifest mocks base method. -func (m *MockOCI) GetManifest(ctx context.Context, name, tag string) (any, error) { +func (m *MockOCI) GetManifest(ctx context.Context, name, tag string) (*v1.Manifest, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetManifest", ctx, name, tag) - ret0, _ := ret[0].(any) + ret0, _ := ret[0].(*v1.Manifest) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1036,6 +1053,20 @@ func (mr *MockOCIMockRecorder) GetReferrer(ctx, name, tag, artifactType any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReferrer", reflect.TypeOf((*MockOCI)(nil).GetReferrer), ctx, name, tag, artifactType) } +// GetRegistry mocks base method. +func (m *MockOCI) GetRegistry() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegistry") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetRegistry indicates an expected call of GetRegistry. +func (mr *MockOCIMockRecorder) GetRegistry() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegistry", reflect.TypeOf((*MockOCI)(nil).GetRegistry)) +} + // ListTags mocks base method. func (m *MockOCI) ListTags(ctx context.Context, name string) ([]string, error) { m.ctrl.T.Helper() diff --git a/internal/providers/oci/oci.go b/internal/providers/oci/oci.go index 73e0fc44e1..82871a1b55 100644 --- a/internal/providers/oci/oci.go +++ b/internal/providers/oci/oci.go @@ -20,7 +20,9 @@ import ( "fmt" "strings" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/stacklok/minder/internal/constants" @@ -32,17 +34,19 @@ import ( type OCI struct { cred provifv1.Credential - baseURL string + registry string + baseURL string } // Ensure that the OCI client implements the OCI interface var _ provifv1.OCI = (*OCI)(nil) // New creates a new OCI client -func New(cred provifv1.Credential, baseURL string) *OCI { +func New(cred provifv1.Credential, registry, baseURL string) *OCI { return &OCI{ - cred: cred, - baseURL: baseURL, + cred: cred, + registry: registry, + baseURL: baseURL, } } @@ -134,25 +138,49 @@ func (o *OCI) GetReferrer(ctx context.Context, contname, tag, artifactType strin // GetManifest returns the manifest for the given tag of the given container in the given namespace // for the OCI provider. It returns the manifest as a golang struct given the OCI spec. -func (o *OCI) GetManifest(ctx context.Context, contname, tag string) (any, error) { +func (o *OCI) GetManifest(ctx context.Context, contname, tag string) (*v1.Manifest, error) { ref, err := o.getReference(contname, tag) if err != nil { - return "", fmt.Errorf("failed to get reference: %w", err) + return nil, fmt.Errorf("failed to get reference: %w", err) } img, err := remote.Image(ref, remote.WithContext(ctx), remote.WithUserAgent(constants.ServerUserAgent)) if err != nil { - return "", fmt.Errorf("failed to get image: %w", err) + return nil, fmt.Errorf("failed to get image: %w", err) } man, err := img.Manifest() if err != nil { - return "", fmt.Errorf("failed to get manifest: %w", err) + return nil, fmt.Errorf("failed to get manifest: %w", err) } return man, nil } +// GetRegistry returns the registry name +func (o *OCI) GetRegistry() string { + return o.registry +} + +// GetAuthenticator returns the authenticator for the OCI provider +func (o *OCI) GetAuthenticator() (authn.Authenticator, error) { + if o.cred == nil { + return authn.Anonymous, nil + } + + oauth2cred, ok := o.cred.(provifv1.OAuth2TokenCredential) + if !ok { + return nil, fmt.Errorf("credential is not an OAuth2 token credential") + } + s := oauth2cred.GetAsOAuth2TokenSource() + t, err := s.Token() + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + return &authn.Bearer{Token: t.AccessToken}, nil +} + // getReferenceString returns the reference string for a given container name and tag func (o *OCI) getReferenceString(contname, tag string) string { return fmt.Sprintf("%s/%s:%s", o.baseURL, contname, tag) diff --git a/internal/verifier/sigstore/container/container.go b/internal/verifier/sigstore/container/container.go index 201d1d10e6..f4334deff7 100644 --- a/internal/verifier/sigstore/container/container.go +++ b/internal/verifier/sigstore/container/container.go @@ -120,7 +120,7 @@ func (c *containerAuth) getRegistry() string { func Verify( ctx context.Context, sev *verify.SignedEntityVerifier, - owner, artifact, version string, + owner, artifact, checksumref string, authOpts ...AuthMethod, ) ([]verifyif.Result, error) { logger := zerolog.Ctx(ctx) @@ -128,10 +128,10 @@ func Verify( cauth := newContainerAuth(authOpts...) logger.Info(). - Str("imageRef", BuildImageRef(cauth.getRegistry(), owner, artifact, version)). + Str("imageRef", BuildImageRef(cauth.getRegistry(), owner, artifact, checksumref)). Msg("verifying container artifact") // Construct the bundle(s) - OCI image or GitHub's attestation endpoint - bundles, err := getSigstoreBundles(ctx, owner, artifact, version, cauth) + bundles, err := getSigstoreBundles(ctx, owner, artifact, checksumref, cauth) if err != nil && !errors.Is(err, ErrProvenanceNotFoundOrIncomplete) { // We got some other unexpected error prior to querying for the signature/attestation return nil, err @@ -198,15 +198,15 @@ func getVerifiedResults( // getSigstoreBundles returns the sigstore bundles, either through the OCI registry or the GitHub attestation endpoint func getSigstoreBundles( ctx context.Context, - owner, artifact, version string, + owner, artifact, checksumref string, auth *containerAuth, ) ([]sigstoreBundle, error) { - imageRef := BuildImageRef(auth.getRegistry(), owner, artifact, version) + imageRef := BuildImageRef(auth.getRegistry(), owner, artifact, checksumref) // Try to build a bundle from the OCI image reference bundles, err := bundleFromOCIImage(ctx, imageRef, auth.getAuthenticator(owner)) if errors.Is(err, ErrProvenanceNotFoundOrIncomplete) && auth.ghClient != nil { // If we failed to find the signature in the OCI image, try to build a bundle from the GitHub attestation endpoint - return bundleFromGHAttestationEndpoint(ctx, auth.ghClient, owner, version) + return bundleFromGHAttestationEndpoint(ctx, auth.ghClient, owner, checksumref) } else if err != nil { return nil, fmt.Errorf("error getting bundle from OCI image: %w", err) } @@ -225,12 +225,12 @@ type AttestationReply struct { } func bundleFromGHAttestationEndpoint( - ctx context.Context, ghCli provifv1.GitHub, owner, version string, + ctx context.Context, ghCli provifv1.GitHub, owner, checksumref string, ) ([]sigstoreBundle, error) { logger := zerolog.Ctx(ctx) // Get the attestation reply from the GitHub attestation endpoint - attestationReply, err := getAttestationReply(ctx, ghCli, owner, version) + attestationReply, err := getAttestationReply(ctx, ghCli, owner, checksumref) if err != nil { return nil, fmt.Errorf("error getting attestation reply: %w", err) } @@ -244,7 +244,7 @@ func bundleFromGHAttestationEndpoint( continue } - digest, err := getDigestFromVersion(version) + digest, err := getDigestFromVersion(checksumref) if err != nil { logger.Err(err).Msg("error getting digest from version") continue @@ -268,12 +268,15 @@ func bundleFromGHAttestationEndpoint( } -func getAttestationReply(ctx context.Context, ghCli provifv1.GitHub, owner, version string) (*AttestationReply, error) { +func getAttestationReply( + ctx context.Context, + ghCli provifv1.GitHub, + owner, checksumref string) (*AttestationReply, error) { if ghCli == nil { return nil, fmt.Errorf("no github client available") } - url := fmt.Sprintf("orgs/%s/attestations/%s", owner, version) + url := fmt.Sprintf("orgs/%s/attestations/%s", owner, checksumref) req, err := ghCli.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) @@ -611,8 +614,8 @@ func getBundleMsgSignature(simpleSigningLayer v1.Descriptor) (*protobundle.Bundl } // BuildImageRef returns the OCI image reference -func BuildImageRef(registry, owner, artifact, version string) string { - return fmt.Sprintf("%s/%s/%s@%s", registry, owner, artifact, version) +func BuildImageRef(registry, owner, artifact, checksum string) string { + return fmt.Sprintf("%s/%s/%s@%s", registry, owner, artifact, checksum) } type sigstoreBundle struct { diff --git a/internal/verifier/sigstore/sigstore.go b/internal/verifier/sigstore/sigstore.go index 3863e78e02..74d568a643 100644 --- a/internal/verifier/sigstore/sigstore.go +++ b/internal/verifier/sigstore/sigstore.go @@ -148,7 +148,7 @@ func verifierOptions(trustedRoot string) ([]verify.VerifierOption, error) { // Verify verifies an artifact func (s *Sigstore) Verify(ctx context.Context, artifactType verifyif.ArtifactType, - owner, artifact, version string) ([]verifyif.Result, error) { + owner, artifact, checksumref string) ([]verifyif.Result, error) { var err error var res []verifyif.Result // Sanitize the input @@ -157,7 +157,7 @@ func (s *Sigstore) Verify(ctx context.Context, artifactType verifyif.ArtifactTyp // Process verification based on the artifact type switch artifactType { case verifyif.ArtifactTypeContainer: - res, err = s.VerifyContainer(ctx, owner, artifact, version) + res, err = s.VerifyContainer(ctx, owner, artifact, checksumref) default: err = fmt.Errorf("unknown artifact type: %s", artifactType) } @@ -166,9 +166,9 @@ func (s *Sigstore) Verify(ctx context.Context, artifactType verifyif.ArtifactTyp } // VerifyContainer verifies a container artifact using sigstore -func (s *Sigstore) VerifyContainer(ctx context.Context, owner, artifact, version string) ( +func (s *Sigstore) VerifyContainer(ctx context.Context, owner, artifact, checksumref string) ( []verifyif.Result, error) { - return container.Verify(ctx, s.verifier, owner, artifact, version, s.authOpts...) + return container.Verify(ctx, s.verifier, owner, artifact, checksumref, s.authOpts...) } // sanitizeInput sanitizes the input parameters diff --git a/internal/verifier/verifyif/verifyif.go b/internal/verifier/verifyif/verifyif.go index 34c3fe48ad..012fd3beaa 100644 --- a/internal/verifier/verifyif/verifyif.go +++ b/internal/verifier/verifyif/verifyif.go @@ -42,7 +42,7 @@ type Result struct { // ArtifactVerifier is the interface for artifact verifiers type ArtifactVerifier interface { Verify(ctx context.Context, artifactType ArtifactType, - owner, name, version string) ([]Result, error) + owner, name, checksumref string) ([]Result, error) VerifyContainer(ctx context.Context, - owner, artifact, version string) ([]Result, error) + owner, artifact, checksumref string) ([]Result, error) } diff --git a/pkg/providers/v1/providers.go b/pkg/providers/v1/providers.go index ecee0736ee..cb832ca62f 100644 --- a/pkg/providers/v1/providers.go +++ b/pkg/providers/v1/providers.go @@ -26,6 +26,8 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-playground/validator/v10" + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-github/v61/github" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" @@ -153,7 +155,13 @@ type OCI interface { // GetManifest returns the manifest for the given tag of the given container in the given namespace // for the OCI provider. It returns the manifest as a golang struct given the OCI spec. // TODO - Define the manifest struct - GetManifest(ctx context.Context, name, tag string) (any, error) + GetManifest(ctx context.Context, name, tag string) (*v1.Manifest, error) + + // GetRegistry returns the registry name + GetRegistry() string + + // GetAuthenticator returns the authenticator for the OCI provider + GetAuthenticator() (authn.Authenticator, error) } // ParseAndValidate parses the given provider configuration and validates it.