Support list artifact with_sbom_overview option (#20244)

Signed-off-by: stonezdj <stone.zhang@broadcom.com>
Co-authored-by: stonezdj <daojunz@vmware.com>
This commit is contained in:
stonezdj(Daojun Zhang) 2024-04-10 22:47:45 +08:00 committed by GitHub
parent 89995075a7
commit 5d7c668028
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 277 additions and 37 deletions

View File

@ -17,6 +17,7 @@ package scan
import (
"bytes"
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
@ -674,12 +675,23 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
return reports, nil
}
func isSBOMMimeTypes(mimeTypes []string) bool {
for _, mimeType := range mimeTypes {
if mimeType == v1.MimeTypeSBOMReport {
return true
}
}
return false
}
// GetSummary ...
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
if artifact == nil {
return nil, errors.New("no way to get report summaries for nil artifact")
}
if isSBOMMimeTypes(mimeTypes) {
return bc.GetSBOMSummary(ctx, artifact, mimeTypes)
}
// Get reports first
rps, err := bc.GetReport(ctx, artifact, mimeTypes)
if err != nil {
@ -708,6 +720,30 @@ func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact
return summaries, nil
}
func (bc *basicController) GetSBOMSummary(ctx context.Context, art *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
if art == nil {
return nil, errors.New("no way to get report summaries for nil artifact")
}
r, err := bc.sc.GetRegistrationByProject(ctx, art.ProjectID)
if err != nil {
return nil, errors.Wrap(err, "scan controller: get sbom summary")
}
reports, err := bc.manager.GetBy(ctx, art.Digest, r.UUID, mimeTypes)
if err != nil {
return nil, err
}
if len(reports) == 0 {
return map[string]interface{}{}, nil
}
reportContent := reports[0].Report
if len(reportContent) == 0 {
log.Warning("no content for current report")
}
result := map[string]interface{}{}
err = json.Unmarshal([]byte(reportContent), &result)
return result, err
}
// GetScanLog ...
func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact, uuid string) ([]byte, error) {
if len(uuid) == 0 {

View File

@ -78,7 +78,7 @@ type ControllerTestSuite struct {
taskMgr *tasktesting.Manager
reportMgr *reporttesting.Manager
ar artifact.Controller
c Controller
c *basicController
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
cache *mockcache.Cache
}
@ -180,7 +180,19 @@ func (suite *ControllerTestSuite) SetupSuite() {
},
}
sbomReport := []*scan.Report{
{
ID: 12,
UUID: "rp-uuid-002",
Digest: "digest-code",
RegistrationUUID: "uuid001",
MimeType: "application/vnd.scanner.adapter.sbom.report.harbor+json; version=1.0",
Status: "Success",
Report: `{"sbom_digest": "sha256:1234567890", "scan_status": "Success", "duration": 3, "start_time": "2021-09-01T00:00:00Z", "end_time": "2021-09-01T00:00:03Z"}`,
},
}
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeNativeReport}).Return(reports, nil)
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(sbomReport, nil)
mgr.On("Get", mock.Anything, "rp-uuid-001").Return(reports[0], nil)
mgr.On("UpdateReportData", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil)
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
@ -620,3 +632,26 @@ func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs .
return extraAttrs
}
func (suite *ControllerTestSuite) TestGenerateSBOMSummary() {
sum, err := suite.c.GetSBOMSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeSBOMReport})
suite.Nil(err)
suite.NotNil(sum)
status := sum["scan_status"]
suite.NotNil(status)
dgst := sum["sbom_digest"]
suite.NotNil(dgst)
suite.Equal("Success", status)
suite.Equal("sha256:1234567890", dgst)
}
func TestIsSBOMMimeTypes(t *testing.T) {
// Test with a slice containing the SBOM mime type
assert.True(t, isSBOMMimeTypes([]string{v1.MimeTypeSBOMReport}))
// Test with a slice not containing the SBOM mime type
assert.False(t, isSBOMMimeTypes([]string{"application/vnd.oci.image.manifest.v1+json"}))
// Test with an empty slice
assert.False(t, isSBOMMimeTypes([]string{}))
}

View File

