Merge branch 'main' into main

This commit is contained in:
Orlix 2024-06-10 11:40:54 +03:00 committed by GitHub
commit ed0bbf0c8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
292 changed files with 11787 additions and 1274 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -56,3 +56,5 @@ src/server/v2.0/models/
src/server/v2.0/restapi/
.editorconfig
harborclient/
openapi-generator-cli.jar

View File

@ -165,6 +165,7 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 2.8 | 1.20.6 |
| 2.9 | 1.21.3 |
| 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.

View File

@ -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) .

View File

@ -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.

View File

@ -1 +1 @@
v2.11.0
v2.12.0

View File

@ -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'
$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'
@ -6801,6 +6816,8 @@ definitions:
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'
@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -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:

View File

@ -29,3 +29,15 @@ 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;
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)
);

View File

@ -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

View File

@ -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'}

View 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))

View 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 doesnt 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 %}

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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 {

View File

@ -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},

View File

@ -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},

View File

@ -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, "//"))
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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"
@ -58,6 +59,9 @@ import (
var (
// Ctl is a global artifact controller instance
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
}

View File

@ -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{})
}

View File

@ -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"`

View File

@ -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

View 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
}

View 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 = &registry.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{})
}

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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{

View File

@ -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})
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)

View File

@ -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

View File

@ -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"`
}

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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"
@ -72,6 +71,7 @@ const (
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 {

View File

@ -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,6 +64,8 @@ import (
type ControllerTestSuite struct {
suite.Suite
scanHandler *scanTest.Handler
artifactCtl *artifacttesting.Controller
accessoryMgr *accessorytesting.Manager
originalArtifactCtl artifact.Controller
@ -71,13 +74,14 @@ type ControllerTestSuite struct {
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})

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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:

View File

@ -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
}
}

View File

@ -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,9 +129,25 @@ 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 {

View File

@ -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
}

View File

@ -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:

View File

@ -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

View File

@ -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=

View File

@ -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,

View File

@ -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() {

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -27,4 +27,5 @@ const (
DigestOfIconAccCosign = "sha256:20401d5b3a0f6dbc607c8d732eb08471af4ae6b19811a4efce8c6a724aed2882"
DigestOfIconAccNotation = "sha256:3ac706e102bbe9362b400aa162df58135d35e66b9c3bee2165de92022d25fe34"
DigestOfIconAccNydus = "sha256:dfcb6617cd9c144358dc1b305b87bbe34f0b619f1e329116e6aee2e41f2e34cf"
DigestOfIconAccSBOM = "sha256:c19f80c357cd7e90d2a01b9ae3e2eb62ce447a2662bb590a19177d72d550bdae"
)

View File

@ -33,6 +33,7 @@ var (
model.TypeCosignSignature: icon.DigestOfIconAccCosign,
model.TypeNotationSignature: icon.DigestOfIconAccNotation,
model.TypeNydusAccelerator: icon.DigestOfIconAccNydus,
model.TypeHarborSBOM: icon.DigestOfIconAccSBOM,
}
)

View File

@ -77,8 +77,8 @@ const (
// TypeSubject ...
TypeSubject = "subject.accessory"
// TypeHarborSBOM identifies harbor.sbom
TypeHarborSBOM = "harbor.sbom"
// TypeHarborSBOM identifies sbom.harbor
TypeHarborSBOM = "sbom.harbor"
)
// AccessoryData ...

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -68,6 +68,7 @@ type Registration struct {
// 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"`
Capabilities map[string]interface{} `orm:"-" json:"capabilities,omitempty"`
}
// TableName for Endpoint
@ -151,18 +152,23 @@ 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 {
capType := capability.Type
if len(capType) == 0 {
capType = v1.ScanTypeVulnerability
}
if scanType == capType {
for _, mt := range capability.ConsumesMimeTypes {
if mt == mimeType {
return capability.ProducesMimeTypes
}
}
}
}
return nil
}

69
src/pkg/scan/handler.go Normal file
View 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)
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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)

View File

@ -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

View 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)
}

View File

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

View File

@ -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
}

View 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))
}

View 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
}

View 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"`
}

View File

@ -0,0 +1,47 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
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
View 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
}

View 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{})
}

View File

@ -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
}

View File

@ -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{
r := &model.Robot{
Name: "admin",
Secret: "Harbor12345",
},
}
annotations := map[string]string{

View File

@ -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

View 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
}

View 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)
}

View File

@ -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 {

View File

@ -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,25 +394,62 @@ 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
// 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, "->")
return fmt.Sprintf("select id from execution where extra_attrs->%s->>?=?", s)
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 {
return fmt.Sprintf("execution:id:%d:vendor:%s:status_outdate", id, vendor)
}

Some files were not shown because too many files have changed in this diff Show More