mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-28 21:37:31 +02:00
feat: return scan report and summary by header (#13898)
Add X-Accept-Vulnerabilities header to the list/get artifact and get artifact vulnerability addition APIs, and these APIs will traverse the mime types in this header and return the first report and summary found from the mime type. Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
511bd86930
commit
ed31cf9417
@ -367,6 +367,7 @@ paths:
|
|||||||
- $ref: '#/parameters/query'
|
- $ref: '#/parameters/query'
|
||||||
- $ref: '#/parameters/page'
|
- $ref: '#/parameters/page'
|
||||||
- $ref: '#/parameters/pageSize'
|
- $ref: '#/parameters/pageSize'
|
||||||
|
- $ref: '#/parameters/acceptVulnerabilities'
|
||||||
- name: with_tag
|
- name: with_tag
|
||||||
in: query
|
in: query
|
||||||
description: Specify whether the tags are included inside the returning artifacts
|
description: Specify whether the tags are included inside the returning artifacts
|
||||||
@ -465,6 +466,7 @@ paths:
|
|||||||
- $ref: '#/parameters/reference'
|
- $ref: '#/parameters/reference'
|
||||||
- $ref: '#/parameters/page'
|
- $ref: '#/parameters/page'
|
||||||
- $ref: '#/parameters/pageSize'
|
- $ref: '#/parameters/pageSize'
|
||||||
|
- $ref: '#/parameters/acceptVulnerabilities'
|
||||||
- name: with_tag
|
- name: with_tag
|
||||||
in: query
|
in: query
|
||||||
description: Specify whether the tags are inclued inside the returning artifacts
|
description: Specify whether the tags are inclued inside the returning artifacts
|
||||||
@ -699,6 +701,38 @@ paths:
|
|||||||
$ref: '#/responses/404'
|
$ref: '#/responses/404'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/500'
|
$ref: '#/responses/500'
|
||||||
|
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/additions/vulnerabilities:
|
||||||
|
get:
|
||||||
|
summary: Get the vulnerabilities addition of the specific artifact
|
||||||
|
description: Get the vulnerabilities addition of the artifact specified by the reference under the project and repository.
|
||||||
|
tags:
|
||||||
|
- artifact
|
||||||
|
operationId: getVulnerabilitiesAddition
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectName'
|
||||||
|
- $ref: '#/parameters/repositoryName'
|
||||||
|
- $ref: '#/parameters/reference'
|
||||||
|
- $ref: '#/parameters/acceptVulnerabilities'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success
|
||||||
|
headers:
|
||||||
|
Content-Type:
|
||||||
|
description: The content type of the vulnerabilities addition
|
||||||
|
type: string
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/additions/{addition}:
|
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/additions/{addition}:
|
||||||
get:
|
get:
|
||||||
summary: Get the addition of the specific artifact
|
summary: Get the addition of the specific artifact
|
||||||
@ -715,7 +749,7 @@ paths:
|
|||||||
in: path
|
in: path
|
||||||
description: The type of addition.
|
description: The type of addition.
|
||||||
type: string
|
type: string
|
||||||
enum: [build_history, values.yaml, readme.md, dependencies, vulnerabilities]
|
enum: [build_history, values.yaml, readme.md, dependencies]
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@ -2337,6 +2371,14 @@ parameters:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
|
acceptVulnerabilities:
|
||||||
|
name: X-Accept-Vulnerabilities
|
||||||
|
in: header
|
||||||
|
type: string
|
||||||
|
default: 'application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0'
|
||||||
|
description: |-
|
||||||
|
A comma-separated lists of MIME types for the scan report or scan summary. The first mime type will be used when the report found for it.
|
||||||
|
Currently the mime type supports 'application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0' and 'application/vnd.security.vulnerability.report; version=1.1'
|
||||||
projectName:
|
projectName:
|
||||||
name: project_name
|
name: project_name
|
||||||
in: path
|
in: path
|
||||||
|
@ -16,6 +16,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -27,14 +28,15 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/controller/artifact"
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
"github.com/goharbor/harbor/src/controller/artifact/processor"
|
|
||||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
"github.com/goharbor/harbor/src/controller/event/metadata"
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/controller/repository"
|
"github.com/goharbor/harbor/src/controller/repository"
|
||||||
"github.com/goharbor/harbor/src/controller/scan"
|
"github.com/goharbor/harbor/src/controller/scan"
|
||||||
"github.com/goharbor/harbor/src/controller/tag"
|
"github.com/goharbor/harbor/src/controller/tag"
|
||||||
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/pkg/notification"
|
"github.com/goharbor/harbor/src/pkg/notification"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
|
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
@ -42,10 +44,6 @@ import (
|
|||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
vulnerabilitiesAddition = "vulnerabilities"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newArtifactAPI() *artifactAPI {
|
func newArtifactAPI() *artifactAPI {
|
||||||
return &artifactAPI{
|
return &artifactAPI{
|
||||||
artCtl: artifact.Ctl,
|
artCtl: artifact.Ctl,
|
||||||
@ -100,7 +98,7 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
|
|||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assembler := assembler.NewVulAssembler(boolValue(params.WithScanOverview))
|
assembler := assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities))
|
||||||
var artifacts []*models.Artifact
|
var artifacts []*models.Artifact
|
||||||
for _, art := range arts {
|
for _, art := range arts {
|
||||||
artifact := &model.Artifact{}
|
artifact := &model.Artifact{}
|
||||||
@ -131,7 +129,7 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif
|
|||||||
art := &model.Artifact{}
|
art := &model.Artifact{}
|
||||||
art.Artifact = *artifact
|
art.Artifact = *artifact
|
||||||
|
|
||||||
assembler.NewVulAssembler(boolValue(params.WithScanOverview)).WithArtifacts(art).Assemble(ctx)
|
assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
|
||||||
|
|
||||||
return operation.NewGetArtifactOK().WithPayload(art.ToSwagger())
|
return operation.NewGetArtifactOK().WithPayload(art.ToSwagger())
|
||||||
}
|
}
|
||||||
@ -335,6 +333,59 @@ func (a *artifactAPI) ListTags(ctx context.Context, params operation.ListTagsPar
|
|||||||
WithPayload(ts)
|
WithPayload(ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *artifactAPI) GetVulnerabilitiesAddition(ctx context.Context, params operation.GetVulnerabilitiesAdditionParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnerabilities := make(map[string]interface{})
|
||||||
|
|
||||||
|
for _, mimeType := range parseScanReportMimeTypes(params.XAcceptVulnerabilities) {
|
||||||
|
reports, err := a.scanCtl.GetReport(ctx, artifact, []string{mimeType})
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rp := range reports {
|
||||||
|
// Resolve scan report data only when it is ready
|
||||||
|
if len(rp.Report) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vrp, err := report.ResolveData(rp.MimeType, []byte(rp.Report), report.WithArtifactDigest(rp.Digest))
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := vulnerabilities[rp.MimeType]; ok {
|
||||||
|
r, err := report.Merge(rp.MimeType, v, vrp)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
vulnerabilities[rp.MimeType] = r
|
||||||
|
} else {
|
||||||
|
vulnerabilities[rp.MimeType] = vrp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vulnerabilities) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ := json.Marshal(vulnerabilities)
|
||||||
|
|
||||||
|
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAdditionParams) middleware.Responder {
|
func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAdditionParams) middleware.Responder {
|
||||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
|
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
|
||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
@ -345,13 +396,7 @@ func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAddit
|
|||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var addition *processor.Addition
|
addition, err := a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition))
|
||||||
|
|
||||||
if params.Addition == vulnerabilitiesAddition {
|
|
||||||
addition, err = resolveVulnerabilitiesAddition(ctx, artifact)
|
|
||||||
} else {
|
|
||||||
addition, err = a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition))
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
@ -393,7 +438,7 @@ func (a *artifactAPI) RemoveLabel(ctx context.Context, params operation.RemoveLa
|
|||||||
func option(withTag, withImmutableStatus, withLabel, withSignature *bool) *artifact.Option {
|
func option(withTag, withImmutableStatus, withLabel, withSignature *bool) *artifact.Option {
|
||||||
option := &artifact.Option{
|
option := &artifact.Option{
|
||||||
WithTag: true, // return the tag by default
|
WithTag: true, // return the tag by default
|
||||||
WithLabel: boolValue(withLabel),
|
WithLabel: lib.BoolValue(withLabel),
|
||||||
}
|
}
|
||||||
|
|
||||||
if withTag != nil {
|
if withTag != nil {
|
||||||
@ -402,8 +447,8 @@ func option(withTag, withImmutableStatus, withLabel, withSignature *bool) *artif
|
|||||||
|
|
||||||
if option.WithTag {
|
if option.WithTag {
|
||||||
option.TagOption = &tag.Option{
|
option.TagOption = &tag.Option{
|
||||||
WithImmutableStatus: boolValue(withImmutableStatus),
|
WithImmutableStatus: lib.BoolValue(withImmutableStatus),
|
||||||
WithSignature: boolValue(withSignature),
|
WithSignature: lib.BoolValue(withSignature),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +15,20 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||||
|
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||||
|
scantesting "github.com/goharbor/harbor/src/testing/controller/scan"
|
||||||
|
"github.com/goharbor/harbor/src/testing/mock"
|
||||||
|
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"testing"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
@ -50,3 +61,134 @@ func TestParse(t *testing.T) {
|
|||||||
repository, reference, err = parse(input)
|
repository, reference, err = parse(input)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArtifactTestSuite struct {
|
||||||
|
htesting.Suite
|
||||||
|
|
||||||
|
artCtl *artifacttesting.Controller
|
||||||
|
scanCtl *scantesting.Controller
|
||||||
|
|
||||||
|
report1 *scan.Report
|
||||||
|
report2 *scan.Report
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ArtifactTestSuite) SetupSuite() {
|
||||||
|
suite.artCtl = &artifacttesting.Controller{}
|
||||||
|
suite.scanCtl = &scantesting.Controller{}
|
||||||
|
|
||||||
|
suite.Config = &restapi.Config{
|
||||||
|
ArtifactAPI: &artifactAPI{
|
||||||
|
artCtl: suite.artCtl,
|
||||||
|
scanCtl: suite.scanCtl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Suite.SetupSuite()
|
||||||
|
|
||||||
|
mock.OnAnything(projectCtlMock, "GetByName").Return(&project.Project{ProjectID: 1}, nil)
|
||||||
|
|
||||||
|
suite.report1 = &scan.Report{
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
Report: "{}",
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.report2 = &scan.Report{
|
||||||
|
MimeType: v1.MimeTypeGenericVulnerabilityReport,
|
||||||
|
Report: "{}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ArtifactTestSuite) onGetReport(mimeType string, reports ...*scan.Report) {
|
||||||
|
suite.scanCtl.On("GetReport", mock.Anything, mock.Anything, []string{mimeType}).Return(reports, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ArtifactTestSuite) TestGetVulnerabilitiesAddition() {
|
||||||
|
times := 6
|
||||||
|
suite.Security.On("IsAuthenticated").Return(true).Times(times)
|
||||||
|
suite.Security.On("IsSysAdmin").Return(true).Times(times)
|
||||||
|
mock.OnAnything(suite.Security, "Can").Return(true).Times(times)
|
||||||
|
mock.OnAnything(suite.artCtl, "GetByReference").Return(&artifact.Artifact{}, nil).Times(times)
|
||||||
|
|
||||||
|
url := "/projects/library/repositories/photon/artifacts/2.0/additions/vulnerabilities"
|
||||||
|
|
||||||
|
{
|
||||||
|
// report not found for the default X-Accept-Vulnerabilities
|
||||||
|
suite.onGetReport(v1.MimeTypeNativeReport)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.Empty(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// report found for the default X-Accept-Vulnerabilities
|
||||||
|
suite.onGetReport(v1.MimeTypeNativeReport, suite.report1)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.NotEmpty(body)
|
||||||
|
suite.Contains(body, v1.MimeTypeNativeReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// report found for the X-Accept-Vulnerabilities of "application/vnd.security.vulnerability.report; version=1.1"
|
||||||
|
suite.onGetReport(v1.MimeTypeGenericVulnerabilityReport, suite.report2)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body, map[string]string{"X-Accept-Vulnerabilities": v1.MimeTypeGenericVulnerabilityReport})
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.NotEmpty(body)
|
||||||
|
suite.Contains(body, v1.MimeTypeGenericVulnerabilityReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// report found for "application/vnd.security.vulnerability.report; version=1.1"
|
||||||
|
// and the X-Accept-Vulnerabilities is "application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
|
||||||
|
suite.onGetReport(v1.MimeTypeGenericVulnerabilityReport, suite.report2)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body, map[string]string{"X-Accept-Vulnerabilities": v1.MimeTypeGenericVulnerabilityReport + "," + v1.MimeTypeNativeReport})
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.NotEmpty(body)
|
||||||
|
suite.Contains(body, v1.MimeTypeGenericVulnerabilityReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// report not found for "application/vnd.security.vulnerability.report; version=1.1"
|
||||||
|
// report found for "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
|
||||||
|
// and the X-Accept-Vulnerabilities is "application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
|
||||||
|
suite.onGetReport(v1.MimeTypeGenericVulnerabilityReport)
|
||||||
|
suite.onGetReport(v1.MimeTypeNativeReport, suite.report1)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body, map[string]string{"X-Accept-Vulnerabilities": v1.MimeTypeGenericVulnerabilityReport + "," + v1.MimeTypeNativeReport})
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.NotEmpty(body)
|
||||||
|
suite.Contains(body, v1.MimeTypeNativeReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// report not found for "application/vnd.security.vulnerability.report; version=1.1"
|
||||||
|
// report not found for "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
|
||||||
|
// and the X-Accept-Vulnerabilities is "application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
|
||||||
|
suite.onGetReport(v1.MimeTypeGenericVulnerabilityReport)
|
||||||
|
suite.onGetReport(v1.MimeTypeNativeReport)
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
res, err := suite.GetJSON(url, &body, map[string]string{"X-Accept-Vulnerabilities": v1.MimeTypeGenericVulnerabilityReport + "," + v1.MimeTypeNativeReport})
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(200, res.StatusCode)
|
||||||
|
suite.Empty(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ArtifactTestSuite{})
|
||||||
|
}
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/controller/scan"
|
"github.com/goharbor/harbor/src/controller/scan"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"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"
|
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,12 +28,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewVulAssembler returns vul assembler
|
// NewVulAssembler returns vul assembler
|
||||||
func NewVulAssembler(withScanOverview bool) *VulAssembler {
|
func NewVulAssembler(withScanOverview bool, mimeTypes []string) *VulAssembler {
|
||||||
return &VulAssembler{
|
return &VulAssembler{
|
||||||
scanChecker: scan.NewChecker(),
|
scanChecker: scan.NewChecker(),
|
||||||
scanCtl: scan.DefaultController,
|
scanCtl: scan.DefaultController,
|
||||||
|
|
||||||
withScanOverview: withScanOverview,
|
withScanOverview: withScanOverview,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +45,7 @@ type VulAssembler struct {
|
|||||||
|
|
||||||
artifacts []*model.Artifact
|
artifacts []*model.Artifact
|
||||||
withScanOverview bool
|
withScanOverview bool
|
||||||
|
mimeTypes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithArtifacts set artifacts for the assembler
|
// WithArtifacts set artifacts for the assembler
|
||||||
@ -72,11 +73,14 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
|
|||||||
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
|
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
|
||||||
|
|
||||||
if assembler.withScanOverview {
|
if assembler.withScanOverview {
|
||||||
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport})
|
for _, mimeType := range assembler.mimeTypes {
|
||||||
if err != nil {
|
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{mimeType})
|
||||||
log.Warningf("get scan summary of artifact %s@%s failed, error:%v", artifact.RepositoryName, artifact.Digest, err)
|
if err != nil {
|
||||||
} else if len(overview) > 0 {
|
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, mimeType, err)
|
||||||
artifact.ScanOverview = overview
|
} else if len(overview) > 0 {
|
||||||
|
artifact.ScanOverview = overview
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ func (suite *VulAssemblerTestSuite) TestScannable() {
|
|||||||
scanChecker: checker,
|
scanChecker: checker,
|
||||||
scanCtl: scanCtl,
|
scanCtl: scanCtl,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
|
mimeTypes: []string{"mimeType"},
|
||||||
}
|
}
|
||||||
|
|
||||||
mock.OnAnything(checker, "IsScannable").Return(true, nil)
|
mock.OnAnything(checker, "IsScannable").Return(true, nil)
|
||||||
|
@ -36,6 +36,10 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseProjectCtl = project.Ctl
|
||||||
|
)
|
||||||
|
|
||||||
// BaseAPI base API handler
|
// BaseAPI base API handler
|
||||||
type BaseAPI struct{}
|
type BaseAPI struct{}
|
||||||
|
|
||||||
@ -68,7 +72,7 @@ func (b *BaseAPI) HasProjectPermission(ctx context.Context, projectIDOrName inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if projectName != "" {
|
if projectName != "" {
|
||||||
p, err := project.Ctl.GetByName(ctx, projectName)
|
p, err := baseProjectCtl.GetByName(ctx, projectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get project %s: %v", projectName, err)
|
log.Errorf("failed to get project %s: %v", projectName, err)
|
||||||
return false
|
return false
|
||||||
|
@ -15,9 +15,17 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
|
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
projectCtlMock *projecttesting.Controller
|
||||||
)
|
)
|
||||||
|
|
||||||
type baseHandlerTestSuite struct {
|
type baseHandlerTestSuite struct {
|
||||||
@ -108,3 +116,15 @@ func (b *baseHandlerTestSuite) TestLinks() {
|
|||||||
func TestBaseHandler(t *testing.T) {
|
func TestBaseHandler(t *testing.T) {
|
||||||
suite.Run(t, &baseHandlerTestSuite{})
|
suite.Run(t, &baseHandlerTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
projectCtlMock = &projecttesting.Controller{}
|
||||||
|
|
||||||
|
baseProjectCtl = projectCtlMock
|
||||||
|
|
||||||
|
exitVal := m.Run()
|
||||||
|
|
||||||
|
baseProjectCtl = project.Ctl
|
||||||
|
|
||||||
|
os.Exit(exitVal)
|
||||||
|
}
|
||||||
|
@ -15,19 +15,14 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/controller/artifact"
|
|
||||||
"github.com/goharbor/harbor/src/controller/artifact/processor"
|
|
||||||
"github.com/goharbor/harbor/src/controller/scan"
|
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,49 +41,24 @@ const (
|
|||||||
ScheduleNone = "None"
|
ScheduleNone = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
func boolValue(v *bool) bool {
|
func parseScanReportMimeTypes(header *string) []string {
|
||||||
if v != nil {
|
var mimeTypes []string
|
||||||
return *v
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
if header != nil {
|
||||||
}
|
for _, mimeType := range strings.Split(*header, ",") {
|
||||||
|
mimeType = strings.TrimSpace(mimeType)
|
||||||
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*processor.Addition, error) {
|
switch mimeType {
|
||||||
reports, err := scan.DefaultController.GetReport(ctx, artifact, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport})
|
case v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport:
|
||||||
if err != nil {
|
mimeTypes = append(mimeTypes, mimeType)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
vulnerabilities := make(map[string]interface{})
|
|
||||||
for _, rp := range reports {
|
|
||||||
// Resolve scan report data only when it is ready
|
|
||||||
if len(rp.Report) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
vrp, err := report.ResolveData(rp.MimeType, []byte(rp.Report), report.WithArtifactDigest(rp.Digest))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := vulnerabilities[rp.MimeType]; ok {
|
|
||||||
r, err := report.Merge(rp.MimeType, v, vrp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
vulnerabilities[rp.MimeType] = r
|
|
||||||
} else {
|
|
||||||
vulnerabilities[rp.MimeType] = vrp
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content, _ := json.Marshal(vulnerabilities)
|
if len(mimeTypes) == 0 {
|
||||||
|
mimeTypes = append(mimeTypes, v1.MimeTypeNativeReport)
|
||||||
|
}
|
||||||
|
|
||||||
return &processor.Addition{
|
return mimeTypes
|
||||||
Content: content,
|
|
||||||
ContentType: "application/json",
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unescapePathParams(params interface{}, fieldNames ...string) error {
|
func unescapePathParams(params interface{}, fieldNames ...string) error {
|
||||||
|
@ -63,34 +63,39 @@ func (suite *Suite) TearDownSuite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DoReq ...
|
// DoReq ...
|
||||||
func (suite *Suite) DoReq(method string, url string, body io.Reader, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) DoReq(method string, url string, body io.Reader, headers ...map[string]string) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(method, suite.ts.URL+"/api/v2.0"+url, body)
|
req, err := http.NewRequest(method, suite.ts.URL+"/api/v2.0"+url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
contentType := "application/json"
|
if len(headers) > 0 {
|
||||||
if len(contentTypes) > 0 {
|
for key, value := range headers[0] {
|
||||||
contentType = contentTypes[0]
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if contentType == "" {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", contentType)
|
|
||||||
|
|
||||||
return suite.tc.Do(req)
|
return suite.tc.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete ...
|
// Delete ...
|
||||||
func (suite *Suite) Delete(url string, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) Delete(url string, headers ...map[string]string) (*http.Response, error) {
|
||||||
return suite.DoReq(http.MethodDelete, url, nil, contentTypes...)
|
return suite.DoReq(http.MethodDelete, url, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get ...
|
// Get ...
|
||||||
func (suite *Suite) Get(url string, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) Get(url string, headers ...map[string]string) (*http.Response, error) {
|
||||||
return suite.DoReq(http.MethodGet, url, nil, contentTypes...)
|
return suite.DoReq(http.MethodGet, url, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJSON ...
|
// GetJSON ...
|
||||||
func (suite *Suite) GetJSON(url string, js interface{}) (*http.Response, error) {
|
func (suite *Suite) GetJSON(url string, js interface{}, headers ...map[string]string) (*http.Response, error) {
|
||||||
res, err := suite.Get(url)
|
res, err := suite.Get(url, headers...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -113,8 +118,8 @@ func (suite *Suite) GetJSON(url string, js interface{}) (*http.Response, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Patch ...
|
// Patch ...
|
||||||
func (suite *Suite) Patch(url string, body io.Reader, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) Patch(url string, body io.Reader, headers ...map[string]string) (*http.Response, error) {
|
||||||
return suite.DoReq(http.MethodPatch, url, body, contentTypes...)
|
return suite.DoReq(http.MethodPatch, url, body, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchJSON ...
|
// PatchJSON ...
|
||||||
@ -128,8 +133,8 @@ func (suite *Suite) PatchJSON(url string, js interface{}) (*http.Response, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post ...
|
// Post ...
|
||||||
func (suite *Suite) Post(url string, body io.Reader, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) Post(url string, body io.Reader, headers ...map[string]string) (*http.Response, error) {
|
||||||
return suite.DoReq(http.MethodPost, url, body, contentTypes...)
|
return suite.DoReq(http.MethodPost, url, body, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostJSON ...
|
// PostJSON ...
|
||||||
@ -143,8 +148,8 @@ func (suite *Suite) PostJSON(url string, js interface{}) (*http.Response, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Put ...
|
// Put ...
|
||||||
func (suite *Suite) Put(url string, body io.Reader, contentTypes ...string) (*http.Response, error) {
|
func (suite *Suite) Put(url string, body io.Reader, headers ...map[string]string) (*http.Response, error) {
|
||||||
return suite.DoReq(http.MethodPut, url, body, contentTypes...)
|
return suite.DoReq(http.MethodPut, url, body, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutJSON ...
|
// PutJSON ...
|
||||||
|
Loading…
Reference in New Issue
Block a user