mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 08:38:03 +01:00
rebase: resolve the code confilcts with master
Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
commit
0f16913635
1
.gitignore
vendored
1
.gitignore
vendored
@ -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/
|
||||
|
6
Makefile
6
Makefile
@ -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)
|
||||
|
@ -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.
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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..."
|
||||
|
20
make/photon/clair-adapter/Dockerfile
Normal file
20
make/photon/clair-adapter/Dockerfile
Normal 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"]
|
7
make/photon/clair-adapter/Dockerfile.binary
Normal file
7
make/photon/clair-adapter/Dockerfile.binary
Normal 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
|
39
make/photon/clair-adapter/builder
Executable file
39
make/photon/clair-adapter/builder
Executable 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
|
@ -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)
|
||||
|
1
make/photon/prepare/templates/clair-adapter/env.jinja
Normal file
1
make/photon/prepare/templates/clair-adapter/env.jinja
Normal file
@ -0,0 +1 @@
|
||||
SCANNER_CLAIR_URL={{clair_url}}
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
18
make/photon/prepare/utils/clair_adapter.py
Normal file
18
make/photon/prepare/utils/clair_adapter.py
Normal 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)
|
@ -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'
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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},
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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" {
|
||||
|
@ -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{})
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 ...
|
||||
|
11
src/pkg/immutabletag/match/matcher.go
Normal file
11
src/pkg/immutabletag/match/matcher.go
Normal 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)
|
||||
}
|
88
src/pkg/immutabletag/match/rule/match.go
Normal file
88
src/pkg/immutabletag/match/rule/match.go
Normal 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,
|
||||
}
|
||||
}
|
162
src/pkg/immutabletag/match/rule/match_test.go
Normal file
162
src/pkg/immutabletag/match/rule/match_test.go
Normal 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")
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
44
src/pkg/scan/init.go
Normal 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
|
||||
}
|
@ -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";
|
||||
|
@ -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(() => {
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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> {{'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> {{'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> {{'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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1 @@
|
||||
<canvas class="canvas" #barChart> HTML5 canvas not supported </canvas>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
];
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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`;
|
||||
}
|
||||
}
|
||||
|
@ -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> {{'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> <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> <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>
|
||||
|
@ -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');
|
||||
|
@ -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';
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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'
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
8
src/portal/src/app/account/account-settings/account.ts
Normal file
8
src/portal/src/app/account/account-settings/account.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export class ResetSecret {
|
||||
input_secret: string;
|
||||
confirm_secret: string;
|
||||
constructor() {
|
||||
this.confirm_secret = "";
|
||||
this.input_secret = "";
|
||||
}
|
||||
}
|
@ -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]
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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(() => {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}));
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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(() => {
|
||||
|
@ -105,6 +105,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.confirmSub) {
|
||||
console.log(this.confirmSub);
|
||||
this.confirmSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user