rebase: resolve the code confilcts with master

Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
Steven Zou 2019-10-17 17:42:41 +08:00
commit 0f16913635
203 changed files with 5248 additions and 918 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ src/common/dao/dao.test
jobservice/test
src/portal/coverage/
src/portal/lib/coverage/
src/portal/dist/
src/portal/html-report/
src/portal/node_modules/

View File

@ -106,6 +106,7 @@ CLAIRDBVERSION=$(VERSIONTAG)
MIGRATORVERSION=$(VERSIONTAG)
REDISVERSION=$(VERSIONTAG)
NOTARYMIGRATEVERSION=v3.5.4
CLAIRADAPTERVERSION=c7db8b15
# version of chartmuseum
CHARTMUSEUMVERSION=v0.9.0
@ -115,6 +116,7 @@ VERSION_TAG: $(VERSIONTAG)
REGISTRY_VERSION: $(REGISTRYVERSION)
NOTARY_VERSION: $(NOTARYVERSION)
CLAIR_VERSION: $(CLAIRVERSION)
CLAIR_ADAPTER_VERSION: $(CLAIRADAPTERVERSION)
CHARTMUSEUM_VERSION: $(CHARTMUSEUMVERSION)
endef
@ -251,7 +253,7 @@ ifeq ($(NOTARYFLAG), true)
DOCKERSAVE_PARA+= goharbor/notary-server-photon:$(NOTARYVERSION)-$(VERSIONTAG) goharbor/notary-signer-photon:$(NOTARYVERSION)-$(VERSIONTAG)
endif
ifeq ($(CLAIRFLAG), true)
DOCKERSAVE_PARA+= goharbor/clair-photon:$(CLAIRVERSION)-$(VERSIONTAG)
DOCKERSAVE_PARA+= goharbor/clair-photon:$(CLAIRVERSION)-$(VERSIONTAG) goharbor/clair-adapter-photon:$(CLAIRADAPTERVERSION)-$(VERSIONTAG)
endif
ifeq ($(MIGRATORFLAG), true)
DOCKERSAVE_PARA+= goharbor/harbor-migrator:$(MIGRATORVERSION)
@ -305,7 +307,7 @@ prepare: update_prepare_version
build:
make -f $(MAKEFILEPATH_PHOTON)/Makefile build -e DEVFLAG=$(DEVFLAG) \
-e REGISTRYVERSION=$(REGISTRYVERSION) -e NGINXVERSION=$(NGINXVERSION) -e NOTARYVERSION=$(NOTARYVERSION) -e NOTARYMIGRATEVERSION=$(NOTARYMIGRATEVERSION) \
-e CLAIRVERSION=$(CLAIRVERSION) -e CLAIRDBVERSION=$(CLAIRDBVERSION) -e VERSIONTAG=$(VERSIONTAG) \
-e CLAIRVERSION=$(CLAIRVERSION) -e CLAIRADAPTERVERSION=$(CLAIRADAPTERVERSION) -e CLAIRDBVERSION=$(CLAIRDBVERSION) -e VERSIONTAG=$(VERSIONTAG) \
-e BUILDBIN=$(BUILDBIN) -e REDISVERSION=$(REDISVERSION) -e MIGRATORVERSION=$(MIGRATORVERSION) \
-e CHARTMUSEUMVERSION=$(CHARTMUSEUMVERSION) -e DOCKERIMAGENAME_CHART_SERVER=$(DOCKERIMAGENAME_CHART_SERVER) \
-e NPM_REGISTRY=$(NPM_REGISTRY)

View File

