mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-24 19:25:19 +01:00
Merge branch 'main' into main
This commit is contained in:
commit
ed0bbf0c8d
20
.github/workflows/CI.yml
vendored
20
.github/workflows/CI.yml
vendored
@ -41,10 +41,10 @@ jobs:
|
||||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@ -102,10 +102,10 @@ jobs:
|
||||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@ -157,10 +157,10 @@ jobs:
|
||||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@ -212,10 +212,10 @@ jobs:
|
||||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@ -265,10 +265,10 @@ jobs:
|
||||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
4
.github/workflows/build-package.yml
vendored
4
.github/workflows/build-package.yml
vendored
@ -23,10 +23,10 @@ jobs:
|
||||
with:
|
||||
version: '430.0.0'
|
||||
- run: gcloud info
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- name: Setup Docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
3
.github/workflows/codeql-analysis.yml
vendored
3
.github/workflows/codeql-analysis.yml
vendored
@ -47,5 +47,8 @@ jobs:
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
# to make sure autobuild success, specifify golang version in go.mod
|
||||
# https://github.com/github/codeql/issues/15647#issuecomment-2003768106
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
2
.github/workflows/conformance_test.yml
vendored
2
.github/workflows/conformance_test.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
- name: Set up Go 1.21
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.3
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -56,3 +56,5 @@ src/server/v2.0/models/
|
||||
src/server/v2.0/restapi/
|
||||
.editorconfig
|
||||
|
||||
harborclient/
|
||||
openapi-generator-cli.jar
|
||||
|
@ -164,7 +164,8 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
|
||||
| 2.7 | 1.19.4 |
|
||||
| 2.8 | 1.20.6 |
|
||||
| 2.9 | 1.21.3 |
|
||||
| 2.10 | 1.21.8 |
|
||||
| 2.10 | 1.21.8 |
|
||||
| 2.11 | 1.22.3 |
|
||||
|
||||
|
||||
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.
|
||||
|
8
Makefile
8
Makefile
@ -104,8 +104,8 @@ PREPARE_VERSION_NAME=versions
|
||||
|
||||
#versions
|
||||
REGISTRYVERSION=v2.8.3-patch-redis
|
||||
TRIVYVERSION=v0.49.1
|
||||
TRIVYADAPTERVERSION=v0.30.22
|
||||
TRIVYVERSION=v0.51.2
|
||||
TRIVYADAPTERVERSION=v0.31.2
|
||||
|
||||
# version of registry for pulling the source code
|
||||
REGISTRY_SRC_TAG=v2.8.3
|
||||
@ -140,7 +140,7 @@ GOINSTALL=$(GOCMD) install
|
||||
GOTEST=$(GOCMD) test
|
||||
GODEP=$(GOTEST) -i
|
||||
GOFMT=gofmt -w
|
||||
GOBUILDIMAGE=golang:1.21.8
|
||||
GOBUILDIMAGE=golang:1.22.3
|
||||
GOBUILDPATHINCONTAINER=/harbor
|
||||
|
||||
# go build
|
||||
@ -312,7 +312,7 @@ gen_apis: lint_apis
|
||||
|
||||
|
||||
MOCKERY_IMAGENAME=$(IMAGENAMESPACE)/mockery
|
||||
MOCKERY_VERSION=v2.35.4
|
||||
MOCKERY_VERSION=v2.42.2
|
||||
MOCKERY=$(RUNCONTAINER) ${MOCKERY_IMAGENAME}:${MOCKERY_VERSION}
|
||||
MOCKERY_IMAGE_BUILD_CMD=${DOCKERBUILD} -f ${TOOLSPATH}/mockery/Dockerfile --build-arg GOLANG=${GOBUILDIMAGE} --build-arg MOCKERY_VERSION=${MOCKERY_VERSION} -t ${MOCKERY_IMAGENAME}:$(MOCKERY_VERSION) .
|
||||
|
||||
|
@ -16,9 +16,9 @@ Patch releases are based on the major/minor release branch, the release cadence
|
||||
### Minor Release Support Matrix
|
||||
| Version | Supported |
|
||||
|----------------| ------------------ |
|
||||
| Harbor v2.11.x | :white_check_mark: |
|
||||
| Harbor v2.10.x | :white_check_mark: |
|
||||
| Harbor v2.9.x | :white_check_mark: |
|
||||
| Harbor v2.8.x | :white_check_mark: |
|
||||
|
||||
### Upgrade path and support policy
|
||||
The upgrade path for Harbor is (1) 2.2.x patch releases are always compatible with its major and minor version. For example, previous released 2.2.x can be upgraded to most recent 2.2.3 release. (2) Harbor only supports two previous minor releases to upgrade to current minor release. For example, 2.3.0 will only support 2.1.0 and 2.2.0 to upgrade from, 2.0.0 to 2.3.0 is not supported. One should upgrade to 2.2.0 first, then to 2.3.0.
|
||||
|
@ -996,7 +996,7 @@ paths:
|
||||
description: Specify whether the SBOM overview is included in returning artifacts, when this option is true, the SBOM overview will be included in the response
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
default: false
|
||||
- name: with_signature
|
||||
in: query
|
||||
description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true"
|
||||
@ -1176,11 +1176,11 @@ paths:
|
||||
- $ref: '#/parameters/projectName'
|
||||
- $ref: '#/parameters/repositoryName'
|
||||
- $ref: '#/parameters/reference'
|
||||
- name: scan_request_type
|
||||
- name: scanType
|
||||
in: body
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/definitions/ScanRequestType'
|
||||
schema:
|
||||
$ref: '#/definitions/ScanType'
|
||||
responses:
|
||||
'202':
|
||||
$ref: '#/responses/202'
|
||||
@ -1192,6 +1192,8 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'422':
|
||||
$ref: '#/responses/422'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/stop:
|
||||
@ -1206,6 +1208,12 @@ paths:
|
||||
- $ref: '#/parameters/projectName'
|
||||
- $ref: '#/parameters/repositoryName'
|
||||
- $ref: '#/parameters/reference'
|
||||
- name: scanType
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ScanType'
|
||||
description: 'The scan type: Vulnerabilities, SBOM'
|
||||
responses:
|
||||
'202':
|
||||
$ref: '#/responses/202'
|
||||
@ -1217,6 +1225,8 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'422':
|
||||
$ref: '#/responses/422'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/{report_id}/log:
|
||||
@ -1470,6 +1480,8 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'422':
|
||||
$ref: '#/responses/422'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/labels:
|
||||
@ -4817,6 +4829,8 @@ paths:
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'422':
|
||||
$ref: '#/responses/422'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/schedules:
|
||||
@ -6450,6 +6464,14 @@ responses:
|
||||
type: string
|
||||
schema:
|
||||
$ref: '#/definitions/Errors'
|
||||
'422':
|
||||
description: Unsupported Type
|
||||
headers:
|
||||
X-Request-Id:
|
||||
description: The ID of the corresponding request for the response
|
||||
type: string
|
||||
schema:
|
||||
$ref: '#/definitions/Errors'
|
||||
'500':
|
||||
description: Internal server error
|
||||
headers:
|
||||
@ -6760,13 +6782,6 @@ definitions:
|
||||
type: string
|
||||
description: Version of the scanner adapter
|
||||
example: "v0.9.1"
|
||||
ScanRequestType:
|
||||
type: object
|
||||
properties:
|
||||
scan_type:
|
||||
type: string
|
||||
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
|
||||
enum: [vulnerability, sbom]
|
||||
ScanOverview:
|
||||
type: object
|
||||
description: 'The scan overview attached in the metadata of tag'
|
||||
@ -6791,16 +6806,18 @@ definitions:
|
||||
description: 'The status of the generating SBOM task'
|
||||
sbom_digest:
|
||||
type: string
|
||||
description: 'The digest of the generated SBOM accessory'
|
||||
description: 'The digest of the generated SBOM accessory'
|
||||
report_id:
|
||||
type: string
|
||||
description: 'id of the native scan report'
|
||||
example: '5f62c830-f996-11e9-957f-0242c0a89008'
|
||||
duration:
|
||||
example: '5f62c830-f996-11e9-957f-0242c0a89008'
|
||||
duration:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 'Time in seconds required to create the report'
|
||||
example: 300
|
||||
scanner:
|
||||
$ref: '#/definitions/Scanner'
|
||||
NativeReportSummary:
|
||||
type: object
|
||||
description: 'The summary for the native report'
|
||||
@ -8431,7 +8448,7 @@ definitions:
|
||||
description: Indicates the capabilities of the scanner, e.g. support_vulnerability or support_sbom.
|
||||
additionalProperties: True
|
||||
example: {"support_vulnerability": true, "support_sbom": true}
|
||||
|
||||
|
||||
ScannerRegistrationReq:
|
||||
type: object
|
||||
required:
|
||||
@ -9980,3 +9997,10 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
description: Links of the vulnerability
|
||||
ScanType:
|
||||
type: object
|
||||
properties:
|
||||
scan_type:
|
||||
type: string
|
||||
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
|
||||
enum: [ vulnerability, sbom ]
|
BIN
icons/sbom.png
Normal file
BIN
icons/sbom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
@ -115,6 +115,11 @@ trivy:
|
||||
#
|
||||
# insecure The flag to skip verifying registry certificate
|
||||
insecure: false
|
||||
#
|
||||
# timeout The duration to wait for scan completion.
|
||||
# There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
|
||||
timeout: 5m0s
|
||||
#
|
||||
# github_token The GitHub access token to download Trivy DB
|
||||
#
|
||||
# Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
|
||||
@ -169,7 +174,7 @@ log:
|
||||
# port: 5140
|
||||
|
||||
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
|
||||
_version: 2.10.0
|
||||
_version: 2.11.0
|
||||
|
||||
# Uncomment external_database if using external database.
|
||||
# external_database:
|
||||
|
@ -28,4 +28,16 @@ then set column artifact_type as not null
|
||||
*/
|
||||
UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL;
|
||||
|
||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
|
||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sbom_report
|
||||
(
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
uuid VARCHAR(64) UNIQUE NOT NULL,
|
||||
artifact_id INT NOT NULL,
|
||||
registration_uuid VARCHAR(64) NOT NULL,
|
||||
mime_type VARCHAR(256) NOT NULL,
|
||||
media_type VARCHAR(256) NOT NULL,
|
||||
report JSON,
|
||||
UNIQUE(artifact_id, registration_uuid, mime_type, media_type)
|
||||
);
|
@ -10,7 +10,7 @@ from migrations import accept_versions
|
||||
@click.command()
|
||||
@click.option('-i', '--input', 'input_', required=True, help="The path of original config file")
|
||||
@click.option('-o', '--output', default='', help="the path of output config file")
|
||||
@click.option('-t', '--target', default='2.10.0', help="target version of input path")
|
||||
@click.option('-t', '--target', default='2.11.0', help="target version of input path")
|
||||
def migrate(input_, output, target):
|
||||
"""
|
||||
migrate command will migrate config file style to specific version
|
||||
|
@ -2,4 +2,4 @@ import os
|
||||
|
||||
MIGRATION_BASE_DIR = os.path.dirname(__file__)
|
||||
|
||||
accept_versions = {'1.9.0', '1.10.0', '2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0', '2.8.0', '2.9.0','2.10.0'}
|
||||
accept_versions = {'1.9.0', '1.10.0', '2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0', '2.8.0', '2.9.0','2.10.0', '2.11.0'}
|
21
make/photon/prepare/migrations/version_2_11_0/__init__.py
Normal file
21
make/photon/prepare/migrations/version_2_11_0/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
||||
from utils.migration import read_conf
|
||||
|
||||
revision = '2.11.0'
|
||||
down_revisions = ['2.10.0']
|
||||
|
||||
def migrate(input_cfg, output_cfg):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
tpl = Environment(
|
||||
loader=FileSystemLoader(current_dir),
|
||||
undefined=StrictUndefined,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
autoescape = select_autoescape()
|
||||
).get_template('harbor.yml.jinja')
|
||||
|
||||
config_dict = read_conf(input_cfg)
|
||||
|
||||
with open(output_cfg, 'w') as f:
|
||||
f.write(tpl.render(**config_dict))
|
737
make/photon/prepare/migrations/version_2_11_0/harbor.yml.jinja
Normal file
737
make/photon/prepare/migrations/version_2_11_0/harbor.yml.jinja
Normal file
@ -0,0 +1,737 @@
|
||||
# Configuration file of Harbor
|
||||
|
||||
# The IP address or hostname to access admin UI and registry service.
|
||||
# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
|
||||
hostname: {{ hostname }}
|
||||
|
||||
# http related config
|
||||
{% if http is defined %}
|
||||
http:
|
||||
# port for http, default is 80. If https enabled, this port will redirect to https port
|
||||
port: {{ http.port }}
|
||||
{% else %}
|
||||
# http:
|
||||
# # port for http, default is 80. If https enabled, this port will redirect to https port
|
||||
# port: 80
|
||||
{% endif %}
|
||||
|
||||
{% if https is defined %}
|
||||
# https related config
|
||||
https:
|
||||
# https port for harbor, default is 443
|
||||
port: {{ https.port }}
|
||||
# The path of cert and key files for nginx
|
||||
certificate: {{ https.certificate }}
|
||||
private_key: {{ https.private_key }}
|
||||
# enable strong ssl ciphers (default: false)
|
||||
{% if strong_ssl_ciphers is defined %}
|
||||
strong_ssl_ciphers: {{ strong_ssl_ciphers | lower }}
|
||||
{% else %}
|
||||
strong_ssl_ciphers: false
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# https related config
|
||||
# https:
|
||||
# # https port for harbor, default is 443
|
||||
# port: 443
|
||||
# # The path of cert and key files for nginx
|
||||
# certificate: /your/certificate/path
|
||||
# private_key: /your/private/key/path
|
||||
# enable strong ssl ciphers (default: false)
|
||||
# strong_ssl_ciphers: false
|
||||
{% endif %}
|
||||
|
||||
# # Harbor will set ipv4 enabled only by default if this block is not configured
|
||||
# # Otherwise, please uncomment this block to configure your own ip_family stacks
|
||||
{% if ip_family is defined %}
|
||||
ip_family:
|
||||
# ipv6Enabled set to true if ipv6 is enabled in docker network, currently it affected the nginx related component
|
||||
{% if ip_family.ipv6 is defined %}
|
||||
ipv6:
|
||||
enabled: {{ ip_family.ipv6.enabled | lower }}
|
||||
{% else %}
|
||||
ipv6:
|
||||
enabled: false
|
||||
{% endif %}
|
||||
# ipv4Enabled set to true by default, currently it affected the nginx related component
|
||||
{% if ip_family.ipv4 is defined %}
|
||||
ipv4:
|
||||
enabled: {{ ip_family.ipv4.enabled | lower }}
|
||||
{% else %}
|
||||
ipv4:
|
||||
enabled: true
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# ip_family:
|
||||
# # ipv6Enabled set to true if ipv6 is enabled in docker network, currently it affected the nginx related component
|
||||
# ipv6:
|
||||
# enabled: false
|
||||
# # ipv4Enabled set to true by default, currently it affected the nginx related component
|
||||
# ipv4:
|
||||
# enabled: true
|
||||
{% endif %}
|
||||
|
||||
{% if internal_tls is defined %}
|
||||
# Uncomment following will enable tls communication between all harbor components
|
||||
internal_tls:
|
||||
# set enabled to true means internal tls is enabled
|
||||
enabled: {{ internal_tls.enabled | lower }}
|
||||
{% if internal_tls.dir is defined %}
|
||||
# put your cert and key files on dir
|
||||
dir: {{ internal_tls.dir }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# internal_tls:
|
||||
# # set enabled to true means internal tls is enabled
|
||||
# enabled: true
|
||||
# # put your cert and key files on dir
|
||||
# dir: /etc/harbor/tls/internal
|
||||
{% endif %}
|
||||
|
||||
# Uncomment external_url if you want to enable external proxy
|
||||
# And when it enabled the hostname will no longer used
|
||||
{% if external_url is defined %}
|
||||
external_url: {{ external_url }}
|
||||
{% else %}
|
||||
# external_url: https://reg.mydomain.com:8433
|
||||
{% endif %}
|
||||
|
||||
# The initial password of Harbor admin
|
||||
# It only works in first time to install harbor
|
||||
# Remember Change the admin password from UI after launching Harbor.
|
||||
{% if harbor_admin_password is defined %}
|
||||
harbor_admin_password: {{ harbor_admin_password }}
|
||||
{% else %}
|
||||
harbor_admin_password: Harbor12345
|
||||
{% endif %}
|
||||
|
||||
# Harbor DB configuration
|
||||
database:
|
||||
{% if database is defined %}
|
||||
# The password for the root user of Harbor DB. Change this before any production use.
|
||||
password: {{ database.password}}
|
||||
# The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained.
|
||||
max_idle_conns: {{ database.max_idle_conns }}
|
||||
# The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections.
|
||||
# Note: the default number of connections is 1024 for postgres of harbor.
|
||||
max_open_conns: {{ database.max_open_conns }}
|
||||
# The maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's age.
|
||||
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
{% if database.conn_max_lifetime is defined %}
|
||||
conn_max_lifetime: {{ database.conn_max_lifetime }}
|
||||
{% else %}
|
||||
conn_max_lifetime: 5m
|
||||
{% endif %}
|
||||
# The maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's idle time.
|
||||
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
{% if database.conn_max_idle_time is defined %}
|
||||
conn_max_idle_time: {{ database.conn_max_idle_time }}
|
||||
{% else %}
|
||||
conn_max_idle_time: 0
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# The password for the root user of Harbor DB. Change this before any production use.
|
||||
password: root123
|
||||
# The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained.
|
||||
max_idle_conns: 100
|
||||
# The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections.
|
||||
# Note: the default number of connections is 1024 for postgres of harbor.
|
||||
max_open_conns: 900
|
||||
# The maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's age.
|
||||
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
conn_max_lifetime: 5m
|
||||
# The maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's idle time.
|
||||
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
conn_max_idle_time: 0
|
||||
{% endif %}
|
||||
|
||||
{% if data_volume is defined %}
|
||||
# The default data volume
|
||||
data_volume: {{ data_volume }}
|
||||
{% else %}
|
||||
# The default data volume
|
||||
data_volume: /data
|
||||
{% endif %}
|
||||
|
||||
# Harbor Storage settings by default is using /data dir on local filesystem
|
||||
# Uncomment storage_service setting If you want to using external storage
|
||||
{% if storage_service is defined %}
|
||||
storage_service:
|
||||
{% for key, value in storage_service.items() %}
|
||||
{% if key == 'ca_bundle' %}
|
||||
# # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
|
||||
# # of registry's and chart repository's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
|
||||
ca_bundle: {{ value if value is not none else '' }}
|
||||
{% elif key == 'redirect' %}
|
||||
# # set disable to true when you want to disable registry redirect
|
||||
redirect:
|
||||
{% if storage_service.redirect.disabled is defined %}
|
||||
disable: {{ storage_service.redirect.disabled | lower}}
|
||||
{% else %}
|
||||
disable: {{ storage_service.redirect.disable | lower}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
|
||||
# # for more info about this configuration please refer https://distribution.github.io/distribution/about/configuration/
|
||||
# # and https://distribution.github.io/distribution/storage-drivers/
|
||||
{{ key }}:
|
||||
{% for k, v in value.items() %}
|
||||
{{ k }}: {{ v if v is not none else '' }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
# storage_service:
|
||||
# # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
|
||||
# # of registry's and chart repository's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
|
||||
# ca_bundle:
|
||||
|
||||
# # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
|
||||
# # for more info about this configuration please refer https://distribution.github.io/distribution/about/configuration/
|
||||
# # and https://distribution.github.io/distribution/storage-drivers/
|
||||
# filesystem:
|
||||
# maxthreads: 100
|
||||
# # set disable to true when you want to disable registry redirect
|
||||
# redirect:
|
||||
# disable: false
|
||||
{% endif %}
|
||||
|
||||
# Trivy configuration
|
||||
#
|
||||
# Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases.
|
||||
# It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached
|
||||
# in the local file system. In addition, the database contains the update timestamp so Trivy can detect whether it
|
||||
# should download a newer version from the Internet or use the cached one. Currently, the database is updated every
|
||||
# 12 hours and published as a new release to GitHub.
|
||||
{% if trivy is defined %}
|
||||
trivy:
|
||||
# ignoreUnfixed The flag to display only fixed vulnerabilities
|
||||
{% if trivy.ignore_unfixed is defined %}
|
||||
ignore_unfixed: {{ trivy.ignore_unfixed | lower }}
|
||||
{% else %}
|
||||
ignore_unfixed: false
|
||||
{% endif %}
|
||||
# skipUpdate The flag to enable or disable Trivy DB downloads from GitHub
|
||||
#
|
||||
# You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues.
|
||||
# If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and
|
||||
# `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path.
|
||||
{% if trivy.skip_update is defined %}
|
||||
skip_update: {{ trivy.skip_update | lower }}
|
||||
{% else %}
|
||||
skip_update: false
|
||||
{% endif %}
|
||||
{% if trivy.skip_java_db_update is defined %}
|
||||
# skipJavaDBUpdate If the flag is enabled you have to manually download the `trivy-java.db` file and mount it in the
|
||||
# `/home/scanner/.cache/trivy/java-db/trivy-java.db` path
|
||||
skip_java_db_update: {{ trivy.skip_java_db_update | lower }}
|
||||
{% else %}
|
||||
skip_java_db_update: false
|
||||
{% endif %}
|
||||
#
|
||||
{% if trivy.offline_scan is defined %}
|
||||
offline_scan: {{ trivy.offline_scan | lower }}
|
||||
{% else %}
|
||||
offline_scan: false
|
||||
{% endif %}
|
||||
#
|
||||
# Comma-separated list of what security issues to detect. Possible values are `vuln`, `config` and `secret`. Defaults to `vuln`.
|
||||
{% if trivy.security_check is defined %}
|
||||
security_check: {{ trivy.security_check }}
|
||||
{% else %}
|
||||
security_check: vuln
|
||||
{% endif %}
|
||||
#
|
||||
# insecure The flag to skip verifying registry certificate
|
||||
{% if trivy.insecure is defined %}
|
||||
insecure: {{ trivy.insecure | lower }}
|
||||
{% else %}
|
||||
insecure: false
|
||||
{% endif %}
|
||||
#
|
||||
{% if trivy.timeout is defined %}
|
||||
# timeout The duration to wait for scan completion.
|
||||
# There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
|
||||
timeout: {{ trivy.timeout}}
|
||||
{% else %}
|
||||
timeout: 5m0s
|
||||
{% endif %}
|
||||
#
|
||||
# github_token The GitHub access token to download Trivy DB
|
||||
#
|
||||
# Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
|
||||
# for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
|
||||
# requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
|
||||
# https://developer.github.com/v3/#rate-limiting
|
||||
#
|
||||
# You can create a GitHub token by following the instructions in
|
||||
# https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
|
||||
#
|
||||
{% if trivy.github_token is defined %}
|
||||
github_token: {{ trivy.github_token }}
|
||||
{% else %}
|
||||
# github_token: xxx
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# trivy:
|
||||
# # ignoreUnfixed The flag to display only fixed vulnerabilities
|
||||
# ignore_unfixed: false
|
||||
# # skipUpdate The flag to enable or disable Trivy DB downloads from GitHub
|
||||
# #
|
||||
# # You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues.
|
||||
# # If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and
|
||||
# # `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path.
|
||||
# skip_update: false
|
||||
# #
|
||||
# # skipJavaDBUpdate If the flag is enabled you have to manually download the `trivy-java.db` file and mount it in the
|
||||
# # `/home/scanner/.cache/trivy/java-db/trivy-java.db` path
|
||||
# skip_java_db_update: false
|
||||
# #
|
||||
# #The offline_scan option prevents Trivy from sending API requests to identify dependencies.
|
||||
# # Scanning JAR files and pom.xml may require Internet access for better detection, but this option tries to avoid it.
|
||||
# # For example, the offline mode will not try to resolve transitive dependencies in pom.xml when the dependency doesn't
|
||||
# # exist in the local repositories. It means a number of detected vulnerabilities might be fewer in offline mode.
|
||||
# # It would work if all the dependencies are in local.
|
||||
# # This option doesn’t affect DB download. You need to specify "skip-update" as well as "offline-scan" in an air-gapped environment.
|
||||
# offline_scan: false
|
||||
# #
|
||||
# # insecure The flag to skip verifying registry certificate
|
||||
# insecure: false
|
||||
# # github_token The GitHub access token to download Trivy DB
|
||||
# #
|
||||
# # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
|
||||
# # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
|
||||
# # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
|
||||
# # https://developer.github.com/v3/#rate-limiting
|
||||
# #
|
||||
# # timeout The duration to wait for scan completion.
|
||||
# # There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
|
||||
# timeout: 5m0s
|
||||
# #
|
||||
# # You can create a GitHub token by following the instructions in
|
||||
# # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
|
||||
# #
|
||||
# # github_token: xxx
|
||||
{% endif %}
|
||||
|
||||
jobservice:
|
||||
# Maximum number of job workers in job service
|
||||
{% if jobservice is defined %}
|
||||
max_job_workers: {{ jobservice.max_job_workers }}
|
||||
# The jobLoggers backend name, only support "STD_OUTPUT", "FILE" and/or "DB"
|
||||
{% if jobservice.job_loggers is defined %}
|
||||
job_loggers:
|
||||
{% for job_logger in jobservice.job_loggers %}
|
||||
- {{job_logger}}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
job_loggers:
|
||||
- STD_OUTPUT
|
||||
- FILE
|
||||
# - DB
|
||||
{% endif %}
|
||||
# The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`)
|
||||
{% if jobservice.logger_sweeper_duration is defined %}
|
||||
logger_sweeper_duration: {{ jobservice.logger_sweeper_duration }}
|
||||
{% else %}
|
||||
logger_sweeper_duration: 1
|
||||
{% endif %}
|
||||
{% else %}
|
||||
max_job_workers: 10
|
||||
# The jobLoggers backend name, only support "STD_OUTPUT", "FILE" and/or "DB"
|
||||
job_loggers:
|
||||
- STD_OUTPUT
|
||||
- FILE
|
||||
# - DB
|
||||
# The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`)
|
||||
logger_sweeper_duration: 1
|
||||
{% endif %}
|
||||
|
||||
notification:
|
||||
# Maximum retry count for webhook job
|
||||
{% if notification is defined %}
|
||||
webhook_job_max_retry: {{ notification.webhook_job_max_retry}}
|
||||
# HTTP client timeout for webhook job
|
||||
{% if notification.webhook_job_http_client_timeout is defined %}
|
||||
webhook_job_http_client_timeout: {{ notification.webhook_job_http_client_timeout }}
|
||||
{% else %}
|
||||
webhook_job_http_client_timeout: 3 #seconds
|
||||
{% endif %}
|
||||
{% else %}
|
||||
webhook_job_max_retry: 3
|
||||
# HTTP client timeout for webhook job
|
||||
webhook_job_http_client_timeout: 3 #seconds
|
||||
{% endif %}
|
||||
|
||||
# Log configurations
|
||||
log:
|
||||
# options are debug, info, warning, error, fatal
|
||||
{% if log is defined %}
|
||||
level: {{ log.level }}
|
||||
# configs for logs in local storage
|
||||
local:
|
||||
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
rotate_count: {{ log.local.rotate_count }}
|
||||
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
|
||||
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
|
||||
# are all valid.
|
||||
rotate_size: {{ log.local.rotate_size }}
|
||||
# The directory on your host that store log
|
||||
location: {{ log.local.location }}
|
||||
{% if log.external_endpoint is defined %}
|
||||
external_endpoint:
|
||||
# protocol used to transmit log to external endpoint, options is tcp or udp
|
||||
protocol: {{ log.external_endpoint.protocol }}
|
||||
# The host of external endpoint
|
||||
host: {{ log.external_endpoint.host }}
|
||||
# Port of external endpoint
|
||||
port: {{ log.external_endpoint.port }}
|
||||
{% else %}
|
||||
# Uncomment following lines to enable external syslog endpoint.
|
||||
# external_endpoint:
|
||||
# # protocol used to transmit log to external endpoint, options is tcp or udp
|
||||
# protocol: tcp
|
||||
# # The host of external endpoint
|
||||
# host: localhost
|
||||
# # Port of external endpoint
|
||||
# port: 5140
|
||||
{% endif %}
|
||||
{% else %}
|
||||
level: info
|
||||
# configs for logs in local storage
|
||||
local:
|
||||
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
rotate_count: 50
|
||||
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
|
||||
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
|
||||
# are all valid.
|
||||
rotate_size: 200M
|
||||
# The directory on your host that store log
|
||||
location: /var/log/harbor
|
||||
|
||||
# Uncomment following lines to enable external syslog endpoint.
|
||||
# external_endpoint:
|
||||
# # protocol used to transmit log to external endpoint, options is tcp or udp
|
||||
# protocol: tcp
|
||||
# # The host of external endpoint
|
||||
# host: localhost
|
||||
# # Port of external endpoint
|
||||
# port: 5140
|
||||
{% endif %}
|
||||
|
||||
|
||||
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
|
||||
_version: 2.11.0
|
||||
{% if external_database is defined %}
|
||||
# Uncomment external_database if using external database.
|
||||
external_database:
|
||||
harbor:
|
||||
host: {{ external_database.harbor.host }}
|
||||
port: {{ external_database.harbor.port }}
|
||||
db_name: {{ external_database.harbor.db_name }}
|
||||
username: {{ external_database.harbor.username }}
|
||||
password: {{ external_database.harbor.password }}
|
||||
ssl_mode: {{ external_database.harbor.ssl_mode }}
|
||||
max_idle_conns: {{ external_database.harbor.max_idle_conns}}
|
||||
max_open_conns: {{ external_database.harbor.max_open_conns}}
|
||||
{% else %}
|
||||
# Uncomment external_database if using external database.
|
||||
# external_database:
|
||||
# harbor:
|
||||
# host: harbor_db_host
|
||||
# port: harbor_db_port
|
||||
# db_name: harbor_db_name
|
||||
# username: harbor_db_username
|
||||
# password: harbor_db_password
|
||||
# ssl_mode: disable
|
||||
# max_idle_conns: 2
|
||||
# max_open_conns: 0
|
||||
{% endif %}
|
||||
|
||||
{% if redis is defined %}
|
||||
redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
{% if redis.registry_db_index is defined %}
|
||||
registry_db_index: {{ redis.registry_db_index }}
|
||||
{% else %}
|
||||
# # registry_db_index: 1
|
||||
{% endif %}
|
||||
{% if redis.jobservice_db_index is defined %}
|
||||
jobservice_db_index: {{ redis.jobservice_db_index }}
|
||||
{% else %}
|
||||
# # jobservice_db_index: 2
|
||||
{% endif %}
|
||||
{% if redis.trivy_db_index is defined %}
|
||||
trivy_db_index: {{ redis.trivy_db_index }}
|
||||
{% else %}
|
||||
# # trivy_db_index: 5
|
||||
{% endif %}
|
||||
{% if redis.harbor_db_index is defined %}
|
||||
harbor_db_index: {{ redis.harbor_db_index }}
|
||||
{% else %}
|
||||
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
|
||||
# # harbor_db_index: 6
|
||||
{% endif %}
|
||||
{% if redis.cache_layer_db_index is defined %}
|
||||
cache_layer_db_index: {{ redis.cache_layer_db_index }}
|
||||
{% else %}
|
||||
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
|
||||
# # cache_layer_db_index: 7
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# Uncomment redis if need to customize redis db
|
||||
# redis:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# # registry_db_index: 1
|
||||
# # jobservice_db_index: 2
|
||||
# # trivy_db_index: 5
|
||||
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
|
||||
# # harbor_db_index: 6
|
||||
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
|
||||
# # cache_layer_db_index: 7
|
||||
{% endif %}
|
||||
|
||||
{% if external_redis is defined %}
|
||||
external_redis:
|
||||
# support redis, redis+sentinel
|
||||
# host for redis: <host_redis>:<port_redis>
|
||||
# host for redis+sentinel:
|
||||
# <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
|
||||
host: {{ external_redis.host }}
|
||||
password: {{ external_redis.password }}
|
||||
# Redis AUTH command was extended in Redis 6, it is possible to use it in the two-arguments AUTH <username> <password> form.
|
||||
{% if external_redis.username is defined %}
|
||||
username: {{ external_redis.username }}
|
||||
{% else %}
|
||||
# username:
|
||||
{% endif %}
|
||||
# sentinel_master_set must be set to support redis+sentinel
|
||||
#sentinel_master_set:
|
||||
# db_index 0 is for core, it's unchangeable
|
||||
registry_db_index: {{ external_redis.registry_db_index }}
|
||||
jobservice_db_index: {{ external_redis.jobservice_db_index }}
|
||||
trivy_db_index: 5
|
||||
idle_timeout_seconds: 30
|
||||
{% if external_redis.harbor_db_index is defined %}
|
||||
harbor_db_index: {{ redis.harbor_db_index }}
|
||||
{% else %}
|
||||
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
|
||||
# # harbor_db_index: 6
|
||||
{% endif %}
|
||||
{% if external_redis.cache_layer_db_index is defined %}
|
||||
cache_layer_db_index: {{ redis.cache_layer_db_index }}
|
||||
{% else %}
|
||||
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
|
||||
# # cache_layer_db_index: 7
|
||||
{% endif %}
|
||||
{% else %}
|
||||
# Uncomments external_redis if using external Redis server
|
||||
# external_redis:
|
||||
# # support redis, redis+sentinel
|
||||
# # host for redis: <host_redis>:<port_redis>
|
||||
# # host for redis+sentinel:
|
||||
# # <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
|
||||
# host: redis:6379
|
||||
# password:
|
||||
# # Redis AUTH command was extended in Redis 6, it is possible to use it in the two-arguments AUTH <username> <password> form.
|
||||
# # username:
|
||||
# # sentinel_master_set must be set to support redis+sentinel
|
||||
# #sentinel_master_set:
|
||||
# # db_index 0 is for core, it's unchangeable
|
||||
# registry_db_index: 1
|
||||
# jobservice_db_index: 2
|
||||
# trivy_db_index: 5
|
||||
# idle_timeout_seconds: 30
|
||||
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
|
||||
# # harbor_db_index: 6
|
||||
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
|
||||
# # cache_layer_db_index: 7
|
||||
{% endif %}
|
||||
|
||||
{% if uaa is defined %}
|
||||
# Uncomment uaa for trusting the certificate of uaa instance that is hosted via self-signed cert.
|
||||
uaa:
|
||||
ca_file: {{ uaa.ca_file }}
|
||||
{% else %}
|
||||
# Uncomment uaa for trusting the certificate of uaa instance that is hosted via self-signed cert.
|
||||
# uaa:
|
||||
# ca_file: /path/to/ca
|
||||
{% endif %}
|
||||
|
||||
|
||||
# Global proxy
|
||||
# Config http proxy for components, e.g. http://my.proxy.com:3128
|
||||
# Components doesn't need to connect to each others via http proxy.
|
||||
# Remove component from `components` array if want disable proxy
|
||||
# for it. If you want use proxy for replication, MUST enable proxy
|
||||
# for core and jobservice, and set `http_proxy` and `https_proxy`.
|
||||
# Add domain to the `no_proxy` field, when you want disable proxy
|
||||
# for some special registry.
|
||||
{% if proxy is defined %}
|
||||
proxy:
|
||||
http_proxy: {{ proxy.http_proxy or ''}}
|
||||
https_proxy: {{ proxy.https_proxy or ''}}
|
||||
no_proxy: {{ proxy.no_proxy or ''}}
|
||||
{% if proxy.components is defined %}
|
||||
components:
|
||||
{% for component in proxy.components %}
|
||||
{% if component != 'clair' %}
|
||||
- {{component}}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
proxy:
|
||||
http_proxy:
|
||||
https_proxy:
|
||||
no_proxy:
|
||||
components:
|
||||
- core
|
||||
- jobservice
|
||||
- trivy
|
||||
{% endif %}
|
||||
|
||||
{% if metric is defined %}
|
||||
metric:
|
||||
enabled: {{ metric.enabled }}
|
||||
port: {{ metric.port }}
|
||||
path: {{ metric.path }}
|
||||
{% else %}
|
||||
# metric:
|
||||
# enabled: false
|
||||
# port: 9090
|
||||
# path: /metrics
|
||||
{% endif %}
|
||||
|
||||
# Trace related config
|
||||
# only can enable one trace provider(jaeger or otel) at the same time,
|
||||
# and when using jaeger as provider, can only enable it with agent mode or collector mode.
|
||||
# if using jaeger collector mode, uncomment endpoint and uncomment username, password if needed
|
||||
# if using jaeger agetn mode uncomment agent_host and agent_port
|
||||
{% if trace is defined %}
|
||||
trace:
|
||||
enabled: {{ trace.enabled | lower}}
|
||||
sample_rate: {{ trace.sample_rate }}
|
||||
# # namespace used to differentiate different harbor services
|
||||
{% if trace.namespace is defined %}
|
||||
namespace: {{ trace.namespace }}
|
||||
{% else %}
|
||||
# namespace:
|
||||
{% endif %}
|
||||
# # attributes is a key value dict contains user defined attributes used to initialize trace provider
|
||||
{% if trace.attributes is defined%}
|
||||
attributes:
|
||||
{% for name, value in trace.attributes.items() %}
|
||||
{{name}}: {{value}}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
# attributes:
|
||||
# application: harbor
|
||||
{% endif %}
|
||||
{% if trace.jaeger is defined%}
|
||||
jaeger:
|
||||
endpoint: {{trace.jaeger.endpoint or '' }}
|
||||
username: {{trace.jaeger.username or ''}}
|
||||
password: {{trace.jaeger.password or ''}}
|
||||
agent_host: {{trace.jaeger.agent_host or ''}}
|
||||
agent_port: {{trace.jaeger.agent_port or ''}}
|
||||
{% else %}
|
||||
# jaeger:
|
||||
# endpoint:
|
||||
# username:
|
||||
# password:
|
||||
# agent_host:
|
||||
# agent_port:
|
||||
{% endif %}
|
||||
{% if trace. otel is defined %}
|
||||
otel:
|
||||
endpoint: {{trace.otel.endpoint or '' }}
|
||||
url_path: {{trace.otel.url_path or '' }}
|
||||
compression: {{trace.otel.compression | lower }}
|
||||
insecure: {{trace.otel.insecure | lower }}
|
||||
timeout: {{trace.otel.timeout or '' }}
|
||||
{% else %}
|
||||
# otel:
|
||||
# endpoint: hostname:4318
|
||||
# url_path: /v1/traces
|
||||
# compression: false
|
||||
# insecure: true
|
||||
# # timeout is in seconds
|
||||
# timeout: 10
|
||||
{% endif%}
|
||||
{% else %}
|
||||
# trace:
|
||||
# enabled: true
|
||||
# # set sample_rate to 1 if you wanna sampling 100% of trace data; set 0.5 if you wanna sampling 50% of trace data, and so forth
|
||||
# sample_rate: 1
|
||||
# # # namespace used to differentiate different harbor services
|
||||
# # namespace:
|
||||
# # # attributes is a key value dict contains user defined attributes used to initialize trace provider
|
||||
# # attributes:
|
||||
# # application: harbor
|
||||
# # jaeger:
|
||||
# # endpoint: http://hostname:14268/api/traces
|
||||
# # username:
|
||||
# # password:
|
||||
# # agent_host: hostname
|
||||
# # agent_port: 6831
|
||||
# # otel:
|
||||
# # endpoint: hostname:4318
|
||||
# # url_path: /v1/traces
|
||||
# # compression: false
|
||||
# # insecure: true
|
||||
# # # timeout is in seconds
|
||||
# # timeout: 10
|
||||
{% endif %}
|
||||
|
||||
# enable purge _upload directories
|
||||
{% if upload_purging is defined %}
|
||||
upload_purging:
|
||||
enabled: {{ upload_purging.enabled | lower}}
|
||||
age: {{ upload_purging.age }}
|
||||
interval: {{ upload_purging.interval }}
|
||||
dryrun: {{ upload_purging.dryrun | lower}}
|
||||
{% else %}
|
||||
upload_purging:
|
||||
enabled: true
|
||||
# remove files in _upload directories which exist for a period of time, default is one week.
|
||||
age: 168h
|
||||
# the interval of the purge operations
|
||||
interval: 24h
|
||||
dryrun: false
|
||||
{% endif %}
|
||||
|
||||
# Cache layer related config
|
||||
{% if cache is defined %}
|
||||
cache:
|
||||
enabled: {{ cache.enabled | lower}}
|
||||
expire_hours: {{ cache.expire_hours }}
|
||||
{% else %}
|
||||
cache:
|
||||
enabled: false
|
||||
expire_hours: 24
|
||||
{% endif %}
|
||||
|
||||
# Harbor core configurations
|
||||
# Uncomment to enable the following harbor core related configuration items.
|
||||
{% if core is defined %}
|
||||
core:
|
||||
# The provider for updating project quota(usage), there are 2 options, redis or db,
|
||||
# by default is implemented by db but you can switch the updation via redis which
|
||||
# can improve the performance of high concurrent pushing to the same project,
|
||||
# and reduce the database connections spike and occupies.
|
||||
# By redis will bring up some delay for quota usage updation for display, so only
|
||||
# suggest switch provider to redis if you were ran into the db connections spike aroud
|
||||
# the scenario of high concurrent pushing to same project, no improvment for other scenes.
|
||||
quota_update_provider: {{ core.quota_update_provider }}
|
||||
{% else %}
|
||||
# core:
|
||||
# # The provider for updating project quota(usage), there are 2 options, redis or db,
|
||||
# # by default is implemented by db but you can switch the updation via redis which
|
||||
# # can improve the performance of high concurrent pushing to the same project,
|
||||
# # and reduce the database connections spike and occupies.
|
||||
# # By redis will bring up some delay for quota usage updation for display, so only
|
||||
# # suggest switch provider to redis if you were ran into the db connections spike around
|
||||
# # the scenario of high concurrent pushing to same project, no improvement for other scenes.
|
||||
# quota_update_provider: redis # Or db
|
||||
{% endif %}
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.21.8
|
||||
FROM golang:1.22.3
|
||||
|
||||
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
|
||||
ENV BUILDTAGS include_oss include_gcs
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.21.8
|
||||
FROM golang:1.22.3
|
||||
|
||||
ADD . /go/src/github.com/aquasecurity/harbor-scanner-trivy/
|
||||
WORKDIR /go/src/github.com/aquasecurity/harbor-scanner-trivy/
|
||||
|
@ -19,7 +19,7 @@ TEMP=$(mktemp -d ${TMPDIR-/tmp}/trivy-adapter.XXXXXX)
|
||||
git clone https://github.com/aquasecurity/harbor-scanner-trivy.git $TEMP
|
||||
cd $TEMP; git checkout $VERSION; cd -
|
||||
|
||||
echo "Building Trivy adapter binary based on golang:1.21.8..."
|
||||
echo "Building Trivy adapter binary based on golang:1.22.3..."
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t trivy-adapter-golang $TEMP
|
||||
|
||||
|
@ -48,20 +48,23 @@ func main() {
|
||||
log.Fatalf("Failed to connect to Database, error: %v\n", err)
|
||||
}
|
||||
defer db.Close()
|
||||
c := make(chan struct{}, 1)
|
||||
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
defer close(c)
|
||||
|
||||
err := db.Ping()
|
||||
for ; err != nil; err = db.Ping() {
|
||||
log.Println("Failed to Ping DB, sleep for 1 second.")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
c <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
case <-time.After(30 * time.Second):
|
||||
log.Fatal("Failed to connect DB after 30 seconds, time out. \n")
|
||||
}
|
||||
|
||||
row := db.QueryRow(pgSQLCheckColStmt)
|
||||
var tblCount, colCount int
|
||||
if err := row.Scan(&tblCount, &colCount); err != nil {
|
||||
|
@ -51,6 +51,7 @@ const (
|
||||
ResourceRobot = Resource("robot")
|
||||
ResourceNotificationPolicy = Resource("notification-policy")
|
||||
ResourceScan = Resource("scan")
|
||||
ResourceSBOM = Resource("sbom")
|
||||
ResourceScanner = Resource("scanner")
|
||||
ResourceArtifact = Resource("artifact")
|
||||
ResourceTag = Resource("tag")
|
||||
@ -182,6 +183,10 @@ var (
|
||||
{Resource: ResourceScan, Action: ActionRead},
|
||||
{Resource: ResourceScan, Action: ActionStop},
|
||||
|
||||
{Resource: ResourceSBOM, Action: ActionCreate},
|
||||
{Resource: ResourceSBOM, Action: ActionStop},
|
||||
{Resource: ResourceSBOM, Action: ActionRead},
|
||||
|
||||
{Resource: ResourceTag, Action: ActionCreate},
|
||||
{Resource: ResourceTag, Action: ActionList},
|
||||
{Resource: ResourceTag, Action: ActionDelete},
|
||||
|
@ -86,6 +86,9 @@ var (
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
|
||||
@ -122,10 +125,7 @@ var (
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
@ -169,6 +169,9 @@ var (
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -223,6 +226,7 @@ var (
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -267,6 +271,7 @@ var (
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -290,6 +295,7 @@ var (
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
|
@ -313,11 +313,11 @@ func ValidateCronString(cron string) error {
|
||||
// sort.Slice(input, func(i, j int) bool {
|
||||
// return MostMatchSorter(input[i].GroupName, input[j].GroupName, matchWord)
|
||||
// })
|
||||
//
|
||||
// a is the field to be used for sorting, b is the other field, matchWord is the word to be matched
|
||||
// the return value is true if a is less than b
|
||||
// for example, search with "user", input is {"harbor_user", "user", "users, "admin_user"}
|
||||
// it returns with this order {"user", "users", "admin_user", "harbor_user"}
|
||||
|
||||
func MostMatchSorter(a, b string, matchWord string) bool {
|
||||
// exact match always first
|
||||
if a == matchWord {
|
||||
@ -333,7 +333,7 @@ func MostMatchSorter(a, b string, matchWord string) bool {
|
||||
return len(a) < len(b)
|
||||
}
|
||||
|
||||
// IsLocalPath checks if path is local
|
||||
// IsLocalPath checks if path is local, includes the empty path
|
||||
func IsLocalPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")
|
||||
return len(path) == 0 || (strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//"))
|
||||
}
|
||||
|
@ -501,6 +501,7 @@ func TestIsLocalPath(t *testing.T) {
|
||||
{"other_site1", args{"//www.myexample.com"}, false},
|
||||
{"other_site2", args{"https://www.myexample.com"}, false},
|
||||
{"other_site", args{"http://www.myexample.com"}, false},
|
||||
{"empty_path", args{""}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -92,6 +92,7 @@ func parseV1alpha1Icon(artifact *artifact.Artifact, manifest *v1.Manifest, reg r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer icon.Close()
|
||||
// check the size of the size <= 1MB
|
||||
data, err := io.ReadAll(io.LimitReader(icon, 1<<20))
|
||||
if err != nil {
|
||||
|
@ -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"
|
||||
@ -57,7 +58,10 @@ import (
|
||||
|
||||
var (
|
||||
// Ctl is a global artifact controller instance
|
||||
Ctl = NewController()
|
||||
Ctl = NewController()
|
||||
skippedContentTypes = map[string]struct{}{
|
||||
"application/vnd.in-toto+json": {},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@ -73,6 +77,7 @@ var (
|
||||
chart.ArtifactTypeChart: icon.DigestOfIconChart,
|
||||
cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB,
|
||||
wasm.ArtifactTypeWASM: icon.DigestOfIconWASM,
|
||||
sbom.ArtifactTypeSBOM: icon.DigestOfIconAccSBOM,
|
||||
}
|
||||
)
|
||||
|
||||
@ -111,6 +116,8 @@ type Controller interface {
|
||||
RemoveLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
|
||||
// Walk walks the artifact tree rooted at root, calling walkFn for each artifact in the tree, including root.
|
||||
Walk(ctx context.Context, root *Artifact, walkFn func(*Artifact) error, option *Option) error
|
||||
// HasUnscannableLayer check artifact with digest if has unscannable layer
|
||||
HasUnscannableLayer(ctx context.Context, dgst string) (bool, error)
|
||||
}
|
||||
|
||||
// NewController creates an instance of the default artifact controller
|
||||
@ -324,12 +331,6 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot, isAcces
|
||||
return err
|
||||
}
|
||||
|
||||
if isAccessory {
|
||||
if err := c.accessoryMgr.DeleteAccessories(ctx, q.New(q.KeyWords{"ArtifactID": art.ID, "Digest": art.Digest})); err != nil && !errors.IsErr(err, errors.NotFoundCode) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// the child artifact is referenced by some tags, skip
|
||||
if !isRoot && len(art.Tags) > 0 {
|
||||
return nil
|
||||
@ -352,6 +353,12 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot, isAcces
|
||||
return nil
|
||||
}
|
||||
|
||||
if isAccessory {
|
||||
if err := c.accessoryMgr.DeleteAccessories(ctx, q.New(q.KeyWords{"ArtifactID": art.ID, "Digest": art.Digest})); err != nil && !errors.IsErr(err, errors.NotFoundCode) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete accessories if contains any
|
||||
for _, acc := range art.Accessories {
|
||||
// only hard ref accessory should be removed
|
||||
@ -757,3 +764,21 @@ func (c *controller) populateAccessories(ctx context.Context, art *Artifact) {
|
||||
}
|
||||
art.Accessories = accs
|
||||
}
|
||||
|
||||
// HasUnscannableLayer check if it is a in-toto sbom, if it contains any blob with a content_type is application/vnd.in-toto+json, then consider as in-toto sbom
|
||||
func (c *controller) HasUnscannableLayer(ctx context.Context, dgst string) (bool, error) {
|
||||
if len(dgst) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
blobs, err := c.blobMgr.GetByArt(ctx, dgst)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, b := range blobs {
|
||||
if _, exist := skippedContentTypes[b.ContentType]; exist {
|
||||
log.Debugf("the artifact with digest %v is unscannable, because it contains content type: %v", dgst, b.ContentType)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
accessorymodel "github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
basemodel "github.com/goharbor/harbor/src/pkg/accessory/model/base"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/blob/models"
|
||||
"github.com/goharbor/harbor/src/pkg/label/model"
|
||||
repomodel "github.com/goharbor/harbor/src/pkg/repository/model"
|
||||
model_tag "github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||
@ -678,6 +679,29 @@ func (c *controllerTestSuite) TestWalk() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestIsIntoto() {
|
||||
blobs := []*models.Blob{
|
||||
{Digest: "sha256:00000", ContentType: "application/vnd.oci.image.manifest.v1+json"},
|
||||
{Digest: "sha256:22222", ContentType: "application/vnd.oci.image.config.v1+json"},
|
||||
{Digest: "sha256:11111", ContentType: "application/vnd.in-toto+json"},
|
||||
}
|
||||
c.blobMgr.On("GetByArt", mock.Anything, mock.Anything).Return(blobs, nil).Once()
|
||||
isIntoto, err := c.ctl.HasUnscannableLayer(context.Background(), "sha256: 77777")
|
||||
c.Nil(err)
|
||||
c.True(isIntoto)
|
||||
|
||||
blobs2 := []*models.Blob{
|
||||
{Digest: "sha256:00000", ContentType: "application/vnd.oci.image.manifest.v1+json"},
|
||||
{Digest: "sha256:22222", ContentType: "application/vnd.oci.image.config.v1+json"},
|
||||
{Digest: "sha256:11111", ContentType: "application/vnd.oci.image.layer.v1.tar+gzip"},
|
||||
}
|
||||
|
||||
c.blobMgr.On("GetByArt", mock.Anything, mock.Anything).Return(blobs2, nil).Once()
|
||||
isIntoto2, err := c.ctl.HasUnscannableLayer(context.Background(), "sha256: 8888")
|
||||
c.Nil(err)
|
||||
c.False(isIntoto2)
|
||||
}
|
||||
|
||||
func TestControllerTestSuite(t *testing.T) {
|
||||
suite.Run(t, &controllerTestSuite{})
|
||||
}
|
||||
|
@ -80,6 +80,19 @@ func (artifact *Artifact) SetAdditionLink(addition, version string) {
|
||||
artifact.AdditionLinks[addition] = &AdditionLink{HREF: href, Absolute: false}
|
||||
}
|
||||
|
||||
func (artifact *Artifact) SetSBOMAdditionLink(sbomDgst string, version string) {
|
||||
if artifact.AdditionLinks == nil {
|
||||
artifact.AdditionLinks = make(map[string]*AdditionLink)
|
||||
}
|
||||
addition := "sboms"
|
||||
projectName, repo := utils.ParseRepository(artifact.RepositoryName)
|
||||
// encode slash as %252F
|
||||
repo = repository.Encode(repo)
|
||||
href := fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", version, projectName, repo, sbomDgst, addition)
|
||||
|
||||
artifact.AdditionLinks[addition] = &AdditionLink{HREF: href, Absolute: false}
|
||||
}
|
||||
|
||||
// AdditionLink is a link via that the addition can be fetched
|
||||
type AdditionLink struct {
|
||||
HREF string `json:"href"`
|
||||
|
@ -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
|
||||
|
89
src/controller/artifact/processor/sbom/sbom.go
Normal file
89
src/controller/artifact/processor/sbom/sbom.go
Normal file
@ -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 (
|
||||
// ArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor
|
||||
ArtifactTypeSBOM = "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 ArtifactTypeSBOM
|
||||
}
|
166
src/controller/artifact/processor/sbom/sbom_test.go
Normal file
166
src/controller/artifact/processor/sbom/sbom_test.go
Normal file
@ -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(ArtifactTypeSBOM, suite.processor.GetArtifactType(context.Background(), &artifact.Artifact{}))
|
||||
}
|
||||
|
||||
func TestSBOMProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &SBOMProcessorTestSuite{})
|
||||
}
|
@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
sbomprocessor "github.com/goharbor/harbor/src/controller/artifact/processor/sbom"
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||
"github.com/goharbor/harbor/src/controller/repository"
|
||||
@ -36,6 +37,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg"
|
||||
pkgArt "github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/sbom"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
)
|
||||
|
||||
@ -72,6 +75,8 @@ type ArtifactEventHandler struct {
|
||||
execMgr task.ExecutionManager
|
||||
// reportMgr for managing scan reports
|
||||
reportMgr report.Manager
|
||||
// sbomReportMgr
|
||||
sbomReportMgr sbom.Manager
|
||||
// artMgr for managing artifacts
|
||||
artMgr pkgArt.Manager
|
||||
|
||||
@ -258,6 +263,11 @@ func (a *ArtifactEventHandler) onPush(ctx context.Context, event *event.Artifact
|
||||
if err := autoScan(ctx, &artifact.Artifact{Artifact: *event.Artifact}, event.Tags...); err != nil {
|
||||
log.Errorf("scan artifact %s@%s failed, error: %v", event.Artifact.RepositoryName, event.Artifact.Digest, err)
|
||||
}
|
||||
|
||||
log.Debugf("auto generate sbom is triggered for artifact event %+v", event)
|
||||
if err := autoGenSBOM(ctx, &artifact.Artifact{Artifact: *event.Artifact}); err != nil {
|
||||
log.Errorf("generate sbom for artifact %s@%s failed, error: %v", event.Artifact.RepositoryName, event.Artifact.Digest, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
@ -314,6 +324,17 @@ func (a *ArtifactEventHandler) onDelete(ctx context.Context, event *event.Artifa
|
||||
log.Errorf("failed to delete scan reports of artifact %v, error: %v", unrefDigests, err)
|
||||
}
|
||||
|
||||
// delete sbom_report when the subject artifact is deleted
|
||||
if err := sbom.Mgr.DeleteByArtifactID(ctx, event.Artifact.ID); err != nil {
|
||||
log.Errorf("failed to delete sbom reports of artifact ID %v, error: %v", event.Artifact.ID, err)
|
||||
}
|
||||
|
||||
// delete sbom_report when the accessory artifact is deleted
|
||||
if event.Artifact.Type == sbomprocessor.ArtifactTypeSBOM && len(event.Artifact.Digest) > 0 {
|
||||
if err := sbom.Mgr.DeleteByExtraAttr(ctx, v1.MimeTypeSBOMReport, "sbom_digest", event.Artifact.Digest); err != nil {
|
||||
log.Errorf("failed to delete sbom reports of with sbom digest %v, error: %v", event.Artifact.Digest, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,9 @@ import (
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
// autoScan scan artifact when the project of the artifact enable auto scan
|
||||
@ -37,9 +39,26 @@ func autoScan(ctx context.Context, a *artifact.Artifact, tags ...string) error {
|
||||
return orm.WithTransaction(func(ctx context.Context) error {
|
||||
options := []scan.Option{}
|
||||
if len(tags) > 0 {
|
||||
options = append(options, scan.WithTag(tags[0]))
|
||||
options = append(options, scan.WithTag(tags[0]), scan.WithFromEvent(true))
|
||||
}
|
||||
|
||||
return scan.DefaultController.Scan(ctx, a, options...)
|
||||
})(orm.SetTransactionOpNameToContext(ctx, "tx-auto-scan"))
|
||||
}
|
||||
|
||||
func autoGenSBOM(ctx context.Context, a *artifact.Artifact) error {
|
||||
proj, err := project.Ctl.Get(ctx, a.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !proj.AutoSBOMGen() {
|
||||
return nil
|
||||
}
|
||||
// transaction here to work with the image index
|
||||
return orm.WithTransaction(func(ctx context.Context) error {
|
||||
options := []scan.Option{}
|
||||
options = append(options, scan.WithScanType(v1.ScanTypeSbom), scan.WithFromEvent(true))
|
||||
log.Debugf("sbom scan controller artifact %+v, options %+v", a, options)
|
||||
return scan.DefaultController.Scan(ctx, a, options...)
|
||||
})(orm.SetTransactionOpNameToContext(ctx, "tx-auto-gen-sbom"))
|
||||
}
|
||||
|
@ -95,6 +95,34 @@ func (suite *AutoScanTestSuite) TestAutoScan() {
|
||||
suite.Nil(autoScan(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanSBOM() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaAutoSBOMGen: "true",
|
||||
},
|
||||
}, nil)
|
||||
suite.scanController.On("Scan", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
art := &artifact.Artifact{}
|
||||
|
||||
suite.Nil(autoGenSBOM(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanSBOMFalse() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaAutoSBOMGen: "false",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
suite.scanController.On("Scan", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
art := &artifact.Artifact{}
|
||||
|
||||
suite.Nil(autoGenSBOM(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanFailed() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/handler/util"
|
||||
eventModel "github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
@ -104,6 +105,9 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent,
|
||||
RepoFullName: event.Artifact.Repository,
|
||||
RepoType: repoType,
|
||||
},
|
||||
Scan: &eventModel.Scan{
|
||||
ScanType: event.ScanType,
|
||||
},
|
||||
},
|
||||
Operator: event.Operator,
|
||||
}
|
||||
@ -138,17 +142,29 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent,
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Add scan overview
|
||||
summaries, err := scan.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "construct scan payload")
|
||||
scanSummaries := map[string]interface{}{}
|
||||
if event.ScanType == v1.ScanTypeVulnerability {
|
||||
scanSummaries, err = scan.DefaultController.GetSummary(ctx, art, event.ScanType, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "construct scan payload")
|
||||
}
|
||||
}
|
||||
|
||||
sbomOverview := map[string]interface{}{}
|
||||
if event.ScanType == v1.ScanTypeSbom {
|
||||
sbomOverview, err = scan.DefaultController.GetSummary(ctx, art, event.ScanType, []string{v1.MimeTypeSBOMReport})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "construct scan payload")
|
||||
}
|
||||
}
|
||||
|
||||
// Add scan overview and sbom overview
|
||||
resource := &model.Resource{
|
||||
Tag: event.Artifact.Tag,
|
||||
Digest: event.Artifact.Digest,
|
||||
ResourceURL: resURL,
|
||||
ScanOverview: summaries,
|
||||
ScanOverview: scanSummaries,
|
||||
SBOMOverview: sbomOverview,
|
||||
}
|
||||
payload.EventData.Resources = append(payload.EventData.Resources, resource)
|
||||
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
// ScanImageMetaData defines meta data of image scanning event
|
||||
type ScanImageMetaData struct {
|
||||
Artifact *v1.Artifact
|
||||
ScanType string
|
||||
Status string
|
||||
Operator string
|
||||
}
|
||||
@ -55,6 +56,7 @@ func (si *ScanImageMetaData) Resolve(evt *event.Event) error {
|
||||
Artifact: si.Artifact,
|
||||
OccurAt: time.Now(),
|
||||
Operator: si.Operator,
|
||||
ScanType: si.ScanType,
|
||||
}
|
||||
|
||||
evt.Topic = topic
|
||||
|
@ -74,3 +74,9 @@ type RetentionRule struct {
|
||||
// Selector attached to the rule for filtering scope (e.g: repositories or namespaces)
|
||||
ScopeSelectors map[string][]*rule.Selector `json:"scope_selectors,omitempty"`
|
||||
}
|
||||
|
||||
// Scan describes scan infos
|
||||
type Scan struct {
|
||||
// ScanType the scan type
|
||||
ScanType string `json:"scan_type,omitempty"`
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ func (p *PushArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
|
||||
ResourceType: "artifact"}
|
||||
|
||||
if len(p.Tags) == 0 {
|
||||
auditLog.Resource = fmt.Sprintf("%s:%s",
|
||||
auditLog.Resource = fmt.Sprintf("%s@%s",
|
||||
p.Artifact.RepositoryName, p.Artifact.Digest)
|
||||
} else {
|
||||
auditLog.Resource = fmt.Sprintf("%s:%s",
|
||||
@ -188,7 +188,7 @@ func (p *PullArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
|
||||
ResourceType: "artifact"}
|
||||
|
||||
if len(p.Tags) == 0 {
|
||||
auditLog.Resource = fmt.Sprintf("%s:%s",
|
||||
auditLog.Resource = fmt.Sprintf("%s@%s",
|
||||
p.Artifact.RepositoryName, p.Artifact.Digest)
|
||||
} else {
|
||||
auditLog.Resource = fmt.Sprintf("%s:%s",
|
||||
@ -222,7 +222,7 @@ func (d *DeleteArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) {
|
||||
Operation: rbac.ActionDelete.String(),
|
||||
Username: d.Operator,
|
||||
ResourceType: "artifact",
|
||||
Resource: fmt.Sprintf("%s:%s", d.Artifact.RepositoryName, d.Artifact.Digest)}
|
||||
Resource: fmt.Sprintf("%s@%s", d.Artifact.RepositoryName, d.Artifact.Digest)}
|
||||
return auditLog, nil
|
||||
}
|
||||
|
||||
@ -289,6 +289,7 @@ func (d *DeleteTagEvent) String() string {
|
||||
// ScanImageEvent is scanning image related event data to publish
|
||||
type ScanImageEvent struct {
|
||||
EventType string
|
||||
ScanType string
|
||||
Artifact *v1.Artifact
|
||||
OccurAt time.Time
|
||||
Operator string
|
||||
|
@ -69,6 +69,10 @@ var (
|
||||
path: "./icons/wasm.png",
|
||||
resize: true,
|
||||
},
|
||||
icon.DigestOfIconAccSBOM: {
|
||||
path: "./icons/sbom.png",
|
||||
resize: true,
|
||||
},
|
||||
icon.DigestOfIconDefault: {
|
||||
path: "./icons/default.png",
|
||||
resize: true,
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
@ -45,7 +46,7 @@ type Controller interface {
|
||||
// Count get the total amount of project members
|
||||
Count(ctx context.Context, projectNameOrID interface{}, query *q.Query) (int, error)
|
||||
// IsProjectAdmin judges if the user is a project admin of any project
|
||||
IsProjectAdmin(ctx context.Context, memberID int) (bool, error)
|
||||
IsProjectAdmin(ctx context.Context, member commonmodels.User) (bool, error)
|
||||
}
|
||||
|
||||
// Request - Project Member Request
|
||||
@ -261,8 +262,8 @@ func (c *controller) Delete(ctx context.Context, projectNameOrID interface{}, me
|
||||
return c.mgr.Delete(ctx, p.ProjectID, memberID)
|
||||
}
|
||||
|
||||
func (c *controller) IsProjectAdmin(ctx context.Context, memberID int) (bool, error) {
|
||||
members, err := c.projectMgr.ListAdminRolesOfUser(ctx, memberID)
|
||||
func (c *controller) IsProjectAdmin(ctx context.Context, member commonmodels.User) (bool, error) {
|
||||
members, err := c.projectMgr.ListAdminRolesOfUser(ctx, member)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ func (suite *MemberControllerTestSuite) TestAddProjectMemberWithUserGroup() {
|
||||
|
||||
func (suite *MemberControllerTestSuite) TestIsProjectAdmin() {
|
||||
mock.OnAnything(suite.projectMgr, "ListAdminRolesOfUser").Return([]models.Member{models.Member{ID: 2, ProjectID: 2}}, nil)
|
||||
ok, err := suite.controller.IsProjectAdmin(context.Background(), 2)
|
||||
ok, err := suite.controller.IsProjectAdmin(context.Background(), comModels.User{UserID: 1})
|
||||
suite.NoError(err)
|
||||
suite.True(ok)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package flow
|
||||
|
||||
@ -18,6 +18,10 @@ type mockFactory struct {
|
||||
func (_m *mockFactory) AdapterPattern() *model.AdapterPattern {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AdapterPattern")
|
||||
}
|
||||
|
||||
var r0 *model.AdapterPattern
|
||||
if rf, ok := ret.Get(0).(func() *model.AdapterPattern); ok {
|
||||
r0 = rf()
|
||||
@ -34,6 +38,10 @@ func (_m *mockFactory) AdapterPattern() *model.AdapterPattern {
|
||||
func (_m *mockFactory) Create(_a0 *model.Registry) (adapter.Adapter, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 adapter.Adapter
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*model.Registry) (adapter.Adapter, error)); ok {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package flow
|
||||
|
||||
@ -21,6 +21,10 @@ type mockAdapter struct {
|
||||
func (_m *mockAdapter) BlobExist(repository string, digest string) (bool, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for BlobExist")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) (bool, error)); ok {
|
||||
@ -45,6 +49,10 @@ func (_m *mockAdapter) BlobExist(repository string, digest string) (bool, error)
|
||||
func (_m *mockAdapter) CanBeMount(digest string) (bool, string, error) {
|
||||
ret := _m.Called(digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CanBeMount")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 string
|
||||
var r2 error
|
||||
@ -76,6 +84,10 @@ func (_m *mockAdapter) CanBeMount(digest string) (bool, string, error) {
|
||||
func (_m *mockAdapter) DeleteManifest(repository string, reference string) error {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteManifest")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, reference)
|
||||
@ -90,6 +102,10 @@ func (_m *mockAdapter) DeleteManifest(repository string, reference string) error
|
||||
func (_m *mockAdapter) DeleteTag(repository string, tag string) error {
|
||||
ret := _m.Called(repository, tag)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteTag")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, tag)
|
||||
@ -104,6 +120,10 @@ func (_m *mockAdapter) DeleteTag(repository string, tag string) error {
|
||||
func (_m *mockAdapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) {
|
||||
ret := _m.Called(filters)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FetchArtifacts")
|
||||
}
|
||||
|
||||
var r0 []*model.Resource
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]*model.Filter) ([]*model.Resource, error)); ok {
|
||||
@ -130,6 +150,10 @@ func (_m *mockAdapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resourc
|
||||
func (_m *mockAdapter) HealthCheck() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for HealthCheck")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
@ -154,6 +178,10 @@ func (_m *mockAdapter) HealthCheck() (string, error) {
|
||||
func (_m *mockAdapter) Info() (*model.RegistryInfo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Info")
|
||||
}
|
||||
|
||||
var r0 *model.RegistryInfo
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (*model.RegistryInfo, error)); ok {
|
||||
@ -180,6 +208,10 @@ func (_m *mockAdapter) Info() (*model.RegistryInfo, error) {
|
||||
func (_m *mockAdapter) ListTags(repository string) ([]string, error) {
|
||||
ret := _m.Called(repository)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListTags")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
|
||||
@ -206,6 +238,10 @@ func (_m *mockAdapter) ListTags(repository string) ([]string, error) {
|
||||
func (_m *mockAdapter) ManifestExist(repository string, reference string) (bool, *distribution.Descriptor, error) {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ManifestExist")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 *distribution.Descriptor
|
||||
var r2 error
|
||||
@ -239,6 +275,10 @@ func (_m *mockAdapter) ManifestExist(repository string, reference string) (bool,
|
||||
func (_m *mockAdapter) MountBlob(srcRepository string, digest string, dstRepository string) error {
|
||||
ret := _m.Called(srcRepository, digest, dstRepository)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MountBlob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(srcRepository, digest, dstRepository)
|
||||
@ -253,6 +293,10 @@ func (_m *mockAdapter) MountBlob(srcRepository string, digest string, dstReposit
|
||||
func (_m *mockAdapter) PrepareForPush(_a0 []*model.Resource) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PrepareForPush")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]*model.Resource) error); ok {
|
||||
r0 = rf(_a0)
|
||||
@ -267,6 +311,10 @@ func (_m *mockAdapter) PrepareForPush(_a0 []*model.Resource) error {
|
||||
func (_m *mockAdapter) PullBlob(repository string, digest string) (int64, io.ReadCloser, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullBlob")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 io.ReadCloser
|
||||
var r2 error
|
||||
@ -300,6 +348,10 @@ func (_m *mockAdapter) PullBlob(repository string, digest string) (int64, io.Rea
|
||||
func (_m *mockAdapter) PullBlobChunk(repository string, digest string, blobSize int64, start int64, end int64) (int64, io.ReadCloser, error) {
|
||||
ret := _m.Called(repository, digest, blobSize, start, end)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullBlobChunk")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 io.ReadCloser
|
||||
var r2 error
|
||||
@ -340,6 +392,10 @@ func (_m *mockAdapter) PullManifest(repository string, reference string, acceptt
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullManifest")
|
||||
}
|
||||
|
||||
var r0 distribution.Manifest
|
||||
var r1 string
|
||||
var r2 error
|
||||
@ -373,6 +429,10 @@ func (_m *mockAdapter) PullManifest(repository string, reference string, acceptt
|
||||
func (_m *mockAdapter) PushBlob(repository string, digest string, size int64, blob io.Reader) error {
|
||||
ret := _m.Called(repository, digest, size, blob)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushBlob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, int64, io.Reader) error); ok {
|
||||
r0 = rf(repository, digest, size, blob)
|
||||
@ -387,6 +447,10 @@ func (_m *mockAdapter) PushBlob(repository string, digest string, size int64, bl
|
||||
func (_m *mockAdapter) PushBlobChunk(repository string, digest string, size int64, chunk io.Reader, start int64, end int64, location string) (string, int64, error) {
|
||||
ret := _m.Called(repository, digest, size, chunk, start, end, location)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushBlobChunk")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 int64
|
||||
var r2 error
|
||||
@ -418,6 +482,10 @@ func (_m *mockAdapter) PushBlobChunk(repository string, digest string, size int6
|
||||
func (_m *mockAdapter) PushManifest(repository string, reference string, mediaType string, payload []byte) (string, error) {
|
||||
ret := _m.Called(repository, reference, mediaType, payload)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushManifest")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, []byte) (string, error)); ok {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package replication
|
||||
|
||||
@ -21,6 +21,10 @@ type flowController struct {
|
||||
func (_m *flowController) Start(ctx context.Context, executionID int64, policy *model.Policy, resource *regmodel.Resource) error {
|
||||
ret := _m.Called(ctx, executionID, policy, resource)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Start")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *model.Policy, *regmodel.Resource) error); ok {
|
||||
r0 = rf(ctx, executionID, policy, resource)
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
ar "github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
@ -68,10 +67,11 @@ const (
|
||||
artfiactKey = "artifact"
|
||||
registrationKey = "registration"
|
||||
|
||||
artifactIDKey = "artifact_id"
|
||||
artifactTagKey = "artifact_tag"
|
||||
reportUUIDsKey = "report_uuids"
|
||||
robotIDKey = "robot_id"
|
||||
artifactIDKey = "artifact_id"
|
||||
artifactTagKey = "artifact_tag"
|
||||
reportUUIDsKey = "report_uuids"
|
||||
robotIDKey = "robot_id"
|
||||
enabledCapabilities = "enabled_capabilities"
|
||||
)
|
||||
|
||||
// uuidGenerator is a func template which is for generating UUID.
|
||||
@ -91,6 +91,7 @@ type launchScanJobParam struct {
|
||||
Artifact *ar.Artifact
|
||||
Tag string
|
||||
Reports []*scan.Report
|
||||
Type string
|
||||
}
|
||||
|
||||
// basicController is default implementation of api.Controller interface
|
||||
@ -193,6 +194,18 @@ func (bc *basicController) collectScanningArtifacts(ctx context.Context, r *scan
|
||||
return nil
|
||||
}
|
||||
|
||||
// because there are lots of in-toto sbom artifacts in dockerhub and replicated to Harbor, they are considered as image type
|
||||
// when scanning these type of sbom artifact, the scanner might assume it is image layer with tgz format, and if scanner read the layer with a stream of tgz,
|
||||
// it fail and close the stream abruptly and cause the pannic in the harbor core log
|
||||
// to avoid pannic, skip scan the in-toto sbom artifact sbom artifact
|
||||
unscannable, err := bc.ar.HasUnscannableLayer(ctx, a.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unscannable {
|
||||
return nil
|
||||
}
|
||||
|
||||
supported := hasCapability(r, a)
|
||||
|
||||
if !supported && a.IsImageIndex() {
|
||||
@ -242,23 +255,27 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !scannable {
|
||||
return errors.BadRequestError(nil).WithMessage("the configured scanner %s does not support scanning artifact with mime type %s", r.Name, artifact.ManifestMediaType)
|
||||
}
|
||||
|
||||
// Parse options
|
||||
opts, err := parseOptions(options...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scan controller: scan")
|
||||
}
|
||||
|
||||
if !scannable {
|
||||
if opts.FromEvent {
|
||||
// skip to return err for event related scan
|
||||
return nil
|
||||
}
|
||||
return errors.BadRequestError(nil).WithMessage("the configured scanner %s does not support scanning artifact with mime type %s", r.Name, artifact.ManifestMediaType)
|
||||
}
|
||||
|
||||
var (
|
||||
errs []error
|
||||
launchScanJobParams []*launchScanJobParam
|
||||
)
|
||||
handler := sca.GetScanHandler(opts.GetScanType())
|
||||
for _, art := range artifacts {
|
||||
reports, err := bc.makeReportPlaceholder(ctx, r, art)
|
||||
reports, err := handler.MakePlaceHolder(ctx, art, r)
|
||||
if err != nil {
|
||||
if errors.IsConflictErr(err) {
|
||||
errs = append(errs, err)
|
||||
@ -287,6 +304,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
Artifact: art,
|
||||
Tag: tag,
|
||||
Reports: reports,
|
||||
Type: opts.GetScanType(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -308,11 +326,18 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
"id": r.ID,
|
||||
"name": r.Name,
|
||||
},
|
||||
enabledCapabilities: map[string]interface{}{
|
||||
"type": opts.GetScanType(),
|
||||
},
|
||||
}
|
||||
if op := operator.FromContext(ctx); op != "" {
|
||||
extraAttrs["operator"] = op
|
||||
}
|
||||
executionID, err := bc.execMgr.Create(ctx, job.ImageScanJobVendorType, artifact.ID, task.ExecutionTriggerManual, extraAttrs)
|
||||
vendorType := handler.JobVendorType()
|
||||
// for vulnerability and generate sbom, use different vendor type
|
||||
// because the execution reaper only keep the latest execution for the vendor type IMAGE_SCAN
|
||||
// both vulnerability and sbom need to keep the latest scan execution to get the latest scan status
|
||||
executionID, err := bc.execMgr.Create(ctx, vendorType, artifact.ID, task.ExecutionTriggerManual, extraAttrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -324,7 +349,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
for _, launchScanJobParam := range launchScanJobParams {
|
||||
launchScanJobParam.ExecutionID = opts.ExecutionID
|
||||
|
||||
if err := bc.launchScanJob(ctx, launchScanJobParam); err != nil {
|
||||
if err := bc.launchScanJob(ctx, launchScanJobParam, opts); err != nil {
|
||||
log.G(ctx).Warningf("scan artifact %s@%s failed, error: %v", artifact.RepositoryName, artifact.Digest, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@ -339,15 +364,17 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
}
|
||||
|
||||
// Stop scan job of a given artifact
|
||||
func (bc *basicController) Stop(ctx context.Context, artifact *ar.Artifact) error {
|
||||
func (bc *basicController) Stop(ctx context.Context, artifact *ar.Artifact, capType string) error {
|
||||
if artifact == nil {
|
||||
return errors.New("nil artifact to stop scan")
|
||||
}
|
||||
query := q.New(q.KeyWords{"extra_attrs.artifact.digest": artifact.Digest})
|
||||
vendorType := sca.GetScanHandler(capType).JobVendorType()
|
||||
query := q.New(q.KeyWords{"vendor_type": vendorType, "extra_attrs.artifact.digest": artifact.Digest, "extra_attrs.enabled_capabilities.type": capType})
|
||||
executions, err := bc.execMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(executions) == 0 {
|
||||
message := fmt.Sprintf("no scan job for artifact digest=%v", artifact.Digest)
|
||||
return errors.BadRequestError(nil).WithMessage(message)
|
||||
@ -543,61 +570,6 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact) ([]*scan.Report, error) {
|
||||
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType)
|
||||
|
||||
oldReports, err := bc.manager.GetBy(bc.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bc.assembleReports(ctx, oldReports...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(oldReports) > 0 {
|
||||
for _, oldReport := range oldReports {
|
||||
if !job.Status(oldReport.Status).Final() {
|
||||
return nil, errors.ConflictError(nil).WithMessage("a previous scan process is %s", oldReport.Status)
|
||||
}
|
||||
}
|
||||
|
||||
for _, oldReport := range oldReports {
|
||||
if err := bc.manager.Delete(ctx, oldReport.UUID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reports []*scan.Report
|
||||
|
||||
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) {
|
||||
report := &scan.Report{
|
||||
Digest: art.Digest,
|
||||
RegistrationUUID: r.UUID,
|
||||
MimeType: pm,
|
||||
}
|
||||
|
||||
create := func(ctx context.Context) error {
|
||||
reportUUID, err := bc.manager.Create(ctx, report)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report.UUID = reportUUID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reports = append(reports, report)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// GetReport ...
|
||||
func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) ([]*scan.Report, error) {
|
||||
if artifact == nil {
|
||||
@ -673,37 +645,9 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Get reports first
|
||||
rps, err := bc.GetReport(ctx, artifact, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaries := make(map[string]interface{}, len(rps))
|
||||
for _, rp := range rps {
|
||||
sum, err := report.GenerateSummary(rp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s, ok := summaries[rp.MimeType]; ok {
|
||||
r, err := report.MergeSummary(rp.MimeType, s, sum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaries[rp.MimeType] = r
|
||||
} else {
|
||||
summaries[rp.MimeType] = sum
|
||||
}
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) {
|
||||
handler := sca.GetScanHandler(scanType)
|
||||
return handler.GetSummary(ctx, artifact, mimeTypes)
|
||||
}
|
||||
|
||||
// GetScanLog ...
|
||||
@ -739,7 +683,7 @@ func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact
|
||||
if !scanTaskForArtifacts(t, artifactMap) {
|
||||
return nil, errors.NotFoundError(nil).WithMessage("scan log with uuid: %s not found", uuid)
|
||||
}
|
||||
for _, reportUUID := range getReportUUIDs(t.ExtraAttrs) {
|
||||
for _, reportUUID := range GetReportUUIDs(t.ExtraAttrs) {
|
||||
reportUUIDToTasks[reportUUID] = t
|
||||
}
|
||||
}
|
||||
@ -820,14 +764,6 @@ func scanTaskForArtifacts(task *task.Task, artifactMap map[int64]interface{}) bo
|
||||
return exist
|
||||
}
|
||||
|
||||
// DeleteReports ...
|
||||
func (bc *basicController) DeleteReports(ctx context.Context, digests ...string) error {
|
||||
if err := bc.manager.DeleteByDigests(ctx, digests...); err != nil {
|
||||
return errors.Wrap(err, "scan controller: delete reports")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artifact, allowlist allowlist.CVESet, allowlistIsExpired bool) (*Vulnerable, error) {
|
||||
if artifact == nil {
|
||||
return nil, errors.New("no way to get vulnerable for nil artifact")
|
||||
@ -912,7 +848,7 @@ func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artif
|
||||
}
|
||||
|
||||
// makeRobotAccount creates a robot account based on the arguments for scanning.
|
||||
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration) (*robot.Robot, error) {
|
||||
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration, permission []*types.Policy) (*robot.Robot, error) {
|
||||
// Use uuid as name to avoid duplicated entries.
|
||||
UUID, err := bc.uuid()
|
||||
if err != nil {
|
||||
@ -934,16 +870,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
|
||||
{
|
||||
Kind: "project",
|
||||
Namespace: projectName,
|
||||
Access: []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
},
|
||||
Access: permission,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -962,7 +889,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
|
||||
}
|
||||
|
||||
// launchScanJob launches a job to run scan
|
||||
func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam) error {
|
||||
func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam, opts *Options) error {
|
||||
// don't launch scan job for the artifact which is not supported by the scanner
|
||||
if !hasCapability(param.Registration, param.Artifact) {
|
||||
return nil
|
||||
@ -980,7 +907,12 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
|
||||
return errors.Wrap(err, "scan controller: launch scan job")
|
||||
}
|
||||
|
||||
robot, err := bc.makeRobotAccount(ctx, param.Artifact.ProjectID, param.Artifact.RepositoryName, param.Registration)
|
||||
// Get Scanner handler by scan type to separate the scan logic for different scan types
|
||||
handler := sca.GetScanHandler(param.Type)
|
||||
if handler == nil {
|
||||
return fmt.Errorf("failed to get scan handler, type is %v", param.Type)
|
||||
}
|
||||
robot, err := bc.makeRobotAccount(ctx, param.Artifact.ProjectID, param.Artifact.RepositoryName, param.Registration, handler.RequiredPermissions())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scan controller: launch scan job")
|
||||
}
|
||||
@ -998,6 +930,11 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
|
||||
MimeType: param.Artifact.ManifestMediaType,
|
||||
Size: param.Artifact.Size,
|
||||
},
|
||||
RequestType: []*v1.ScanType{
|
||||
{
|
||||
Type: opts.GetScanType(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rJSON, err := param.Registration.ToJSON()
|
||||
@ -1028,7 +965,8 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
|
||||
params[sca.JobParameterRequest] = sJSON
|
||||
params[sca.JobParameterMimes] = mimes
|
||||
params[sca.JobParameterRobot] = robotJSON
|
||||
|
||||
// because there is only one task type implementation
|
||||
// both the vulnerability scan and generate sbom use the same job type for now
|
||||
j := &task.Job{
|
||||
Name: job.ImageScanJobVendorType,
|
||||
Metadata: &job.Metadata{
|
||||
@ -1121,7 +1059,7 @@ func (bc *basicController) assembleReports(ctx context.Context, reports ...*scan
|
||||
|
||||
reportUUIDToTasks := map[string]*task.Task{}
|
||||
for _, task := range tasks {
|
||||
for _, reportUUID := range getReportUUIDs(task.ExtraAttrs) {
|
||||
for _, reportUUID := range GetReportUUIDs(task.ExtraAttrs) {
|
||||
reportUUIDToTasks[reportUUID] = task
|
||||
}
|
||||
}
|
||||
@ -1192,7 +1130,8 @@ func getArtifactTag(extraAttrs map[string]interface{}) string {
|
||||
return tag
|
||||
}
|
||||
|
||||
func getReportUUIDs(extraAttrs map[string]interface{}) []string {
|
||||
// GetReportUUIDs returns the report UUIDs from the extra attributes
|
||||
func GetReportUUIDs(extraAttrs map[string]interface{}) []string {
|
||||
var reportUUIDs []string
|
||||
|
||||
if extraAttrs != nil {
|
||||
|
@ -54,6 +54,7 @@ import (
|
||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
|
||||
scanTest "github.com/goharbor/harbor/src/testing/pkg/scan"
|
||||
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
||||
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
|
||||
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
@ -63,21 +64,24 @@ import (
|
||||
type ControllerTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
scanHandler *scanTest.Handler
|
||||
|
||||
artifactCtl *artifacttesting.Controller
|
||||
accessoryMgr *accessorytesting.Manager
|
||||
originalArtifactCtl artifact.Controller
|
||||
|
||||
tagCtl *tagtesting.FakeController
|
||||
|
||||
registration *scanner.Registration
|
||||
artifact *artifact.Artifact
|
||||
rawReport string
|
||||
registration *scanner.Registration
|
||||
artifact *artifact.Artifact
|
||||
wrongArtifact *artifact.Artifact
|
||||
rawReport string
|
||||
|
||||
execMgr *tasktesting.ExecutionManager
|
||||
taskMgr *tasktesting.Manager
|
||||
reportMgr *reporttesting.Manager
|
||||
ar artifact.Controller
|
||||
c Controller
|
||||
c *basicController
|
||||
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
|
||||
cache *mockcache.Cache
|
||||
}
|
||||
@ -89,6 +93,8 @@ func TestController(t *testing.T) {
|
||||
|
||||
// SetupSuite ...
|
||||
func (suite *ControllerTestSuite) SetupSuite() {
|
||||
suite.scanHandler = &scanTest.Handler{}
|
||||
sca.RegisterScanHanlder(v1.ScanTypeVulnerability, suite.scanHandler)
|
||||
suite.originalArtifactCtl = artifact.Ctl
|
||||
suite.artifactCtl = &artifacttesting.Controller{}
|
||||
artifact.Ctl = suite.artifactCtl
|
||||
@ -100,6 +106,9 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
suite.artifact.Digest = "digest-code"
|
||||
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
||||
|
||||
suite.wrongArtifact = &artifact.Artifact{Artifact: art.Artifact{ID: 2, ProjectID: 1}}
|
||||
suite.wrongArtifact.Digest = "digest-wrong"
|
||||
|
||||
m := &v1.ScannerAdapterMetadata{
|
||||
Scanner: &v1.Scanner{
|
||||
Name: "Trivy",
|
||||
@ -107,6 +116,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Capabilities: []*v1.ScannerCapability{{
|
||||
Type: v1.ScanTypeVulnerability,
|
||||
ConsumesMimeTypes: []string{
|
||||
v1.MimeTypeOCIArtifact,
|
||||
v1.MimeTypeDockerArtifact,
|
||||
@ -114,7 +124,17 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
ProducesMimeTypes: []string{
|
||||
v1.MimeTypeNativeReport,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: v1.ScanTypeSbom,
|
||||
ConsumesMimeTypes: []string{
|
||||
v1.MimeTypeOCIArtifact,
|
||||
},
|
||||
ProducesMimeTypes: []string{
|
||||
v1.MimeTypeSBOMReport,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: v1.ScannerProperties{
|
||||
"extra": "testing",
|
||||
},
|
||||
@ -179,9 +199,24 @@ 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"}`,
|
||||
},
|
||||
}
|
||||
|
||||
emptySBOMReport := []*scan.Report{{Report: ``, UUID: "rp-uuid-004"}}
|
||||
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("GetBy", mock.Anything, suite.wrongArtifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(emptySBOMReport, 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("Update", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil)
|
||||
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
|
||||
suite.reportMgr = mgr
|
||||
|
||||
@ -307,6 +342,8 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
reportConverter: &postprocessorstesting.ScanReportV1ToV2Converter{},
|
||||
cache: func() cache.Cache { return suite.cache },
|
||||
}
|
||||
mock.OnAnything(suite.scanHandler, "JobVendorType").Return("IMAGE_SCAN")
|
||||
|
||||
}
|
||||
|
||||
// TearDownSuite ...
|
||||
@ -316,9 +353,23 @@ func (suite *ControllerTestSuite) TearDownSuite() {
|
||||
|
||||
// TestScanControllerScan ...
|
||||
func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||
rpts := []*scan.Report{
|
||||
{UUID: "uuid"},
|
||||
}
|
||||
requiredPermission := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
{
|
||||
// artifact not provieded
|
||||
suite.Require().Error(suite.c.Scan(context.TODO(), nil))
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Times(3)
|
||||
}
|
||||
|
||||
{
|
||||
@ -337,6 +388,8 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||
|
||||
mock.OnAnything(suite.execMgr, "Create").Return(int64(1), nil).Once()
|
||||
mock.OnAnything(suite.taskMgr, "Create").Return(int64(1), nil).Once()
|
||||
mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once()
|
||||
mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once()
|
||||
|
||||
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||
|
||||
@ -356,7 +409,10 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||
}, nil).Once()
|
||||
|
||||
mock.OnAnything(suite.reportMgr, "Delete").Return(fmt.Errorf("delete failed")).Once()
|
||||
|
||||
mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once()
|
||||
mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once()
|
||||
mock.OnAnything(suite.execMgr, "Create").Return(int64(1), nil).Once()
|
||||
mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed to create task")).Once()
|
||||
suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact))
|
||||
}
|
||||
|
||||
@ -371,7 +427,9 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{
|
||||
{ExtraAttrs: suite.makeExtraAttrs(int64(1), "rp-uuid-001"), Status: "Running"},
|
||||
}, nil).Once()
|
||||
|
||||
mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once()
|
||||
mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once()
|
||||
mock.OnAnything(suite.execMgr, "Create").Return(int64(0), fmt.Errorf("failed to create execution")).Once()
|
||||
suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact))
|
||||
}
|
||||
}
|
||||
@ -380,7 +438,7 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||
func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||
{
|
||||
// artifact not provieded
|
||||
suite.Require().Error(suite.c.Stop(context.TODO(), nil))
|
||||
suite.Require().Error(suite.c.Stop(context.TODO(), nil, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
@ -392,7 +450,7 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().NoError(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().NoError(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
@ -402,7 +460,7 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
@ -411,12 +469,13 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanControllerGetReport ...
|
||||
func (suite *ControllerTestSuite) TestScanControllerGetReport() {
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
@ -432,23 +491,9 @@ func (suite *ControllerTestSuite) TestScanControllerGetReport() {
|
||||
assert.Equal(suite.T(), 1, len(rep))
|
||||
}
|
||||
|
||||
// TestScanControllerGetSummary ...
|
||||
func (suite *ControllerTestSuite) TestScanControllerGetSummary() {
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
mock.OnAnything(suite.accessoryMgr, "List").Return([]accessoryModel.Accessory{}, nil).Once()
|
||||
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
walkFn(suite.artifact)
|
||||
}).Once()
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once()
|
||||
|
||||
sum, err := suite.c.GetSummary(ctx, suite.artifact, []string{v1.MimeTypeNativeReport})
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 1, len(sum))
|
||||
}
|
||||
|
||||
// TestScanControllerGetScanLog ...
|
||||
func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{
|
||||
{
|
||||
@ -459,6 +504,13 @@ func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
||||
|
||||
mock.OnAnything(suite.taskMgr, "GetLog").Return([]byte("log"), nil).Once()
|
||||
|
||||
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
walkFn(suite.artifact)
|
||||
}).Once()
|
||||
|
||||
mock.OnAnything(suite.accessoryMgr, "List").Return(nil, nil)
|
||||
|
||||
bytes, err := suite.c.GetScanLog(ctx, &artifact.Artifact{Artifact: art.Artifact{ID: 1, ProjectID: 1}}, "rp-uuid-001")
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Condition(suite.T(), func() (success bool) {
|
||||
@ -469,6 +521,7 @@ func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
||||
|
||||
func (suite *ControllerTestSuite) TestScanControllerGetMultiScanLog() {
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Times(4)
|
||||
suite.taskMgr.On("ListScanTasksByReportUUID", ctx, "rp-uuid-001").Return([]*task.Task{
|
||||
{
|
||||
ID: 1,
|
||||
@ -531,7 +584,22 @@ func (suite *ControllerTestSuite) TestScanAll() {
|
||||
{
|
||||
// no artifacts found when scan all
|
||||
executionID := int64(1)
|
||||
|
||||
rpts := []*scan.Report{
|
||||
{UUID: "uuid"},
|
||||
}
|
||||
requiredPermission := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once()
|
||||
mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once()
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
suite.execMgr.On(
|
||||
"Create", mock.Anything, "SCAN_ALL", int64(0), "SCHEDULE",
|
||||
mock.Anything).Return(executionID, nil).Once()
|
||||
@ -572,8 +640,6 @@ func (suite *ControllerTestSuite) TestScanAll() {
|
||||
walkFn(suite.artifact)
|
||||
}).Once()
|
||||
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once()
|
||||
|
||||
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
||||
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
|
||||
mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed")).Once()
|
||||
@ -600,16 +666,6 @@ func (suite *ControllerTestSuite) TestStopScanAll() {
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *ControllerTestSuite) TestDeleteReports() {
|
||||
suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(nil).Once()
|
||||
|
||||
suite.NoError(suite.c.DeleteReports(context.TODO(), "digest"))
|
||||
|
||||
suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(fmt.Errorf("failed")).Once()
|
||||
|
||||
suite.Error(suite.c.DeleteReports(context.TODO(), "digest"))
|
||||
}
|
||||
|
||||
func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs ...string) map[string]interface{} {
|
||||
b, _ := json.Marshal(map[string]interface{}{reportUUIDsKey: reportUUIDs})
|
||||
|
||||
|
@ -120,6 +120,13 @@ func scanTaskStatusChange(ctx context.Context, taskID int64, status string) (err
|
||||
if operator, ok := exec.ExtraAttrs["operator"].(string); ok {
|
||||
e.Operator = operator
|
||||
}
|
||||
|
||||
// extract ScanType if exist in ExtraAttrs
|
||||
if c, ok := exec.ExtraAttrs["enabled_capabilities"].(map[string]interface{}); ok {
|
||||
if Type, ok := c["type"].(string); ok {
|
||||
e.ScanType = Type
|
||||
}
|
||||
}
|
||||
// fire event
|
||||
notification.AddEvent(ctx, e)
|
||||
}
|
||||
|
@ -86,6 +86,18 @@ func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool
|
||||
return artifact.ErrBreak
|
||||
}
|
||||
|
||||
// because there are lots of in-toto sbom artifacts in dockerhub and replicated to Harbor, they are considered as image type
|
||||
// when scanning these type of sbom artifact, the scanner might assume it is image layer with tgz format, and if scanner read the layer with a stream of tgz,
|
||||
// it fail and close the stream abruptly and cause the pannic in the harbor core log
|
||||
// to avoid pannic, skip scan the in-toto sbom artifact sbom artifact
|
||||
unscannable, err := c.artifactCtl.HasUnscannableLayer(ctx, a.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unscannable {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ func (suite *CheckerTestSuite) TestIsScannable() {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
walkFn(art)
|
||||
})
|
||||
|
||||
mock.OnAnything(c.artifactCtl, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
isScannable, err := c.IsScannable(context.TODO(), art)
|
||||
suite.Nil(err)
|
||||
suite.False(isScannable)
|
||||
@ -97,6 +97,7 @@ func (suite *CheckerTestSuite) TestIsScannable() {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
walkFn(art)
|
||||
})
|
||||
mock.OnAnything(c.artifactCtl, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
|
||||
isScannable, err := c.IsScannable(context.TODO(), art)
|
||||
suite.Nil(err)
|
||||
|
@ -55,10 +55,11 @@ type Controller interface {
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// artifact *artifact.Artifact : the artifact whose scan job to be stopped
|
||||
// capType string : the capability type of the scanner, vulnerability or SBOM.
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
Stop(ctx context.Context, artifact *artifact.Artifact) error
|
||||
Stop(ctx context.Context, artifact *artifact.Artifact, capType string) error
|
||||
|
||||
// GetReport gets the reports for the given artifact identified by the digest
|
||||
//
|
||||
@ -82,7 +83,7 @@ type Controller interface {
|
||||
// Returns:
|
||||
// map[string]interface{} : report summaries indexed by mime types
|
||||
// error : non nil error if any errors occurred
|
||||
GetSummary(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error)
|
||||
GetSummary(ctx context.Context, artifact *artifact.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error)
|
||||
|
||||
// Get the scan log for the specified artifact with the given digest
|
||||
//
|
||||
@ -95,15 +96,6 @@ type Controller interface {
|
||||
// error : non nil error if any errors occurred
|
||||
GetScanLog(ctx context.Context, art *artifact.Artifact, uuid string) ([]byte, error)
|
||||
|
||||
// Delete the reports related with the specified digests
|
||||
//
|
||||
// Arguments:
|
||||
// digests ...string : specify one or more digests whose reports will be deleted
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
DeleteReports(ctx context.Context, digests ...string) error
|
||||
|
||||
// Scan all the artifacts
|
||||
//
|
||||
// Arguments:
|
||||
|
@ -14,17 +14,20 @@
|
||||
|
||||
package scan
|
||||
|
||||
import v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
|
||||
// Options keep the settings/configurations for scanning.
|
||||
type Options struct {
|
||||
ExecutionID int64 // The execution id to scan artifact
|
||||
Tag string // The tag of the artifact to scan
|
||||
ScanType string // The scan type could be sbom or vulnerability
|
||||
FromEvent bool // indicate the current call from event or not
|
||||
}
|
||||
|
||||
// GetScanType returns the scan type. for backward compatibility, the default type is vulnerability.
|
||||
func (o *Options) GetScanType() string {
|
||||
if len(o.ScanType) == 0 {
|
||||
o.ScanType = "vulnerability"
|
||||
o.ScanType = v1.ScanTypeVulnerability
|
||||
}
|
||||
return o.ScanType
|
||||
}
|
||||
@ -61,3 +64,11 @@ func WithScanType(scanType string) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFromEvent set the caller's source
|
||||
func WithFromEvent(fromEvent bool) Option {
|
||||
return func(options *Options) error {
|
||||
options.FromEvent = fromEvent
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ const (
|
||||
proScannerMetaKey = "projectScanner"
|
||||
statusUnhealthy = "unhealthy"
|
||||
statusHealthy = "healthy"
|
||||
// RetrieveCapFailMsg the message indicate failed to retrieve the scanner capabilities
|
||||
RetrieveCapFailMsg = "failed to retrieve scanner capabilities, error %v"
|
||||
)
|
||||
|
||||
// DefaultController is a singleton api controller for plug scanners
|
||||
@ -79,7 +81,12 @@ func (bc *basicController) ListRegistrations(ctx context.Context, query *q.Query
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "api controller: list registrations")
|
||||
}
|
||||
|
||||
for _, r := range l {
|
||||
if err := bc.RetrieveCap(ctx, r); err != nil {
|
||||
log.Warningf(RetrieveCapFailMsg, err)
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@ -122,10 +129,26 @@ func (bc *basicController) GetRegistration(ctx context.Context, registrationUUID
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "api controller: get registration")
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := bc.RetrieveCap(ctx, r); err != nil {
|
||||
log.Warningf(RetrieveCapFailMsg, err)
|
||||
return r, nil
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (bc *basicController) RetrieveCap(ctx context.Context, r *scanner.Registration) error {
|
||||
mt, err := bc.Ping(ctx, r)
|
||||
if err != nil {
|
||||
logger.Errorf("Get registration error: %s", err)
|
||||
return err
|
||||
}
|
||||
r.Capabilities = mt.ConvertCapability()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegistrationExists ...
|
||||
func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool {
|
||||
registration, err := bc.manager.Get(ctx, registrationUUID)
|
||||
|
@ -113,7 +113,7 @@ type Controller interface {
|
||||
// Arguments:
|
||||
// ctx context.Context : the context.Context for this method
|
||||
// projectID int64 : the ID of the given project
|
||||
// scannerID string : the UUID of the the scanner
|
||||
// scannerID string : the UUID of the scanner
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
@ -154,4 +154,7 @@ type Controller interface {
|
||||
// *v1.ScannerAdapterMetadata : metadata returned by the scanner if successfully ping
|
||||
// error : non nil error if any errors occurred
|
||||
GetMetadata(ctx context.Context, registrationUUID string) (*v1.ScannerAdapterMetadata, error)
|
||||
|
||||
// RetrieveCap retrieve scanner capabilities
|
||||
RetrieveCap(ctx context.Context, r *scanner.Registration) error
|
||||
}
|
||||
|
@ -70,6 +70,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/oidc"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/sbom"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
pkguser "github.com/goharbor/harbor/src/pkg/user"
|
||||
"github.com/goharbor/harbor/src/pkg/version"
|
||||
"github.com/goharbor/harbor/src/server"
|
||||
@ -103,14 +105,14 @@ func gracefulShutdown(closing, done chan struct{}, shutdowns ...func()) {
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
log.Infof("capture system signal %s, to close \"closing\" channel", <-signals)
|
||||
close(closing)
|
||||
shutdownChan := make(chan struct{}, 1)
|
||||
shutdownChan := make(chan struct{})
|
||||
go func() {
|
||||
defer close(shutdownChan)
|
||||
for _, s := range shutdowns {
|
||||
s()
|
||||
}
|
||||
<-done
|
||||
log.Infof("Goroutines exited normally")
|
||||
shutdownChan <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
|
100
src/go.mod
100
src/go.mod
@ -1,41 +1,41 @@
|
||||
module github.com/goharbor/harbor/src
|
||||
|
||||
go 1.21
|
||||
go 1.22.3
|
||||
|
||||
require (
|
||||
github.com/FZambia/sentinel v1.1.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/aws/aws-sdk-go v1.50.24
|
||||
github.com/aws/aws-sdk-go v1.53.14
|
||||
github.com/beego/beego/v2 v2.0.6
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0
|
||||
github.com/bmatcuk/doublestar v1.3.4
|
||||
github.com/casbin/casbin v1.9.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/cloudevents/sdk-go/v2 v2.15.2
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/dghubble/sling v1.1.0
|
||||
github.com/docker/distribution v2.8.2+incompatible
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-openapi/errors v0.21.0
|
||||
github.com/go-openapi/errors v0.22.0
|
||||
github.com/go-openapi/loads v0.21.2 // indirect
|
||||
github.com/go-openapi/runtime v0.26.2
|
||||
github.com/go-openapi/spec v0.20.11 // indirect
|
||||
github.com/go-openapi/strfmt v0.22.0
|
||||
github.com/go-openapi/swag v0.22.7
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-openapi/swag v0.23.0
|
||||
github.com/go-openapi/validate v0.22.3 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
|
||||
github.com/gocraft/work v0.5.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/google/go-containerregistry v0.19.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/csrf v1.6.2
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/graph-gophers/dataloader v5.0.0+incompatible
|
||||
@ -48,38 +48,38 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.233+incompatible
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.97
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.46.1
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
|
||||
go.opentelemetry.io/otel v1.24.0
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.138
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.51.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0
|
||||
go.opentelemetry.io/otel v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.0.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
|
||||
go.opentelemetry.io/otel/sdk v1.24.0
|
||||
go.opentelemetry.io/otel/trace v1.24.0
|
||||
go.uber.org/ratelimit v0.2.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0
|
||||
go.opentelemetry.io/otel/sdk v1.26.0
|
||||
go.opentelemetry.io/otel/trace v1.27.0
|
||||
go.uber.org/ratelimit v0.3.1
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/oauth2 v0.19.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/time v0.5.0
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
helm.sh/helm/v3 v3.14.2
|
||||
k8s.io/api v0.29.0
|
||||
k8s.io/apimachinery v0.29.0
|
||||
helm.sh/helm/v3 v3.14.4
|
||||
k8s.io/api v0.30.0
|
||||
k8s.io/apimachinery v0.30.0
|
||||
k8s.io/client-go v0.29.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute v1.24.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v37.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
@ -93,7 +93,7 @@ require (
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Unknwon/goconfig v0.0.0-20160216183935-5f601ca6ef4d // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
@ -107,7 +107,7 @@ require (
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.21.4 // indirect
|
||||
@ -115,11 +115,10 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@ -136,10 +135,10 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@ -148,9 +147,9 @@ require (
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/robfig/cron v1.0.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
@ -159,31 +158,30 @@ require (
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 // indirect
|
||||
go.mongodb.org/mongo-driver v1.13.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.19.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
google.golang.org/api v0.149.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
google.golang.org/api v0.162.0 // indirect
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
|
||||
google.golang.org/grpc v1.61.1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/grpc v1.63.2 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
|
219
src/go.sum
219
src/go.sum
@ -5,8 +5,8 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
|
||||
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
@ -68,8 +68,6 @@ github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1L
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97 h1:bNE5ID4C3YOkROfvBjXJUG53gyb+8az3TQN02LqnGBk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@ -78,15 +76,16 @@ github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:W
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.50.24 h1:3o2Pg7mOoVL0jv54vWtuafoZqAeEXLhm1tltWA2GcEw=
|
||||
github.com/aws/aws-sdk-go v1.50.24/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.53.14 h1:SzhkC2Pzag0iRW8WBb80RzKdGXDydJR9LAMs2GyKJ2M=
|
||||
github.com/aws/aws-sdk-go v1.53.14/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/beego/beego/v2 v2.0.6 h1:21Aqz3+RzUE1yP9a5xdU6LK54n9Z7NLEJtR4PE7NrPQ=
|
||||
github.com/beego/beego/v2 v2.0.6/go.mod h1:CH2/JIaB4ceGYVQlYqTAFft4pVk/ol1ZkakUrUvAyns=
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw=
|
||||
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@ -97,8 +96,8 @@ github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQ
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM=
|
||||
github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
@ -112,8 +111,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
@ -131,8 +130,8 @@ github.com/dghubble/sling v1.1.0/go.mod h1:ZcPRuLm0qrcULW2gOrjXrAWgf76sahqSyxXyV
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw=
|
||||
github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0=
|
||||
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg=
|
||||
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA=
|
||||
github.com/distribution/distribution v2.8.2+incompatible h1:k9+4DKdOG+quPFZXT/mUsiQrGu9vYCp+dXpuPkuqhk8=
|
||||
github.com/distribution/distribution v2.8.2+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
@ -169,11 +168,12 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
|
||||
@ -182,7 +182,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@ -190,8 +189,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc=
|
||||
github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo=
|
||||
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||
github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
|
||||
github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
|
||||
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
@ -208,14 +207,14 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6
|
||||
github.com/go-openapi/spec v0.20.11 h1:J/TzFDLTt4Rcl/l1PmyErvkqlJDncGvPTMnCI39I4gY=
|
||||
github.com/go-openapi/spec v0.20.11/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
|
||||
github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
|
||||
github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
|
||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
|
||||
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.22.3 h1:KxG9mu5HBRYbecRb37KRCihvGGtND2aXziBAv0NNfyI=
|
||||
github.com/go-openapi/validate v0.22.3/go.mod h1:kVxh31KbfsxU8ZyoHaDbLBWU5CnMdqBUEtadQ2G4d5M=
|
||||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
||||
@ -239,10 +238,10 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@ -260,8 +259,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.8.8 h1:f6cXq6RRfiyrOJEV7p3JhLDlmawGBVBBP1MggY8Mo4E=
|
||||
github.com/gomodule/redigo v1.8.8/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
||||
@ -299,19 +298,19 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/csrf v1.6.2 h1:QqQ/OWwuFp4jMKgBFAzJVW3FMULdyUW7JoM4pEWuqKg=
|
||||
github.com/gorilla/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug=
|
||||
github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
@ -454,8 +453,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@ -507,8 +504,8 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE=
|
||||
github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@ -529,22 +526,22 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/robfig/cron v1.0.0 h1:slmQxIUH6U9ruw4XoJ7C2pyyx4yYeiHx8S9pNootHsM=
|
||||
github.com/robfig/cron v1.0.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@ -593,8 +590,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -604,8 +602,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.233+incompatible h1:q+D/Y9jla3afgsIihtyhwyl0c2W+eRWNM9ohVwPiiPw=
|
||||
@ -622,13 +620,11 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.97 h1:JykYagPlleFuFIrk90uigS1UyIZPRIYX6TnC6FErWP4=
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.97/go.mod h1:oht5AKDJsk0fY6tV2ViqaVlOO14KSRmXZlI8ikK60Tg=
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.138 h1:u1dL+Dc1kWBTrufU4LrspRdvjhkxNESWfMHR/G4Pvcg=
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.138/go.mod h1:oht5AKDJsk0fY6tV2ViqaVlOO14KSRmXZlI8ikK60Tg=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -639,32 +635,32 @@ go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQc
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
|
||||
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.46.1 h1:Ifzy1lucGMQJh6wPRxusde8bWaDhYjSNOqDyn6Hb4TM=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.46.1/go.mod h1:YfFNem80G9UZ/mL5zd5GGXZSy95eXK+RhzIWBkLjLSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.51.0 h1:rXpHmgy1pMXlfv3W1T5ctoDA3QeTFjNq/YwCmwrfr8Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.51.0/go.mod h1:9uIRD3NZrM7QMQEGeKhr7V4xSDTMku3MPOVs8iZ3VVk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
||||
go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
|
||||
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.0.0 h1:cLhx8llHw02h5JTqGqaRbYn+QVKHmrzD9vEbKnSPk5U=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.0.0/go.mod h1:q10N1AolE1JjqKrFJK2tYw0iZpmX+HBaXBtuCzRnBGQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38=
|
||||
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
|
||||
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
|
||||
go.opentelemetry.io/otel/sdk v1.0.0/go.mod h1:PCrDHlSy5x1kjezSdL37PhbFUMjrsLRshJ2zCzeXwbM=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
|
||||
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
|
||||
go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
|
||||
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
@ -678,8 +674,8 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
|
||||
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
@ -703,8 +699,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -730,8 +726,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -758,13 +754,13 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
|
||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -817,16 +813,16 @@ golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -834,12 +830,11 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
@ -872,8 +867,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -885,8 +880,6 @@ google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@ -898,12 +891,12 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@ -912,8 +905,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
|
||||
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -962,22 +955,22 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
helm.sh/helm/v3 v3.14.2 h1:V71fv+NGZv0icBlr+in1MJXuUIHCiPG1hW9gEBISTIA=
|
||||
helm.sh/helm/v3 v3.14.2/go.mod h1:2itvvDv2WSZXTllknfQo6j7u3VVgMAvm8POCDgYH424=
|
||||
helm.sh/helm/v3 v3.14.4 h1:6FSpEfqyDalHq3kUr4gOMThhgY55kXUEjdQoyODYnrM=
|
||||
helm.sh/helm/v3 v3.14.4/go.mod h1:Tje7LL4gprZpuBNTbG34d1Xn5NmRT3OWfBRwpOSer9I=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A=
|
||||
k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA=
|
||||
k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o=
|
||||
k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis=
|
||||
k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
|
||||
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
|
||||
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
|
||||
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
|
||||
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
@ -22,6 +22,8 @@ const (
|
||||
|
||||
// ImageScanJobVendorType is name of scan job it will be used as key to register to job service.
|
||||
ImageScanJobVendorType = "IMAGE_SCAN"
|
||||
// SBOMJobVendorType key to create sbom generate execution.
|
||||
SBOMJobVendorType = "SBOM"
|
||||
// GarbageCollectionVendorType job name
|
||||
GarbageCollectionVendorType = "GARBAGE_COLLECTION"
|
||||
// ReplicationVendorType : the name of the replication job in job service
|
||||
@ -52,6 +54,7 @@ var (
|
||||
// executionSweeperCount stores the count for execution retained
|
||||
executionSweeperCount = map[string]int64{
|
||||
ImageScanJobVendorType: 1,
|
||||
SBOMJobVendorType: 1,
|
||||
ScanAllVendorType: 1,
|
||||
PurgeAuditVendorType: 10,
|
||||
ExecSweepVendorType: 10,
|
||||
|
@ -36,6 +36,8 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/rest"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/sbom"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package mgt
|
||||
|
||||
@ -18,6 +18,10 @@ type MockManager struct {
|
||||
func (_m *MockManager) GetJob(jobID string) (*job.Stats, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJob")
|
||||
}
|
||||
|
||||
var r0 *job.Stats
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*job.Stats, error)); ok {
|
||||
@ -44,6 +48,10 @@ func (_m *MockManager) GetJob(jobID string) (*job.Stats, error) {
|
||||
func (_m *MockManager) GetJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJobs")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
@ -77,6 +85,10 @@ func (_m *MockManager) GetJobs(q *query.Parameter) ([]*job.Stats, int64, error)
|
||||
func (_m *MockManager) GetPeriodicExecution(pID string, q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(pID, q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPeriodicExecution")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
@ -110,6 +122,10 @@ func (_m *MockManager) GetPeriodicExecution(pID string, q *query.Parameter) ([]*
|
||||
func (_m *MockManager) GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScheduledJobs")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
@ -143,6 +159,10 @@ func (_m *MockManager) GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64
|
||||
func (_m *MockManager) SaveJob(_a0 *job.Stats) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveJob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*job.Stats) error); ok {
|
||||
r0 = rf(_a0)
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package period
|
||||
|
||||
@ -13,6 +13,10 @@ type MockScheduler struct {
|
||||
func (_m *MockScheduler) Schedule(policy *Policy) (int64, error) {
|
||||
ret := _m.Called(policy)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Schedule")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*Policy) (int64, error)); ok {
|
||||
@ -42,6 +46,10 @@ func (_m *MockScheduler) Start() {
|
||||
func (_m *MockScheduler) UnSchedule(policyID string) error {
|
||||
ret := _m.Called(policyID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UnSchedule")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(policyID)
|
||||
|
26
src/lib/cache/mock_cache_test.go
vendored
26
src/lib/cache/mock_cache_test.go
vendored
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package cache
|
||||
|
||||
@ -18,6 +18,10 @@ type mockCache struct {
|
||||
func (_m *mockCache) Contains(ctx context.Context, key string) bool {
|
||||
ret := _m.Called(ctx, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Contains")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = rf(ctx, key)
|
||||
@ -32,6 +36,10 @@ func (_m *mockCache) Contains(ctx context.Context, key string) bool {
|
||||
func (_m *mockCache) Delete(ctx context.Context, key string) error {
|
||||
ret := _m.Called(ctx, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, key)
|
||||
@ -46,6 +54,10 @@ func (_m *mockCache) Delete(ctx context.Context, key string) error {
|
||||
func (_m *mockCache) Fetch(ctx context.Context, key string, value interface{}) error {
|
||||
ret := _m.Called(ctx, key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Fetch")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok {
|
||||
r0 = rf(ctx, key, value)
|
||||
@ -60,6 +72,10 @@ func (_m *mockCache) Fetch(ctx context.Context, key string, value interface{}) e
|
||||
func (_m *mockCache) Ping(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ping")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
@ -81,6 +97,10 @@ func (_m *mockCache) Save(ctx context.Context, key string, value interface{}, ex
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}, ...time.Duration) error); ok {
|
||||
r0 = rf(ctx, key, value, expiration...)
|
||||
@ -95,6 +115,10 @@ func (_m *mockCache) Save(ctx context.Context, key string, value interface{}, ex
|
||||
func (_m *mockCache) Scan(ctx context.Context, match string) (Iterator, error) {
|
||||
ret := _m.Called(ctx, match)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Scan")
|
||||
}
|
||||
|
||||
var r0 Iterator
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (Iterator, error)); ok {
|
||||
|
@ -27,4 +27,5 @@ const (
|
||||
DigestOfIconAccCosign = "sha256:20401d5b3a0f6dbc607c8d732eb08471af4ae6b19811a4efce8c6a724aed2882"
|
||||
DigestOfIconAccNotation = "sha256:3ac706e102bbe9362b400aa162df58135d35e66b9c3bee2165de92022d25fe34"
|
||||
DigestOfIconAccNydus = "sha256:dfcb6617cd9c144358dc1b305b87bbe34f0b619f1e329116e6aee2e41f2e34cf"
|
||||
DigestOfIconAccSBOM = "sha256:c19f80c357cd7e90d2a01b9ae3e2eb62ce447a2662bb590a19177d72d550bdae"
|
||||
)
|
||||
|
@ -33,6 +33,7 @@ var (
|
||||
model.TypeCosignSignature: icon.DigestOfIconAccCosign,
|
||||
model.TypeNotationSignature: icon.DigestOfIconAccNotation,
|
||||
model.TypeNydusAccelerator: icon.DigestOfIconAccNydus,
|
||||
model.TypeHarborSBOM: icon.DigestOfIconAccSBOM,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -77,8 +77,8 @@ const (
|
||||
// TypeSubject ...
|
||||
TypeSubject = "subject.accessory"
|
||||
|
||||
// TypeHarborSBOM identifies harbor.sbom
|
||||
TypeHarborSBOM = "harbor.sbom"
|
||||
// TypeHarborSBOM identifies sbom.harbor
|
||||
TypeHarborSBOM = "sbom.harbor"
|
||||
)
|
||||
|
||||
// AccessoryData ...
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
@ -75,8 +76,8 @@ func (m *Manager) ListRoles(ctx context.Context, projectID int64, userID int, gr
|
||||
return m.delegator.ListRoles(ctx, projectID, userID, groupIDs...)
|
||||
}
|
||||
|
||||
func (m *Manager) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) {
|
||||
return m.delegator.ListAdminRolesOfUser(ctx, userID)
|
||||
func (m *Manager) ListAdminRolesOfUser(ctx context.Context, user commonmodels.User) ([]models.Member, error) {
|
||||
return m.delegator.ListAdminRolesOfUser(ctx, user)
|
||||
}
|
||||
|
||||
func (m *Manager) Delete(ctx context.Context, id int64) error {
|
||||
|
@ -42,6 +42,7 @@ type EventData struct {
|
||||
Repository *Repository `json:"repository,omitempty"`
|
||||
Replication *model.Replication `json:"replication,omitempty"`
|
||||
Retention *model.Retention `json:"retention,omitempty"`
|
||||
Scan *model.Scan `json:"scan,omitempty"`
|
||||
Custom map[string]string `json:"custom_attributes,omitempty"`
|
||||
}
|
||||
|
||||
@ -51,6 +52,7 @@ type Resource struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
ScanOverview map[string]interface{} `json:"scan_overview,omitempty"`
|
||||
SBOMOverview map[string]interface{} `json:"sbom_overview,omitempty"`
|
||||
}
|
||||
|
||||
// Repository info of notification event
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
@ -43,7 +44,7 @@ type DAO interface {
|
||||
// ListRoles the roles of user for the specific project
|
||||
ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error)
|
||||
// ListAdminRolesOfUser returns the roles of user for the all projects
|
||||
ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error)
|
||||
ListAdminRolesOfUser(ctx context.Context, user commonmodels.User) ([]models.Member, error)
|
||||
}
|
||||
|
||||
// New returns an instance of the default DAO
|
||||
@ -202,19 +203,39 @@ func (d *dao) ListRoles(ctx context.Context, projectID int64, userID int, groupI
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (d *dao) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) {
|
||||
func (d *dao) ListAdminRolesOfUser(ctx context.Context, user commonmodels.User) ([]models.Member, error) {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql := `select b.* from project as a left join project_member as b on a.project_id = b.project_id where a.deleted = 'f' and b.entity_id = ? and b.entity_type = 'u' and b.role = 1;`
|
||||
|
||||
var members []models.Member
|
||||
_, err = o.Raw(sql, userID).QueryRows(&members)
|
||||
var membersU []models.Member
|
||||
sqlU := `select b.* from project as a left join project_member as b on a.project_id = b.project_id where a.deleted = 'f' and b.entity_id = ? and b.entity_type = 'u' and b.role = 1;`
|
||||
_, err = o.Raw(sqlU, user.UserID).QueryRows(&membersU)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var membersG []models.Member
|
||||
if len(user.GroupIDs) > 0 {
|
||||
var params []interface{}
|
||||
params = append(params, user.GroupIDs)
|
||||
sqlG := fmt.Sprintf(`select b.* from project as a
|
||||
left join project_member as b on a.project_id = b.project_id
|
||||
where a.deleted = 'f' and b.entity_id in ( %s ) and b.entity_type = 'g' and b.role = 1;`, orm.ParamPlaceholderForIn(len(user.GroupIDs)))
|
||||
_, err = o.Raw(sqlG, params).QueryRows(&membersG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var members []models.Member
|
||||
if len(membersU) > 0 {
|
||||
members = append(members, membersU...)
|
||||
}
|
||||
if len(membersG) > 0 {
|
||||
members = append(members, membersG...)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
@ -341,6 +342,42 @@ func (suite *DaoTestSuite) TestListByMember() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestListAdminRolesOfUser() {
|
||||
{
|
||||
// projectAdmin and user groups
|
||||
suite.WithUser(func(userID int64, username string) {
|
||||
project := &models.Project{
|
||||
Name: utils.GenerateRandomString(),
|
||||
OwnerID: int(userID),
|
||||
}
|
||||
projectID, err := suite.dao.Create(orm.Context(), project)
|
||||
suite.Nil(err)
|
||||
|
||||
defer suite.dao.Delete(orm.Context(), projectID)
|
||||
|
||||
suite.WithUserGroup(func(groupID int64, groupName string) {
|
||||
|
||||
o, err := orm.FromContext(orm.Context())
|
||||
if err != nil {
|
||||
suite.Fail("got error %v", err)
|
||||
}
|
||||
|
||||
var pid int64
|
||||
suite.Nil(o.Raw("INSERT INTO project_member (project_id, entity_id, role, entity_type) values (?, ?, ?, ?) RETURNING id", projectID, groupID, common.RoleProjectAdmin, "g").QueryRow(&pid))
|
||||
defer o.Raw("DELETE FROM project_member WHERE id = ?", pid)
|
||||
|
||||
userTest := commonmodels.User{
|
||||
UserID: int(userID),
|
||||
GroupIDs: []int{int(groupID)},
|
||||
}
|
||||
roles, err := suite.dao.ListAdminRolesOfUser(orm.Context(), userTest)
|
||||
suite.Nil(err)
|
||||
suite.Len(roles, 2)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DaoTestSuite) TestListRoles() {
|
||||
{
|
||||
// only projectAdmin
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
@ -47,7 +48,7 @@ type Manager interface {
|
||||
ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error)
|
||||
|
||||
// ListAdminRolesOfUser returns the roles of user for the all projects
|
||||
ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error)
|
||||
ListAdminRolesOfUser(ctx context.Context, user commonmodels.User) ([]models.Member, error)
|
||||
}
|
||||
|
||||
// New returns a default implementation of Manager
|
||||
@ -124,6 +125,6 @@ func (m *manager) ListRoles(ctx context.Context, projectID int64, userID int, gr
|
||||
}
|
||||
|
||||
// ListAdminRolesOfUser returns the roles of user for the all projects
|
||||
func (m *manager) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) {
|
||||
return m.dao.ListAdminRolesOfUser(ctx, userID)
|
||||
func (m *manager) ListAdminRolesOfUser(ctx context.Context, user commonmodels.User) ([]models.Member, error) {
|
||||
return m.dao.ListAdminRolesOfUser(ctx, user)
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ func (c *Client) getProjects() ([]*Project, error) {
|
||||
|
||||
func (c *Client) getProjectsByName(name string) ([]*Project, error) {
|
||||
var projects []*Project
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects?search=%s&search_namespaces=true&per_page=50", c.url, name)
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects?search=%s&membership=true&search_namespaces=true&per_page=50", c.url, name)
|
||||
if err := c.GetAndIteratePagination(urlAPI, &projects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -8,15 +8,16 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
adp "github.com/goharbor/harbor/src/pkg/reg/adapter"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/adapter/native"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
volcCR "github.com/volcengine/volcengine-go-sdk/service/cr"
|
||||
"github.com/volcengine/volcengine-go-sdk/volcengine"
|
||||
"github.com/volcengine/volcengine-go-sdk/volcengine/credentials"
|
||||
volcSession "github.com/volcengine/volcengine-go-sdk/volcengine/session"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
adp "github.com/goharbor/harbor/src/pkg/reg/adapter"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/adapter/native"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
)
|
||||
|
||||
func getMockAdapter_withoutCred(t *testing.T, hasCred, health bool) (*adapter, *httptest.Server) {
|
||||
@ -94,16 +95,17 @@ func TestAdapter_NewAdapter_InvalidURL(t *testing.T) {
|
||||
assert.Nil(t, adapter)
|
||||
}
|
||||
|
||||
func TestAdapter_NewAdapter_PingFailed(t *testing.T) {
|
||||
factory, _ := adp.GetFactory(model.RegistryTypeVolcCR)
|
||||
adapter, err := factory.Create(&model.Registry{
|
||||
Type: model.RegistryTypeVolcCR,
|
||||
Credential: &model.Credential{},
|
||||
URL: "https://cr-test-cn-beijing.cr.volces.com",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, adapter)
|
||||
}
|
||||
// remove it because failed
|
||||
// func TestAdapter_NewAdapter_PingFailed(t *testing.T) {
|
||||
// factory, _ := adp.GetFactory(model.RegistryTypeVolcCR)
|
||||
// adapter, err := factory.Create(&model.Registry{
|
||||
// Type: model.RegistryTypeVolcCR,
|
||||
// Credential: &model.Credential{},
|
||||
// URL: "https://cr-test-cn-beijing.cr.volces.com",
|
||||
// })
|
||||
// assert.Error(t, err)
|
||||
// assert.Nil(t, adapter)
|
||||
// }
|
||||
|
||||
func TestAdapter_Info(t *testing.T) {
|
||||
a, s := getMockAdapter_withoutCred(t, true, true)
|
||||
|
@ -16,6 +16,7 @@ package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
@ -38,6 +39,8 @@ type DAO interface {
|
||||
UpdateReportData(ctx context.Context, uuid string, report string) error
|
||||
// Update update report
|
||||
Update(ctx context.Context, r *Report, cols ...string) error
|
||||
// DeleteByExtraAttr delete the scan_report by mimeType and extra attribute
|
||||
DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error
|
||||
}
|
||||
|
||||
// New returns an instance of the default DAO
|
||||
@ -110,3 +113,14 @@ func (d *dao) Update(ctx context.Context, r *Report, cols ...string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dao) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delReportSQL := "delete from scan_report where mime_type = ? and report::jsonb @> ?"
|
||||
dgstJSONStr := fmt.Sprintf(`{"%s":"%s"}`, attrName, attrValue)
|
||||
_, err = o.Raw(delReportSQL, mimeType, dgstJSONStr).Exec()
|
||||
return err
|
||||
}
|
||||
|
@ -53,7 +53,6 @@ func (suite *ReportTestSuite) SetupTest() {
|
||||
RegistrationUUID: "ruuid",
|
||||
MimeType: v1.MimeTypeNativeReport,
|
||||
}
|
||||
|
||||
suite.create(r)
|
||||
}
|
||||
|
||||
@ -61,6 +60,8 @@ func (suite *ReportTestSuite) SetupTest() {
|
||||
func (suite *ReportTestSuite) TearDownTest() {
|
||||
_, err := suite.dao.DeleteMany(orm.Context(), q.Query{Keywords: q.KeyWords{"uuid": "uuid"}})
|
||||
require.NoError(suite.T(), err)
|
||||
_, err = suite.dao.DeleteMany(orm.Context(), q.Query{Keywords: q.KeyWords{"uuid": "uuid3"}})
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
// TestReportList tests list reports with query parameters.
|
||||
@ -95,7 +96,7 @@ func (suite *ReportTestSuite) TestReportUpdateReportData() {
|
||||
err := suite.dao.UpdateReportData(orm.Context(), "uuid", "{}")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
l, err := suite.dao.List(orm.Context(), nil)
|
||||
l, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"uuid": "uuid"}))
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(1, len(l))
|
||||
suite.Equal("{}", l[0].Report)
|
||||
|
@ -66,8 +66,9 @@ type Registration struct {
|
||||
Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"`
|
||||
|
||||
// Timestamps
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
Capabilities map[string]interface{} `orm:"-" json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// TableName for Endpoint
|
||||
@ -151,15 +152,20 @@ func (r *Registration) HasCapability(manifestMimeType string) bool {
|
||||
}
|
||||
|
||||
// GetProducesMimeTypes returns produces mime types for the artifact
|
||||
func (r *Registration) GetProducesMimeTypes(mimeType string) []string {
|
||||
func (r *Registration) GetProducesMimeTypes(mimeType string, scanType string) []string {
|
||||
if r.Metadata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, capability := range r.Metadata.Capabilities {
|
||||
for _, mt := range capability.ConsumesMimeTypes {
|
||||
if mt == mimeType {
|
||||
return capability.ProducesMimeTypes
|
||||
capType := capability.Type
|
||||
if len(capType) == 0 {
|
||||
capType = v1.ScanTypeVulnerability
|
||||
}
|
||||
if scanType == capType {
|
||||
for _, mt := range capability.ConsumesMimeTypes {
|
||||
if mt == mimeType {
|
||||
return capability.ProducesMimeTypes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
src/pkg/scan/handler.go
Normal file
69
src/pkg/scan/handler.go
Normal file
@ -0,0 +1,69 @@
|
||||
// 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 scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
var handlerRegistry = map[string]Handler{}
|
||||
|
||||
// RegisterScanHanlder register scanner handler
|
||||
func RegisterScanHanlder(requestType string, handler Handler) {
|
||||
handlerRegistry[requestType] = handler
|
||||
}
|
||||
|
||||
// GetScanHandler get the handler
|
||||
func GetScanHandler(requestType string) Handler {
|
||||
return handlerRegistry[requestType]
|
||||
}
|
||||
|
||||
// Handler handler for scan job, it could be implement by different scan type, such as vulnerability, sbom
|
||||
type Handler interface {
|
||||
// RequestProducesMineTypes returns the produces mime types
|
||||
RequestProducesMineTypes() []string
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
RequiredPermissions() []*types.Policy
|
||||
// RequestParameters defines the parameters for scan request
|
||||
RequestParameters() map[string]interface{}
|
||||
// PostScan defines the operation after scan
|
||||
PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
|
||||
ReportHandler
|
||||
// JobVendorType returns the job vendor type
|
||||
JobVendorType() string
|
||||
}
|
||||
|
||||
// ReportHandler handler for scan report, it could be sbom report or vulnerability report
|
||||
type ReportHandler interface {
|
||||
// URLParameter defines the parameters for scan report
|
||||
URLParameter(sr *v1.ScanRequest) (string, error)
|
||||
// Update update the report data in the database by UUID
|
||||
Update(ctx context.Context, uuid string, report string) error
|
||||
// MakePlaceHolder make the report place holder, if exist, delete it and create a new one
|
||||
MakePlaceHolder(ctx context.Context, art *artifact.Artifact, r *scanner.Registration) (rps []*scan.Report, err error)
|
||||
// GetPlaceHolder get the the report place holder
|
||||
GetPlaceHolder(ctx context.Context, artRepo string, artDigest string, scannerUUID string, mimeType string) (rp *scan.Report, err error)
|
||||
// GetSummary get the summary of the report
|
||||
GetSummary(ctx context.Context, ar *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error)
|
||||
}
|
@ -35,7 +35,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
@ -145,6 +144,7 @@ func (j *Job) Validate(params job.Parameters) error {
|
||||
func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
// Get logger
|
||||
myLogger := ctx.GetLogger()
|
||||
startTime := time.Now()
|
||||
|
||||
// shouldStop checks if the job should be stopped
|
||||
shouldStop := func() bool {
|
||||
@ -160,6 +160,11 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
r, _ := extractRegistration(params)
|
||||
req, _ := ExtractScanReq(params)
|
||||
mimeTypes, _ := extractMimeTypes(params)
|
||||
scanType := v1.ScanTypeVulnerability
|
||||
if len(req.RequestType) > 0 {
|
||||
scanType = req.RequestType[0].Type
|
||||
}
|
||||
handler := GetScanHandler(scanType)
|
||||
|
||||
// Print related infos to log
|
||||
printJSONParameter(JobParamRegistration, removeRegistrationAuthInfo(r), myLogger)
|
||||
@ -236,29 +241,24 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
|
||||
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
|
||||
|
||||
rawReport, err := client.GetScanReport(resp.ID, m)
|
||||
reportURLParameter, err := handler.URLParameter(req)
|
||||
if err != nil {
|
||||
errs[i] = errors.Wrap(err, "scan job: get report url")
|
||||
return
|
||||
}
|
||||
rawReport, err := fetchScanReportFromScanner(client, resp.ID, m, reportURLParameter)
|
||||
if err != nil {
|
||||
// Not ready yet
|
||||
if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok {
|
||||
// Reset to the new check interval
|
||||
tm.Reset(time.Duration(notReadyErr.RetryAfter) * time.Second)
|
||||
myLogger.Infof("Report with mime type %s is not ready yet, retry after %d seconds", m, notReadyErr.RetryAfter)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
errs[i] = errors.Wrap(err, fmt.Sprintf("check scan report with mime type %s", m))
|
||||
errs[i] = errors.Wrap(err, fmt.Sprintf("scan job: fetch scan report, mimetype %v", m))
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the data is aligned with the v1 spec.
|
||||
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
||||
errs[i] = errors.Wrap(err, "scan job: resolve report data")
|
||||
return
|
||||
}
|
||||
|
||||
rawReports[i] = rawReport
|
||||
|
||||
return
|
||||
case <-ctx.SystemContext().Done():
|
||||
// Terminated by system
|
||||
@ -292,33 +292,19 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
// Log error to the job log
|
||||
if err != nil {
|
||||
myLogger.Error(err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for i, mimeType := range mimeTypes {
|
||||
reports, err := report.Mgr.GetBy(ctx.SystemContext(), req.Artifact.Digest, r.UUID, []string{mimeType})
|
||||
rp, err := handler.GetPlaceHolder(ctx.SystemContext(), req.Artifact.Repository, req.Artifact.Digest, r.UUID, mimeType)
|
||||
if err != nil {
|
||||
myLogger.Error("Failed to get report for artifact %s of mimetype %s, error %v", req.Artifact.Digest, mimeType, err)
|
||||
|
||||
return err
|
||||
}
|
||||
myLogger.Debugf("Converting report ID %s to the new V2 schema", rp.UUID)
|
||||
|
||||
if len(reports) == 0 {
|
||||
myLogger.Error("No report found for artifact %s of mimetype %s, error %v", req.Artifact.Digest, mimeType, err)
|
||||
|
||||
return errors.NotFoundError(nil).WithMessage("no report found to update data")
|
||||
}
|
||||
|
||||
rp := reports[0]
|
||||
|
||||
logger.Debugf("Converting report ID %s to the new V2 schema", rp.UUID)
|
||||
|
||||
// use a new ormer here to use the short db connection
|
||||
_, reportData, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), rp.UUID, rp.RegistrationUUID, rp.Digest, rawReports[i])
|
||||
reportData, err := handler.PostScan(ctx, req, rp, rawReports[i], startTime, robotAccount)
|
||||
if err != nil {
|
||||
myLogger.Errorf("Failed to convert vulnerability data to new schema for report %s, error %v", rp.UUID, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -326,18 +312,28 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
// this is required since the top level layers relay on the vuln.Report struct that
|
||||
// contains additional metadata within the report which if stored in the new columns within the scan_report table
|
||||
// would be redundant
|
||||
if err := report.Mgr.UpdateReportData(ctx.SystemContext(), rp.UUID, reportData); err != nil {
|
||||
if err := handler.Update(ctx.SystemContext(), rp.UUID, reportData); err != nil {
|
||||
myLogger.Errorf("Failed to update report data for report %s, error %v", rp.UUID, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
myLogger.Debugf("Converted report ID %s to the new V2 schema", rp.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchScanReportFromScanner(client v1.Client, requestID string, mimType string, urlParameter string) (rawReport string, err error) {
|
||||
rawReport, err = client.GetScanReport(requestID, mimType, urlParameter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Make sure the data is aligned with the v1 spec.
|
||||
if _, err = report.ResolveData(mimType, []byte(rawReport)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return rawReport, nil
|
||||
}
|
||||
|
||||
// ExtractScanReq extracts the scan request from the job parameters.
|
||||
func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
|
||||
v, ok := params[JobParameterRequest]
|
||||
@ -361,7 +357,20 @@ func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqType := v1.ScanTypeVulnerability
|
||||
// attach the request with ProducesMimeTypes and Parameters
|
||||
if len(req.RequestType) > 0 {
|
||||
// current only support requestType with one element for each request
|
||||
if len(req.RequestType[0].Type) > 0 {
|
||||
reqType = req.RequestType[0].Type
|
||||
}
|
||||
handler := GetScanHandler(reqType)
|
||||
if handler == nil {
|
||||
return nil, errors.Errorf("failed to get scan handler, request type %v", reqType)
|
||||
}
|
||||
req.RequestType[0].ProducesMimeTypes = handler.RequestProducesMineTypes()
|
||||
req.RequestType[0].Parameters = handler.RequestParameters()
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@ -394,6 +403,7 @@ func removeScanAuthInfo(sr *v1.ScanRequest) string {
|
||||
URL: sr.Registry.URL,
|
||||
Authorization: "[HIDDEN]",
|
||||
},
|
||||
RequestType: sr.RequestType,
|
||||
}
|
||||
|
||||
str, err := req.ToJSON()
|
||||
|
@ -19,15 +19,19 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
mockjobservice "github.com/goharbor/harbor/src/testing/jobservice"
|
||||
mocktesting "github.com/goharbor/harbor/src/testing/mock"
|
||||
v1testing "github.com/goharbor/harbor/src/testing/pkg/scan/rest/v1"
|
||||
@ -35,10 +39,11 @@ import (
|
||||
|
||||
// JobTestSuite is a test suite to test the scan job.
|
||||
type JobTestSuite struct {
|
||||
suite.Suite
|
||||
htesting.Suite
|
||||
|
||||
defaultClientPool v1.ClientPool
|
||||
mcp *v1testing.ClientPool
|
||||
reportIDs []string
|
||||
}
|
||||
|
||||
// TestJob is the entry of JobTestSuite.
|
||||
@ -48,6 +53,7 @@ func TestJob(t *testing.T) {
|
||||
|
||||
// SetupSuite sets up test env for JobTestSuite.
|
||||
func (suite *JobTestSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
mcp := &v1testing.ClientPool{}
|
||||
suite.defaultClientPool = v1.DefaultClientPool
|
||||
v1.DefaultClientPool = mcp
|
||||
@ -55,9 +61,12 @@ func (suite *JobTestSuite) SetupSuite() {
|
||||
suite.mcp = mcp
|
||||
}
|
||||
|
||||
// TeraDownSuite clears test env for TeraDownSuite.
|
||||
func (suite *JobTestSuite) TeraDownSuite() {
|
||||
// TearDownSuite clears test env for TearDownSuite.
|
||||
func (suite *JobTestSuite) TearDownSuite() {
|
||||
v1.DefaultClientPool = suite.defaultClientPool
|
||||
for _, id := range suite.reportIDs {
|
||||
_ = report.Mgr.Delete(suite.Context(), id)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJob tests the scan job
|
||||
@ -151,3 +160,40 @@ func (suite *JobTestSuite) TestJob() {
|
||||
err = j.Run(ctx, jp)
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
func (suite *JobTestSuite) TestfetchScanReportFromScanner() {
|
||||
vulnRpt := &vuln.Report{
|
||||
GeneratedAt: time.Now().UTC().String(),
|
||||
Scanner: &v1.Scanner{
|
||||
Name: "Trivy",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Severity: vuln.High,
|
||||
}
|
||||
rptContent, err := json.Marshal(vulnRpt)
|
||||
require.NoError(suite.T(), err)
|
||||
rawContent := string(rptContent)
|
||||
ctx := suite.Context()
|
||||
dgst := "sha256:mydigest"
|
||||
uuid := `7f20b1b9-6117-4a2e-820b-e4cc0401f15a`
|
||||
scannerUUID := `7f20b1b9-6117-4a2e-820b-e4cc0401f15b`
|
||||
rpt := &scan.Report{
|
||||
UUID: uuid,
|
||||
RegistrationUUID: scannerUUID,
|
||||
Digest: dgst,
|
||||
MimeType: v1.MimeTypeDockerArtifact,
|
||||
Report: rawContent,
|
||||
}
|
||||
|
||||
ctx = suite.Context()
|
||||
rptID, err := report.Mgr.Create(ctx, rpt)
|
||||
suite.reportIDs = append(suite.reportIDs, rptID)
|
||||
require.NoError(suite.T(), err)
|
||||
client := &v1testing.Client{}
|
||||
client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport, mock.Anything).Return(rawContent, nil)
|
||||
parameters := "sbom_media_type=application/spdx+json"
|
||||
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport, parameters)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), rawContent, rawRept)
|
||||
}
|
||||
|
@ -354,25 +354,28 @@ func (c *nativeToRelationalSchemaConverter) updateReport(ctx context.Context, vu
|
||||
return report.Mgr.Update(ctx, r, "CriticalCnt", "HighCnt", "MediumCnt", "LowCnt", "NoneCnt", "UnknownCnt", "FixableCnt")
|
||||
}
|
||||
|
||||
// CVSS ...
|
||||
type CVSS struct {
|
||||
NVD Nvd `json:"nvd"`
|
||||
// CVS ...
|
||||
type CVS struct {
|
||||
CVSS map[string]map[string]interface{} `json:"CVSS"`
|
||||
}
|
||||
|
||||
// Nvd ...
|
||||
type Nvd struct {
|
||||
V3Score float64 `json:"V3Score"`
|
||||
}
|
||||
|
||||
func parseScoreFromVendorAttribute(ctx context.Context, vendorAttribute string) (NvdV3Score float64) {
|
||||
var data map[string]CVSS
|
||||
func parseScoreFromVendorAttribute(ctx context.Context, vendorAttribute string) float64 {
|
||||
var data CVS
|
||||
err := json.Unmarshal([]byte(vendorAttribute), &data)
|
||||
if err != nil {
|
||||
log.G(ctx).Errorf("failed to parse vendor_attribute, error %v", err)
|
||||
return 0
|
||||
}
|
||||
if cvss, ok := data["CVSS"]; ok {
|
||||
return cvss.NVD.V3Score
|
||||
|
||||
// set the nvd as the first priority, if it's unavailable, return the first V3Score available.
|
||||
if val, ok := data.CVSS["nvd"]["V3Score"]; ok {
|
||||
return val.(float64)
|
||||
}
|
||||
|
||||
for vendor := range data.CVSS {
|
||||
if val, ok := data.CVSS[vendor]["V3Score"]; ok {
|
||||
return val.(float64)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@ -578,6 +578,8 @@ func Test_parseScoreFromVendorAttribute(t *testing.T) {
|
||||
{"both", args{`{"CVSS":{"nvd":{"V3Score":5.5,"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"},"redhat":{"V3Score":6.2,"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}}}`}, 5.5},
|
||||
{"both2", args{`{"CVSS":{"nvd":{"V2Score":7.2,"V2Vector":"AV:L/AC:L/Au:N/C:C/I:C/A:C","V3Score":7.8,"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"},"redhat":{"V3Score":7.8,"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}}}`}, 7.8},
|
||||
{"none", args{`{"CVSS":{"nvd":{"V2Score":7.2,"V2Vector":"AV:L/AC:L/Au:N/C:C/I:C/A:C","V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"},"redhat":{"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}}}`}, 0},
|
||||
{"redhatonly", args{`{"CVSS":{"redhat":{"V3Score":8.8, "V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}}}`}, 8.8},
|
||||
{"nvdnov3butredhat", args{`{"CVSS":{"nvd":{"V2Score":7.2,"V2Vector":"AV:L/AC:L/Au:N/C:C/I:C/A:C"},"redhat":{"V3Score":7.8,"V3Vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}}}`}, 7.8},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -104,6 +104,8 @@ type Manager interface {
|
||||
|
||||
// Update update report information
|
||||
Update(ctx context.Context, r *scan.Report, cols ...string) error
|
||||
// DeleteByExtraAttr delete scan_report by sbom_digest
|
||||
DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error
|
||||
}
|
||||
|
||||
// basicManager is a default implementation of report manager.
|
||||
@ -226,3 +228,7 @@ func (bm *basicManager) List(ctx context.Context, query *q.Query) ([]*scan.Repor
|
||||
func (bm *basicManager) Update(ctx context.Context, r *scan.Report, cols ...string) error {
|
||||
return bm.dao.Update(ctx, r, cols...)
|
||||
}
|
||||
|
||||
func (bm *basicManager) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error {
|
||||
return bm.dao.DeleteByExtraAttr(ctx, mimeType, attrName, attrValue)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ type Client interface {
|
||||
// Returns:
|
||||
// string : the scan report of the given artifact
|
||||
// error : non nil error if any errors occurred
|
||||
GetScanReport(scanRequestID, reportMIMEType string) (string, error)
|
||||
GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error)
|
||||
}
|
||||
|
||||
// basicClient is default implementation of the Client interface
|
||||
@ -97,7 +97,7 @@ func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Cli
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
@ -167,7 +167,7 @@ func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) {
|
||||
}
|
||||
|
||||
// GetScanReport ...
|
||||
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
|
||||
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error) {
|
||||
if len(scanRequestID) == 0 {
|
||||
return "", errors.New("empty scan request ID")
|
||||
}
|
||||
@ -177,8 +177,11 @@ func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (strin
|
||||
}
|
||||
|
||||
def := c.spec.GetScanReport(scanRequestID, reportMIMEType)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, def.URL, nil)
|
||||
reportURL := def.URL
|
||||
if len(urlParameter) > 0 {
|
||||
reportURL = fmt.Sprintf("%s?%s", def.URL, urlParameter)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, reportURL, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "v1 client: get scan report")
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (suite *ClientTestSuite) TestClientSubmitScan() {
|
||||
|
||||
// TestClientGetScanReportError tests getting report failed
|
||||
func (suite *ClientTestSuite) TestClientGetScanReportError() {
|
||||
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport)
|
||||
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport, "")
|
||||
require.Error(suite.T(), err)
|
||||
assert.Condition(suite.T(), func() (success bool) {
|
||||
success = strings.Index(err.Error(), "error") != -1
|
||||
@ -82,14 +82,14 @@ func (suite *ClientTestSuite) TestClientGetScanReportError() {
|
||||
|
||||
// TestClientGetScanReport tests getting report
|
||||
func (suite *ClientTestSuite) TestClientGetScanReport() {
|
||||
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport)
|
||||
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport, "")
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotEmpty(suite.T(), res)
|
||||
}
|
||||
|
||||
// TestClientGetScanReportNotReady tests the case that the report is not ready
|
||||
func (suite *ClientTestSuite) TestClientGetScanReportNotReady() {
|
||||
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport)
|
||||
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport, "")
|
||||
require.Error(suite.T(), err)
|
||||
require.Condition(suite.T(), func() (success bool) {
|
||||
_, success = err.(*ReportNotReadyError)
|
||||
|
@ -21,6 +21,17 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
supportVulnerability = "support_vulnerability"
|
||||
supportSBOM = "support_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
|
||||
// returned result.
|
||||
@ -98,7 +109,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
|
||||
}
|
||||
@ -112,6 +123,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 {
|
||||
@ -138,6 +158,28 @@ func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapabil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertCapability converts the capability to map, used in get scanner API
|
||||
func (md *ScannerAdapterMetadata) ConvertCapability() map[string]interface{} {
|
||||
capabilities := make(map[string]interface{})
|
||||
oldScanner := true
|
||||
for _, c := range md.Capabilities {
|
||||
if len(c.Type) > 0 {
|
||||
oldScanner = false
|
||||
}
|
||||
if c.Type == ScanTypeVulnerability {
|
||||
capabilities[supportVulnerability] = true
|
||||
} else if c.Type == ScanTypeSbom {
|
||||
capabilities[supportSBOM] = true
|
||||
}
|
||||
}
|
||||
if oldScanner && len(capabilities) == 0 {
|
||||
// to compatible with old version scanner, suppose they should always support scan vulnerability when capability is empty
|
||||
capabilities[supportVulnerability] = true
|
||||
capabilities[supportSBOM] = false
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
// Artifact represents an artifact stored in Registry.
|
||||
type Artifact struct {
|
||||
// ID of the namespace (project). It will not be sent to scanner adapter.
|
||||
@ -164,6 +206,8 @@ type Registry struct {
|
||||
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry for getting or exchanging token.
|
||||
// For example, `Basic: Base64(username:password)`.
|
||||
Authorization string `json:"authorization"`
|
||||
// Insecure is an indicator of https or http.
|
||||
Insecure bool `json:"insecure"`
|
||||
}
|
||||
|
||||
// ScanRequest represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
|
||||
@ -173,6 +217,18 @@ type ScanRequest struct {
|
||||
Registry *Registry `json:"registry"`
|
||||
// Artifact to be scanned.
|
||||
Artifact *Artifact `json:"artifact"`
|
||||
// RequestType
|
||||
RequestType []*ScanType `json:"enabled_capabilities"`
|
||||
}
|
||||
|
||||
// ScanType represent the type of the scan request
|
||||
type ScanType struct {
|
||||
// Type sets the type of the scan, it could be sbom or vulnerability, default is vulnerability
|
||||
Type string `json:"type"`
|
||||
// ProducesMimeTypes defines scanreport should be
|
||||
ProducesMimeTypes []string `json:"produces_mime_types"`
|
||||
// Parameters extra parameters
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
// FromJSON parses ScanRequest from json data
|
||||
|
41
src/pkg/scan/rest/v1/models_test.go
Normal file
41
src/pkg/scan/rest/v1/models_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
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")
|
||||
}
|
||||
|
||||
func TestConvertCapability(t *testing.T) {
|
||||
md := &ScannerAdapterMetadata{
|
||||
Capabilities: []*ScannerCapability{
|
||||
{Type: ScanTypeSbom},
|
||||
{Type: ScanTypeVulnerability},
|
||||
},
|
||||
}
|
||||
result := md.ConvertCapability()
|
||||
assert.Equal(t, result[supportSBOM], true)
|
||||
assert.Equal(t, result[supportVulnerability], true)
|
||||
}
|
||||
|
||||
func TestConvertCapabilityOldScaner(t *testing.T) {
|
||||
md := &ScannerAdapterMetadata{
|
||||
Capabilities: []*ScannerCapability{
|
||||
{
|
||||
ConsumesMimeTypes: []string{"application/vnd.oci.image.manifest.v1+json", "application/vnd.docker.distribution.manifest.v2+json"},
|
||||
ProducesMimeTypes: []string{MimeTypeNativeReport},
|
||||
},
|
||||
},
|
||||
}
|
||||
result := md.ConvertCapability()
|
||||
assert.Equal(t, result[supportSBOM], false)
|
||||
assert.Equal(t, result[supportVulnerability], true)
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
||||
|
126
src/pkg/scan/sbom/dao/dao.go
Normal file
126
src/pkg/scan/sbom/dao/dao.go
Normal file
@ -0,0 +1,126 @@
|
||||
// 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 dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(model.Report))
|
||||
}
|
||||
|
||||
// DAO is the data access object interface for sbom report
|
||||
type DAO interface {
|
||||
// Create creates new report
|
||||
Create(ctx context.Context, r *model.Report) (int64, error)
|
||||
// DeleteMany delete the reports according to the query
|
||||
DeleteMany(ctx context.Context, query q.Query) (int64, error)
|
||||
// List lists the reports with given query parameters.
|
||||
List(ctx context.Context, query *q.Query) ([]*model.Report, error)
|
||||
// UpdateReportData only updates the `report` column with conditions matched.
|
||||
UpdateReportData(ctx context.Context, uuid string, report string) error
|
||||
// Update update report
|
||||
Update(ctx context.Context, r *model.Report, cols ...string) error
|
||||
// DeleteByExtraAttr delete the scan_report by mimeType and extra attribute
|
||||
DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error
|
||||
}
|
||||
|
||||
// New returns an instance of the default DAO
|
||||
func New() DAO {
|
||||
return &dao{}
|
||||
}
|
||||
|
||||
type dao struct{}
|
||||
|
||||
// Create creates new sbom report
|
||||
func (d *dao) Create(ctx context.Context, r *model.Report) (int64, error) {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return o.Insert(r)
|
||||
}
|
||||
|
||||
func (d *dao) DeleteMany(ctx context.Context, query q.Query) (int64, error) {
|
||||
if len(query.Keywords) == 0 {
|
||||
return 0, errors.New("delete all sbom reports at once is not allowed")
|
||||
}
|
||||
|
||||
qs, err := orm.QuerySetter(ctx, &model.Report{}, &query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return qs.Delete()
|
||||
}
|
||||
|
||||
func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.Report, error) {
|
||||
qs, err := orm.QuerySetter(ctx, &model.Report{}, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reports := []*model.Report{}
|
||||
if _, err = qs.All(&reports); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// UpdateReportData only updates the `report` column with conditions matched.
|
||||
func (d *dao) UpdateReportData(ctx context.Context, uuid string, report string) error {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qt := o.QueryTable(new(model.Report))
|
||||
|
||||
data := make(orm.Params)
|
||||
data["report"] = report
|
||||
|
||||
_, err = qt.Filter("uuid", uuid).Update(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dao) Update(ctx context.Context, r *model.Report, cols ...string) error {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := o.Update(r, cols...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dao) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error {
|
||||
o, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delReportSQL := "delete from sbom_report where mime_type = ? and report::jsonb @> ?"
|
||||
dgstJSONStr := fmt.Sprintf(`{"%s":"%s"}`, attrName, attrValue)
|
||||
_, err = o.Raw(delReportSQL, mimeType, dgstJSONStr).Exec()
|
||||
return err
|
||||
}
|
133
src/pkg/scan/sbom/dao/dao_test.go
Normal file
133
src/pkg/scan/sbom/dao/dao_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
)
|
||||
|
||||
// ReportTestSuite is test suite of testing report DAO.
|
||||
type ReportTestSuite struct {
|
||||
htesting.Suite
|
||||
dao DAO
|
||||
}
|
||||
|
||||
// TestReport is the entry of ReportTestSuite.
|
||||
func TestReport(t *testing.T) {
|
||||
suite.Run(t, &ReportTestSuite{})
|
||||
}
|
||||
|
||||
// SetupSuite prepares env for test suite.
|
||||
func (suite *ReportTestSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
suite.dao = New()
|
||||
}
|
||||
|
||||
// SetupTest prepares env for test case.
|
||||
func (suite *ReportTestSuite) SetupTest() {
|
||||
sbomReport := &model.Report{
|
||||
UUID: "uuid",
|
||||
ArtifactID: 111,
|
||||
RegistrationUUID: "ruuid",
|
||||
MimeType: v1.MimeTypeSBOMReport,
|
||||
ReportSummary: `{"sbom_digest": "sha256:abc"}`,
|
||||
}
|
||||
suite.create(sbomReport)
|
||||
}
|
||||
|
||||
// TearDownTest clears enf for test case.
|
||||
func (suite *ReportTestSuite) TearDownTest() {
|
||||
_, err := suite.dao.DeleteMany(orm.Context(), q.Query{Keywords: q.KeyWords{"uuid": "uuid"}})
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
func (suite *ReportTestSuite) TestDeleteReportBySBOMDigest() {
|
||||
l, err := suite.dao.List(orm.Context(), nil)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, len(l))
|
||||
err = suite.dao.DeleteByExtraAttr(orm.Context(), v1.MimeTypeSBOMReport, "sbom_digest", "sha256:abc")
|
||||
suite.Require().NoError(err)
|
||||
l2, err := suite.dao.List(orm.Context(), nil)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(0, len(l2))
|
||||
}
|
||||
|
||||
func (suite *ReportTestSuite) create(r *model.Report) {
|
||||
id, err := suite.dao.Create(orm.Context(), r)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Condition(func() (success bool) {
|
||||
success = id > 0
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// TestReportUpdateReportData tests update the report data.
|
||||
func (suite *ReportTestSuite) TestReportUpdateReportData() {
|
||||
err := suite.dao.UpdateReportData(orm.Context(), "uuid", "{}")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
l, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"uuid": "uuid"}))
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(1, len(l))
|
||||
suite.Equal("{}", l[0].ReportSummary)
|
||||
|
||||
err = suite.dao.UpdateReportData(orm.Context(), "uuid", "{\"a\": 900}")
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *ReportTestSuite) TestUpdate() {
|
||||
err := suite.dao.Update(orm.Context(), &model.Report{
|
||||
UUID: "uuid",
|
||||
ArtifactID: 111,
|
||||
RegistrationUUID: "ruuid",
|
||||
MimeType: v1.MimeTypeSBOMReport,
|
||||
ReportSummary: `{"sbom_digest": "sha256:abc"}`,
|
||||
}, "report")
|
||||
suite.Require().NoError(err)
|
||||
query1 := &q.Query{
|
||||
PageSize: 1,
|
||||
PageNumber: 1,
|
||||
Keywords: map[string]interface{}{
|
||||
"artifact_id": 111,
|
||||
"registration_uuid": "ruuid",
|
||||
"mime_type": v1.MimeTypeSBOMReport,
|
||||
},
|
||||
}
|
||||
l, err := suite.dao.List(orm.Context(), query1)
|
||||
suite.Require().Equal(1, len(l))
|
||||
suite.Equal(l[0].ReportSummary, `{"sbom_digest": "sha256:abc"}`)
|
||||
}
|
||||
|
||||
// TestReportList tests list reports with query parameters.
|
||||
func (suite *ReportTestSuite) TestReportList() {
|
||||
query1 := &q.Query{
|
||||
PageSize: 1,
|
||||
PageNumber: 1,
|
||||
Keywords: map[string]interface{}{
|
||||
"artifact_id": 111,
|
||||
"registration_uuid": "ruuid",
|
||||
"mime_type": v1.MimeTypeSBOMReport,
|
||||
},
|
||||
}
|
||||
l, err := suite.dao.List(orm.Context(), query1)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(1, len(l))
|
||||
|
||||
query2 := &q.Query{
|
||||
PageSize: 1,
|
||||
PageNumber: 1,
|
||||
Keywords: map[string]interface{}{
|
||||
"artifact_id": 222,
|
||||
},
|
||||
}
|
||||
l, err = suite.dao.List(orm.Context(), query2)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(0, len(l))
|
||||
}
|
203
src/pkg/scan/sbom/manager.go
Normal file
203
src/pkg/scan/sbom/manager.go
Normal file
@ -0,0 +1,203 @@
|
||||
// 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"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/sbom/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
)
|
||||
|
||||
var (
|
||||
// Mgr is the global sbom report manager
|
||||
Mgr = NewManager()
|
||||
)
|
||||
|
||||
// Manager is used to manage the sbom reports.
|
||||
type Manager interface {
|
||||
// Create a new report record.
|
||||
//
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// r *scan.Report : report model to be created
|
||||
//
|
||||
// Returns:
|
||||
// string : uuid of the new report
|
||||
// error : non nil error if any errors occurred
|
||||
//
|
||||
Create(ctx context.Context, r *model.Report) (string, error)
|
||||
|
||||
// Delete delete report by uuid
|
||||
//
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// uuid string : uuid of the report to delete
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
//
|
||||
Delete(ctx context.Context, uuid string) error
|
||||
|
||||
// UpdateReportData update the report data (with JSON format) of the given report.
|
||||
//
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// uuid string : uuid to identify the report
|
||||
// report string : report JSON data
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
//
|
||||
UpdateReportData(ctx context.Context, uuid string, report string) error
|
||||
|
||||
// GetBy the reports for the given digest by other properties.
|
||||
//
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// artifact_id int64 : the artifact id
|
||||
// registrationUUID string : [optional] the report generated by which registration.
|
||||
// If it is empty, reports by all the registrations are retrieved.
|
||||
// mimeTypes []string : [optional] mime types of the reports requiring
|
||||
// If empty array is specified, reports with all the supported mimes are retrieved.
|
||||
//
|
||||
// Returns:
|
||||
// []*Report : sbom report list
|
||||
// error : non nil error if any errors occurred
|
||||
GetBy(ctx context.Context, artifactID int64, registrationUUID string, mimeType string, mediaType string) ([]*model.Report, error)
|
||||
// List reports according to the query
|
||||
//
|
||||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// query *q.Query : the query to list the reports
|
||||
//
|
||||
// Returns:
|
||||
// []*scan.Report : report list
|
||||
// error : non nil error if any errors occurred
|
||||
List(ctx context.Context, query *q.Query) ([]*model.Report, error)
|
||||
|
||||
// Update update report information
|
||||
Update(ctx context.Context, r *model.Report, cols ...string) error
|
||||
// DeleteByExtraAttr delete scan_report by sbom_digest
|
||||
DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error
|
||||
// DeleteByArtifactID delete sbom report by artifact id
|
||||
DeleteByArtifactID(ctx context.Context, artifactID int64) error
|
||||
}
|
||||
|
||||
// basicManager is a default implementation of report manager.
|
||||
type basicManager struct {
|
||||
dao dao.DAO
|
||||
}
|
||||
|
||||
// NewManager news basic manager.
|
||||
func NewManager() Manager {
|
||||
return &basicManager{
|
||||
dao: dao.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create ...
|
||||
func (bm *basicManager) Create(ctx context.Context, r *model.Report) (string, error) {
|
||||
// Validate report object
|
||||
if r == nil {
|
||||
return "", errors.New("nil sbom report object")
|
||||
}
|
||||
|
||||
if r.ArtifactID == 0 || len(r.RegistrationUUID) == 0 || len(r.MimeType) == 0 || len(r.MediaType) == 0 {
|
||||
return "", errors.New("malformed sbom report object")
|
||||
}
|
||||
|
||||
r.UUID = uuid.New().String()
|
||||
|
||||
// Insert
|
||||
if _, err := bm.dao.Create(ctx, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.UUID, nil
|
||||
}
|
||||
|
||||
func (bm *basicManager) Delete(ctx context.Context, uuid string) error {
|
||||
query := q.Query{Keywords: q.KeyWords{"uuid": uuid}}
|
||||
count, err := bm.dao.DeleteMany(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.Errorf("no report with uuid %s deleted", uuid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBy ...
|
||||
func (bm *basicManager) GetBy(ctx context.Context, artifactID int64, registrationUUID string,
|
||||
mimeType string, mediaType string) ([]*model.Report, error) {
|
||||
if artifactID == 0 {
|
||||
return nil, errors.New("no artifact id to get sbom report data")
|
||||
}
|
||||
|
||||
kws := make(map[string]interface{})
|
||||
kws["artifact_id"] = artifactID
|
||||
if len(registrationUUID) > 0 {
|
||||
kws["registration_uuid"] = registrationUUID
|
||||
}
|
||||
if len(mimeType) > 0 {
|
||||
kws["mine_type"] = mimeType
|
||||
}
|
||||
if len(mediaType) > 0 {
|
||||
kws["media_type"] = mediaType
|
||||
}
|
||||
// Query all
|
||||
query := &q.Query{
|
||||
PageNumber: 0,
|
||||
Keywords: kws,
|
||||
}
|
||||
|
||||
return bm.dao.List(ctx, query)
|
||||
}
|
||||
|
||||
// UpdateReportData ...
|
||||
func (bm *basicManager) UpdateReportData(ctx context.Context, uuid string, report string) error {
|
||||
if len(uuid) == 0 {
|
||||
return errors.New("missing uuid")
|
||||
}
|
||||
|
||||
if len(report) == 0 {
|
||||
return errors.New("missing report JSON data")
|
||||
}
|
||||
|
||||
return bm.dao.UpdateReportData(ctx, uuid, report)
|
||||
}
|
||||
|
||||
func (bm *basicManager) List(ctx context.Context, query *q.Query) ([]*model.Report, error) {
|
||||
return bm.dao.List(ctx, query)
|
||||
}
|
||||
|
||||
func (bm *basicManager) Update(ctx context.Context, r *model.Report, cols ...string) error {
|
||||
return bm.dao.Update(ctx, r, cols...)
|
||||
}
|
||||
|
||||
func (bm *basicManager) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error {
|
||||
return bm.dao.DeleteByExtraAttr(ctx, mimeType, attrName, attrValue)
|
||||
}
|
||||
|
||||
func (bm *basicManager) DeleteByArtifactID(ctx context.Context, artifactID int64) error {
|
||||
_, err := bm.dao.DeleteMany(ctx, *q.New(q.KeyWords{"ArtifactID": artifactID}))
|
||||
return err
|
||||
}
|
46
src/pkg/scan/sbom/model/report.go
Normal file
46
src/pkg/scan/sbom/model/report.go
Normal file
@ -0,0 +1,46 @@
|
||||
// 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 v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
|
||||
// Report sbom report.
|
||||
// Identified by the `artifact_id`, `registration_uuid` and `mime_type`.
|
||||
type Report struct {
|
||||
ID int64 `orm:"pk;auto;column(id)"`
|
||||
UUID string `orm:"unique;column(uuid)"`
|
||||
ArtifactID int64 `orm:"column(artifact_id)"`
|
||||
RegistrationUUID string `orm:"column(registration_uuid)"`
|
||||
MimeType string `orm:"column(mime_type)"`
|
||||
MediaType string `orm:"column(media_type)"`
|
||||
ReportSummary string `orm:"column(report);type(json)"`
|
||||
}
|
||||
|
||||
// TableName for sbom report
|
||||
func (r *Report) TableName() string {
|
||||
return "sbom_report"
|
||||
}
|
||||
|
||||
// RawSBOMReport the original report of the sbom report get from scanner
|
||||
type RawSBOMReport struct {
|
||||
// Time of generating this report
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
// Scanner of generating this report
|
||||
Scanner *v1.Scanner `json:"scanner"`
|
||||
// MediaType the media type of the report, e.g. application/spdx+json
|
||||
MediaType string `json:"media_type"`
|
||||
// SBOM sbom content
|
||||
SBOM map[string]interface{} `json:"sbom,omitempty"`
|
||||
}
|
47
src/pkg/scan/sbom/model/summary.go
Normal file
47
src/pkg/scan/sbom/model/summary.go
Normal 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
|
||||
|
||||
const (
|
||||
// SBOMRepository ...
|
||||
SBOMRepository = "sbom_repository"
|
||||
// SBOMDigest ...
|
||||
SBOMDigest = "sbom_digest"
|
||||
// StartTime ...
|
||||
StartTime = "start_time"
|
||||
// EndTime ...
|
||||
EndTime = "end_time"
|
||||
// Duration ...
|
||||
Duration = "duration"
|
||||
// ScanStatus ...
|
||||
ScanStatus = "scan_status"
|
||||
// ReportID ...
|
||||
ReportID = "report_id"
|
||||
// Scanner ...
|
||||
Scanner = "scanner"
|
||||
)
|
||||
|
||||
// Summary includes the sbom summary information
|
||||
type Summary map[string]interface{}
|
||||
|
||||
// SBOMAccArt returns the repository and digest of the SBOM
|
||||
func (s Summary) SBOMAccArt() (repo, digest string) {
|
||||
if repo, ok := s[SBOMRepository].(string); ok {
|
||||
if digest, ok := s[SBOMDigest].(string); ok {
|
||||
return repo, digest
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
349
src/pkg/scan/sbom/sbom.go
Normal file
349
src/pkg/scan/sbom/sbom.go
Normal file
@ -0,0 +1,349 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
scanCtl "github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
accessoryModel "github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
scanModel "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
sbom "github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
|
||||
sc "github.com/goharbor/harbor/src/controller/scanner"
|
||||
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
sbomMimeType = "application/vnd.goharbor.harbor.sbom.v1"
|
||||
sbomMediaTypeSpdx = "application/spdx+json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
scan.RegisterScanHanlder(v1.ScanTypeSbom, &scanHandler{
|
||||
GenAccessoryFunc: scan.GenAccessoryArt,
|
||||
RegistryServer: registry,
|
||||
SBOMMgrFunc: func() Manager { return Mgr },
|
||||
TaskMgrFunc: func() task.Manager { return task.Mgr },
|
||||
ArtifactControllerFunc: func() artifact.Controller { return artifact.Ctl },
|
||||
ScanControllerFunc: func() scanCtl.Controller { return scanCtl.DefaultController },
|
||||
ScannerControllerFunc: func() sc.Controller { return sc.DefaultController },
|
||||
cloneCtx: orm.Clone,
|
||||
})
|
||||
}
|
||||
|
||||
// scanHandler defines the Handler to generate sbom
|
||||
type scanHandler struct {
|
||||
GenAccessoryFunc func(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error)
|
||||
RegistryServer func(ctx context.Context) (string, bool)
|
||||
SBOMMgrFunc func() Manager
|
||||
TaskMgrFunc func() task.Manager
|
||||
ArtifactControllerFunc func() artifact.Controller
|
||||
ScanControllerFunc func() scanCtl.Controller
|
||||
ScannerControllerFunc func() sc.Controller
|
||||
cloneCtx func(ctx context.Context) context.Context
|
||||
}
|
||||
|
||||
// RequestProducesMineTypes defines the mine types produced by the scan handler
|
||||
func (h *scanHandler) RequestProducesMineTypes() []string {
|
||||
return []string{v1.MimeTypeSBOMReport}
|
||||
}
|
||||
|
||||
// RequestParameters defines the parameters for scan request
|
||||
func (h *scanHandler) RequestParameters() map[string]interface{} {
|
||||
return map[string]interface{}{"sbom_media_types": []string{sbomMediaTypeSpdx}}
|
||||
}
|
||||
|
||||
// PostScan defines task specific operations after the scan is complete
|
||||
func (h *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) {
|
||||
sbomContent, s, err := retrieveSBOMContent(rawReport)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
scanReq := v1.ScanRequest{
|
||||
Registry: sr.Registry,
|
||||
Artifact: sr.Artifact,
|
||||
}
|
||||
// the registry server url is core by default, need to replace it with real registry server url
|
||||
scanReq.Registry.URL, scanReq.Registry.Insecure = h.RegistryServer(ctx.SystemContext())
|
||||
if len(scanReq.Registry.URL) == 0 {
|
||||
return "", fmt.Errorf("empty registry server")
|
||||
}
|
||||
myLogger := ctx.GetLogger()
|
||||
myLogger.Debugf("Pushing accessory artifact to %s/%s", scanReq.Registry.URL, scanReq.Artifact.Repository)
|
||||
dgst, err := h.GenAccessoryFunc(scanReq, sbomContent, h.annotations(), sbomMimeType, robot)
|
||||
if err != nil {
|
||||
myLogger.Errorf("error when create accessory from image %v", err)
|
||||
return "", err
|
||||
}
|
||||
return h.generateReport(startTime, sr.Artifact.Repository, dgst, "Success", s)
|
||||
}
|
||||
|
||||
// URLParameter defines the parameters for scan report url
|
||||
func (h *scanHandler) URLParameter(_ *v1.ScanRequest) (string, error) {
|
||||
return fmt.Sprintf("sbom_media_type=%s", url.QueryEscape(sbomMediaTypeSpdx)), nil
|
||||
}
|
||||
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
func (h *scanHandler) RequiredPermissions() []*types.Policy {
|
||||
return []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPush,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// annotations defines the annotations for the accessory artifact
|
||||
func (h *scanHandler) annotations() map[string]string {
|
||||
t := time.Now().Format(time.RFC3339)
|
||||
return map[string]string{
|
||||
"created": t,
|
||||
"created-by": "Harbor",
|
||||
"org.opencontainers.artifact.created": t,
|
||||
"org.opencontainers.artifact.description": "SPDX JSON SBOM",
|
||||
}
|
||||
}
|
||||
|
||||
func (h *scanHandler) generateReport(startTime time.Time, repository, digest, status string, scanner *v1.Scanner) (string, error) {
|
||||
summary := sbom.Summary{}
|
||||
endTime := time.Now()
|
||||
summary[sbom.StartTime] = startTime
|
||||
summary[sbom.EndTime] = endTime
|
||||
summary[sbom.Duration] = int64(endTime.Sub(startTime).Seconds())
|
||||
summary[sbom.SBOMRepository] = repository
|
||||
summary[sbom.SBOMDigest] = digest
|
||||
summary[sbom.ScanStatus] = status
|
||||
summary[sbom.Scanner] = scanner
|
||||
rep, err := json.Marshal(summary)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(rep), nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) Update(ctx context.Context, uuid string, report string) error {
|
||||
mgr := h.SBOMMgrFunc()
|
||||
if err := mgr.UpdateReportData(ctx, uuid, report); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract server name from config, and remove the protocol prefix
|
||||
func registry(ctx context.Context) (string, bool) {
|
||||
cfgMgr, ok := config.FromContext(ctx)
|
||||
if ok {
|
||||
extURL := cfgMgr.Get(context.Background(), common.ExtEndpoint).GetString()
|
||||
insecure := strings.HasPrefix(extURL, "http://")
|
||||
server := strings.TrimPrefix(extURL, "https://")
|
||||
server = strings.TrimPrefix(server, "http://")
|
||||
return server, insecure
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// retrieveSBOMContent retrieves the "sbom" field from the raw report
|
||||
func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) {
|
||||
rpt := sbom.RawSBOMReport{}
|
||||
err := json.Unmarshal([]byte(rawReport), &rpt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sbomContent, err := json.Marshal(rpt.SBOM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sbomContent, rpt.Scanner, nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) MakePlaceHolder(ctx context.Context, art *artifact.Artifact, r *scanner.Registration) (rps []*scanModel.Report, err error) {
|
||||
mgr := h.SBOMMgrFunc()
|
||||
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeSbom)
|
||||
if len(mimeTypes) == 0 {
|
||||
return nil, errors.New("no mime types to make report placeholders")
|
||||
}
|
||||
if err := h.delete(ctx, art, mimeTypes[0], r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reports []*scanModel.Report
|
||||
for _, mt := range mimeTypes {
|
||||
report := &sbom.Report{
|
||||
ArtifactID: art.ID,
|
||||
RegistrationUUID: r.UUID,
|
||||
MimeType: mt,
|
||||
MediaType: sbomMediaTypeSpdx,
|
||||
}
|
||||
|
||||
create := func(ctx context.Context) error {
|
||||
reportUUID, err := mgr.Create(ctx, report)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report.UUID = reportUUID
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder-sbom")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reports = append(reports, &scanModel.Report{
|
||||
RegistrationUUID: r.UUID,
|
||||
MimeType: mt,
|
||||
UUID: report.UUID,
|
||||
})
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// delete deletes the sbom report and accessory
|
||||
func (h *scanHandler) delete(ctx context.Context, art *artifact.Artifact, mimeTypes string, r *scanner.Registration) error {
|
||||
mgr := h.SBOMMgrFunc()
|
||||
sbomReports, err := mgr.GetBy(h.cloneCtx(ctx), art.ID, r.UUID, mimeTypes, sbomMediaTypeSpdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// check if any report has running task associate with it
|
||||
taskMgr := h.TaskMgrFunc()
|
||||
for _, rpt := range sbomReports {
|
||||
if !taskMgr.IsTaskFinished(ctx, rpt.UUID) {
|
||||
return errors.ConflictError(nil).WithMessage("a previous sbom generate process is running")
|
||||
}
|
||||
}
|
||||
|
||||
for _, rpt := range sbomReports {
|
||||
if rpt.MimeType != v1.MimeTypeSBOMReport {
|
||||
continue
|
||||
}
|
||||
if err := mgr.Delete(ctx, rpt.UUID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := h.deleteSBOMAccessory(ctx, art.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteSBOMAccessory check if current report has sbom accessory info, if there is, delete it
|
||||
func (h *scanHandler) deleteSBOMAccessory(ctx context.Context, artID int64) error {
|
||||
artifactCtl := h.ArtifactControllerFunc()
|
||||
art, err := artifactCtl.Get(ctx, artID, &artifact.Option{
|
||||
WithAccessory: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if art == nil {
|
||||
return nil
|
||||
}
|
||||
for _, acc := range art.Accessories {
|
||||
if acc.GetData().Type == accessoryModel.TypeHarborSBOM {
|
||||
if err := artifactCtl.Delete(ctx, acc.GetData().ArtifactID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) GetPlaceHolder(ctx context.Context, artRepo string, artDigest, scannerUUID string, mimeType string) (rp *scanModel.Report, err error) {
|
||||
artifactCtl := h.ArtifactControllerFunc()
|
||||
a, err := artifactCtl.GetByReference(ctx, artRepo, artDigest, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mgr := h.SBOMMgrFunc()
|
||||
rpts, err := mgr.GetBy(ctx, a.ID, scannerUUID, mimeType, sbomMediaTypeSpdx)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get report for artifact %s@%s of mimetype %s, error %v", artRepo, artDigest, mimeType, err)
|
||||
return nil, err
|
||||
}
|
||||
if len(rpts) == 0 {
|
||||
logger.Errorf("No report found for artifact %s@%s of mimetype %s, error %v", artRepo, artDigest, mimeType, err)
|
||||
return nil, errors.NotFoundError(nil).WithMessage("no report found to update data")
|
||||
}
|
||||
return &scanModel.Report{
|
||||
UUID: rpts[0].UUID,
|
||||
MimeType: rpts[0].MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) GetSummary(ctx context.Context, art *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) {
|
||||
if len(mimeTypes) == 0 {
|
||||
return nil, errors.New("no mime types to get report summaries")
|
||||
}
|
||||
if art == nil {
|
||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
||||
}
|
||||
ds := h.ScannerControllerFunc()
|
||||
r, err := ds.GetRegistrationByProject(ctx, art.ProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get sbom summary failed")
|
||||
}
|
||||
reports, err := h.SBOMMgrFunc().GetBy(ctx, art.ID, r.UUID, mimeTypes[0], sbomMediaTypeSpdx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) == 0 {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
reportContent := reports[0].ReportSummary
|
||||
result := map[string]interface{}{}
|
||||
if len(reportContent) == 0 {
|
||||
status := h.TaskMgrFunc().RetrieveStatusFromTask(ctx, reports[0].UUID)
|
||||
if len(status) > 0 {
|
||||
result[sbom.ReportID] = reports[0].UUID
|
||||
result[sbom.ScanStatus] = status
|
||||
}
|
||||
log.Debug("no content for current report")
|
||||
return result, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(reportContent), &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (h *scanHandler) JobVendorType() string {
|
||||
return job.SBOMJobVendorType
|
||||
}
|
267
src/pkg/scan/sbom/sbom_test.go
Normal file
267
src/pkg/scan/sbom/sbom_test.go
Normal file
@ -0,0 +1,267 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sc "github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/controller/scanner"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
accessoryModel "github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
basemodel "github.com/goharbor/harbor/src/pkg/accessory/model/base"
|
||||
art "github.com/goharbor/harbor/src/pkg/artifact"
|
||||
sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
artifactTest "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
scanTest "github.com/goharbor/harbor/src/testing/controller/scan"
|
||||
scannerTest "github.com/goharbor/harbor/src/testing/controller/scanner"
|
||||
"github.com/goharbor/harbor/src/testing/jobservice"
|
||||
sbomTest "github.com/goharbor/harbor/src/testing/pkg/scan/sbom"
|
||||
taskTest "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
)
|
||||
|
||||
var registeredScanner = &scanner.Registration{
|
||||
UUID: "uuid",
|
||||
Metadata: &v1.ScannerAdapterMetadata{
|
||||
Capabilities: []*v1.ScannerCapability{
|
||||
{Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}},
|
||||
{Type: v1.ScanTypeSbom, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_scanHandler_ReportURLParameter(t *testing.T) {
|
||||
type args struct {
|
||||
in0 *v1.ScanRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"normal test", args{&v1.ScanRequest{}}, "sbom_media_type=application%2Fspdx%2Bjson", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
got, err := v.URLParameter(tt.args.in0)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("URLParameter() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("URLParameter() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_scanHandler_RequiredPermissions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []*types.Policy
|
||||
}{
|
||||
{"normal test", []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPush,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
if got := v.RequiredPermissions(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("RequiredPermissions() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_scanHandler_RequestProducesMineTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []string
|
||||
}{
|
||||
{"normal test", []string{v1.MimeTypeSBOMReport}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
if got := v.RequestProducesMineTypes(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("RequestProducesMineTypes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockGetRegistry(ctx context.Context) (string, bool) {
|
||||
return "myharbor.example.com", false
|
||||
}
|
||||
|
||||
func mockGenAccessory(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error) {
|
||||
return "sha256:1234567890", nil
|
||||
}
|
||||
|
||||
type SBOMTestSuite struct {
|
||||
htesting.Suite
|
||||
handler *scanHandler
|
||||
sbomManager *sbomTest.Manager
|
||||
taskMgr *taskTest.Manager
|
||||
artifactCtl *artifactTest.Controller
|
||||
artifact *artifact.Artifact
|
||||
wrongArtifact *artifact.Artifact
|
||||
scanController *scanTest.Controller
|
||||
scannerController *scannerTest.Controller
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) SetupSuite() {
|
||||
suite.sbomManager = &sbomTest.Manager{}
|
||||
suite.taskMgr = &taskTest.Manager{}
|
||||
suite.artifactCtl = &artifactTest.Controller{}
|
||||
suite.scannerController = &scannerTest.Controller{}
|
||||
suite.scanController = &scanTest.Controller{}
|
||||
|
||||
suite.handler = &scanHandler{
|
||||
GenAccessoryFunc: mockGenAccessory,
|
||||
RegistryServer: mockGetRegistry,
|
||||
SBOMMgrFunc: func() Manager { return suite.sbomManager },
|
||||
TaskMgrFunc: func() task.Manager { return suite.taskMgr },
|
||||
ArtifactControllerFunc: func() artifact.Controller { return suite.artifactCtl },
|
||||
ScanControllerFunc: func() sc.Controller { return suite.scanController },
|
||||
ScannerControllerFunc: func() scanner.Controller { return suite.scannerController },
|
||||
cloneCtx: func(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
|
||||
suite.artifact = &artifact.Artifact{Artifact: art.Artifact{ID: 1}}
|
||||
suite.artifact.Type = "IMAGE"
|
||||
suite.artifact.ProjectID = 1
|
||||
suite.artifact.RepositoryName = "library/photon"
|
||||
suite.artifact.Digest = "digest-code"
|
||||
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
||||
|
||||
suite.wrongArtifact = &artifact.Artifact{Artifact: art.Artifact{ID: 2, ProjectID: 1}}
|
||||
suite.wrongArtifact.Digest = "digest-wrong"
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) TestPostScan() {
|
||||
req := &v1.ScanRequest{
|
||||
Registry: &v1.Registry{
|
||||
URL: "myregistry.example.com",
|
||||
},
|
||||
Artifact: &v1.Artifact{
|
||||
Repository: "library/nosql",
|
||||
},
|
||||
}
|
||||
robot := &model.Robot{
|
||||
Name: "robot",
|
||||
Secret: "mysecret",
|
||||
}
|
||||
startTime := time.Now()
|
||||
rawReport := `{"sbom": { "key": "value" }}`
|
||||
ctx := &jobservice.MockJobContext{}
|
||||
ctx.On("GetLogger").Return(&jobservice.MockJobLogger{})
|
||||
accessory, err := suite.handler.PostScan(ctx, req, nil, rawReport, startTime, robot)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().NotEmpty(accessory)
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) TestMakeReportPlaceHolder() {
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
acc := &basemodel.Default{
|
||||
Data: accessoryModel.AccessoryData{
|
||||
ID: 1,
|
||||
ArtifactID: 2,
|
||||
SubArtifactDigest: "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180",
|
||||
Type: accessoryModel.TypeHarborSBOM,
|
||||
},
|
||||
}
|
||||
art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, Digest: "digest", ManifestMediaType: v1.MimeTypeDockerArtifact},
|
||||
Accessories: []accessoryModel.Accessory{acc}}
|
||||
r := &scanner.Registration{
|
||||
UUID: "uuid",
|
||||
Metadata: &v1.ScannerAdapterMetadata{
|
||||
Capabilities: []*v1.ScannerCapability{
|
||||
{Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}},
|
||||
{Type: v1.ScanTypeSbom, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}},
|
||||
},
|
||||
},
|
||||
}
|
||||
mock.OnAnything(suite.sbomManager, "GetBy").Return([]*sbomModel.Report{{UUID: "uuid"}}, nil).Once()
|
||||
mock.OnAnything(suite.sbomManager, "Create").Return("uuid", nil).Once()
|
||||
mock.OnAnything(suite.sbomManager, "Delete").Return(nil).Once()
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{{Status: "Success"}}, nil)
|
||||
mock.OnAnything(suite.taskMgr, "IsTaskFinished").Return(true).Once()
|
||||
mock.OnAnything(suite.artifactCtl, "Get").Return(art, nil)
|
||||
mock.OnAnything(suite.artifactCtl, "Delete").Return(nil)
|
||||
rps, err := suite.handler.MakePlaceHolder(ctx, art, r)
|
||||
require.NoError(suite.T(), err)
|
||||
suite.Equal(1, len(rps))
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) TestGetSBOMSummary() {
|
||||
r := registeredScanner
|
||||
rpts := []*sbomModel.Report{
|
||||
{UUID: "rp-uuid-004", MimeType: v1.MimeTypeSBOMReport, ReportSummary: `{"scan_status":"Success", "sbom_digest": "sha256:1234567890"}`},
|
||||
}
|
||||
mock.OnAnything(suite.scannerController, "GetRegistrationByProject").Return(r, nil)
|
||||
mock.OnAnything(suite.sbomManager, "GetBy").Return(rpts, nil)
|
||||
sum, err := suite.handler.GetSummary(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)
|
||||
tasks := []*task.Task{{Status: "Error"}}
|
||||
suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once()
|
||||
sum2, err := suite.handler.GetSummary(context.TODO(), suite.wrongArtifact, []string{v1.MimeTypeSBOMReport})
|
||||
suite.Nil(err)
|
||||
suite.NotNil(sum2)
|
||||
|
||||
}
|
||||
|
||||
func (suite *SBOMTestSuite) TestGetReportPlaceHolder() {
|
||||
mock.OnAnything(suite.sbomManager, "GetBy").Return([]*sbomModel.Report{{UUID: "uuid"}}, nil).Once()
|
||||
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(suite.artifact, nil).Twice()
|
||||
rp, err := suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType")
|
||||
require.NoError(suite.T(), err)
|
||||
suite.Equal("uuid", rp.UUID)
|
||||
mock.OnAnything(suite.sbomManager, "GetBy").Return(nil, nil).Once()
|
||||
rp, err = suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType")
|
||||
require.Error(suite.T(), err)
|
||||
}
|
||||
|
||||
func TestExampleTestSuite(t *testing.T) {
|
||||
suite.Run(t, &SBOMTestSuite{})
|
||||
}
|
@ -15,9 +15,7 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
@ -30,26 +28,19 @@ import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
http_common "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
// Insecure ...
|
||||
type Insecure bool
|
||||
|
||||
// RemoteOptions ...
|
||||
func (i Insecure) RemoteOptions() []remote.Option {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: bool(i)}
|
||||
func RemoteOptions() []remote.Option {
|
||||
tr := http_common.GetHTTPTransport(http_common.WithInsecure(true))
|
||||
return []remote.Option{remote.WithTransport(tr)}
|
||||
}
|
||||
|
||||
type referrer struct {
|
||||
Insecure
|
||||
}
|
||||
|
||||
// GenAccessoryArt composes the accessory oci object and push it back to harbor core as an accessory of the scanned artifact.
|
||||
func GenAccessoryArt(sq v1sq.ScanRequest, accData []byte, accAnnotations map[string]string, mediaType string, robot robot.Robot) (string, error) {
|
||||
func GenAccessoryArt(sq v1sq.ScanRequest, accData []byte, accAnnotations map[string]string, mediaType string, robot *model.Robot) (string, error) {
|
||||
accArt, err := mutate.Append(empty.Image, mutate.Addendum{
|
||||
Layer: static.NewLayer(accData, ocispec.MediaTypeImageLayer),
|
||||
History: v1.History{
|
||||
@ -86,10 +77,13 @@ func GenAccessoryArt(sq v1sq.ScanRequest, accData []byte, accAnnotations map[str
|
||||
return "", err
|
||||
}
|
||||
accRef, err := name.ParseReference(fmt.Sprintf("%s/%s@%s", sq.Registry.URL, sq.Artifact.Repository, dgst.String()))
|
||||
if sq.Registry.Insecure {
|
||||
accRef, err = name.ParseReference(fmt.Sprintf("%s/%s@%s", sq.Registry.URL, sq.Artifact.Repository, dgst.String()), name.Insecure)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
opts := append(referrer{Insecure: true}.RemoteOptions(), remote.WithAuth(&authn.Basic{Username: robot.Name, Password: robot.Secret}))
|
||||
opts := append(RemoteOptions(), remote.WithAuth(&authn.Basic{Username: robot.Name, Password: robot.Secret}))
|
||||
if err := remote.Write(accRef, accArt, opts...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
rm "github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
@ -47,11 +46,9 @@ func TestGenAccessoryArt(t *testing.T) {
|
||||
Digest: "sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7",
|
||||
},
|
||||
}
|
||||
r := robot.Robot{
|
||||
Robot: rm.Robot{
|
||||
Name: "admin",
|
||||
Secret: "Harbor12345",
|
||||
},
|
||||
r := &model.Robot{
|
||||
Name: "admin",
|
||||
Secret: "Harbor12345",
|
||||
}
|
||||
|
||||
annotations := map[string]string{
|
||||
|
@ -33,6 +33,9 @@ type Report struct {
|
||||
Vulnerabilities []*VulnerabilityItem `json:"vulnerabilities"`
|
||||
|
||||
vulnerabilityItemList *VulnerabilityItemList
|
||||
|
||||
// SBOM sbom content
|
||||
SBOM map[string]interface{} `json:"sbom,omitempty"`
|
||||
}
|
||||
|
||||
// GetVulnerabilityItemList returns VulnerabilityItemList from the Vulnerabilities of report
|
||||
|
307
src/pkg/scan/vulnerability/vul.go
Normal file
307
src/pkg/scan/vulnerability/vul.go
Normal file
@ -0,0 +1,307 @@
|
||||
// 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 vulnerability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
scanCtl "github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
scanJob "github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
)
|
||||
|
||||
func init() {
|
||||
scanJob.RegisterScanHanlder(v1.ScanTypeVulnerability, &scanHandler{
|
||||
reportConverter: postprocessors.Converter,
|
||||
ReportMgrFunc: func() report.Manager { return report.Mgr },
|
||||
TaskMgrFunc: func() task.Manager { return task.Mgr },
|
||||
ScanControllerFunc: func() scanCtl.Controller { return scanCtl.DefaultController },
|
||||
cloneCtx: orm.Clone,
|
||||
})
|
||||
}
|
||||
|
||||
// scanHandler defines the handler for scan vulnerability
|
||||
type scanHandler struct {
|
||||
reportConverter postprocessors.NativeScanReportConverter
|
||||
ReportMgrFunc func() report.Manager
|
||||
TaskMgrFunc func() task.Manager
|
||||
ScanControllerFunc func() scanCtl.Controller
|
||||
cloneCtx func(ctx context.Context) context.Context
|
||||
}
|
||||
|
||||
func (h *scanHandler) MakePlaceHolder(ctx context.Context, art *artifact.Artifact,
|
||||
r *scanner.Registration) (rps []*scan.Report, err error) {
|
||||
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeVulnerability)
|
||||
reportMgr := h.ReportMgrFunc()
|
||||
oldReports, err := reportMgr.GetBy(h.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := h.assembleReports(ctx, oldReports...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(oldReports) > 0 {
|
||||
for _, oldReport := range oldReports {
|
||||
if !job.Status(oldReport.Status).Final() {
|
||||
return nil, errors.ConflictError(nil).WithMessage("a previous scan process is %s", oldReport.Status)
|
||||
}
|
||||
}
|
||||
|
||||
for _, oldReport := range oldReports {
|
||||
if err := reportMgr.Delete(ctx, oldReport.UUID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reports []*scan.Report
|
||||
|
||||
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeVulnerability) {
|
||||
rpt := &scan.Report{
|
||||
Digest: art.Digest,
|
||||
RegistrationUUID: r.UUID,
|
||||
MimeType: pm,
|
||||
}
|
||||
|
||||
create := func(ctx context.Context) error {
|
||||
reportUUID, err := reportMgr.Create(ctx, rpt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpt.UUID = reportUUID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reports = append(reports, rpt)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) assembleReports(ctx context.Context, reports ...*scan.Report) error {
|
||||
reportUUIDs := make([]string, len(reports))
|
||||
for i, report := range reports {
|
||||
reportUUIDs[i] = report.UUID
|
||||
}
|
||||
|
||||
tasks, err := h.listScanTasks(ctx, reportUUIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reportUUIDToTasks := map[string]*task.Task{}
|
||||
for _, task := range tasks {
|
||||
for _, reportUUID := range scanCtl.GetReportUUIDs(task.ExtraAttrs) {
|
||||
reportUUIDToTasks[reportUUID] = task
|
||||
}
|
||||
}
|
||||
|
||||
for _, report := range reports {
|
||||
if task, ok := reportUUIDToTasks[report.UUID]; ok {
|
||||
report.Status = task.Status
|
||||
report.StartTime = task.StartTime
|
||||
report.EndTime = task.EndTime
|
||||
} else {
|
||||
report.Status = job.ErrorStatus.String()
|
||||
}
|
||||
|
||||
completeReport, err := h.reportConverter.FromRelationalSchema(ctx, report.UUID, report.Digest, report.Report)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report.Report = completeReport
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listScanTasks returns the tasks of the reports
|
||||
func (h *scanHandler) listScanTasks(ctx context.Context, reportUUIDs []string) ([]*task.Task, error) {
|
||||
if len(reportUUIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tasks := make([]*task.Task, len(reportUUIDs))
|
||||
errs := make([]error, len(reportUUIDs))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, reportUUID := range reportUUIDs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(ix int, reportUUID string) {
|
||||
defer wg.Done()
|
||||
|
||||
task, err := h.getScanTask(h.cloneCtx(ctx), reportUUID)
|
||||
if err == nil {
|
||||
tasks[ix] = task
|
||||
} else if !errors.IsNotFoundErr(err) {
|
||||
errs[ix] = err
|
||||
} else {
|
||||
log.G(ctx).Warningf("task for the scan report %s not found", reportUUID)
|
||||
}
|
||||
}(i, reportUUID)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var results []*task.Task
|
||||
for _, task := range tasks {
|
||||
if task != nil {
|
||||
results = append(results, task)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) getScanTask(ctx context.Context, reportUUID string) (*task.Task, error) {
|
||||
// NOTE: the method uses the postgres' unique operations and should consider here if support other database in the future.
|
||||
taskMgr := h.TaskMgrFunc()
|
||||
tasks, err := taskMgr.ListScanTasksByReportUUID(ctx, reportUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return nil, errors.NotFoundError(nil).WithMessage("task for report %s not found", reportUUID)
|
||||
}
|
||||
|
||||
return tasks[0], nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) GetPlaceHolder(ctx context.Context, _ string, artDigest, scannerUUID string,
|
||||
mimeType string) (rp *scan.Report, err error) {
|
||||
reportMgr := h.ReportMgrFunc()
|
||||
reports, err := reportMgr.GetBy(ctx, artDigest, scannerUUID, []string{mimeType})
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get report for artifact %s of mimetype %s, error %v", artDigest, mimeType, err)
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) == 0 {
|
||||
logger.Errorf("no report found for artifact %s of mimetype %s, error %v", artDigest, mimeType, err)
|
||||
return nil, errors.NotFoundError(nil).WithMessage("no report found to update data")
|
||||
}
|
||||
return reports[0], nil
|
||||
}
|
||||
|
||||
// RequestProducesMineTypes returns the produces mime types
|
||||
func (h *scanHandler) RequestProducesMineTypes() []string {
|
||||
return []string{v1.MimeTypeGenericVulnerabilityReport}
|
||||
}
|
||||
|
||||
// RequestParameters defines the parameters for scan request
|
||||
func (h *scanHandler) RequestParameters() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
func (h *scanHandler) RequiredPermissions() []*types.Policy {
|
||||
return []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PostScan ...
|
||||
func (h *scanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string,
|
||||
_ time.Time, _ *model.Robot) (string, error) {
|
||||
// use a new ormer here to use the short db connection
|
||||
_, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID,
|
||||
origRp.RegistrationUUID, origRp.Digest, rawReport)
|
||||
return refreshedReport, err
|
||||
}
|
||||
|
||||
// URLParameter vulnerability doesn't require any scan report parameters
|
||||
func (h *scanHandler) URLParameter(_ *v1.ScanRequest) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) Update(ctx context.Context, uuid string, rpt string) error {
|
||||
reportMgr := h.ReportMgrFunc()
|
||||
if err := reportMgr.UpdateReportData(ctx, uuid, rpt); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) GetSummary(ctx context.Context, ar *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) {
|
||||
bc := h.ScanControllerFunc()
|
||||
if ar == nil {
|
||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
||||
}
|
||||
// Get reports first
|
||||
rps, err := bc.GetReport(ctx, ar, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summaries := make(map[string]interface{}, len(rps))
|
||||
for _, rp := range rps {
|
||||
sum, err := report.GenerateSummary(rp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s, ok := summaries[rp.MimeType]; ok {
|
||||
r, err := report.MergeSummary(rp.MimeType, s, sum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaries[rp.MimeType] = r
|
||||
} else {
|
||||
summaries[rp.MimeType] = sum
|
||||
}
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (h *scanHandler) JobVendorType() string {
|
||||
return job.ImageScanJobVendorType
|
||||
}
|
225
src/pkg/scan/vulnerability/vul_test.go
Normal file
225
src/pkg/scan/vulnerability/vul_test.go
Normal file
@ -0,0 +1,225 @@
|
||||
package vulnerability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
scanCtl "github.com/goharbor/harbor/src/controller/scan"
|
||||
art "github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||
scanCtlTest "github.com/goharbor/harbor/src/testing/controller/scan"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
|
||||
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
|
||||
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
accessoryModel "github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/testing/jobservice"
|
||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
||||
)
|
||||
|
||||
func TestRequiredPermissions(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
expected := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
|
||||
result := v.RequiredPermissions()
|
||||
|
||||
assert.Equal(t, expected, result, "RequiredPermissions should return correct permissions")
|
||||
}
|
||||
|
||||
func TestPostScan(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
ctx := &jobservice.MockJobContext{}
|
||||
artifact := &v1.Artifact{}
|
||||
origRp := &scan.Report{}
|
||||
rawReport := ""
|
||||
|
||||
mocker := &postprocessorstesting.ScanReportV1ToV2Converter{}
|
||||
mocker.On("ToRelationalSchema", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, "original report", nil)
|
||||
postprocessors.Converter = mocker
|
||||
sr := &v1.ScanRequest{Artifact: artifact}
|
||||
refreshedReport, err := v.PostScan(ctx, sr, origRp, rawReport, time.Now(), &model.Robot{})
|
||||
assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report")
|
||||
assert.Nil(t, err, "PostScan should not return an error")
|
||||
}
|
||||
|
||||
func TestScanHandler_RequiredPermissions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []*types.Policy
|
||||
}{
|
||||
{"normal", []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanHandler_ReportURLParameter(t *testing.T) {
|
||||
type args struct {
|
||||
in0 *v1.ScanRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{"normal", args{&v1.ScanRequest{}}, "", assert.NoError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
got, err := v.URLParameter(tt.args.in0)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("URLParameter(%v)", tt.args.in0)) {
|
||||
return
|
||||
}
|
||||
assert.Equalf(t, tt.want, got, "URLParameter(%v)", tt.args.in0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanHandler_RequestProducesMineTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []string
|
||||
}{
|
||||
{"normal", []string{v1.MimeTypeGenericVulnerabilityReport}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &scanHandler{}
|
||||
assert.Equalf(t, tt.want, v.RequestProducesMineTypes(), "RequestProducesMineTypes()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type VulHandlerTestSuite struct {
|
||||
htesting.Suite
|
||||
ar *artifacttesting.Controller
|
||||
accessoryMgr *accessorytesting.Manager
|
||||
artifact *artifact.Artifact
|
||||
taskMgr *tasktesting.Manager
|
||||
reportMgr *reporttesting.Manager
|
||||
scanController *scanCtlTest.Controller
|
||||
handler *scanHandler
|
||||
}
|
||||
|
||||
func (suite *VulHandlerTestSuite) SetupSuite() {
|
||||
suite.ar = &artifacttesting.Controller{}
|
||||
suite.accessoryMgr = &accessorytesting.Manager{}
|
||||
suite.taskMgr = &tasktesting.Manager{}
|
||||
suite.scanController = &scanCtlTest.Controller{}
|
||||
suite.reportMgr = &reporttesting.Manager{}
|
||||
suite.artifact = &artifact.Artifact{Artifact: art.Artifact{ID: 1}}
|
||||
suite.artifact.Type = "IMAGE"
|
||||
suite.artifact.ProjectID = 1
|
||||
suite.artifact.RepositoryName = "library/photon"
|
||||
suite.artifact.Digest = "digest-code"
|
||||
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
||||
suite.handler = &scanHandler{
|
||||
reportConverter: postprocessors.Converter,
|
||||
ReportMgrFunc: func() report.Manager { return suite.reportMgr },
|
||||
TaskMgrFunc: func() task.Manager { return suite.taskMgr },
|
||||
ScanControllerFunc: func() scanCtl.Controller { return suite.scanController },
|
||||
cloneCtx: func(ctx context.Context) context.Context { return ctx },
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *VulHandlerTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func TestExampleTestSuite(t *testing.T) {
|
||||
suite.Run(t, &VulHandlerTestSuite{})
|
||||
}
|
||||
|
||||
// TestScanControllerGetSummary ...
|
||||
func (suite *VulHandlerTestSuite) TestScanControllerGetSummary() {
|
||||
rpts := []*scan.Report{
|
||||
{UUID: "uuid", MimeType: v1.MimeTypeGenericVulnerabilityReport},
|
||||
}
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
||||
mock.OnAnything(suite.accessoryMgr, "List").Return([]accessoryModel.Accessory{}, nil).Once()
|
||||
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
||||
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
||||
walkFn(suite.artifact)
|
||||
}).Once()
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once()
|
||||
mock.OnAnything(suite.scanController, "GetReport").Return(rpts, nil).Once()
|
||||
sum, err := suite.handler.GetSummary(ctx, suite.artifact, []string{v1.MimeTypeGenericVulnerabilityReport})
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 1, len(sum))
|
||||
}
|
||||
|
||||
func (suite *VulHandlerTestSuite) TestMakeReportPlaceHolder() {
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, Digest: "digest", ManifestMediaType: v1.MimeTypeDockerArtifact}}
|
||||
r := &scanner.Registration{
|
||||
UUID: "uuid",
|
||||
Metadata: &v1.ScannerAdapterMetadata{
|
||||
Capabilities: []*v1.ScannerCapability{
|
||||
{Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}},
|
||||
},
|
||||
},
|
||||
}
|
||||
// mimeTypes := []string{v1.MimeTypeGenericVulnerabilityReport}
|
||||
mock.OnAnything(suite.reportMgr, "GetBy").Return([]*scan.Report{{UUID: "uuid"}}, nil).Once()
|
||||
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
|
||||
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{{Status: "Success"}}, nil)
|
||||
rps, err := suite.handler.MakePlaceHolder(ctx, art, r)
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 1, len(rps))
|
||||
}
|
||||
|
||||
func (suite *VulHandlerTestSuite) TestGetReportPlaceHolder() {
|
||||
mock.OnAnything(suite.reportMgr, "GetBy").Return([]*scan.Report{{UUID: "uuid"}}, nil).Once()
|
||||
rp, err := suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType")
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), "uuid", rp.UUID)
|
||||
mock.OnAnything(suite.reportMgr, "GetBy").Return(nil, fmt.Errorf("not found")).Once()
|
||||
rp, err = suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType")
|
||||
require.Error(suite.T(), err)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package scheduler
|
||||
|
||||
@ -18,6 +18,10 @@ type mockDAO struct {
|
||||
func (_m *mockDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
|
||||
@ -42,6 +46,10 @@ func (_m *mockDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
func (_m *mockDAO) Create(ctx context.Context, s *schedule) (int64, error) {
|
||||
ret := _m.Called(ctx, s)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *schedule) (int64, error)); ok {
|
||||
@ -66,6 +74,10 @@ func (_m *mockDAO) Create(ctx context.Context, s *schedule) (int64, error) {
|
||||
func (_m *mockDAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
@ -80,6 +92,10 @@ func (_m *mockDAO) Delete(ctx context.Context, id int64) error {
|
||||
func (_m *mockDAO) Get(ctx context.Context, id int64) (*schedule, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 *schedule
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*schedule, error)); ok {
|
||||
@ -106,6 +122,10 @@ func (_m *mockDAO) Get(ctx context.Context, id int64) (*schedule, error) {
|
||||
func (_m *mockDAO) List(ctx context.Context, query *q.Query) ([]*schedule, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []*schedule
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*schedule, error)); ok {
|
||||
@ -139,6 +159,10 @@ func (_m *mockDAO) Update(ctx context.Context, s *schedule, props ...string) err
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *schedule, ...string) error); ok {
|
||||
r0 = rf(ctx, s, props...)
|
||||
@ -153,6 +177,10 @@ func (_m *mockDAO) Update(ctx context.Context, s *schedule, props ...string) err
|
||||
func (_m *mockDAO) UpdateRevision(ctx context.Context, id int64, revision int64) (int64, error) {
|
||||
ret := _m.Called(ctx, id, revision)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateRevision")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (int64, error)); ok {
|
||||
|
@ -343,6 +343,12 @@ func (e *executionDAO) refreshStatus(ctx context.Context, id int64) (bool, strin
|
||||
return status != execution.Status, status, false, err
|
||||
}
|
||||
|
||||
type jsonbStru struct {
|
||||
keyPrefix string
|
||||
key string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.QuerySeter, error) {
|
||||
qs, err := orm.QuerySetter(ctx, &Execution{}, query)
|
||||
if err != nil {
|
||||
@ -352,39 +358,32 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
||||
// append the filter for "extra attrs"
|
||||
if query != nil && len(query.Keywords) > 0 {
|
||||
var (
|
||||
key string
|
||||
keyPrefix string
|
||||
value interface{}
|
||||
jsonbStrus []jsonbStru
|
||||
args []interface{}
|
||||
)
|
||||
for key, value = range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") {
|
||||
keyPrefix = "ExtraAttrs."
|
||||
break
|
||||
|
||||
for key, value := range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") && key != "ExtraAttrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "ExtraAttrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
if strings.HasPrefix(key, "extra_attrs.") {
|
||||
keyPrefix = "extra_attrs."
|
||||
break
|
||||
if strings.HasPrefix(key, "extra_attrs.") && key != "extra_attrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(keyPrefix) == 0 || keyPrefix == key {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
// key = extra_attrs.artifact.digest
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
// key = extra_attrs.a.b.c
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
keys := strings.Split(strings.TrimPrefix(key, keyPrefix), ".")
|
||||
var args []interface{}
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, value)
|
||||
inClause, err := orm.CreateInClause(ctx, buildInClauseSQLForExtraAttrs(keys), args...)
|
||||
idSQL, args := buildInClauseSQLForExtraAttrs(jsonbStrus)
|
||||
inClause, err := orm.CreateInClause(ctx, idSQL, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -395,23 +394,60 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
||||
}
|
||||
|
||||
// Param keys is strings.Split() after trim "extra_attrs."/"ExtraAttrs." prefix
|
||||
func buildInClauseSQLForExtraAttrs(keys []string) string {
|
||||
switch len(keys) {
|
||||
case 0:
|
||||
// won't fall into this case, as the if condition on "keyPrefix == key"
|
||||
// act as a place holder to ensure "default" is equivalent to "len(keys) >= 2"
|
||||
return ""
|
||||
case 1:
|
||||
return "select id from execution where extra_attrs->>?=?"
|
||||
default:
|
||||
// len(keys) >= 2
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
return fmt.Sprintf("select id from execution where extra_attrs->%s->>?=?", s)
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
//
|
||||
// key = extra_attrs.artifact.digest
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
//
|
||||
// key = extra_attrs.a.b.c
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []interface{}) {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var cond string
|
||||
var args []interface{}
|
||||
sql := "select id from execution where"
|
||||
|
||||
for i, jsonbStr := range jsonbStrus {
|
||||
if jsonbStr.key == "" || jsonbStr.value == "" {
|
||||
return "", nil
|
||||
}
|
||||
keys := strings.Split(strings.TrimPrefix(jsonbStr.key, jsonbStr.keyPrefix), ".")
|
||||
if len(keys) == 1 {
|
||||
if i == 0 {
|
||||
cond += "extra_attrs->>?=?"
|
||||
} else {
|
||||
cond += " and extra_attrs->>?=?"
|
||||
}
|
||||
}
|
||||
if len(keys) >= 2 {
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
if i == 0 {
|
||||
cond += fmt.Sprintf("extra_attrs->%s->>?=?", s)
|
||||
} else {
|
||||
cond += fmt.Sprintf(" and extra_attrs->%s->>?=?", s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, jsonbStr.value)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", sql, cond), args
|
||||
}
|
||||
|
||||
func buildExecStatusOutdateKey(id int64, vendor string) string {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user