@ -21,12 +21,11 @@ import (
"github.com/goharbor/harbor/src/lib/errors"
)
const (
// ScanTypeVulnerability the scan type for vulnerability
ScanTypeVulnerability = "vulnerability"
// ScanTypeSbom the scan type for sbom
ScanTypeSbom = "sbom"
)
var supportedMimeTypes = []string{
MimeTypeNativeReport,
MimeTypeGenericVulnerabilityReport,
MimeTypeSBOMReport,
}
// Scanner represents metadata of a Scanner Adapter which allow Harbor to lookup a scanner capable of
// scanning a given Artifact stored in its registry and making sure that it can interpret a
@ -105,7 +104,7 @@ func (md *ScannerAdapterMetadata) Validate() error {
// either of v1.MimeTypeNativeReport OR v1.MimeTypeGenericVulnerabilityReport is required
found = false
for _, pm := range ca.ProducesMimeTypes {
if pm == MimeTypeNativeReport || pm == MimeTypeGenericVulnerabilityReport {
if isSupportedMimeType(pm) {
found = true
break
}
@ -119,6 +118,15 @@ func (md *ScannerAdapterMetadata) Validate() error {
return nil
}
func isSupportedMimeType(mimeType string) bool {
for _, mt := range supportedMimeTypes {
if mt == mimeType {
return true
}
}
return false
}
// HasCapability returns true when mine type of the artifact support by the scanner
func (md *ScannerAdapterMetadata) HasCapability(mimeType string) bool {
for _, capability := range md.Capabilities {

View File

@ -0,0 +1,15 @@
package v1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSupportedMimeType(t *testing.T) {
// Test with a supported mime type
assert.True(t, isSupportedMimeType(MimeTypeSBOMReport), "isSupportedMimeType should return true for supported mime types")
// Test with an unsupported mime type
assert.False(t, isSupportedMimeType("unsupported/mime-type"), "isSupportedMimeType should return false for unsupported mime types")
}

View File

@ -39,9 +39,14 @@ const (
MimeTypeScanRequest = "application/vnd.scanner.adapter.scan.request+json; version=1.0"
// MimeTypeScanResponse defines the mime type for scan response
MimeTypeScanResponse = "application/vnd.scanner.adapter.scan.response+json; version=1.0"
// MimeTypeSBOMReport
MimeTypeSBOMReport = "application/vnd.security.sbom.report+json; version=1.0"
// MimeTypeGenericVulnerabilityReport defines the MIME type for the generic report with enhanced information
MimeTypeGenericVulnerabilityReport = "application/vnd.security.vulnerability.report; version=1.1"
ScanTypeVulnerability = "vulnerability"
ScanTypeSbom = "sbom"
apiPrefix = "/api/v1"
)

View File

@ -107,8 +107,8 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
if err != nil {
return a.SendError(ctx, err)
}
assembler := assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities))
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
assembler := assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities))
var artifacts []*models.Artifact
for _, art := range arts {
artifact := &model.Artifact{}
@ -138,8 +138,9 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif
}
art := &model.Artifact{}
art.Artifact = *artifact
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
err = assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
err = assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
if err != nil {
log.Warningf("failed to assemble vulnerabilities with artifact, error: %v", err)
}

View File

@ -20,43 +20,48 @@ import (
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
)
const (
vulnerabilitiesAddition = "vulnerabilities"
startTime = "start_time"
endTime = "end_time"
scanStatus = "scan_status"
sbomDigest = "sbom_digest"
duration = "duration"
)
// NewVulAssembler returns vul assembler
func NewVulAssembler(withScanOverview bool, mimeTypes []string) *VulAssembler {
return &VulAssembler{
scanChecker: scan.NewChecker(),
scanCtl: scan.DefaultController,
withScanOverview: withScanOverview,
mimeTypes: mimeTypes,
// NewScanReportAssembler returns vul assembler
func NewScanReportAssembler(option *model.OverviewOptions, mimeTypes []string) *ScanReportAssembler {
return &ScanReportAssembler{
overviewOption: option,
scanChecker: scan.NewChecker(),
scanCtl: scan.DefaultController,
mimeTypes: mimeTypes,
}
}
// VulAssembler vul assembler
type VulAssembler struct {
// ScanReportAssembler vul assembler
type ScanReportAssembler struct {
scanChecker scan.Checker
scanCtl scan.Controller
artifacts []*model.Artifact
withScanOverview bool
mimeTypes []string
artifacts []*model.Artifact
mimeTypes []string
overviewOption *model.OverviewOptions
}
// WithArtifacts set artifacts for the assembler
func (assembler *VulAssembler) WithArtifacts(artifacts ...*model.Artifact) *VulAssembler {
func (assembler *ScanReportAssembler) WithArtifacts(artifacts ...*model.Artifact) *ScanReportAssembler {
assembler.artifacts = artifacts
return assembler
}
// Assemble assemble vul for the artifacts
func (assembler *VulAssembler) Assemble(ctx context.Context) error {
func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error {
version := lib.GetAPIVersion(ctx)
for _, artifact := range assembler.artifacts {
@ -72,7 +77,7 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
if assembler.withScanOverview {
if assembler.overviewOption.WithVuln {
for _, mimeType := range assembler.mimeTypes {
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{mimeType})
if err != nil {
@ -83,6 +88,20 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
}
}
}
if assembler.overviewOption.WithSBOM {
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeSBOMReport})
if err != nil {
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, v1.MimeTypeSBOMReport, err)
} else if len(overview) > 0 {
artifact.SBOMOverView = map[string]interface{}{
startTime: overview[startTime],
endTime: overview[endTime],
scanStatus: overview[scanStatus],
sbomDigest: overview[sbomDigest],
duration: overview[duration],
}
}
}
}
return nil

View File

@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/suite"
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/testing/controller/scan"
"github.com/goharbor/harbor/src/testing/mock"
@ -33,11 +34,11 @@ func (suite *VulAssemblerTestSuite) TestScannable() {
checker := &scan.Checker{}
scanCtl := &scan.Controller{}
assembler := VulAssembler{
scanChecker: checker,
scanCtl: scanCtl,
withScanOverview: true,
mimeTypes: []string{"mimeType"},
assembler := ScanReportAssembler{
scanChecker: checker,
scanCtl: scanCtl,
overviewOption: model.NewOverviewOptions(model.WithVuln(true)),
mimeTypes: []string{"mimeType"},
}
mock.OnAnything(checker, "IsScannable").Return(true, nil)
@ -56,10 +57,10 @@ func (suite *VulAssemblerTestSuite) TestNotScannable() {
checker := &scan.Checker{}
scanCtl := &scan.Controller{}
assembler := VulAssembler{
scanChecker: checker,
scanCtl: scanCtl,
withScanOverview: true,
assembler := ScanReportAssembler{
scanChecker: checker,
scanCtl: scanCtl,
overviewOption: model.NewOverviewOptions(model.WithVuln(true)),
}
mock.OnAnything(checker, "IsScannable").Return(false, nil)
@ -74,6 +75,32 @@ func (suite *VulAssemblerTestSuite) TestNotScannable() {
scanCtl.AssertNotCalled(suite.T(), "GetSummary")
}
func (suite *VulAssemblerTestSuite) TestAssembleSBOMOverview() {
checker := &scan.Checker{}
scanCtl := &scan.Controller{}
assembler := ScanReportAssembler{
scanChecker: checker,
scanCtl: scanCtl,
overviewOption: model.NewOverviewOptions(model.WithSBOM(true)),
mimeTypes: []string{v1sq.MimeTypeSBOMReport},
}
mock.OnAnything(checker, "IsScannable").Return(true, nil)
overview := map[string]interface{}{
"sbom_digest": "sha256:123456",
"scan_status": "Success",
}
mock.OnAnything(scanCtl, "GetSummary").Return(overview, nil)
var artifact model.Artifact
err := assembler.WithArtifacts(&artifact).Assemble(context.TODO())
suite.Nil(err)
suite.Equal(artifact.SBOMOverView["sbom_digest"], "sha256:123456")
suite.Equal(artifact.SBOMOverView["scan_status"], "Success")
}
func TestVulAssemblerTestSuite(t *testing.T) {
suite.Run(t, &VulAssemblerTestSuite{})
}

View File

@ -28,7 +28,9 @@ import (
// Artifact model
type Artifact struct {
artifact.Artifact
// TODO: rename to VulOverview
ScanOverview map[string]interface{} `json:"scan_overview"`
SBOMOverView map[string]interface{} `json:"sbom_overview"`
}
// ToSwagger converts the artifact to the swagger model
@ -84,6 +86,18 @@ func (a *Artifact) ToSwagger() *models.Artifact {
art.ScanOverview[key] = summary
}
}
if len(a.SBOMOverView) > 0 {
js, err := json.Marshal(a.SBOMOverView)
if err != nil {
log.Warningf("convert sbom summary failed, error: %v", err)
}
sbomOverview := &models.SBOMOverview{}
err = json.Unmarshal(js, sbomOverview)
if err != nil {
log.Warningf("failed to get sbom summary: error: %v", err)
}
art.SbomOverview = sbomOverview
}
return art
}

View File

@ -0,0 +1,47 @@
// 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 model
// OverviewOptions define the option to query overview info
type OverviewOptions struct {
WithVuln bool
WithSBOM bool
}
// Option define the func to build options
type Option func(*OverviewOptions)
// NewOverviewOptions create a new OverviewOptions
func NewOverviewOptions(options ...Option) *OverviewOptions {
opts := &OverviewOptions{}
for _, f := range options {
f(opts)
}
return opts
}
// WithVuln set the option to query vulnerability info
func WithVuln(enable bool) Option {
return func(o *OverviewOptions) {
o.WithVuln = enable
}
}
// WithSBOM set the option to query SBOM info
func WithSBOM(enable bool) Option {
return func(o *OverviewOptions) {
o.WithSBOM = enable
}
}

View File

@ -0,0 +1,33 @@
// 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 model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOverviewOptions(t *testing.T) {
// Test NewOverviewOptions with WithVuln and WithSBOM
opts := NewOverviewOptions(WithVuln(true), WithSBOM(true))
assert.True(t, opts.WithVuln)
assert.True(t, opts.WithSBOM)
// Test NewOverviewOptions with WithVuln and WithSBOM set to false
opts = NewOverviewOptions(WithVuln(false), WithSBOM(false))
assert.False(t, opts.WithVuln)
assert.False(t, opts.WithSBOM)
}