@ -1,43 +0,0 @@
## CNCF Community Code of Conduct v1.0
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering
an open and welcoming community, we pledge to respect all people who contribute
through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses,
without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers
commit themselves to fairly and consistently applying these principles to every aspect
of managing this project. Project maintainers who do not follow or enforce the Code of
Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a CNCF project maintainer, Sarah Novotny <sarahnovotny@google.com>, and/or Dan Kohn <dan@linuxfoundation.org>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CNCF Events Code of Conduct
CNCF events are governed by the Linux Foundation [Code of Conduct](https://events.linuxfoundation.org/code-of-conduct/) available on the event page. This is designed to be compatible with the above policy and also includes more details on responding to incidents.

View File

@ -1,20 +1,21 @@
# Demo Server Guide
**Important!**
- Please note that this demo server is **ONLY** for your trial of Harbor functionalities.
- Please note that this demo server is **ONLY** to be used for experimenting with Harbor functionality.
- Please **DO NOT** upload any sensitive images to this server.
- We will **CLEAN AND RESET** the server every **TWO Days**.
- You can only experience the non-admin functionalities on this server. Please follow the **[Installation Guide](installation_guide.md)** to set up a Harbor server to try more advanced features.
- Please do not push large images(>100MB) as the server has limited storage.
- This is **NOT** a production environment and we are not responsible for any loss of data, functionality, or service.
- We will **CLEAN AND RESET** the server every **couple of days**.
- You can only experience the non-admin functionalities on this server. Please follow the **[Installation Guide](installation_guide.md)** to set up a Harbor server to try the more advanced features.
- Please do not push large images (>100MB) as the server has limited storage.
If you encounter any problems during using the demo server, please open an issue or ping us on [Slack](https://github.com/goharbor/harbor#community).
**Usage**
- 1> The address of the demo server is [https://demo.goharbor.io](https://demo.goharbor.io)
- 2> You can register a new user by yourself
- 3> Then you can use the account/password created in step 2 to log in
- The address of the demo server is [https://demo.goharbor.io](https://demo.goharbor.io)
- You can register as a new user
- Then you can use the account/password created in step 2 to log in
```
docker login demo.goharbor.io
```
You can refer to [User Guide](user_guide.md) for more details on how to use Harbor.
You can refer to the [User Guide](user_guide.md) for more details on how to use Harbor.

View File

@ -4008,7 +4008,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/ImmutableTagRule'
$ref: '#/definitions/ImmutableTagRule'
'400':
description: Illegal format of provided ID value.
'401':
@ -5115,35 +5115,7 @@ definitions:
description: 'The signature of image, defined by RepoSignature. If it is null, the image is unsigned.'
scan_overview:
type: object
description: The overview of the scan result. This is an optional property.
properties:
digest:
type: string
description: The digest of the image.
scan_status:
type: string
description: 'The status of the scan job, it can be "pending", "running", "finished", "error".'
job_id:
type: integer
description: The ID of the job on jobservice to scan the image.
severity:
type: integer
description: '0-Not scanned, 1-Negligible, 2-Unknown, 3-Low, 4-Medium, 5-High'
details_key:
type: string
description: 'The top layer name of this image in Clair, this is for calling Clair API to get the vulnerability list of this image.'
components:
type: object
description: The components overview of the image.
properties:
total:
type: integer
description: Total number of the components in this image.
summary:
description: List of number of components of different severities.
type: array
items:
$ref: '#/definitions/ComponentOverviewEntry'
description: The overview of the scan result.
labels:
type: array
description: The label list.
@ -6274,12 +6246,10 @@ definitions:
$ref: '#/definitions/RetentionRule'
trigger:
type: object
items:
$ref: '#/definitions/RetentionRuleTrigger'
$ref: '#/definitions/RetentionRuleTrigger'
scope:
type: object
items:
$ref: '#/definitions/RetentionPolicyScope'
$ref: '#/definitions/RetentionPolicyScope'
RetentionRuleTrigger:
type: object

View File

@ -64,6 +64,10 @@ DOCKERFILEPATH_CLAIR=$(DOCKERFILEPATH)/clair
DOCKERFILENAME_CLAIR=Dockerfile
DOCKERIMAGENAME_CLAIR=goharbor/clair-photon
DOCKERFILEPATH_CLAIR_ADAPTER=$(DOCKERFILEPATH)/clair-adapter
DOCKERFILENAME_CLAIR_ADAPTER=Dockerfile
DOCKERIMAGENAME_CLAIR_ADAPTER=goharbor/clair-adapter-photon
DOCKERFILEPATH_NGINX=$(DOCKERFILEPATH)/nginx
DOCKERFILENAME_NGINX=Dockerfile
DOCKERIMAGENAME_NGINX=goharbor/nginx-photon
@ -141,6 +145,16 @@ _build_clair:
echo "Done." ; \
fi
_build_clair_adapter:
# TODO: add support to fetch clair adapter binary from google storage ranther than build from source
@if [ "$(CLAIRFLAG)" = "true" ] ; then \
cd $(DOCKERFILEPATH_CLAIR_ADAPTER) && $(DOCKERFILEPATH_CLAIR_ADAPTER)/builder $(CLAIRADAPTERVERSION) && cd - ; \
echo "building clair adapter container for photon..." ; \
$(DOCKERBUILD) -f $(DOCKERFILEPATH_CLAIR_ADAPTER)/$(DOCKERFILENAME_CLAIR_ADAPTER) -t $(DOCKERIMAGENAME_CLAIR_ADAPTER):$(CLAIRADAPTERVERSION)-$(VERSIONTAG) . ; \
rm -rf $(DOCKERFILEPATH_CLAIR_ADAPTER)/binary; \
echo "Done." ; \
fi
_build_chart_server:
@if [ "$(CHARTFLAG)" = "true" ] ; then \
if [ "$(BUILDBIN)" != "true" ] ; then \
@ -209,7 +223,7 @@ define _get_binary
$(WGET) --timeout 30 --no-check-certificate $1 -O $2
endef
build: _build_prepare _build_db _build_portal _build_core _build_jobservice _build_log _build_nginx _build_registry _build_registryctl _build_notary _build_clair _build_redis _build_migrator _build_chart_server
build: _build_prepare _build_db _build_portal _build_core _build_jobservice _build_log _build_nginx _build_registry _build_registryctl _build_notary _build_clair _build_clair_adapter _build_redis _build_migrator _build_chart_server
cleanimage:
@echo "cleaning image for photon..."

View File

@ -0,0 +1,20 @@
FROM photon:2.0
RUN tdnf install -y sudo >>/dev/null\
&& tdnf clean all \
&& mkdir /clair-adapter/ \
&& groupadd -r -g 10000 clair-adapter \
&& useradd --no-log-init -m -r -g 10000 -u 10000 clair-adapter
COPY ./make/photon/clair-adapter/binary/harbor-scanner-clair /clair-adapter/clair-adapter
RUN chown -R 10000:10000 /clair-adapter \
&& chmod u+x /clair-adapter/clair-adapter
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1:8080/healthy || exit 1
USER clair-adapter
ENTRYPOINT ["/clair-adapter/clair-adapter"]

View File

@ -0,0 +1,7 @@
FROM golang:1.12.5
ADD . /go/src/github.com/goharbor/harbor-scanner-clair/
WORKDIR /go/src/github.com/goharbor/harbor-scanner-clair/
RUN export GOOS=linux GO111MODULE=on CGO_ENABLED=0 && \
go build github.com/goharbor/harbor-scanner-clair/cmd/harbor-scanner-clair

View File

@ -0,0 +1,39 @@
#!/bin/bash
set +e
if [ -z $1 ]; then
error "Please set the 'version' variable"
exit 1
fi
VERSION="$1"
set -e
# the temp folder to store binary file...
mkdir -p binary
rm -rf binary/harbor-scanner-clair || true
cd `dirname $0`
cur=$PWD
# the temp folder to store distribution source code...
TEMP=`mktemp -d ${TMPDIR-/tmp}/clair-adapter.XXXXXX`
git clone https://github.com/danielpacak/harbor-scanner-clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo 'build the clair adapter binary bases on the golang:1.12.5...'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-adapter-golang $TEMP
echo 'copy the clair adapter binary to local...'
ID=$(docker create clair-adapter-golang)
docker cp $ID:/go/src/github.com/goharbor/harbor-scanner-clair/harbor-scanner-clair binary
docker rm -f $ID
docker rmi -f clair-adapter-golang
echo "Build clair adapter binary success, then to build photon image..."
cd $cur
rm -rf $TEMP

View File

@ -13,6 +13,7 @@ from utils.core import prepare_core
from utils.notary import prepare_notary
from utils.log import prepare_log_configs
from utils.clair import prepare_clair
from utils.clair_adapter import prepare_clair_adapter
from utils.chart import prepare_chartmuseum
from utils.docker_compose import prepare_docker_compose
from utils.nginx import prepare_nginx, nginx_confd_dir
@ -54,6 +55,7 @@ def main(conf, with_notary, with_clair, with_chartmuseum):
if with_clair:
prepare_clair(config_dict)
prepare_clair_adapter(config_dict)
if with_chartmuseum:
prepare_chartmuseum(config_dict)

View File

@ -0,0 +1 @@
SCANNER_CLAIR_URL={{clair_url}}

View File

@ -36,6 +36,7 @@ CORE_URL={{core_url}}
CORE_LOCAL_URL={{core_local_url}}
JOBSERVICE_URL={{jobservice_url}}
CLAIR_URL={{clair_url}}
CLAIR_ADAPTER_URL={{clair_adapter_url}}
NOTARY_URL={{notary_url}}
REGISTRY_STORAGE_PROVIDER_NAME={{storage_provider_name}}
READ_ONLY=false

View File

@ -56,7 +56,7 @@ services:
- log
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "registry"
registryctl:
@ -89,7 +89,7 @@ services:
- log
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "registryctl"
{% if external_database == False %}
@ -125,7 +125,7 @@ services:
- log
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "postgresql"
{% endif %}
@ -186,7 +186,7 @@ services:
{% endif %}
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "core"
portal:
@ -307,7 +307,7 @@ services:
- log
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "proxy"
{% if with_notary %}
@ -336,7 +336,7 @@ services:
- notary-signer
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "notary-server"
notary-signer:
@ -366,7 +366,7 @@ services:
{% endif %}
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "notary-signer"
{% endif %}
@ -406,6 +406,29 @@ services:
tag: "clair"
env_file:
./common/config/clair/clair_env
clair-adapter:
networks:
- harbor-clair
container_name: clair-adapter
image: goharbor/clair-adapter-photon:{{clair_adapter_version}}
restart: always
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE
- SETGID
- SETUID
cpu_quota: 50000
dns_search: .
depends_on:
- clair
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "clair-adapter"
env_file:
./common/config/clair-adapter/env
{% endif %}
{% if with_chartmuseum %}
chartmuseum:
@ -439,7 +462,7 @@ services:
{% endif %}
logging:
driver: "syslog"
options:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "chartmuseum"
env_file:

View File

@ -45,7 +45,7 @@ http {
ssl_certificate_key {{ssl_cert_key}};
# Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2;
ssl_ciphers '!aNULL:kECDH+AESGCM:ECDH+AESGCM:RSA+AESGCM:kECDH+AES:ECDH+AES:RSA+AES:';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

View File

@ -0,0 +1,18 @@
import os
from g import templates_dir, config_dir
from .jinja import render_jinja
from .misc import prepare_dir
clair_adapter_template_dir = os.path.join(templates_dir, "clair-adapter")
def prepare_clair_adapter(config_dict):
clair_adapter_config_dir = prepare_dir(config_dir, "clair-adapter")
clair_adapter_env_path = os.path.join(clair_adapter_config_dir, "env")
clair_adapter_env_template = os.path.join(clair_adapter_template_dir, "env.jinja")
render_jinja(
clair_adapter_env_template,
clair_adapter_env_path,
**config_dict)

View File

@ -74,6 +74,7 @@ def parse_yaml_config(config_file_path, with_notary, with_clair, with_chartmuseu
'token_service_url': "http://core:8080/service/token",
'jobservice_url': 'http://jobservice:8080',
'clair_url': 'http://clair:6060',
'clair_adapter_url': 'http://clair-adapter:8080',
'notary_url': 'http://notary-server:4443',
'chart_repository_url': 'http://chartmuseum:9999'
}

View File

@ -11,9 +11,10 @@ docker_compose_yml_path = '/compose_location/docker-compose.yml'
def prepare_docker_compose(configs, with_clair, with_notary, with_chartmuseum):
versions = parse_versions()
VERSION_TAG = versions.get('VERSION_TAG') or 'dev'
REGISTRY_VERSION = versions.get('REGISTRY_VERSION') or 'v2.7.1'
REGISTRY_VERSION = versions.get('REGISTRY_VERSION') or 'v2.7.1-patch-2819-2553'
NOTARY_VERSION = versions.get('NOTARY_VERSION') or 'v0.6.1'
CLAIR_VERSION = versions.get('CLAIR_VERSION') or 'v2.0.9'
CLAIR_ADAPTER_VERSION = versions.get('CLAIR_ADAPTER_VERSION') or ''
CHARTMUSEUM_VERSION = versions.get('CHARTMUSEUM_VERSION') or 'v0.9.0'
rendering_variables = {
@ -22,6 +23,7 @@ def prepare_docker_compose(configs, with_clair, with_notary, with_chartmuseum):
'redis_version': VERSION_TAG,
'notary_version': '{}-{}'.format(NOTARY_VERSION, VERSION_TAG),
'clair_version': '{}-{}'.format(CLAIR_VERSION, VERSION_TAG),
'clair_adapter_version': '{}-{}'.format(CLAIR_ADAPTER_VERSION, VERSION_TAG),
'chartmuseum_version': '{}-{}'.format(CHARTMUSEUM_VERSION, VERSION_TAG),
'data_volume': configs['data_volume'],
'log_location': configs['log_location'],

View File

@ -73,6 +73,7 @@ var (
{Name: common.ClairDBSSLMode, Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_SSLMODE", DefaultValue: "disable", ItemType: &StringType{}, Editable: false},
{Name: common.ClairDBUsername, Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_USERNAME", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
{Name: common.ClairURL, Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_URL", DefaultValue: "http://clair:6060", ItemType: &StringType{}, Editable: false},
{Name: common.ClairAdapterURL, Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_ADAPTER_URL", DefaultValue: "http://clair-adapter:8080", ItemType: &StringType{}, Editable: false},
{Name: common.CoreURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "CORE_URL", DefaultValue: "http://core:8080", ItemType: &StringType{}, Editable: false},
{Name: common.CoreLocalURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "CORE_LOCAL_URL", DefaultValue: "http://127.0.0.1:8080", ItemType: &StringType{}, Editable: false},
@ -125,7 +126,7 @@ var (
{Name: common.RegistryStorageProviderName, Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_STORAGE_PROVIDER_NAME", DefaultValue: "filesystem", ItemType: &StringType{}, Editable: false},
{Name: common.RegistryURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_URL", DefaultValue: "http://registry:5000", ItemType: &StringType{}, Editable: false},
{Name: common.RegistryControllerURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_CONTROLLER_URL", DefaultValue: "http://registryctl:8080", ItemType: &StringType{}, Editable: false},
{Name: common.SelfRegistration, Scope: UserScope, Group: BasicGroup, EnvKey: "SELF_REGISTRATION", DefaultValue: "true", ItemType: &BoolType{}, Editable: false},
{Name: common.SelfRegistration, Scope: UserScope, Group: BasicGroup, EnvKey: "SELF_REGISTRATION", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
{Name: common.TokenExpiration, Scope: UserScope, Group: BasicGroup, EnvKey: "TOKEN_EXPIRATION", DefaultValue: "30", ItemType: &IntType{}, Editable: false},
{Name: common.TokenServiceURL, Scope: SystemScope, Group: BasicGroup, EnvKey: "TOKEN_SERVICE_URL", DefaultValue: "http://core:8080/service/token", ItemType: &StringType{}, Editable: false},

View File

@ -121,6 +121,7 @@ const (
GroupMember = "g"
ReadOnly = "read_only"
ClairURL = "clair_url"
ClairAdapterURL = "clair_adapter_url"
NotaryURL = "notary_url"
DefaultCoreEndpoint = "http://core:8080"
DefaultNotaryEndpoint = "http://notary-server:4443"

View File

@ -54,7 +54,7 @@ func AddBlobsToProject(projectID int64, blobs ...*models.Blob) (int64, error) {
})
}
cnt, err := GetOrmer().InsertMulti(10, projectBlobs)
cnt, err := GetOrmer().InsertMulti(100, projectBlobs)
if err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return cnt, ErrDupRows
@ -121,7 +121,7 @@ func CountSizeOfProject(pid int64) (int64, error) {
var blobs []models.Blob
sql := `
SELECT
SELECT
DISTINCT bb.digest,
bb.id,
bb.content_type,
@ -132,7 +132,7 @@ JOIN artifact_blob afnb
ON af.digest = afnb.digest_af
JOIN BLOB bb
ON afnb.digest_blob = bb.digest
WHERE af.project_id = ?
WHERE af.project_id = ?
AND bb.content_type != ?
`
_, err := GetOrmer().Raw(sql, pid, common.ForeignLayer).QueryRows(&blobs)
@ -152,7 +152,7 @@ AND bb.content_type != ?
func RemoveUntaggedBlobs(pid int64) error {
var blobs []models.Blob
sql := `
SELECT
SELECT
DISTINCT bb.digest,
bb.id,
bb.content_type,
@ -163,7 +163,7 @@ JOIN artifact_blob afnb
ON af.digest = afnb.digest_af
JOIN BLOB bb
ON afnb.digest_blob = bb.digest
WHERE af.project_id = ?
WHERE af.project_id = ?
`
_, err := GetOrmer().Raw(sql, pid).QueryRows(&blobs)
if len(blobs) == 0 {

View File

@ -49,19 +49,20 @@ func TestAddBlobsToProject(t *testing.T) {
OwnerID: 1,
})
require.Nil(t, err)
defer DeleteProject(pid)
for i := 0; i < 88888; i++ {
blobsCount := 88888
for i := 0; i < blobsCount; i++ {
blob := &models.Blob{
ID: int64(100000 + i), // Use fake id to speed this test
Digest: digest.FromString(utils.GenerateRandomString()).String(),
Size: 100,
}
_, err := AddBlob(blob)
require.Nil(t, err)
blobs = append(blobs, blob)
}
cnt, err := AddBlobsToProject(pid, blobs...)
require.Nil(t, err)
require.Equal(t, cnt, int64(88888))
require.Equal(t, cnt, int64(blobsCount))
}
func TestHasBlobInProject(t *testing.T) {

View File

@ -444,7 +444,7 @@ func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) st
func createNestedGroupFilter(userDN string) string {
filter := ""
filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=" + userDN + "))"
filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=" + goldap.EscapeFilter(userDN) + "))"
return filter
}

View File

@ -14,7 +14,7 @@ const (
idParam = ":id"
)
// ChartLabelAPI handles the requests of marking/removing lables to/from charts.
// ChartLabelAPI handles the requests of marking/removing labels to/from charts.
type ChartLabelAPI struct {
LabelResourceAPI
project *models.Project

View File

@ -269,7 +269,7 @@ func (cra *ChartRepositoryAPI) DeleteChartVersion() {
chartName := cra.GetStringFromPath(nameParam)
version := cra.GetStringFromPath(versionParam)
// Try to remove labels from deleting chart if exitsing
// Try to remove labels from deleting chart if existing
if err := cra.removeLabelsFromChart(chartName, version); err != nil {
cra.SendInternalServerError(err)
return
@ -408,7 +408,7 @@ func (cra *ChartRepositoryAPI) DeleteChart() {
}
func (cra *ChartRepositoryAPI) removeLabelsFromChart(chartName, version string) error {
// Try to remove labels from deleting chart if exitsing
// Try to remove labels from deleting chart if existing
resourceID := chartFullName(cra.namespace, chartName, version)
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, resourceID)
if err == nil && len(labels) > 0 {
@ -432,7 +432,7 @@ func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
return false
}
existsing, err := cra.ProjectMgr.Exists(namespace)
existing, err := cra.ProjectMgr.Exists(namespace)
if err != nil {
// Check failed with error
cra.SendInternalServerError(fmt.Errorf("failed to check existence of namespace %s with error: %s", namespace, err.Error()))
@ -440,7 +440,7 @@ func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
}
// Not existing
if !existsing {
if !existing {
cra.SendBadRequestError(fmt.Errorf("namespace %s is not existing", namespace))
return false
}

View File

@ -46,7 +46,7 @@ func (itr *ImmutableTagRuleAPI) Prepare() {
itr.ID = ruleID
}
itr.ctr = immutabletag.NewAPIController(immutabletag.NewDefaultRuleManager())
itr.ctr = immutabletag.ImmuCtr
if strings.EqualFold(itr.Ctx.Request.Method, "get") {
if !itr.requireAccess(rbac.ActionList) {

View File

@ -49,7 +49,7 @@ func (l *LabelAPI) Prepare() {
if method == http.MethodPut || method == http.MethodDelete {
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.SendBadRequestError(errors.New("invalid lable ID"))
l.SendBadRequestError(errors.New("invalid label ID"))
return
}

View File

@ -38,7 +38,7 @@ type QuotaMigrator interface {
// Usage computes the quota usage of all the projects
Usage([]ProjectInfo) ([]ProjectUsage, error)
// Persist record the data to DB, artifact, artifact_blob and blob tabel.
// Persist record the data to DB, artifact, artifact_blob and blob table.
Persist([]ProjectInfo) error
}

View File

@ -284,6 +284,9 @@ func persistPB(projects []quota.ProjectInfo) error {
}
_, err = dao.AddBlobsToProject(pro.ProjectID, blobsOfPro...)
if err != nil {
if err == dao.ErrDupRows {
continue
}
log.Error(err)
return err
}

View File

@ -275,7 +275,7 @@ func (ra *RepositoryAPI) Delete() {
}
log.Debugf("Tag: %s, digest: %s", t, digest)
if _, ok := signedTags[digest]; ok {
log.Errorf("Found signed tag, repostory: %s, tag: %s, deletion will be canceled", repoName, t)
log.Errorf("Found signed tag, repository: %s, tag: %s, deletion will be canceled", repoName, t)
ra.SendPreconditionFailedError(fmt.Errorf("tag %s is signed", t))
return
}
@ -882,7 +882,7 @@ func getManifest(client *registry.Repository,
return result, nil
}
// GetTopRepos returns the most populor repositories
// GetTopRepos returns the most popular repositories
func (ra *RepositoryAPI) GetTopRepos() {
count, err := ra.GetInt("count", 10)
if err != nil || count <= 0 {

View File

@ -108,6 +108,7 @@ func (r *RobotAPI) Post() {
return
}
robotReq.Visible = true
robotReq.ProjectID = r.project.ProjectID
if err := validateRobotReq(r.project, &robotReq); err != nil {
r.SendBadRequestError(err)
@ -144,7 +145,7 @@ func (r *RobotAPI) List() {
}
keywords := make(map[string]interface{})
keywords["ProjectID"] = r.robot.ProjectID
keywords["ProjectID"] = r.project.ProjectID
keywords["Visible"] = true
query := &q.Query{
Keywords: keywords,

View File

@ -385,6 +385,11 @@ func ClairDB() (*models.PostGreSQL, error) {
return clairDB, nil
}
// ClairAdapterEndpoint returns the endpoint of clair adapter instance, by default it's the one deployed within Harbor.
func ClairAdapterEndpoint() string {
return cfgMgr.Get(common.ClairAdapterURL).GetString()
}
// AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string.
func AdmiralEndpoint() string {
if cfgMgr.Get(common.AdmiralEndpoint).GetString() == "NA" {

View File

@ -48,6 +48,8 @@ import (
_ "github.com/goharbor/harbor/src/core/notifier/topic"
"github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/types"
"github.com/goharbor/harbor/src/replication"
@ -215,6 +217,19 @@ func main() {
if err := dao.InitClairDB(clairDB); err != nil {
log.Fatalf("failed to initialize clair database: %v", err)
}
// TODO: change to be internal adapter
reg := &scanner.Registration{
Name: "Clair",
Description: "The clair scanner adapter",
URL: config.ClairAdapterEndpoint(),
Disabled: false,
IsDefault: true,
}
if err := scan.EnsureScanner(reg); err != nil {
log.Fatalf("failed to initialize clair scanner: %v", err)
}
}
closing := make(chan struct{})

View File

@ -8,7 +8,7 @@ type NotificationHandler interface {
Handle(value interface{}) error
// IsStateful returns whether the handler is stateful or not.
// If handler is stateful, it will not be triggerred in parallel.
// If handler is stateful, it will not be triggered in parallel.
// Otherwise, the handler will be triggered concurrently if more
// than one same handler are matched the topics.
IsStateful() bool

View File

@ -26,7 +26,7 @@ type ProjectMetadataManager interface {
Add(projectID int64, meta map[string]string) error
// Delete metadatas whose keys are specified in parameter meta, if it
// is absent, delete all
Delete(projecdtID int64, meta ...string) error
Delete(projectID int64, meta ...string) error
// Update metadatas
Update(projectID int64, meta map[string]string) error
// Get metadatas whose keys are specified in parameter meta, if it is

View File

@ -25,7 +25,7 @@ import (
"github.com/goharbor/harbor/src/core/promgr/pmsdriver"
)
// ProjectManager is the project mamager which abstracts the operations related
// ProjectManager is the project manager which abstracts the operations related
// to projects
type ProjectManager interface {
Get(projectIDOrName interface{}) (*models.Project, error)

View File

@ -35,7 +35,7 @@ var statusMap = map[string]string{
job.JobServiceStatusScheduled: models.JobScheduled,
}
// Handler handles reqeust on /service/notifications/jobs/adminjob/*, which listens to the webhook of jobservice.
// Handler handles request on /service/notifications/jobs/adminjob/*, which listens to the webhook of jobservice.
type Handler struct {
api.BaseController
id int64

View File

@ -43,7 +43,7 @@ var statusMap = map[string]string{
job.JobServiceStatusSuccess: models.JobFinished,
}
// Handler handles reqeust on /service/notifications/jobs/*, which listens to the webhook of jobservice.
// Handler handles request on /service/notifications/jobs/*, which listens to the webhook of jobservice.
type Handler struct {
api.BaseController
id int64

View File

@ -29,7 +29,7 @@ type Handler struct {
// Get handles GET request, it checks the http header for user credentials
// and parse service and scope based on docker registry v2 standard,
// checkes the permission against local DB and generates jwt token.
// checks the permission against local DB and generates jwt token.
func (h *Handler) Get() {
request := h.Ctx.Request
log.Debugf("URL for token request: %s", request.URL.String())

View File

@ -59,7 +59,7 @@ func newRepositoryClient(endpoint, username, repository string) (*registry.Repos
return registry.NewRepository(repository, endpoint, client)
}
// WaitForManifestReady implements exponential sleeep to wait until manifest is ready in registry.
// WaitForManifestReady implements exponential sleep to wait until manifest is ready in registry.
// This is a workaround for https://github.com/docker/distribution/issues/2625
func WaitForManifestReady(repository string, tag string, maxRetry int) bool {
// The initial wait interval, hard-coded to 80ms, interval will be 80ms,200ms,500ms,1.25s,3.124999936s

View File

@ -48,6 +48,8 @@ import (
_ "github.com/goharbor/harbor/src/replication/adapter/quayio"
// register the Helm Hub adapter
_ "github.com/goharbor/harbor/src/replication/adapter/helmhub"
// register the GitLab adapter
_ "github.com/goharbor/harbor/src/replication/adapter/gitlab"
)
// Replication implements the job interface

View File

@ -4,6 +4,11 @@ import (
"github.com/goharbor/harbor/src/pkg/immutabletag/model"
)
var (
// ImmuCtr is a global variable for the default immutable controller implementation
ImmuCtr = NewAPIController(NewDefaultRuleManager())
)
// APIController to handle the requests related with immutabletag
type APIController interface {
// GetImmutableRule ...

View File

@ -0,0 +1,11 @@
package match
import (
"github.com/goharbor/harbor/src/pkg/art"
)
// ImmutableTagMatcher ...
type ImmutableTagMatcher interface {
// Match whether the candidate is in the immutable list
Match(c art.Candidate) (bool, error)
}

View File

@ -0,0 +1,88 @@
package rule
import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/art/selectors/index"
"github.com/goharbor/harbor/src/pkg/immutabletag"
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
"github.com/goharbor/harbor/src/pkg/immutabletag/model"
)
// Matcher ...
type Matcher struct {
pid int64
rules []model.Metadata
}
// Match ...
func (rm *Matcher) Match(c art.Candidate) (bool, error) {
if err := rm.getImmutableRules(); err != nil {
return false, err
}
cands := []*art.Candidate{&c}
for _, r := range rm.rules {
if r.Disabled {
continue
}
// match repositories according to the repository selectors
var repositoryCandidates []*art.Candidate
repositorySelectors := r.ScopeSelectors["repository"]
if len(repositorySelectors) < 1 {
continue
}
repositorySelector := repositorySelectors[0]
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
repositorySelector.Pattern)
if err != nil {
return false, err
}
repositoryCandidates, err = selector.Select(cands)
if err != nil {
return false, err
}
if len(repositoryCandidates) == 0 {
continue
}
// match tag according to the tag selectors
var tagCandidates []*art.Candidate
tagSelectors := r.TagSelectors
if len(tagSelectors) < 0 {
continue
}
tagSelector := r.TagSelectors[0]
selector, err = index.Get(tagSelector.Kind, tagSelector.Decoration,
tagSelector.Pattern)
if err != nil {
return false, err
}
tagCandidates, err = selector.Select(cands)
if err != nil {
return false, err
}
if len(tagCandidates) == 0 {
continue
}
return true, nil
}
return false, nil
}
func (rm *Matcher) getImmutableRules() error {
rules, err := immutabletag.ImmuCtr.ListImmutableRules(rm.pid)
if err != nil {
return err
}
rm.rules = rules
return nil
}
// NewRuleMatcher ...
func NewRuleMatcher(pid int64) match.ImmutableTagMatcher {
return &Matcher{
pid: pid,
}
}

View File

@ -0,0 +1,162 @@
package rule
import (
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag"
"github.com/goharbor/harbor/src/pkg/immutabletag/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// MatchTestSuite ...
type MatchTestSuite struct {
suite.Suite
t *testing.T
assert *assert.Assertions
require *require.Assertions
ctr immutabletag.APIController
ruleID int64
ruleID2 int64
}
// SetupSuite ...
func (s *MatchTestSuite) SetupSuite() {
test.InitDatabaseFromEnv()
s.t = s.T()
s.assert = assert.New(s.t)
s.require = require.New(s.t)
s.ctr = immutabletag.ImmuCtr
}
func (s *MatchTestSuite) TestImmuMatch() {
rule := &model.Metadata{
ID: 1,
ProjectID: 2,
Priority: 1,
Template: "latestPushedK",
Action: "immuablity",
TagSelectors: []*model.Selector{
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
},
},
ScopeSelectors: map[string][]*model.Selector{
"repository": {
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "redis",
},
},
},
}
rule2 := &model.Metadata{
ID: 1,
ProjectID: 2,
Priority: 1,
Template: "latestPushedK",
Action: "immuablity",
TagSelectors: []*model.Selector{
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "**",
},
},
ScopeSelectors: map[string][]*model.Selector{
"repository": {
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "mysql",
},
},
},
}
id, err := s.ctr.CreateImmutableRule(rule)
s.ruleID = id
s.require.NotNil(err)
id, err = s.ctr.CreateImmutableRule(rule2)
s.ruleID2 = id
s.require.NotNil(err)
match := NewRuleMatcher(2)
c1 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "redis",
Tag: "release-1.10",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
}
isMatch, err := match.Match(c1)
s.require.Equal(isMatch, true)
s.require.Nil(err)
c2 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "redis",
Tag: "1.10",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
}
isMatch, err = match.Match(c2)
s.require.Equal(isMatch, false)
s.require.Nil(err)
c3 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "mysql",
Tag: "9.4.8",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1"},
}
isMatch, err = match.Match(c3)
s.require.Equal(isMatch, true)
s.require.Nil(err)
c4 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "hello",
Tag: "world",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1"},
}
isMatch, err = match.Match(c4)
s.require.Equal(isMatch, false)
s.require.Nil(err)
}
// TearDownSuite clears env for test suite
func (s *MatchTestSuite) TearDownSuite() {
err := s.ctr.DeleteImmutableRule(s.ruleID)
require.NoError(s.T(), err, "delete immutable")
err = s.ctr.DeleteImmutableRule(s.ruleID2)
require.NoError(s.T(), err, "delete immutable")
}

View File

@ -99,23 +99,3 @@ func (r *robotAccountDao) DeleteRobotAccount(id int64) error {
_, err := dao.GetOrmer().QueryTable(&model.Robot{}).Filter("ID", id).Delete()
return err
}
func getRobotQuerySetter(query *model.RobotQuery) orm.QuerySeter {
qs := dao.GetOrmer().QueryTable(&model.Robot{})
if query == nil {
return qs
}
if len(query.Name) > 0 {
if query.FuzzyMatchName {
qs = qs.Filter("Name__icontains", query.Name)
} else {
qs = qs.Filter("Name", query.Name)
}
}
if query.ProjectID != 0 {
qs = qs.Filter("ProjectID", query.ProjectID)
}
return qs
}

View File

@ -326,8 +326,8 @@ func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChan
return bc.manager.UpdateStatus(trackID, change.Status, change.Metadata.Revision)
}
// makeRobotAccount creates a robot account based on the arguments for scanning.
func (bc *basicController) makeRobotAccount(pid int64, repository string, ttl int64) (string, error) {
// makeAuthorization creates authorization from a robot account based on the arguments for scanning.
func (bc *basicController) makeAuthorization(pid int64, repository string, ttl int64) (string, error) {
// Use uuid as name to avoid duplicated entries.
UUID, err := bc.uuid()
if err != nil {
@ -344,14 +344,14 @@ func (bc *basicController) makeRobotAccount(pid int64, repository string, ttl in
Action: rbac.ActionPull,
}}
account := &model.RobotCreate{
robotReq := &model.RobotCreate{
Name: UUID,
Description: "for scan",
ProjectID: pid,
Access: access,
}
rb, err := bc.rc.CreateRobotAccount(account)
rb, err := bc.rc.CreateRobotAccount(robotReq)
if err != nil {
return "", errors.Wrap(err, "scan controller: make robot account")
}
@ -369,8 +369,8 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
return "", errors.Wrap(err, "scan controller: launch scan job")
}
// Make a robot account with 30 minutes
robotAccount, err := bc.makeRobotAccount(artifact.NamespaceID, artifact.Repository, 1800)
// Make authorization from a robot account with 30 minutes
authorization, err := bc.makeAuthorization(artifact.NamespaceID, artifact.Repository, 1800)
if err != nil {
return "", errors.Wrap(err, "scan controller: launch scan job")
}
@ -379,7 +379,7 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
scanReq := &v1.ScanRequest{
Registry: &v1.Registry{
URL: externalURL,
Authorization: robotAccount,
Authorization: authorization,
},
Artifact: artifact,
}

View File

@ -15,6 +15,7 @@
package scan
import (
"encoding/base64"
"encoding/json"
"fmt"
"testing"
@ -163,7 +164,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
Action: "pull",
}}
rname := fmt.Sprintf("%s%s", common.RobotPrefix, "the-uuid-123")
rname := "the-uuid-123"
account := &model.RobotCreate{
Name: rname,
Description: "for scan",
@ -172,7 +173,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
}
rc.On("CreateRobotAccount", account).Return(&model.Robot{
ID: 1,
Name: rname,
Name: common.RobotPrefix + rname,
Token: "robot-account",
Description: "for scan",
ProjectID: suite.artifact.NamespaceID,
@ -182,7 +183,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
req := &v1.ScanRequest{
Registry: &v1.Registry{
URL: "https://core.com",
Authorization: "robot-account",
Authorization: "Basic " + base64.StdEncoding.EncodeToString([]byte(common.RobotPrefix+"the-uuid-123:robot-account")),
},
Artifact: suite.artifact,
}

44
src/pkg/scan/init.go Normal file
View File

@ -0,0 +1,44 @@
// 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 (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/q"
sc "github.com/goharbor/harbor/src/pkg/scan/api/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
)
// EnsureScanner ensure the scanner which specially name exists in the system
func EnsureScanner(registration *scanner.Registration) error {
q := &q.Query{
Keywords: map[string]interface{}{"url": registration.URL},
}
registrations, err := sc.DefaultController.ListRegistrations(q)
if err != nil {
return err
}
if len(registrations) == 0 {
if _, err := sc.DefaultController.CreateRegistration(registration); err != nil {
return err
}
log.Infof("initialized scanner named %s", registration.Name)
}
return nil
}

View File

@ -4,6 +4,7 @@ export * from "./service/index";
export * from "./error-handler/index";
export * from "./shared/shared.const";
export * from "./shared/shared.utils";
export * from "./shared/shared.module";
export * from "./utils";
export * from "./log/index";
export * from "./filter/index";

View File

@ -127,7 +127,7 @@ describe('RecentLogComponent (inline template)', () => {
}));
// Will fail after upgrade to angular 6. todo: need to fix it.
it('should support pagination', () => {
xit('should support pagination', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {

View File

@ -7,7 +7,8 @@
<input type="text" class="command-input" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget readonly/>
</span>
<span>
<input type="text" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget1 class="inputTarget">
<textarea name="inputTarget1" [(ngModel)]="defaultValue" class="inputTarget" #inputTarget1 rows="1" cols="{{inputSize}}">
</textarea >
</span>
<span>
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget1" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>

View File

@ -30,6 +30,7 @@ import { UserPermissionDefaultService, UserPermissionService } from "../service/
import { USERSTATICPERMISSION } from "../service/permission-static";
import { of } from "rxjs";
import { delay } from 'rxjs/operators';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
class RouterStub {
@ -161,7 +162,8 @@ describe('RepositoryComponent (inline template)', () => {
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterTestingModule
RouterTestingModule,
BrowserAnimationsModule
],
declarations: [
RepositoryComponent,

View File

@ -64,7 +64,7 @@ export interface Tag extends Base {
author: string;
created: Date;
signature?: string;
scan_overview?: VulnerabilitySummary;
scan_overview?: ScanOverview;
labels: Label[];
push_time?: string;
pull_time?: string;
@ -290,25 +290,43 @@ export enum VulnerabilitySeverity {
export interface VulnerabilityBase {
id: string;
severity: VulnerabilitySeverity;
severity: string;
package: string;
version: string;
}
export interface VulnerabilityItem extends VulnerabilityBase {
link: string;
fixedVersion: string;
links: string[];
fix_version: string;
layer?: string;
description: string;
}
export interface VulnerabilitySummary {
image_digest?: string;
scan_status: string;
job_id?: number;
severity: VulnerabilitySeverity;
components: VulnerabilityComponents;
update_time: Date; // Use as complete timestamp
report_id?: string;
mime_type?: string;
scan_status?: string;
severity?: string;
duration?: number;
summary?: SeveritySummary;
start_time?: Date;
end_time?: Date;
}
export interface SeveritySummary {
total: number;
summary: {[key: string]: number};
}
export interface VulnerabilityDetail {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"?: VulnerabilityReport;
}
export interface VulnerabilityReport {
vulnerabilities?: VulnerabilityItem[];
}
export interface ScanOverview {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"?: VulnerabilitySummary;
}
export interface VulnerabilityComponents {

View File

@ -1,13 +1,14 @@
import { HttpClient } from "@angular/common/http";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Injectable, Inject } from "@angular/core";
import { SERVICE_CONFIG, IServiceConfig } from "../service.config";
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from "../utils";
import { buildHttpRequestOptions, DEFAULT_SUPPORTED_MIME_TYPE, HTTP_JSON_OPTIONS } from "../utils";
import { RequestQueryParams } from "./RequestQueryParams";
import { VulnerabilityItem, VulnerabilitySummary } from "./interface";
import { VulnerabilityDetail, VulnerabilitySummary } from "./interface";
import { map, catchError } from "rxjs/operators";
import { Observable, of, throwError as observableThrowError } from "rxjs";
/**
* Get the vulnerabilities scanning results for the specified tag.
*
@ -46,7 +47,7 @@ export abstract class ScanningResultService {
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<VulnerabilityItem[]>;
| Observable<any>;
/**
* Start a new vulnerability scanning
@ -106,17 +107,22 @@ export class ScanningResultDefaultService extends ScanningResultService {
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<VulnerabilityItem[]> {
| Observable<any> {
if (!repoName || repoName.trim() === "" || !tagId || tagId.trim() === "") {
return observableThrowError("Bad argument");
}
let httpOptions = buildHttpRequestOptions(queryParams);
let requestHeaders = httpOptions.headers as HttpHeaders;
// Change the accept header to the supported report mime types
httpOptions.headers = requestHeaders.set("Accept", DEFAULT_SUPPORTED_MIME_TYPE);
return this.http
.get(
`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`,
buildHttpRequestOptions(queryParams)
`${this._baseUrl}/${repoName}/tags/${tagId}/scan`,
httpOptions
)
.pipe(map(response => response as VulnerabilityItem[])
.pipe(map(response => response as VulnerabilityDetail)
, catchError(error => observableThrowError(error)));
}

View File

@ -32,45 +32,18 @@
<label class="detail-label">{{'TAG.DOCKER_VERSION' | translate }}</label>
<div class="image-details" [title]="tagDetails.docker_version">{{tagDetails.docker_version}}</div>
</section>
<section class="detail-row">
<section class="detail-row" *ngIf="hasCve">
<label class="detail-label">{{'TAG.SCAN_COMPLETION_TIME' | translate }}</label>
<div class="image-details" [title]="scanCompletedDatetime | date">{{scanCompletedDatetime | date}}</div>
</section>
</section>
</div>
</div>
<div *ngIf="withClair" class="col-md-4 col-sm-6">
<div class="vulnerability">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
</div>
<div class="flex-block vulnerabilities-info">
<div class="second-column">
<div class="row-flex">
<div class="icon-position">
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
</div>
<span class="detail-count">{{highCount}}</span> {{packageText(highCount) | translate}} {{haveText(highCount) | translate}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
</div>
<span class="detail-count">{{mediumCount}}</span> {{packageText(mediumCount) | translate}} {{haveText(mediumCount) | translate}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="play" size="22" class="tip-icon-low rotate-90"></clr-icon>
</div>
<span class="detail-count">{{lowCount}}</span> {{packageText(lowCount) | translate}} {{haveText(lowCount) | translate}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="help" size="20"></clr-icon>
</div>
<span class="detail-count">{{unknownCount}}</span> {{packageText(unknownCount) | translate}} {{haveText(unknownCount) | translate}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="vulnerability" [hidden]="hasCve">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
@ -83,7 +56,7 @@
</div>
</section>
<clr-tabs>
<clr-tab *ngIf="hasVulnerabilitiesListPermission && withClair">
<clr-tab *ngIf="hasVulnerabilitiesListPermission">
<button clrTabLink [clrTabLinkInOverflow]="false" class="btn btn-link nav-link" id="tag-vulnerability" [class.active]='isCurrentTabLink("tag-vulnerability")' type="button" (click)='tabLinkClick("tag-vulnerability")'>{{'REPOSITORY.VULNERABILITY' | translate}}</button>
<clr-tab-content id="content1" *clrIfActive="true">
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [projectId]="projectId" [tagId]="tagId"></hbr-vulnerabilities-grid>
@ -96,4 +69,4 @@
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -75,8 +75,6 @@ $size24:24px;
margin-left: 36px;
}
.vulnerability{
margin-left: 50px;
margin-top: -12px;
margin-bottom: 20px;}
.vulnerabilities-info {
@ -151,6 +149,8 @@ $size24:24px;
.tip-icon-low{
color:yellow;
}
.margin-top-5px {
margin-top: 5px;
}

View File

@ -21,7 +21,7 @@ import {
ScanningResultDefaultService
} from "../service/index";
import { FilterComponent } from "../filter/index";
import { VULNERABILITY_SCAN_STATUS } from "../utils";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../utils";
import { VULNERABILITY_DIRECTIVES } from "../vulnerability-scanning/index";
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { ChannelService } from "../channel/channel.service";
@ -43,29 +43,15 @@ describe("TagDetailComponent (inline template)", () => {
let vulSpy: jasmine.Spy;
let manifestSpy: jasmine.Spy;
let mockVulnerability: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.finished,
severity: 5,
update_time: new Date(),
components: {
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
severity: "High",
end_time: new Date(),
summary: {
total: 124,
summary: [
{
severity: 1,
count: 90
},
{
severity: 3,
count: 10
},
{
severity: 4,
count: 10
},
{
severity: 5,
count: 13
}
]
summary: {
"High": 5,
"Low": 5
}
}
};
let mockTag: Tag = {
@ -80,7 +66,9 @@ describe("TagDetailComponent (inline template)", () => {
author: "steven",
created: new Date("2016-11-08T22:41:15.912313785Z"),
signature: null,
scan_overview: mockVulnerability,
scan_overview: {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": mockVulnerability
},
labels: []
};
@ -141,13 +129,13 @@ describe("TagDetailComponent (inline template)", () => {
id: "CVE-2016-" + (8859 + i),
severity:
i % 2 === 0
? VulnerabilitySeverity.HIGH
: VulnerabilitySeverity.MEDIUM,
? VULNERABILITY_SEVERITY.HIGH
: VULNERABILITY_SEVERITY.MEDIUM,
package: "package_" + i,
link: "https://security-tracker.debian.org/tracker/CVE-2016-4484",
links: ["https://security-tracker.debian.org/tracker/CVE-2016-4484"],
layer: "layer_" + i,
version: "4." + i + ".0",
fixedVersion: "4." + i + ".11",
fix_version: "4." + i + ".11",
description: "Mock data"
};
mockData.push(res);

View File

@ -1,12 +1,13 @@
import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
import { TagService, Tag, VulnerabilitySeverity } from "../service/index";
import { TagService, Tag, VulnerabilitySeverity, VulnerabilitySummary } from "../service/index";
import { ErrorHandler } from "../error-handler/index";
import { Label } from "../service/interface";
import { forkJoin } from "rxjs";
import { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { ChannelService } from "../channel/channel.service";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../utils";
const TabLinkContentMap: { [index: string]: string } = {
"tag-history": "history",
@ -26,7 +27,7 @@ export class TagDetailComponent implements OnInit {
_lowCount: number = 0;
_unknownCount: number = 0;
labels: Label;
vulnerabilitySummary: VulnerabilitySummary;
@Input()
tagId: string;
@Input()
@ -73,35 +74,15 @@ export class TagDetailComponent implements OnInit {
}
this.getTagPermissions(this.projectId);
this.channel.tagDetail$.subscribe(tag => {
this.getTagDetails(tag);
this.getTagDetails(tag);
});
}
getTagDetails(tagDetails): void {
getTagDetails(tagDetails: Tag): void {
this.tagDetails = tagDetails;
if (
this.tagDetails &&
this.tagDetails.scan_overview &&
this.tagDetails.scan_overview.components &&
this.tagDetails.scan_overview.components.summary
) {
this.tagDetails.scan_overview.components.summary.forEach(item => {
switch (item.severity) {
case VulnerabilitySeverity.UNKNOWN:
this._unknownCount += item.count;
break;
case VulnerabilitySeverity.LOW:
this._lowCount += item.count;
break;
case VulnerabilitySeverity.MEDIUM:
this._mediumCount += item.count;
break;
case VulnerabilitySeverity.HIGH:
this._highCount += item.count;
break;
default:
break;
}
});
if (tagDetails
&& tagDetails.scan_overview
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
}
}
onBack(): void {
@ -127,26 +108,58 @@ export class TagDetailComponent implements OnInit {
? this.tagDetails.author
: "TAG.ANONYMITY";
}
public get highCount(): number {
return this._highCount;
private getCountByLevel(level: string): number {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.summary
&& this.vulnerabilitySummary.summary.summary) {
return this.vulnerabilitySummary.summary.summary[level];
}
return 0;
}
/**
* count of critical level vulnerabilities
*/
get criticalCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.CRITICAL);
}
public get mediumCount(): number {
return this._mediumCount;
/**
* count of high level vulnerabilities
*/
get highCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.HIGH);
}
public get lowCount(): number {
return this._lowCount;
/**
* count of medium level vulnerabilities
*/
get mediumCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.MEDIUM);
}
public get unknownCount(): number {
return this._unknownCount;
/**
* count of low level vulnerabilities
*/
get lowCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.LOW);
}
/**
* count of unknown vulnerabilities
*/
get unknownCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.UNKNOWN);
}
/**
* count of negligible vulnerabilities
*/
get negligibleCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.NEGLIGIBLE);
}
get hasCve(): boolean {
return this.vulnerabilitySummary
&& this.vulnerabilitySummary.scan_status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.scan_overview
? this.tagDetails.scan_overview.update_time
&& this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]
? this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE].end_time
: null;
}
@ -208,4 +221,38 @@ export class TagDetailComponent implements OnInit {
error => this.errorHandler.error(error)
);
}
passMetadataToChart() {
return [
{
text: 'VULNERABILITY.SEVERITY.CRITICAL',
value: this.criticalCount ? this.criticalCount : 0,
color: 'red'
},
{
text: 'VULNERABILITY.SEVERITY.HIGH',
value: this.highCount ? this.highCount : 0,
color: '#e64524'
},
{
text: 'VULNERABILITY.SEVERITY.MEDIUM',
value: this.mediumCount ? this.mediumCount : 0,
color: 'orange'
},
{
text: 'VULNERABILITY.SEVERITY.LOW',
value: this.lowCount ? this.lowCount : 0,
color: '#007CBB'
},
{
text: 'VULNERABILITY.SEVERITY.NEGLIGIBLE',
value: this.negligibleCount ? this.negligibleCount : 0,
color: 'green'
},
{
text: 'VULNERABILITY.SEVERITY.UNKNOWN',
value: this.unknownCount ? this.unknownCount : 0,
color: 'grey'
},
];
}
}

View File

@ -55,23 +55,23 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" class="datagrid-top" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1 && hasEnabledScanner)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)"><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1)||!hasAddLabelImagePermission" (click)="addLabels(selectedRow)"><clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<clr-dropdown>
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</div>
</clr-dropdown>
</clr-dropdown-menu>
</clr-dropdown>
@ -81,7 +81,7 @@
<clr-dg-column class="flex-max-width" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
@ -98,8 +98,8 @@
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell *ngIf="withClair">
<hbr-vulnerability-bar [tagStatus]="t.scan_overview?.scan_status" [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
<clr-dg-cell>
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>

View File

@ -21,7 +21,6 @@
.embeded-datagrid {
width: 98%;
float: right;
/*add for issue #2688*/
}
.hidden-tag {
@ -249,4 +248,4 @@ clr-datagrid {
::ng-deep .clr-form-control {
margin-top: 0;
}
}

View File

@ -1,5 +1,5 @@
import { ComponentFixture, TestBed, async } from "@angular/core/testing";
import { DebugElement } from "@angular/core";
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from "@angular/core";
import { SharedModule } from "../shared/shared.module";
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
@ -23,8 +23,11 @@ import { LabelDefaultService, LabelService } from "../service/label.service";
import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { OperationService } from "../operation/operation.service";
import { Observable, of } from "rxjs";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { HttpClient } from "@angular/common/http";
describe("TagComponent (inline template)", () => {
@ -35,7 +38,11 @@ describe("TagComponent (inline template)", () => {
let spy: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let spyScanner: jasmine.Spy;
let scannerMock = {
disabled: false,
name: "Clair"
};
let mockTags: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
@ -108,7 +115,12 @@ describe("TagComponent (inline template)", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
SharedModule,
BrowserAnimationsModule,
HttpClientTestingModule
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
declarations: [
TagComponent,
@ -129,9 +141,9 @@ describe("TagComponent (inline template)", () => {
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{ provide: LabelService, useClass: LabelDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: OperationService }
{ provide: OperationService },
]
});
}).compileComponents();
}));
beforeEach(() => {
@ -154,7 +166,9 @@ describe("TagComponent (inline template)", () => {
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, "getTags").and.returnValues(of(mockTags).pipe(delay(0)));
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
let http: HttpClient;
http = fixture.debugElement.injector.get(HttpClient);
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
spyOn(userPermissionService, "getPermission")
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
.and.returnValue(of(mockHasAddLabelImagePermission))
@ -176,6 +190,10 @@ describe("TagComponent (inline template)", () => {
expect(spy.calls.any).toBeTruthy();
}));
it("should load project scanner", async(() => {
expect(spyScanner.calls.count()).toEqual(1);
}));
it("should load and render data", () => {
fixture.detectChanges();
fixture.whenStable().then(() => {

View File

@ -12,43 +12,38 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Component,
OnInit,
ViewChild,
Input,
Output,
EventEmitter,
AfterViewInit,
ChangeDetectorRef,
ElementRef, AfterViewInit
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from "@angular/core";
import { Subject, forkJoin } from "rxjs";
import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators';
import { forkJoin, Observable, Subject, throwError as observableThrowError } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'rxjs/operators';
import { TranslateService } from "@ngx-translate/core";
import { State, Comparator } from "../service/interface";
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
import { TagService, RetagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
import { RequestQueryParams, RetagService, TagService, VulnerabilitySeverity } from "../service/index";
import { ErrorHandler } from "../error-handler/error-handler";
import { ChannelService } from "../channel/index";
import {
ConfirmationTargets,
ConfirmationState,
ConfirmationButtons
} from "../shared/shared.const";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
import { Label, Tag, TagClickEvent, RetagRequest } from "../service/interface";
import {
CustomComparator,
calculatePage,
clone,
CustomComparator,
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
doFiltering,
doSorting,
VULNERABILITY_SCAN_STATUS,
DEFAULT_PAGE_SIZE,
clone,
} from "../utils";
import { CopyInputComponent } from "../push-image/copy-input.component";
@ -58,9 +53,10 @@ import { USERSTATICPERMISSION } from "../service/permission-static";
import { operateChanges, OperateInfo, OperationState } from "../operation/operate";
import { OperationService } from "../operation/operation.service";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { map, catchError } from "rxjs/operators";
import { errorHandler as errorHandFn } from "../shared/shared.utils";
import { Observable, throwError as observableThrowError } from "rxjs";
import { HttpClient } from "@angular/common/http";
import { ClrLoadingState } from "@clr/angular";
export interface LabelState {
iconsShow: boolean;
label: Label;
@ -152,6 +148,8 @@ export class TagComponent implements OnInit, AfterViewInit {
hasRetagImagePermission: boolean;
hasDeleteImagePermission: boolean;
hasScanImagePermission: boolean;
hasEnabledScanner: boolean;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
constructor(
private errorHandler: ErrorHandler,
private tagService: TagService,
@ -161,7 +159,8 @@ export class TagComponent implements OnInit, AfterViewInit {
private translateService: TranslateService,
private ref: ChangeDetectorRef,
private operationService: OperationService,
private channel: ChannelService
private channel: ChannelService,
private http: HttpClient
) { }
ngOnInit() {
@ -169,6 +168,7 @@ export class TagComponent implements OnInit, AfterViewInit {
this.errorHandler.error("Project ID cannot be unset.");
return;
}
this.getProjectScanner();
if (!this.repoName) {
this.errorHandler.error("Repo name cannot be unset.");
return;
@ -529,17 +529,6 @@ export class TagComponent implements OnInit, AfterViewInit {
.subscribe(items => {
// To keep easy use for vulnerability bar
items.forEach((t: Tag) => {
if (!t.scan_overview) {
t.scan_overview = {
scan_status: VULNERABILITY_SCAN_STATUS.stopped,
severity: VulnerabilitySeverity.UNKNOWN,
update_time: new Date(),
components: {
total: 0,
summary: []
}
};
}
if (t.signature !== null) {
signatures.push(t.name);
}
@ -722,28 +711,21 @@ export class TagComponent implements OnInit, AfterViewInit {
// Get vulnerability scanning status
scanStatus(t: Tag): string {
if (t && t.scan_overview && t.scan_overview.scan_status) {
return t.scan_overview.scan_status;
if (t) {
let so = this.handleScanOverview(t.scan_overview);
if (so && so.scan_status) {
return so.scan_status;
}
}
return VULNERABILITY_SCAN_STATUS.unknown;
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
}
existObservablePackage(t: Tag): boolean {
return t.scan_overview &&
t.scan_overview.components &&
t.scan_overview.components.total &&
t.scan_overview.components.total > 0 ? true : false;
}
// Whether show the 'scan now' menu
canScanNow(t: Tag[]): boolean {
if (!this.withClair) { return false; }
if (!this.hasScanImagePermission) { return false; }
let st: string = this.scanStatus(t[0]);
return st !== VULNERABILITY_SCAN_STATUS.pending &&
st !== VULNERABILITY_SCAN_STATUS.running;
return st !== VULNERABILITY_SCAN_STATUS.PENDING &&
st !== VULNERABILITY_SCAN_STATUS.RUNNING;
}
getImagePermissionRule(projectId: number): void {
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,
@ -776,4 +758,26 @@ export class TagComponent implements OnInit, AfterViewInit {
onCpError($event: any): void {
this.copyInput.setPullCommendShow();
}
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.http.get(`/api/projects/${this.projectId}/scanner`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)))
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disable
&& response.health) {
this.hasEnabledScanner = true;
}
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
handleScanOverview(scanOverview: any) {
if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
}
return null;
}
}

View File

@ -225,16 +225,35 @@ export class CustomComparator<T> implements Comparator<T> {
*/
export const DEFAULT_PAGE_SIZE: number = 15;
/**
* The default supported mime type
*/
export const DEFAULT_SUPPORTED_MIME_TYPE = "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0";
/**
* The state of vulnerability scanning
*/
export const VULNERABILITY_SCAN_STATUS = {
unknown: "n/a",
pending: "pending",
running: "running",
error: "error",
stopped: "stopped",
finished: "finished"
// front-end status
NOT_SCANNED: "Not Scanned",
// back-end status
PENDING: "Pending",
RUNNING: "Running",
ERROR: "Error",
STOPPED: "Stopped",
SUCCESS: "Success",
SCHEDULED: "Scheduled"
};
/**
* The severity of vulnerability scanning
*/
export const VULNERABILITY_SEVERITY = {
NEGLIGIBLE: "Negligible",
UNKNOWN: "Unknown",
LOW: "Low",
MEDIUM: "Medium",
HIGH: "High",
CRITICAL: "Critical"
};
/**

View File

@ -0,0 +1 @@
<canvas class="canvas" #barChart> HTML5 canvas not supported </canvas>

View File

@ -0,0 +1,28 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HistogramChartComponent } from './histogram-chart.component';
import { TranslateModule } from "@ngx-translate/core";
describe('HistogramChartComponent', () => {
let component: HistogramChartComponent;
let fixture: ComponentFixture<HistogramChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
],
declarations: [ HistogramChartComponent ],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HistogramChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,132 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
DoCheck,
ElementRef,
Input,
OnInit,
ViewChild
} from '@angular/core';
import { TranslateService } from "@ngx-translate/core";
import { forkJoin } from "rxjs";
@Component({
selector: 'histogram-chart',
templateUrl: './histogram-chart.component.html',
styleUrls: ['./histogram-chart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
@Input()
metadata: Array<{
text: string,
value: number,
color: string
}> = [];
translatedTextArr: Array<string> = [];
@Input()
isWhiteBackground: boolean = false;
max: number;
scale: number;
hasViewInit: boolean = false;
@ViewChild('barChart', { static: false }) barChart: ElementRef;
public context: CanvasRenderingContext2D;
constructor(private translate: TranslateService) { }
ngOnInit() {
this.translateText();
}
ngAfterViewInit(): void {
this.hasViewInit = true;
this.initChart();
}
ngDoCheck() {
if (this.hasViewInit) {
this.initChart();
}
}
translateText() {
if (this.metadata && this.metadata.length > 0) {
let textArr = [];
this.metadata.forEach(item => {
textArr.push(this.translate.get(item.text));
});
forkJoin(textArr).subscribe(
(res: string[]) => {
this.translatedTextArr = res;
if (this.hasViewInit) {
this.initChart();
}
}
);
}
}
initChart() {
if (this.barChart && this.metadata && this.metadata.length > 0) {
this.barChart.nativeElement.width = "240";
this.barChart.nativeElement.height = 25 + 20 * this.metadata.length + "";
this.context = this.barChart.nativeElement.getContext('2d');
this.getMax();
if (this.isWhiteBackground) {
this.context.fillStyle = "#000";
} else {
this.context.fillStyle = "#fff";
}
this.drawLine(50, 0, 50, 5 + this.metadata.length * 20);
this.drawLine(50, 5 + this.metadata.length * 20, 250, 5 + this.metadata.length * 20);
this.drawLine(90, 5 + this.metadata.length * 20, 90, 2 + this.metadata.length * 20);
this.drawLine(130, 5 + this.metadata.length * 20, 130, 2 + this.metadata.length * 20);
this.drawLine(170, 5 + this.metadata.length * 20, 170, 2 + this.metadata.length * 20);
this.drawLine(210, 5 + this.metadata.length * 20, 210, 2 + this.metadata.length * 20);
this.context.font = "12px";
this.context.textAlign = "center";
this.context.fillText(this.scale.toString(), 90, this.metadata.length * 20 + 18, 50);
this.context.fillText((2 * this.scale).toString(), 130, this.metadata.length * 20 + 18, 50);
this.context.fillText((3 * this.scale).toString(), 170, this.metadata.length * 20 + 18, 50);
this.context.fillText((4 * this.scale).toString(), 210, this.metadata.length * 20 + 18, 50);
this.metadata.forEach((item, index) => {
this.drawBar(index, item.color, item.value);
});
}
}
drawBar(index: number, color: string, value: number) {
this.context.textBaseline = "middle";
this.context.textAlign = "left";
this.context.fillStyle = color;
this.context.fillRect(50, 5 + index * 20, value / this.scale * 40, 15);
this.context.fillText(value.toString(), (value / this.scale * 40) + 53, 12 + index * 20, 37);
this.context.textAlign = "right";
let text = "";
if (this.translatedTextArr && this.translatedTextArr.length > 0) {
text = this.translatedTextArr[index];
} else {
text = this.metadata[index].text;
}
this.context.fillText(text, 47, 12 + index * 20, 47);
}
drawLine(x, y, X, Y) {
this.context.beginPath();
this.context.moveTo(x, y);
this.context.lineTo(X, Y);
if (this.isWhiteBackground) {
this.context.strokeStyle = "#000";
} else {
this.context.strokeStyle = "#fff";
}
this.context.stroke();
this.context.closePath();
}
getMax() {
let count = 1;
if (this.metadata && this.metadata.length > 0) {
this.metadata.forEach(item => {
if (item.value > count) {
count = item.value;
}
});
}
this.max = count;
this.scale = Math.ceil(count / 4);
}
}

View File

@ -2,13 +2,19 @@ import { Type } from "@angular/core";
import { ResultGridComponent } from './result-grid.component';
import { ResultBarChartComponent } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component';
import { HistogramChartComponent } from "./histogram-chart/histogram-chart.component";
import { ResultTipHistogramComponent } from "./result-tip-histogram/result-tip-histogram.component";
export * from './result-tip.component';
export * from "./result-grid.component";
export * from './result-bar-chart.component';
export * from './histogram-chart/histogram-chart.component';
export * from './result-tip-histogram/result-tip-histogram.component';
export const VULNERABILITY_DIRECTIVES: Type<any>[] = [
ResultGridComponent,
ResultTipComponent,
ResultBarChartComponent
ResultBarChartComponent,
HistogramChartComponent,
ResultTipHistogramComponent
];

View File

@ -1,7 +1,4 @@
<div class="bar-wrapper">
<div *ngIf="stopped" class="bar-state">
<span class="label">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
</div>
<div *ngIf="queued" class="bar-state">
<span class="label label-orange">{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
</div>
@ -16,10 +13,9 @@
<div class="progress loop loop-height"><progress></progress></div>
</div>
<div *ngIf="completed" class="bar-state bar-state-chart">
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
<hbr-result-tip-histogram [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
</div>
<div *ngIf="unknown" class="bar-state">
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
<span class="unknow-text">{{'VULNERABILITY.STATE.UNKNOWN' | translate}}</span>
<div *ngIf="otherStatus" class="bar-state">
<span class="label">{{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}</span>
</div>
</div>
</div>

View File

@ -16,6 +16,8 @@ import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { ChannelService } from '../channel/index';
import { ResultTipHistogramComponent } from "./result-tip-histogram/result-tip-histogram.component";
import { HistogramChartComponent } from "./histogram-chart/histogram-chart.component";
describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent;
@ -25,24 +27,15 @@ describe('ResultBarChartComponent (inline template)', () => {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
};
let mockData: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.finished,
severity: 5,
update_time: new Date(),
components: {
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
severity: "High",
end_time: new Date(),
summary: {
total: 124,
summary: [{
severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
summary: {
"High": 5,
"Low": 5
}
}
};
@ -53,7 +46,9 @@ describe('ResultBarChartComponent (inline template)', () => {
],
declarations: [
ResultBarChartComponent,
ResultTipComponent],
ResultTipComponent,
ResultTipHistogramComponent,
HistogramChartComponent],
providers: [
ErrorHandler,
ChannelService,
@ -62,7 +57,7 @@ describe('ResultBarChartComponent (inline template)', () => {
{ provide: ScanningResultService, useValue: ScanningResultDefaultService },
{ provide: JobLogService, useValue: JobLogDefaultService}
]
});
}).compileComponents();
}));
@ -83,21 +78,19 @@ describe('ResultBarChartComponent (inline template)', () => {
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
});
it('should show "not scanned" if status is STOPPED', async(() => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.stopped;
it('should show "not scanned" if status is STOPPED', () => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.STOPPED;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('span');
expect(el).toBeTruthy();
expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
expect(el.textContent).toEqual('VULNERABILITY.STATE.OTHER_STATUS');
});
}));
});
it('should show progress if status is SCANNING', async(() => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.running;
it('should show progress if status is SCANNING', () => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.RUNNING;
fixture.detectChanges();
fixture.whenStable().then(() => {
@ -106,12 +99,11 @@ describe('ResultBarChartComponent (inline template)', () => {
let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
expect(el).toBeTruthy();
});
}));
});
it('should show QUEUED if status is QUEUED', async(() => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
it('should show QUEUED if status is QUEUED', () => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.PENDING;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@ -122,19 +114,17 @@ describe('ResultBarChartComponent (inline template)', () => {
expect(el2.textContent).toEqual('VULNERABILITY.STATE.QUEUED');
});
}));
});
it('should show summary bar chart if status is COMPLETED', async(() => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.finished;
it('should show summary bar chart if status is COMPLETED', () => {
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
let el: HTMLElement = fixture.nativeElement.querySelector('hbr-result-tip-histogram');
expect(el).not.toBeNull();
expect(el.style.width).toEqual("73px");
});
}));
});
});

View File

@ -4,11 +4,10 @@ import {
OnInit,
OnDestroy,
ChangeDetectorRef,
ViewChild
} from '@angular/core';
import { Subscription , timer} from "rxjs";
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import { clone, DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SCAN_STATUS } from '../utils';
import {
VulnerabilitySummary,
TagService,
@ -19,7 +18,7 @@ import { ErrorHandler } from '../error-handler/index';
import { ChannelService } from '../channel/index';
import { JobLogService } from "../service/index";
const STATE_CHECK_INTERVAL: number = 2000; // 2s
const STATE_CHECK_INTERVAL: number = 3000; // 3s
const RETRY_TIMES: number = 3;
@Component({
@ -29,7 +28,6 @@ const RETRY_TIMES: number = 3;
})
export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() repoName: string = "";
@Input() tagStatus: string = "";
@Input() tagId: string = "";
@Input() summary: VulnerabilitySummary;
onSubmitting: boolean = false;
@ -48,8 +46,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
) { }
ngOnInit(): void {
if ((this.tagStatus === VULNERABILITY_SCAN_STATUS.running || this.tagStatus === VULNERABILITY_SCAN_STATUS.pending)
&& !this.stateCheckTimer) {
if ((this.status === VULNERABILITY_SCAN_STATUS.RUNNING ||
this.status === VULNERABILITY_SCAN_STATUS.PENDING) &&
!this.stateCheckTimer) {
// Avoid duplicated subscribing
this.stateCheckTimer = timer(0, STATE_CHECK_INTERVAL).subscribe(() => {
this.getSummary();
@ -78,41 +77,37 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
if (this.summary && this.summary.scan_status) {
return this.summary.scan_status;
}
return VULNERABILITY_SCAN_STATUS.stopped;
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
}
public get completed(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.finished;
return this.status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
public get error(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.error;
return this.status === VULNERABILITY_SCAN_STATUS.ERROR;
}
public get queued(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.pending;
return this.status === VULNERABILITY_SCAN_STATUS.PENDING;
}
public get scanning(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.running;
return this.status === VULNERABILITY_SCAN_STATUS.RUNNING;
}
public get stopped(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.stopped;
}
public get unknown(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.unknown;
public get otherStatus(): boolean {
return !(this.completed || this.error || this.queued || this.scanning);
}
scanNow(): void {
if (this.onSubmitting) {
// Avoid duplicated submitting
console.log("duplicated submit");
return;
}
if (!this.repoName || !this.tagId) {
console.log("bad repository or tag");
return;
}
@ -124,10 +119,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
// Forcely change status to queued after successful submitting
this.summary = {
scan_status: VULNERABILITY_SCAN_STATUS.pending,
severity: null,
components: null,
update_time: null
scan_status: VULNERABILITY_SCAN_STATUS.PENDING,
};
// Forcely refresh view
@ -154,8 +146,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.tagService.getTag(this.repoName, this.tagId)
.subscribe((t: Tag) => {
// To keep the same summary reference, use value copy.
this.copyValue(t.scan_overview);
if (t.scan_overview) {
this.copyValue(t.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]);
}
// Forcely refresh view
this.forceRefreshView(1000);
@ -183,11 +176,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
copyValue(newVal: VulnerabilitySummary): void {
if (!this.summary || !newVal || !newVal.scan_status) { return; }
this.summary.scan_status = newVal.scan_status;
this.summary.job_id = newVal.job_id;
this.summary.severity = newVal.severity;
this.summary.components = newVal.components;
this.summary.update_time = newVal.update_time;
this.summary = clone(newVal);
}
forceRefreshView(duration: number): void {
@ -203,8 +192,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}
}, duration);
}
viewLog(): string {
return this.jobLogService.getScanJobBaseUrl() + "/" + this.summary.job_id + "/log";
return `/api/repositories/${this.repoName}/tags/${this.tagId}/scan/${this.summary.report_id}/log`;
}
}

View File

@ -1,51 +1,65 @@
<div class="row result-row">
<div>
<div class="row flex-items-xs-right rightPos">
<div>
<div class="row flex-items-xs-right rightPos">
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filterEvt)="filterVulnerabilities($event)"></hbr-filter>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filterEvt)="filterVulnerabilities($event)"></hbr-filter>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-action-bar>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasScanImagePermission" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-cell><a href="{{res.link}}" target="_blank">{{res.id}}</a></clr-dg-cell>
<clr-dg-cell [ngSwitch]="res.severity">
<span *ngSwitchCase="5" class="label label-danger">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="4" class="label label-medium">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="3" class="label label-low">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="1" class="label">{{severityText(res.severity) | translate}}</span>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-cell>
<span *ngIf="!res.links">{{res.id}}</span>
<a *ngIf="res.links && res.links.length === 1" href="{{res.links[0]}}" target="_blank">{{res.id}}</a>
<span *ngIf="res.links && res.links.length > 1">
{{res.id}}
<clr-signpost>
<clr-signpost-content *clrIfOpen>
<div class="mt-5px" *ngFor="let link of res.links">
<a href="{{link}}" target="_blank">{{link}}</a>
</div>
</clr-signpost-content>
</clr-signpost>
</span>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="res.severity">
<span *ngSwitchCase="'Critical'" class="label label-critical">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'High'" class="label label-danger">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Medium'" class="label label-medium">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Low'" class="label label-low">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Negligible'" class="label label-negligible">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Unknown'" class="label label-unknown">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>
<div *ngIf="res.fixedVersion; else elseBlock">
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon>&nbsp;<span class="font-color-green">{{res.fixedVersion}}</span>
</div>
<ng-template #elseBlock>{{res.fixedVersion}}</ng-template>
</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span>
{{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>
<div *ngIf="res.fix_version; else elseBlock">
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon>&nbsp;<span class="font-color-green">{{res.fix_version}}</span>
</div>
<ng-template #elseBlock>{{res.fix_version}}</ng-template>
</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span> {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { VulnerabilityItem, VulnerabilitySeverity } from '../service/index';
import { VulnerabilityItem } from '../service/index';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { ResultGridComponent } from './result-grid.component';
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
@ -11,6 +11,7 @@ import {ChannelService} from "../channel/channel.service";
import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { of } from "rxjs";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from "../utils";
describe('ResultGridComponent (inline template)', () => {
let component: ResultGridComponent;
let fixture: ComponentFixture<ResultGridComponent>;
@ -49,19 +50,21 @@ describe('ResultGridComponent (inline template)', () => {
serviceConfig = TestBed.get(SERVICE_CONFIG);
scanningService = fixture.debugElement.injector.get(ScanningResultService);
let mockData: VulnerabilityItem[] = [];
let mockData: any = {};
mockData[DEFAULT_SUPPORTED_MIME_TYPE] = {};
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities = [];
for (let i = 0; i < 30; i++) {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
severity: i % 2 === 0 ? VULNERABILITY_SEVERITY.HIGH : VULNERABILITY_SEVERITY.MEDIUM,
package: "package_" + i,
link: "https://security-tracker.debian.org/tracker/CVE-2016-4484",
links: ["https://security-tracker.debian.org/tracker/CVE-2016-4484"],
layer: "layer_" + i,
version: '4.' + i + ".0",
fixedVersion: '4.' + i + '.11',
fix_version: '4.' + i + '.11',
description: "Mock data"
};
mockData.push(res);
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities.push(res);
}
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
@ -107,10 +110,6 @@ describe('ResultGridComponent (inline template)', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
// let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
// expect(de).toBeTruthy();
// let el: HTMLElement = de.nativeElement;
let el: HTMLElement = fixture.nativeElement.querySelector('.datagrid-cell a');
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('CVE-2016-8859');

View File

@ -1,8 +1,7 @@
import { Component, OnInit, Input } from '@angular/core';
import {
ScanningResultService,
VulnerabilityItem,
VulnerabilitySeverity
VulnerabilityItem
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
import { forkJoin } from "rxjs";
@ -10,6 +9,10 @@ import { forkJoin } from "rxjs";
import { ChannelService } from "../channel/channel.service";
import { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
import { finalize } from "rxjs/operators";
@Component({
selector: 'hbr-vulnerabilities-grid',
templateUrl: './result-grid.component.html',
@ -18,7 +21,7 @@ import { USERSTATICPERMISSION } from "../service/permission-static";
export class ResultGridComponent implements OnInit {
scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = [];
loading: boolean = false;
@Input() tagId: string;
@Input() repositoryId: string;
@Input() projectId: number;
@ -40,11 +43,17 @@ export class ResultGridComponent implements OnInit {
}
loadResults(repositoryId: string, tagId: string): void {
this.loading = true;
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
.subscribe((results: VulnerabilityItem[]) => {
this.dataCache = results;
if (results) {
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
.pipe(finalize(() => this.loading = false))
.subscribe((results) => {
if (results && results[DEFAULT_SUPPORTED_MIME_TYPE]) {
let report = results[DEFAULT_SUPPORTED_MIME_TYPE];
if (report.vulnerabilities) {
this.dataCache = report.vulnerabilities;
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
return;
}
}
}, error => { this.errorHandler.error(error); });
}
@ -62,17 +71,19 @@ export class ResultGridComponent implements OnInit {
this.loadResults(this.repositoryId, this.tagId);
}
severityText(severity: VulnerabilitySeverity): string {
severityText(severity: string): string {
switch (severity) {
case VulnerabilitySeverity.HIGH:
case VULNERABILITY_SEVERITY.CRITICAL:
return 'VULNERABILITY.SEVERITY.CRITICAL';
case VULNERABILITY_SEVERITY.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VulnerabilitySeverity.MEDIUM:
case VULNERABILITY_SEVERITY.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VulnerabilitySeverity.LOW:
case VULNERABILITY_SEVERITY.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VulnerabilitySeverity.NONE:
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
case VulnerabilitySeverity.UNKNOWN:
case VULNERABILITY_SEVERITY.UNKNOWN:
return 'VULNERABILITY.SEVERITY.UNKNOWN';
default:
return 'UNKNOWN';

View File

@ -0,0 +1,55 @@
<div class="tip-wrapper tip-position width-210">
<clr-tooltip>
<div clrTooltipTrigger class="tip-block">
<ng-container *ngIf="!isNone">
<div *ngIf="criticalCount > 0" class="tip-wrapper bar-block-critical shadow-critical width-30">{{criticalCount}}</div>
<div *ngIf="highCount > 0" class="margin-left-5 tip-wrapper bar-block-high shadow-high width-30">{{highCount}}</div>
<div *ngIf="mediumCount > 0" class="margin-left-5 tip-wrapper bar-block-medium shadow-medium width-30">{{mediumCount}}</div>
<div *ngIf="lowCount > 0" class="margin-left-5 tip-wrapper bar-block-low shadow-low width-30">{{lowCount}}</div>
<div *ngIf="negligibleCount > 0" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-30">{{negligibleCount}}</div>
<div *ngIf="unknownCount > 0" class="margin-left-5 tip-wrapper bar-block-unknown shadow-unknown width-30">{{unknownCount}}</div>
</ng-container>
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
</div>
<clr-tooltip-content class="w-800" [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
<div class="bar-tooltip-font-larger">
<ng-container *ngIf="isCritical">
<clr-icon shape="exclamation-circle" class="is-error" size="32"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.CRITICAL' | translate | titlecase }}</span></span>
</ng-container>
<ng-container *ngIf="isHigh">
<clr-icon shape="exclamation-triangle" class="is-error" size="32"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.HIGH' | translate | titlecase }}</span></span>
</ng-container>
<ng-container *ngIf="isMedium">
<clr-icon shape="minus-circle" class="tip-icon-medium" size="30"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.MEDIUM' | translate | titlecase}}</span></span>
</ng-container>
<ng-container *ngIf="isLow">
<clr-icon shape="info-circle" class="tip-icon-low" size="32"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.LOW' | translate | titlecase }}</span></span>
</ng-container>
<ng-container *ngIf="isUnknown">
<clr-icon shape="help" size="24" class="help-icon"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.UNKNOWN' | translate | titlecase }}</span></span>
</ng-container>
<ng-container *ngIf="isNegligible">
<clr-icon shape="circle" class="is-success" size="32"></clr-icon>
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.NEGLIGIBLE' | translate | titlecase }}</span></span>
</ng-container>
<ng-container *ngIf="isNone">
<clr-icon shape="check-circle" class="is-success" size="32"></clr-icon>
<span>{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</span>
</ng-container>
</div>
<hr>
<div class="bar-summary bar-tooltip-fon" *ngIf="!isNone">
<histogram-chart [isWhiteBackground]="false" [metadata]="passMetadataToChart()"></histogram-chart>
</div>
<div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date:'short'}}</span>
</div>
</clr-tooltip-content>
</clr-tooltip>
</div>

View File

@ -0,0 +1,222 @@
.bar-wrapper {
width: 144px;
height: 12px;
}
.bar-state {
text-align: center;
.unknow-text {
margin-left: -5px;
}
}
.bar-state-chart {
margin-top: 2px;
.loop-height {
height: 2px;
}
}
.bar-state-error {
position: relative;
top: -4px;
}
.error-text {
position: relative;
top: 1px;
margin-left: -5px;
cursor: pointer;
}
.scanning-button {
height: 24px;
margin-top: 0;
margin-bottom: 0;
vertical-align: middle;
top: -12px;
position: relative;
}
.tip-wrapper {
display: inline-block;
height: 15px;
color: #fff;
text-align: center;
font-size: 10px;
line-height: 15px;
}
.tip-position {
margin-left: -4px;
}
.tip-block {
margin-left: -3px;
}
.bar-block-critical {
background-color: red;
}
.bar-block-high {
background-color: #e64524;
}
.bar-block-medium {
background-color: orange;
}
.bar-block-low {
background-color: #007CBB;
}
.bar-block-none {
background-color: green;
}
.bar-block-unknown {
background-color: grey;
}
.bar-tooltip-font {
font-size: 13px;
color: #ffffff;
}
.bar-tooltip-font-title {
font-weight: 600;
}
.bar-summary {
margin-top: 12px;
text-align: left;
}
.bar-scanning-time {
margin-top: 12px;
}
.bar-summary-item {
margin-top: 3px;
margin-bottom: 3px;
span {
:nth-child(1) {
width: 30px;
text-align: center;
display: inline-block;
}
:nth-child(2) {
width: 28px;
display: inline-block;
}
}
}
.option-right {
padding-right: 16px;
}
.refresh-btn {
cursor: pointer;
:hover {
color: #007CBB;
}
}
.label.label-medium {
background-color: #ffe4a9;
border: 1px solid orange;
color: orange;
}
.tip-icon-medium {
color: orange;
}
.label.label-low {
background: rgba(251, 255, 0, 0.38);
color: #c5c50b;
border: 1px solid #e6e63f;
}
.tip-icon-low {
color: #007CBB;
}
.font-color-green {
color: green;
}
.bar-tooltip-font-larger {
span {
font-size: 16px;
vertical-align: middle
}
}
hr {
border-bottom: 0;
border-color: #aaa;
margin: 6px -10px;
}
.font-weight-600 {
font-weight: 600;
}
.result-row {
position: relative;
}
.help-icon {
margin-left: 3px;
}
.ml-3px {
margin-left: 3px;
}
.shadow-critical {
box-shadow: 1px -1px 1px red;
}
.shadow-high {
box-shadow: 1px -1px 1px #e64524;
}
.shadow-medium {
box-shadow: 1px -1px 1px orange;
}
.shadow-low {
box-shadow: 1px -1px 1px #007CBB;
}
.shadow-none {
box-shadow: 1px -1px 1px green;
}
.shadow-unknown {
box-shadow: 1px -1px 1px gray;
}
.w-360 {
width: 360px !important;
}
.margin-left-5 {
margin-left: 5px;
}
.width-30 {
width: 30px;
}
.width-210 {
width: 210px;
}
.width-150 {
width: 150px;
}

View File

@ -0,0 +1,38 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ResultTipHistogramComponent } from './result-tip-histogram.component';
import { ClarityModule } from "@clr/angular";
import { TranslateModule, TranslateService } from "@ngx-translate/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HistogramChartComponent } from "..";
describe('ResultTipHistogramComponent', () => {
let component: ResultTipHistogramComponent;
let fixture: ComponentFixture<ResultTipHistogramComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot()
],
providers: [
TranslateService
],
declarations: [
ResultTipHistogramComponent,
HistogramChartComponent
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultTipHistogramComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,176 @@
import { Component, Input, OnInit } from '@angular/core';
import { VulnerabilitySummary } from "../../service";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
import { TranslateService } from "@ngx-translate/core";
@Component({
selector: 'hbr-result-tip-histogram',
templateUrl: './result-tip-histogram.component.html',
styleUrls: ['./result-tip-histogram.component.scss']
})
export class ResultTipHistogramComponent implements OnInit {
_tipTitle: string = "";
@Input() vulnerabilitySummary: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.NOT_SCANNED,
severity: "",
};
constructor(private translate: TranslateService) { }
ngOnInit(): void {
let key = "VULNERABILITY.SEVERITY.UNKNOWN";
switch (this.vulnerabilitySummary.severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
key = "VULNERABILITY.SEVERITY.CRITICAL";
break;
case VULNERABILITY_SEVERITY.HIGH:
key = "VULNERABILITY.SEVERITY.HIGH";
break;
case VULNERABILITY_SEVERITY.MEDIUM:
key = "VULNERABILITY.SEVERITY.MEDIUM";
break;
case VULNERABILITY_SEVERITY.LOW:
key = "VULNERABILITY.SEVERITY.LOW";
break;
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
key = "VULNERABILITY.SEVERITY.NEGLIGIBLE";
break;
default:
break;
}
this.translate.get(key).subscribe( (res: string) => {
this._tipTitle = res;
});
}
get tipTitle(): string {
return this._tipTitle;
}
get total(): number {
if (this.vulnerabilitySummary &&
this.vulnerabilitySummary.summary) {
return this.vulnerabilitySummary.summary.total;
}
return 0;
}
get sevSummary(): {[key: string]: number} {
if (this.vulnerabilitySummary &&
this.vulnerabilitySummary.summary) {
return this.vulnerabilitySummary.summary.summary;
}
return null;
}
get criticalCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.CRITICAL];
}
return 0;
}
get highCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
}
return 0;
}
get mediumCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.MEDIUM];
}
return 0;
}
get lowCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.LOW];
}
return 0;
}
get unknownCount(): number {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.summary
&& this.vulnerabilitySummary.summary.summary) {
return this.vulnerabilitySummary.summary.summary[VULNERABILITY_SEVERITY.UNKNOWN];
}
return 0;
}
get negligibleCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.NEGLIGIBLE];
}
return 0;
}
get completeTimestamp(): Date {
return this.vulnerabilitySummary && this.vulnerabilitySummary.end_time ? this.vulnerabilitySummary.end_time : new Date();
}
get isCritical(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.CRITICAL === this.vulnerabilitySummary.severity;
}
get isHigh(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.HIGH === this.vulnerabilitySummary.severity;
}
get isMedium(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.MEDIUM === this.vulnerabilitySummary.severity;
}
get isLow(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.LOW === this.vulnerabilitySummary.severity;
}
get isUnknown(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.UNKNOWN === this.vulnerabilitySummary.severity;
}
get isNegligible(): boolean {
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.NEGLIGIBLE === this.vulnerabilitySummary.severity;
}
get isNone(): boolean {
return this.total === 0;
}
passMetadataToChart() {
return [
{
text: 'VULNERABILITY.SEVERITY.CRITICAL',
value: this.criticalCount ? this.criticalCount : 0,
color: 'red'
},
{
text: 'VULNERABILITY.SEVERITY.HIGH',
value: this.highCount ? this.highCount : 0,
color: '#e64524'
},
{
text: 'VULNERABILITY.SEVERITY.MEDIUM',
value: this.mediumCount ? this.mediumCount : 0,
color: 'orange'
},
{
text: 'VULNERABILITY.SEVERITY.LOW',
value: this.lowCount ? this.lowCount : 0,
color: '#007CBB'
},
{
text: 'VULNERABILITY.SEVERITY.NEGLIGIBLE',
value: this.negligibleCount ? this.negligibleCount : 0,
color: 'green'
},
{
text: 'VULNERABILITY.SEVERITY.UNKNOWN',
value: this.unknownCount ? this.unknownCount : 0,
color: 'grey'
},
];
}
}

View File

@ -15,24 +15,15 @@ describe('ResultTipComponent (inline template)', () => {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
};
let mockData: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.finished,
severity: 5,
update_time: new Date(),
components: {
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
severity: "High",
end_time: new Date(),
summary: {
total: 124,
summary: [{
severity: 1,
count: 90
}, {
severity: 3,
count: 10
}, {
severity: 4,
count: 10
}, {
severity: 5,
count: 13
}]
summary: {
"High": 5,
"Low": 5
}
}
};
@ -66,10 +57,10 @@ describe('ResultTipComponent (inline template)', () => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
expect(el).not.toBeNull();
expect(el.style.width).toEqual("73px");
expect(el.style.width).toEqual("0px");
let el2: HTMLElement = fixture.nativeElement.querySelector('.bar-block-high');
expect(el2).not.toBeNull();
expect(el2.style.width).toEqual("10px");
expect(el2.style.width).toEqual("0px");
});
}));

View File

@ -1,7 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index';
import { TranslateService } from '@ngx-translate/core';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
export const MIN_TIP_WIDTH = 5;
@ -24,13 +22,9 @@ export class ResultTipComponent implements OnInit {
packagesWithVul: number = 0;
@Input() summary: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
severity: VulnerabilitySeverity.UNKNOWN,
update_time: new Date(),
components: {
total: 0,
summary: []
}
scan_status: VULNERABILITY_SCAN_STATUS.NOT_SCANNED,
severity: "",
end_time: new Date(),
};
get scanLevel() {
@ -51,56 +45,9 @@ export class ResultTipComponent implements OnInit {
return level;
}
constructor(private translate: TranslateService) { }
constructor() { }
ngOnInit(): void {
this.totalPackages = this.summary && this.summary.components ? this.summary.components.total : 0;
if (this.summary && this.summary.components && this.summary.components.summary) {
this.summary.components.summary.forEach(item => {
if (item.severity !== VulnerabilitySeverity.NONE) {
this.packagesWithVul += item.count;
}
switch (item.severity) {
case VulnerabilitySeverity.UNKNOWN:
this._unknownCount += item.count;
break;
case VulnerabilitySeverity.NONE:
this._noneCount += item.count;
break;
case VulnerabilitySeverity.LOW:
this._lowCount += item.count;
break;
case VulnerabilitySeverity.MEDIUM:
this._mediumCount += item.count;
break;
case VulnerabilitySeverity.HIGH:
this._highCount += item.count;
break;
default:
break;
}
});
}
this.translate.get(this.packageText(this.totalPackages)).subscribe((p1: string) => {
this.translate.get(this.unitText(this.packagesWithVul)).subscribe((vul: string) => {
if (this.totalPackages === 0) {
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO').subscribe( (res: string) => {
this._tipTitle = res;
});
} else {
let messageKey = 'VULNERABILITY.CHART.TOOLTIPS_TITLE_SINGULAR';
if (this.packagesWithVul > 1) {
messageKey = 'VULNERABILITY.CHART.TOOLTIPS_TITLE';
}
this.translate.get(messageKey, {
totalVulnerability: this.packagesWithVul,
totalPackages: this.totalPackages,
package: p1,
vulnerability: vul
}).subscribe((res: string) => this._tipTitle = res);
}
});
});
}
tipWidth(severity: VulnerabilitySeverity): string {
@ -169,7 +116,7 @@ export class ResultTipComponent implements OnInit {
}
public get completeTimestamp(): Date {
return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
return this.summary && this.summary.end_time ? this.summary.end_time : new Date();
}
public get hasHigh(): boolean {

View File

@ -1,6 +1,6 @@
.bar-wrapper {
width: 120px;
height: 12px;
width: 210px;
height: 15px;
}
.bar-state {
text-align: center !important;
@ -48,7 +48,7 @@
margin-left: -3px;
}
.bar-block-high {
background-color: #e62700;
background-color: #e64524;
}
.bar-block-medium {
background-color: orange;
@ -100,19 +100,10 @@
color: #007CBB;
}
.label.label-medium{
background-color: #ffe4a9;
border: 1px solid orange;
color: orange;
}
.tip-icon-medium {
color: orange;
}
.label.label-low{
background: rgba(251, 255, 0, 0.38);
color: #c5c50b;
border: 1px solid #e6e63f;
}
.tip-icon-low {
color: yellow;
}
@ -144,4 +135,40 @@ hr{
.help-icon {
margin-left: 3px;
}
}
.mt-3px {
margin-top: 5px;
}
.label-critical {
background:red;
color:#621501;
border:1px solid #f8b5b4;
}
.label-danger {
background:#e64524!important;
color:#621501!important;
border:1px solid #f8b5b4!important;
}
.label-medium {
background-color: orange;
color:#621501;
border:1px solid #f8b5b4;
}
.label-low {
background: #007CBB;
color:#cab6b1;
border:1px solid #f8b5b4;
}
.label-negligible {
background-color: green;
color:#bad7ba;
border:1px solid #f8b5b4;
}
.label-unknown {
background-color: grey;
color:#bad7ba;
border:1px solid #f8b5b4;
}

View File

@ -10,8 +10,8 @@ import { throwError as observableThrowError, Observable } from 'rxjs';
export class AccountSettingsModalService {
constructor(private http: HttpClient) { }
generateCli(userId): Observable<any> {
return this.http.post(`/api/users/${userId}/gen_cli_secret`, {}).pipe( map(response => response)
saveNewCli(userId, secretObj): Observable<any> {
return this.http.put(`/api/users/${userId}/cli_secret`, secretObj).pipe( map(response => response)
, catchError(error => observableThrowError(error)));
}
}

View File

@ -4,10 +4,11 @@
<inline-alert (confirmEvt)="confirmYes($event)" (closeEvt)="confirmNo($event)"></inline-alert>
<form #accountSettingsFrom="ngForm" class="clr-form clr-form-horizontal">
<div class="clr-form-control">
<label for="account_settings_username" aria-haspopup="true" class="clr-control-label">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="account_settings_username" aria-haspopup="true"
class="clr-control-label">{{'PROFILE.USER_NAME' | translate}}</label>
<div class="clr-control-container display-flex">
<input class="clr-input" type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username"
size="30">
<input class="clr-input" type="text" name="account_settings_username" [(ngModel)]="account.username"
disabled id="account_settings_username" size="30">
<div *ngIf="canRename" class="rename-tool">
<button [disabled]="RenameOnGoing" (click)="onRename()" class="btn btn-outline btn-sm">
{{'PROFILE.ADMIN_RENAME_BUTTON' | translate}}
@ -22,11 +23,13 @@
</div>
</div>
<div class="clr-form-control">
<label for="account_settings_email" class="required clr-control-label">{{'PROFILE.EMAIL' | translate}}</label>
<label for="account_settings_email"
class="required clr-control-label">{{'PROFILE.EMAIL' | translate}}</label>
<div class="clr-control-container" [class.clr-error]="!getValidationState('account_settings_email')">
<div class="clr-input-wrapper">
<input name="account_settings_email" type="text" #eamilInput="ngModel" class="clr-input" [(ngModel)]="account.email" required
email id="account_settings_email" size="30" (input)='handleValidation("account_settings_email", false)'
<input name="account_settings_email" type="text" #eamilInput="ngModel" class="clr-input"
[(ngModel)]="account.email" required email id="account_settings_email" size="30"
(input)='handleValidation("account_settings_email", false)'
(blur)='handleValidation("account_settings_email", true)'>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkProgress"></span>
@ -38,8 +41,10 @@
</div>
<clr-input-container>
<label [class.required]="!account.oidc_user_meta">{{'PROFILE.FULL_NAME' | translate}}</label>
<input clrInput type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" [required]="!account.oidc_user_meta"
maxLengthExt="20" id="account_settings_full_name" size="30" (input)='handleValidation("account_settings_full_name", false)'
<input clrInput type="text" name="account_settings_full_name" #fullNameInput="ngModel"
[(ngModel)]="account.realname" [required]="!account.oidc_user_meta" maxLengthExt="20"
id="account_settings_full_name" size="30"
(input)='handleValidation("account_settings_full_name", false)'
(blur)='handleValidation("account_settings_full_name", true)'>
<clr-control-error *ngIf="!getValidationState('account_settings_full_name')">
{{'TOOLTIP.FULL_NAME' | translate}}
@ -47,14 +52,14 @@
</clr-input-container>
<clr-input-container>
<label>{{'PROFILE.COMMENT' | translate}}</label>
<input clrInput type="text" #commentInput="ngModel" maxlength="30" size="30" name="account_settings_comments" [(ngModel)]="account.comment"
id="account_settings_comments">
<input clrInput type="text" #commentInput="ngModel" maxlength="30" size="30"
name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments">
<clr-control-error *ngIf="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
{{'TOOLTIP.COMMENT' | translate}}
</clr-control-error>
</clr-input-container>
<div class="clr-form-control cli-secret" *ngIf="account.oidc_user_meta">
<div class="clr-form-control cli-secret" *ngIf="account.oidc_user_meta">
<label class="clr-control-label">{{'PROFILE.CLI_PASSWORD' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="20"></clr-icon>
@ -63,21 +68,59 @@
</clr-tooltip-content>
</clr-tooltip>
</label>
<input class="clr-input" type="password" name="cli_password" disabled [ngModel]="'account.oidc_user_meta.secret'" size="33">
<button (click)="generateCli(account.user_id)" id="generate-cli-btn" class="btn btn-outline btn-sm btn-padding-less" *ngIf="showGenerateCli">
<input class="clr-input input-cli" type="password" name="cli_password" disabled
[ngModel]="'account.oidc_user_meta.secret'" size="33">
<button (click)="generateCli(account.user_id)" id="generate-cli-btn"
class="btn btn-outline btn-sm btn-padding-less" *ngIf="showGenerateCli">
{{'PROFILE.ADMIN_CIL_SECRET_BUTTON' | translate}}
</button>
<button (click)="showSecretDetail=true" id="reset-cli-btn" class="btn btn-outline btn-sm btn-padding-less"
*ngIf="showGenerateCli">
{{'PROFILE.ADMIN_CIL_SECRET_RESET_BUTTON' | translate}}
</button>
<div class="rename-tool reset-cli">
<hbr-copy-input #copyInput (onCopySuccess)="onSuccess($event)" (onCopyError)="onError($event)" iconMode="true" [defaultValue]="account.oidc_user_meta.secret"></hbr-copy-input>
<hbr-copy-input #copyInput (onCopySuccess)="onSuccess($event)" (onCopyError)="onError($event)"
iconMode="true" [defaultValue]="account.oidc_user_meta.secret"></hbr-copy-input>
</div>
<div (click)="showGenerateCliFn()" *ngIf="!showGenerateCli" id="hidden-generate-cli" class="hidden-generate-cli">···</div>
<div (click)="showGenerateCliFn()" *ngIf="!showGenerateCli" id="hidden-generate-cli"
class="hidden-generate-cli">···</div>
</div>
</form>
</div>
<div class="modal-footer">
<span class="spinner spinner-inline loading-top" [hidden]="showProgress === false"></span>
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">{{'BUTTON.OK' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress"
(click)="submit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="showSecretDetail" [clrModalSize]="'sm'" [clrModalStaticBackdrop]="staticBackdrop"
[clrModalClosable]="false">
<h3 class="modal-title">{{'PROFILE.ADMIN_CIL_SECRET_RESET_BUTTON' | translate}}</h3>
<div class="modal-body">
<form #resetSecretFrom="ngForm" class="clr-form reset-cli-form clr-form-horizontal">
<clr-input-container>
<label>{{'PROFILE.NEW_SECRET' | translate}}</label>
<input clrInput type="password" maxlength="30" size="30" required pattern="^(?=.*\d)(?=.*[a-zA-Z]).{8,}$"
name="input_secret" [(ngModel)]="resetForms.input_secret" id="input-secret">
<clr-control-error>
{{'TOOLTIP.NEW_SECRET' | translate}}
</clr-control-error>
</clr-input-container>
<clr-input-container>
<label>{{'PROFILE.CONFIRM_SECRET' | translate}}</label>
<input clrInput type="password" maxlength="30" size="30"
[(ngModel)]="resetForms.confirm_secret" name="confirm_secret" id="confirm-secret">
</clr-input-container>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="closeReset()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="disableChangeCliSecret()" (click)="resetCliSecret(resetSecretFrom.value.input_secret)">{{'BUTTON.CONFIRM' | translate}}</button>
</div>
</clr-modal>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmGenerate($event)"></confirmation-dialog>

View File

@ -5,6 +5,7 @@ clr-modal {
.rename-tool {
.btn {
margin-right: 6px;
margin-left: 5px;
padding-left: 3px;
padding-right: 3px;
}
@ -18,10 +19,15 @@ clr-modal {
align-items: center;
.reset-cli {
height: 30px;
padding-top: 8px;
}
.btn-padding-less {
padding-left: 5px;
padding-right: 5px;
margin-left: 5px;
}
.input-cli {
width: 9.5rem;
}
}
.hidden-generate-cli {
@ -32,7 +38,12 @@ clr-modal {
line-height: 30px;
}
}
.reset-cli-form {
width: 19.5rem;
}
.display-flex {
display: flex;
}
.set-btns {
display: flex;
}

View File

@ -29,6 +29,8 @@ import {
ConfirmationTargets,
ConfirmationButtons
} from "../../shared/shared.const";
import { randomWord } from '../../shared/shared.utils';
import { ResetSecret } from './account';
@Component({
selector: "account-settings-modal",
templateUrl: "account-settings-modal.component.html",
@ -49,13 +51,15 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
originAdminName = "admin";
newAdminName = "admin@harbor.local";
renameConfirmation = false;
// confirmRename = false;
showSecretDetail = false;
resetForms = new ResetSecret();
showGenerateCli: boolean = false;
@ViewChild("confirmationDialog", {static: false})
confirmationDialogComponent: ConfirmationDialogComponent;
accountFormRef: NgForm;
@ViewChild("accountSettingsFrom", {static: true}) accountForm: NgForm;
@ViewChild("resetSecretFrom", {static: true}) resetSecretFrom: NgForm;
@ViewChild(InlineAlertComponent, {static: false}) inlineAlert: InlineAlertComponent;
@ViewChild("copyInput", {static: false}) copyInput: CopyInputComponent;
@ -350,13 +354,26 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
showGenerateCliFn() {
this.showGenerateCli = !this.showGenerateCli;
}
confirmGenerate(confirmData): void {
let userId = confirmData.data;
this.accountSettingsService.generateCli(userId).subscribe(cliSecret => {
this.account.oidc_user_meta.secret = cliSecret.secret;
confirmGenerate(event): void {
this.account.oidc_user_meta.secret = randomWord(9);
this.resetCliSecret(this.account.oidc_user_meta.secret);
}
resetCliSecret(secret) {
let userId = this.account.user_id;
this.accountSettingsService.saveNewCli(userId, {secret: secret}).subscribe(cliSecret => {
this.account.oidc_user_meta.secret = secret;
this.closeReset();
this.inlineAlert.showInlineSuccess({message: 'PROFILE.GENERATE_SUCCESS'});
}, error => {
this.inlineAlert.showInlineError({message: 'PROFILE.GENERATE_ERROR'});
});
}
disableChangeCliSecret() {
return this.resetSecretFrom.invalid || (this.resetSecretFrom.value.input_secret !== this.resetSecretFrom.value.confirm_secret);
}
closeReset() {
this.showSecretDetail = false;
this.resetSecretFrom.resetForm(new ResetSecret());
}
}

View File

@ -0,0 +1,8 @@
export class ResetSecret {
input_secret: string;
confirm_secret: string;
constructor() {
this.confirm_secret = "";
this.input_secret = "";
}
}

View File

@ -1,10 +1,13 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { PasswordSettingService } from './password-setting.service';
xdescribe('PasswordSettingService', () => {
describe('PasswordSettingService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [PasswordSettingService]
});
});

View File

@ -7,6 +7,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
import { FormsModule } from '@angular/forms';
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
describe('SignUpComponent', () => {
let component: SignUpComponent;
@ -16,7 +17,7 @@ describe('SignUpComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SignUpComponent, NewUserFormComponent],
declarations: [SignUpComponent, NewUserFormComponent, InlineAlertComponent],
imports: [
FormsModule,
ClarityModule,
@ -36,6 +37,8 @@ describe('SignUpComponent', () => {
component = fixture.componentInstance;
component.newUserForm =
TestBed.createComponent(NewUserFormComponent).componentInstance;
component.inlineAlert =
TestBed.createComponent(InlineAlertComponent).componentInstance;
fixture.detectChanges();
});

View File

@ -43,7 +43,7 @@ export class SignUpComponent {
newUserForm: NewUserFormComponent;
@ViewChild(InlineAlertComponent, {static: false})
inlienAlert: InlineAlertComponent;
inlineAlert: InlineAlertComponent;
@ViewChild(Modal, {static: false})
modal: Modal;
@ -67,7 +67,7 @@ export class SignUpComponent {
if (this.error != null) {
this.error = null; // clear error
}
this.inlienAlert.close(); // Close alert if being shown
this.inlineAlert.close(); // Close alert if being shown
}
open(): void {
@ -76,7 +76,7 @@ export class SignUpComponent {
this.formValueChanged = false;
this.error = null;
this.onGoing = false;
this.inlienAlert.close();
this.inlineAlert.close();
this.modal.open();
}
@ -87,7 +87,7 @@ export class SignUpComponent {
this.opened = false;
} else {
// Need user confirmation
this.inlienAlert.showInlineConfirmation({
this.inlineAlert.showInlineConfirmation({
message: "ALERT.FORM_CHANGE_CONFIRMATION"
});
}
@ -127,7 +127,7 @@ export class SignUpComponent {
}, error => {
this.onGoing = false;
this.error = error;
this.inlienAlert.showInlineError(error);
this.inlineAlert.showInlineError(error);
});
}
}

View File

@ -1,16 +1,50 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { GlobalSearchComponent } from './global-search.component';
import { SearchTriggerService } from './search-trigger.service';
import { FormsModule } from '@angular/forms';
import { AppConfigService } from '../../app-config.service';
import { SkinableConfig } from "../../skinable-config.service";
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
xdescribe('GlobalSearchComponent', () => {
describe('GlobalSearchComponent', () => {
let component: GlobalSearchComponent;
let fixture: ComponentFixture<GlobalSearchComponent>;
let fakeSearchTriggerService = {
searchClearChan$: {
subscribe: function () {
}
}
};
let fakeAppConfigService = {
isIntegrationMode: function () {
return true;
}
};
let fakeSkinableConfig = {
getProject: function () {
return {
introduction: {}
};
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GlobalSearchComponent]
})
.compileComponents();
imports: [
TranslateModule.forRoot(),
FormsModule,
RouterTestingModule
],
declarations: [GlobalSearchComponent],
providers: [
TranslateService,
{ provide: SearchTriggerService, useValue: fakeSearchTriggerService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: SkinableConfig, useValue: fakeSkinableConfig }
]
}).compileComponents();
}));
beforeEach(() => {

View File

@ -1,11 +1,14 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { GlobalSearchService } from './global-search.service';
xdescribe('GlobalSearchService', () => {
describe('GlobalSearchService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [GlobalSearchService]
providers: [GlobalSearchService],
imports: [
HttpClientTestingModule
]
});
});

View File

@ -1,16 +1,50 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GlobalSearchService } from './global-search.service';
import { SearchResults } from './search-results';
import { SearchTriggerService } from './search-trigger.service';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppConfigService } from './../../app-config.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { SearchResultComponent } from './search-result.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
xdescribe('SearchResultComponent', () => {
describe('SearchResultComponent', () => {
let component: SearchResultComponent;
let fixture: ComponentFixture<SearchResultComponent>;
let fakeSearchResults = null;
let fakeGlobalSearchService = null;
let fakeAppConfigService = null;
let fakeMessageHandlerService = null;
let fakeSearchTriggerService = {
searchTriggerChan$: {
subscribe: function () {
}
},
searchCloseChan$: {
subscribe: function () {
}
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SearchResultComponent]
})
.compileComponents();
imports: [
TranslateModule.forRoot(),
HttpClientTestingModule
],
declarations: [SearchResultComponent],
providers: [
TranslateService,
{ provide: GlobalSearchService, useValue: fakeGlobalSearchService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: SearchTriggerService, useValue: fakeSearchTriggerService },
{ provide: SearchResults, fakeSearchResults }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {

View File

@ -1,16 +1,67 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppConfigService } from '../..//app-config.service';
import { RouterTestingModule } from '@angular/router/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SessionService } from '../../shared/session.service';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { HarborShellComponent } from './harbor-shell.component';
import { ClarityModule } from "@clr/angular";
import { of } from 'rxjs';
xdescribe('HarborShellComponent', () => {
describe('HarborShellComponent', () => {
let component: HarborShellComponent;
let fixture: ComponentFixture<HarborShellComponent>;
let fakeSessionService = {
getCurrentUser: function () {
return { has_admin_role: true };
}
};
let fakeSearchTriggerService = {
searchTriggerChan$: {
subscribe: function () {
}
},
searchCloseChan$: {
subscribe: function () {
}
}
};
let fakeAppConfigService = {
isLdapMode: function () {
return true;
},
isHttpAuthMode: function () {
return false;
},
isOidcMode: function () {
return false;
},
getConfig: function () {
return {
with_clair: true
};
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HarborShellComponent]
})
.compileComponents();
imports: [
RouterTestingModule,
TranslateModule.forRoot(),
ClarityModule,
BrowserAnimationsModule
],
declarations: [HarborShellComponent],
providers: [
TranslateService,
{ provide: SessionService, useValue: fakeSessionService },
{ provide: SearchTriggerService, useValue: fakeSearchTriggerService },
{ provide: AppConfigService, useValue: fakeAppConfigService }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {

View File

@ -358,8 +358,7 @@
</label>
<input clrInput name="oidcGroupClaim" type="text" #oidcClientSecretInput="ngModel"
[(ngModel)]="currentConfig.oidc_groups_claim.value" id="oidcGroupClaim" size="40"
pattern="^\w{1,256}$" [disabled]="disabled(currentConfig.oidc_groups_claim)" />
<clr-control-error>{{'TOOLTIP.OIDC_GROUP_CLAIM_WARNING' | translate}}</clr-control-error>
[disabled]="disabled(currentConfig.oidc_groups_claim)" />
</clr-input-container>
<clr-input-container>

View File

@ -1,14 +1,47 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { ConfirmMessageHandler } from '../config.msg.utils';
import { AppConfigService } from '../../app-config.service';
import { ConfigurationService } from '../config.service';
import { ErrorHandler, SystemInfoService } from '@harbor/ui';
import { ConfigurationAuthComponent } from './config-auth.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';
xdescribe('ConfigurationAuthComponent', () => {
describe('ConfigurationAuthComponent', () => {
let component: ConfigurationAuthComponent;
let fixture: ComponentFixture<ConfigurationAuthComponent>;
let fakeMessageHandlerService = null;
let fakeConfigurationService = null;
let fakeAppConfigService = null;
let fakeConfirmMessageService = null;
let fakeSystemInfoService = {
getSystemInfo: function () {
return of({
external_url: "expectedUrl"
});
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ConfigurationAuthComponent]
imports: [
TranslateModule.forRoot(),
FormsModule
],
declarations: [ConfigurationAuthComponent],
providers: [
ErrorHandler,
TranslateService,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: ConfigurationService, useValue: fakeConfigurationService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: ConfirmMessageHandler, useValue: fakeConfirmMessageService },
{ provide: SystemInfoService, useValue: fakeSystemInfoService }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));

View File

@ -38,6 +38,12 @@
<project-quotas [(allConfig)]="allConfig" (refreshAllconfig)="refreshAllconfig()"></project-quotas>
</clr-tab-content>
</clr-tab>
<clr-tab>
<button id="config-scanners" clrTabLink>{{'SCANNER.SCANNERS' | translate}}</button>
<clr-tab-content id="scanners" *clrIfActive>
<config-scanner></config-scanner>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -1,16 +1,60 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SessionService } from '../shared/session.service';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ClarityModule } from "@clr/angular";
import { AppConfigService } from '../app-config.service';
import { ConfigurationService } from './config.service';
import { ConfigurationComponent } from './config.component';
xdescribe('ConfigurationComponent', () => {
describe('ConfigurationComponent', () => {
let component: ConfigurationComponent;
let fixture: ComponentFixture<ConfigurationComponent>;
let fakeConfirmationDialogService = {
confirmationConfirm$: {
subscribe: function () {
}
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ConfigurationComponent]
})
.compileComponents();
imports: [
TranslateModule.forRoot(),
ClarityModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ConfigurationComponent],
providers: [
TranslateService,
{
provide: SessionService, useValue: {
getCurrentUser: function () {
return "admin";
}
}
},
{ provide: ConfirmationDialogService, useValue: fakeConfirmationDialogService },
{ provide: MessageHandlerService, useValue: null },
{
provide: AppConfigService, useValue: {
getConfig: function () {
return { has_ca_root: true };
}
}
},
{
provide: ConfigurationService, useValue: {
confirmationConfirm$: {
subscribe: function () {
}
}
}
}
]
}).compileComponents();
}));
beforeEach(() => {

View File

@ -105,6 +105,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
if (this.confirmSub) {
console.log(this.confirmSub);
this.confirmSub.unsubscribe();
}
}

View File

@ -22,20 +22,31 @@ import { ConfirmMessageHandler } from "./config.msg.utils";
import { ConfigurationAuthComponent } from "./auth/config-auth.component";
import { ConfigurationEmailComponent } from "./email/config-email.component";
import { RobotApiRepository } from "../project/robot-account/robot.api.repository";
import { ConfigurationScannerComponent } from "./scanner/config-scanner.component";
import { NewScannerModalComponent } from "./scanner/new-scanner-modal/new-scanner-modal.component";
import { NewScannerFormComponent } from "./scanner/new-scanner-form/new-scanner-form.component";
import { ConfigScannerService } from "./scanner/config-scanner.service";
import { ScannerMetadataComponent } from "./scanner/scanner-metadata/scanner-metadata.component";
@NgModule({
imports: [CoreModule, SharedModule],
declarations: [
ConfigurationComponent,
ConfigurationAuthComponent,
ConfigurationEmailComponent
],
exports: [ConfigurationComponent],
providers: [
ConfigurationService,
ConfirmMessageHandler,
RobotApiRepository
]
imports: [CoreModule, SharedModule],
declarations: [
ConfigurationComponent,
ConfigurationAuthComponent,
ConfigurationEmailComponent,
ConfigurationScannerComponent,
NewScannerModalComponent,
NewScannerFormComponent,
ScannerMetadataComponent
],
exports: [ConfigurationComponent],
providers: [
ConfigurationService,
ConfirmMessageHandler,
RobotApiRepository,
ConfigScannerService,
]
})
export class ConfigurationModule {}
export class ConfigurationModule {
}

View File

@ -1,10 +1,13 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ConfigurationService } from './config.service';
xdescribe('ConfigService', () => {
describe('ConfigService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [ConfigurationService]
});
});

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