From a003aee96a75ce180de20aa2f5d7f56a8d42cafb Mon Sep 17 00:00:00 2001 From: stonezdj Date: Tue, 2 Apr 2024 22:05:05 +0800 Subject: [PATCH] Update swagger API to display SBOM content in addition API complete task #20066 Signed-off-by: stonezdj --- src/controller/artifact/controller.go | 1 + .../artifact/processor/chart/chart.go | 2 +- .../artifact/processor/sbom/sbom.go | 89 ++++++++++ .../artifact/processor/sbom/sbom_test.go | 166 ++++++++++++++++++ 4 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 src/controller/artifact/processor/sbom/sbom.go create mode 100644 src/controller/artifact/processor/sbom/sbom_test.go diff --git a/src/controller/artifact/controller.go b/src/controller/artifact/controller.go index cc100211f8f2..50dc8cbde006 100644 --- a/src/controller/artifact/controller.go +++ b/src/controller/artifact/controller.go @@ -29,6 +29,7 @@ import ( "github.com/goharbor/harbor/src/controller/artifact/processor/chart" "github.com/goharbor/harbor/src/controller/artifact/processor/cnab" "github.com/goharbor/harbor/src/controller/artifact/processor/image" + _ "github.com/goharbor/harbor/src/controller/artifact/processor/sbom" "github.com/goharbor/harbor/src/controller/artifact/processor/wasm" "github.com/goharbor/harbor/src/controller/event/metadata" "github.com/goharbor/harbor/src/controller/tag" diff --git a/src/controller/artifact/processor/chart/chart.go b/src/controller/artifact/processor/chart/chart.go index 059d47bbfb01..e7df72b562e6 100644 --- a/src/controller/artifact/processor/chart/chart.go +++ b/src/controller/artifact/processor/chart/chart.go @@ -85,11 +85,11 @@ func (p *processor) AbstractAddition(_ context.Context, artifact *artifact.Artif if err != nil { return nil, err } + defer blob.Close() content, err := io.ReadAll(blob) if err != nil { return nil, err } - blob.Close() chartDetails, err := p.chartOperator.GetDetails(content) if err != nil { return nil, err diff --git a/src/controller/artifact/processor/sbom/sbom.go b/src/controller/artifact/processor/sbom/sbom.go new file mode 100644 index 000000000000..ec0222fb96d1 --- /dev/null +++ b/src/controller/artifact/processor/sbom/sbom.go @@ -0,0 +1,89 @@ +// Copyright Project Harbor Authors +// +// 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 sbom + +import ( + "context" + "encoding/json" + "io" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/goharbor/harbor/src/controller/artifact/processor" + "github.com/goharbor/harbor/src/controller/artifact/processor/base" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/artifact" +) + +const ( + // processorArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor + processorArtifactTypeSBOM = "SBOM" + // processorMediaType is the media type for SBOM, it's scope is only used to register the processor + processorMediaType = "application/vnd.goharbor.harbor.sbom.v1" +) + +func init() { + pc := &Processor{} + pc.ManifestProcessor = base.NewManifestProcessor() + if err := processor.Register(pc, processorMediaType); err != nil { + log.Errorf("failed to register processor for media type %s: %v", processorMediaType, err) + return + } +} + +// Processor is the processor for SBOM +type Processor struct { + *base.ManifestProcessor +} + +// AbstractAddition returns the addition for SBOM +func (m *Processor) AbstractAddition(_ context.Context, art *artifact.Artifact, _ string) (*processor.Addition, error) { + man, _, err := m.RegCli.PullManifest(art.RepositoryName, art.Digest) + if err != nil { + return nil, errors.Wrap(err, "failed to pull manifest") + } + _, payload, err := man.Payload() + if err != nil { + return nil, errors.Wrap(err, "failed to get payload") + } + manifest := &v1.Manifest{} + if err := json.Unmarshal(payload, manifest); err != nil { + return nil, err + } + // SBOM artifact should only have one layer + if len(manifest.Layers) != 1 { + return nil, errors.New(nil).WithCode(errors.NotFoundCode).WithMessage("The sbom is not found") + } + layerDgst := manifest.Layers[0].Digest.String() + _, blob, err := m.RegCli.PullBlob(art.RepositoryName, layerDgst) + if err != nil { + return nil, errors.Wrap(err, "failed to pull the blob") + } + defer blob.Close() + content, err := io.ReadAll(blob) + if err != nil { + return nil, err + } + return &processor.Addition{ + Content: content, + ContentType: processorMediaType, + }, nil +} + +// GetArtifactType the artifact type is used to display the artifact type in the UI +func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string { + return processorArtifactTypeSBOM +} diff --git a/src/controller/artifact/processor/sbom/sbom_test.go b/src/controller/artifact/processor/sbom/sbom_test.go new file mode 100644 index 000000000000..6128c550fcb3 --- /dev/null +++ b/src/controller/artifact/processor/sbom/sbom_test.go @@ -0,0 +1,166 @@ +// Copyright Project Harbor Authors +// +// 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 sbom + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + + "github.com/docker/distribution" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/controller/artifact/processor/base" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/testing/pkg/registry" +) + +type SBOMProcessorTestSuite struct { + suite.Suite + processor *Processor + regCli *registry.Client +} + +func (suite *SBOMProcessorTestSuite) SetupSuite() { + suite.regCli = ®istry.Client{} + suite.processor = &Processor{ + &base.ManifestProcessor{ + RegCli: suite.regCli, + }, + } +} + +func (suite *SBOMProcessorTestSuite) TearDownSuite() { +} + +func (suite *SBOMProcessorTestSuite) TestAbstractAdditionNormal() { + manContent := `{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b", + "size": 498 + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:abc" + }] +}` + sbomContent := "this is a sbom content" + reader := strings.NewReader(sbomContent) + blobReader := io.NopCloser(reader) + mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent)) + suite.Require().NoError(err) + suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once() + suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(int64(123), blobReader, nil).Once() + addition, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom") + suite.Nil(err) + suite.Equal(sbomContent, string(addition.Content)) +} + +func (suite *SBOMProcessorTestSuite) TestAbstractAdditionMultiLayer() { + manContent := `{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b", + "size": 498 + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:abc" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 843, + "digest": "sha256:def" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 531, + "digest": "sha256:123" + } + ] +}` + mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent)) + suite.Require().NoError(err) + suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once() + _, err = suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom") + suite.NotNil(err) +} + +func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullBlobError() { + manContent := `{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b", + "size": 498 + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:abc" + } + ] +}` + mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent)) + suite.Require().NoError(err) + suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once() + suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(int64(123), nil, errors.NotFoundError(fmt.Errorf("not found"))).Once() + addition, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom") + suite.NotNil(err) + suite.Nil(addition) +} +func (suite *SBOMProcessorTestSuite) TestAbstractAdditionNoSBOMLayer() { + manContent := `{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b", + "size": 498 + } +}` + mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent)) + suite.Require().NoError(err) + suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once() + _, err = suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom") + suite.NotNil(err) +} + +func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullManifestError() { + suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(nil, "sha256:123", errors.NotFoundError(fmt.Errorf("not found"))).Once() + _, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom") + suite.NotNil(err) + +} + +func (suite *SBOMProcessorTestSuite) TestGetArtifactType() { + suite.Equal(processorArtifactTypeSBOM, suite.processor.GetArtifactType(context.Background(), &artifact.Artifact{})) +} + +func TestSBOMProcessorTestSuite(t *testing.T) { + suite.Run(t, &SBOMProcessorTestSuite{}) +}