mirror of https://github.com/goharbor/harbor.git
Merge c008c77a03
into c004f2d3e6
This commit is contained in:
commit
d07f1d6084
|
@ -387,6 +387,10 @@ func (bc *basicController) ScanAll(ctx context.Context, trigger string, async bo
|
|||
if op := operator.FromContext(ctx); op != "" {
|
||||
extra["operator"] = op
|
||||
}
|
||||
// propagate optional scan-all scope from context into execution extra attrs
|
||||
if scope := FromContextScope(ctx); scope != nil {
|
||||
extra["scope"] = scope
|
||||
}
|
||||
executionID, err := bc.execMgr.Create(ctx, job.ScanAllVendorType, 0, trigger, extra)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -459,6 +463,72 @@ func (bc *basicController) isScanAllStopped(ctx context.Context, execID int64) b
|
|||
func (bc *basicController) startScanAll(ctx context.Context, executionID int64) error {
|
||||
batchSize := 50
|
||||
|
||||
// Build optional artifact query based on stored scope on execution
|
||||
var artQuery *q.Query
|
||||
if exec, err := bc.execMgr.Get(ctx, executionID); err == nil && exec != nil {
|
||||
if exec.ExtraAttrs != nil {
|
||||
if s, ok := exec.ExtraAttrs["scope"].(map[string]any); ok {
|
||||
artQuery = &q.Query{Keywords: map[string]any{}}
|
||||
// project ids
|
||||
if arr, ok := s["project_ids"].([]any); ok {
|
||||
vals := make([]any, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
vals = append(vals, int64(t))
|
||||
case int64:
|
||||
vals = append(vals, t)
|
||||
}
|
||||
}
|
||||
if len(vals) > 0 {
|
||||
artQuery.Keywords["ProjectID"] = &q.OrList{Values: vals}
|
||||
}
|
||||
}
|
||||
if arr, ok := s["ProjectIDs"].([]any); ok && artQuery.Keywords["ProjectID"] == nil {
|
||||
vals := make([]any, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
vals = append(vals, int64(t))
|
||||
case int64:
|
||||
vals = append(vals, t)
|
||||
}
|
||||
}
|
||||
if len(vals) > 0 {
|
||||
artQuery.Keywords["ProjectID"] = &q.OrList{Values: vals}
|
||||
}
|
||||
}
|
||||
// repositories
|
||||
if arr, ok := s["repositories"].([]any); ok {
|
||||
vals := make([]any, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if name, ok := v.(string); ok {
|
||||
vals = append(vals, name)
|
||||
}
|
||||
}
|
||||
if len(vals) > 0 {
|
||||
artQuery.Keywords["RepositoryName"] = &q.OrList{Values: vals}
|
||||
}
|
||||
}
|
||||
if arr, ok := s["Repositories"].([]any); ok && artQuery.Keywords["RepositoryName"] == nil {
|
||||
vals := make([]any, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if name, ok := v.(string); ok {
|
||||
vals = append(vals, name)
|
||||
}
|
||||
}
|
||||
if len(vals) > 0 {
|
||||
artQuery.Keywords["RepositoryName"] = &q.OrList{Values: vals}
|
||||
}
|
||||
}
|
||||
// if query is empty, keep as nil to scan all
|
||||
if len(artQuery.Keywords) == 0 {
|
||||
artQuery = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary := struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
SubmitCount int `json:"submit_count"`
|
||||
|
|
|
@ -72,6 +72,31 @@ func scanAllCallback(ctx context.Context, param string) error {
|
|||
if op, ok := params["operator"].(string); ok {
|
||||
ctx = context.WithValue(ctx, operator.ContextKey{}, op)
|
||||
}
|
||||
|
||||
// optional: scope
|
||||
if s, ok := params["scope"].(map[string]any); ok {
|
||||
var scope ScanAllScope
|
||||
// project_ids
|
||||
if arr, ok := s["project_ids"].([]any); ok {
|
||||
for _, v := range arr {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
scope.ProjectIDs = append(scope.ProjectIDs, int64(t))
|
||||
case int64:
|
||||
scope.ProjectIDs = append(scope.ProjectIDs, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
// repositories
|
||||
if arr, ok := s["repositories"].([]any); ok {
|
||||
for _, v := range arr {
|
||||
if name, ok := v.(string); ok {
|
||||
scope.Repositories = append(scope.Repositories, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx = WithScanAllScope(ctx, &scope)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := scanCtl.ScanAll(ctx, task.ExecutionTriggerSchedule, true)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scan
|
||||
|
||||
import "context"
|
||||
|
||||
// ScopeHeader is the HTTP header key used to carry scan-all scope JSON.
|
||||
const ScopeHeader = "X-Scan-All-Scope"
|
||||
|
||||
// ScanAllScope defines optional filters for scan-all.
|
||||
// Currently supports filtering by project IDs or repository names.
|
||||
// Leave all fields empty to scan everything (default behavior).
|
||||
type ScanAllScope struct {
|
||||
ProjectIDs []int64 `json:"project_ids,omitempty"`
|
||||
Repositories []string `json:"repositories,omitempty"`
|
||||
}
|
||||
|
||||
// scopeCtxKey is the context key type for storing scope in context
|
||||
// to avoid collisions.
|
||||
type scopeCtxKey struct{}
|
||||
|
||||
// WithScanAllScope returns a new context with the given scope.
|
||||
func WithScanAllScope(ctx context.Context, scope *ScanAllScope) context.Context {
|
||||
if scope == nil {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, scopeCtxKey{}, scope)
|
||||
}
|
||||
|
||||
// FromContextScope returns the ScanAllScope from context if present.
|
||||
func FromContextScope(ctx context.Context) *ScanAllScope {
|
||||
v := ctx.Value(scopeCtxKey{})
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if s, ok := v.(*ScanAllScope); ok {
|
||||
return s
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -12,15 +12,15 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { throwError as observableThrowError, Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { CURRENT_BASE_HREF } from '../../../../shared/units/utils';
|
||||
|
||||
export abstract class ScanApiRepository {
|
||||
abstract postSchedule(param): Observable<any>;
|
||||
abstract postSchedule(param, scopeHeader?: string): Observable<any>;
|
||||
|
||||
abstract putSchedule(param): Observable<any>;
|
||||
abstract putSchedule(param, scopeHeader?: string): Observable<any>;
|
||||
|
||||
abstract getSchedule(): Observable<any>;
|
||||
}
|
||||
|
@ -31,15 +31,15 @@ export class ScanApiDefaultRepository extends ScanApiRepository {
|
|||
super();
|
||||
}
|
||||
|
||||
public postSchedule(param): Observable<any> {
|
||||
public postSchedule(param, scopeHeader?: string): Observable<any> {
|
||||
return this.http
|
||||
.post(`${CURRENT_BASE_HREF}/system/scanAll/schedule`, param)
|
||||
.post(`${CURRENT_BASE_HREF}/system/scanAll/schedule`, param, scopeHeader ? { headers: new HttpHeaders({ 'X-Scan-All-Scope': scopeHeader }) } : undefined)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
|
||||
public putSchedule(param): Observable<any> {
|
||||
public putSchedule(param, scopeHeader?: string): Observable<any> {
|
||||
return this.http
|
||||
.put(`${CURRENT_BASE_HREF}/system/scanAll/schedule`, param)
|
||||
.put(`${CURRENT_BASE_HREF}/system/scanAll/schedule`, param, scopeHeader ? { headers: new HttpHeaders({ 'X-Scan-All-Scope': scopeHeader }) } : undefined)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export class ScanAllRepoService {
|
|||
return this.scanApiRepository.getSchedule();
|
||||
}
|
||||
|
||||
public postSchedule(type, cron): Observable<any> {
|
||||
public postSchedule(type, cron, scopeHeader?: string): Observable<any> {
|
||||
let param = {
|
||||
schedule: {
|
||||
type: type,
|
||||
|
@ -48,10 +48,10 @@ export class ScanAllRepoService {
|
|||
},
|
||||
};
|
||||
|
||||
return this.scanApiRepository.postSchedule(param);
|
||||
return this.scanApiRepository.postSchedule(param, scopeHeader);
|
||||
}
|
||||
|
||||
public putSchedule(type, cron): Observable<any> {
|
||||
public putSchedule(type, cron, scopeHeader?: string): Observable<any> {
|
||||
let param = {
|
||||
schedule: {
|
||||
type: type,
|
||||
|
@ -59,7 +59,7 @@ export class ScanAllRepoService {
|
|||
},
|
||||
};
|
||||
|
||||
return this.scanApiRepository.putSchedule(param);
|
||||
return this.scanApiRepository.putSchedule(param, scopeHeader);
|
||||
}
|
||||
getMetrics(): Observable<ScanningMetrics> {
|
||||
return this.http
|
||||
|
|
|
@ -19,6 +19,23 @@
|
|||
(inputvalue)="saveSchedule($event)"></cron-selection>
|
||||
</div>
|
||||
</section>
|
||||
<section class="form-block">
|
||||
<label class="update-time">Scan scope (optional)</label>
|
||||
<div class="form-group">
|
||||
<label>Select projects to include</label>
|
||||
<select multiple class="clr-input" [(ngModel)]="selectedProjectIds" (change)="loadRepositories()">
|
||||
<option *ngFor="let p of projects" [ngValue]="p.project_id">{{ p.name }}</option>
|
||||
</select>
|
||||
<div class="hint mt-1">Leave empty to scan all projects.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Select repositories (optional)</label>
|
||||
<select multiple class="clr-input" [(ngModel)]="selectedRepositories">
|
||||
<option *ngFor="let r of repositories" [ngValue]="r.name">{{ r.name }}</option>
|
||||
</select>
|
||||
<div class="hint mt-1">If repositories selected, they take precedence over project selection.</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="clr-col-5">
|
||||
<div class="clr-row-3">
|
||||
<div class="btn-scan-right btn-scan margin-top-16px">
|
||||
|
|
|
@ -24,6 +24,10 @@ import {
|
|||
VULNERABILITY_SCAN_STATUS,
|
||||
} from '../../../../shared/units/utils';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Project } from '../../../project/project-config/project-policy-config/project';
|
||||
import { Repository as NewRepository } from '../../../../../../ng-swagger-gen/models/repository';
|
||||
import { RepositoryService as NewRepositoryService } from '../../../../../../ng-swagger-gen/services/repository.service';
|
||||
import { ProjectService } from '../../../../shared/services';
|
||||
import { errorHandler } from '../../../../shared/units/shared.utils';
|
||||
import { ScanAllService } from '../../../../../../ng-swagger-gen/services/scan-all.service';
|
||||
|
||||
|
@ -65,7 +69,9 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
|||
private scanningService: ScanAllRepoService,
|
||||
private newScanAllService: ScanAllService,
|
||||
private errorHandlerEntity: ErrorHandler,
|
||||
private translate: TranslateService
|
||||
private translate: TranslateService,
|
||||
private projectService: ProjectService,
|
||||
private newRepoService: NewRepositoryService
|
||||
) {}
|
||||
|
||||
get scanningMetrics(): ScanningMetrics {
|
||||
|
@ -177,9 +183,23 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
// Scope selection for selective scanning
|
||||
projects: Project[] = [];
|
||||
selectedProjectIds: number[] = [];
|
||||
repositories: NewRepository[] = [];
|
||||
selectedRepositories: string[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getScanText();
|
||||
this.getScanners();
|
||||
// load first page of projects (fetch all via pagination if needed could be added later)
|
||||
this.projectService
|
||||
.listProjects('', undefined, 1, 100)
|
||||
.subscribe(res => {
|
||||
this.projects = (res && res.body) || [];
|
||||
});
|
||||
// reload repos when project selection changes (simple polling via setter would be better in a full refactor)
|
||||
// here we rely on explicit call before saving
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -346,6 +366,39 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
}
|
||||
|
||||
loadRepositories() {
|
||||
this.repositories = [];
|
||||
this.selectedRepositories = [];
|
||||
if (!this.selectedProjectIds || this.selectedProjectIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pageSize = 100;
|
||||
this.selectedProjectIds.forEach(pid => {
|
||||
const proj = this.projects?.find(p => p.project_id === pid);
|
||||
if (!proj) { return; }
|
||||
this.newRepoService
|
||||
.listRepositories({ projectName: proj.name, page: 1, pageSize })
|
||||
.subscribe(list => {
|
||||
if (list && list.length) {
|
||||
this.repositories = this.repositories.concat(list);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildScopeHeader(): string | undefined {
|
||||
// Prefer repo list if specified, else fall back to projects
|
||||
if (this.selectedRepositories && this.selectedRepositories.length) {
|
||||
try {
|
||||
return JSON.stringify({ repositories: this.selectedRepositories });
|
||||
} catch { return undefined; }
|
||||
}
|
||||
if (this.selectedProjectIds && this.selectedProjectIds.length) {
|
||||
try { return JSON.stringify({ project_ids: this.selectedProjectIds }); } catch { return undefined; }
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
saveSchedule(cron: string): void {
|
||||
let schedule = this.schedule;
|
||||
if (
|
||||
|
@ -353,8 +406,9 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
|||
schedule.schedule &&
|
||||
schedule.schedule.type !== SCHEDULE_TYPE_NONE
|
||||
) {
|
||||
const scopeHeader = this.buildScopeHeader();
|
||||
this.scanningService
|
||||
.putSchedule(this.CronScheduleComponent.scheduleType, cron)
|
||||
.putSchedule(this.CronScheduleComponent.scheduleType, cron, scopeHeader)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.translate
|
||||
|
@ -370,8 +424,9 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
);
|
||||
} else {
|
||||
const scopeHeader = this.buildScopeHeader();
|
||||
this.scanningService
|
||||
.postSchedule(this.CronScheduleComponent.scheduleType, cron)
|
||||
.postSchedule(this.CronScheduleComponent.scheduleType, cron, scopeHeader)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.translate
|
||||
|
|
|
@ -16,6 +16,7 @@ package handler
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -90,6 +91,18 @@ func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation
|
|||
|
||||
req := params.Schedule
|
||||
|
||||
// parse optional scope header
|
||||
var scope any
|
||||
if params.HTTPRequest != nil {
|
||||
if h := params.HTTPRequest.Header.Get(scan.ScopeHeader); h != "" {
|
||||
// validate JSON
|
||||
var tmp map[string]any
|
||||
if err := json.Unmarshal([]byte(h), &tmp); err == nil {
|
||||
scope = tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Schedule.Type == ScheduleNone {
|
||||
return operation.NewCreateScanAllScheduleCreated()
|
||||
}
|
||||
|
@ -105,6 +118,14 @@ func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation
|
|||
return s.SendError(ctx, errors.ConflictError(nil).WithMessage(message))
|
||||
}
|
||||
|
||||
// attach scope to context for manual trigger
|
||||
if scope != nil {
|
||||
// best-effort parse
|
||||
b, _ := json.Marshal(scope)
|
||||
var sscope scan.ScanAllScope
|
||||
_ = json.Unmarshal(b, &sscope)
|
||||
ctx = scan.WithScanAllScope(ctx, &sscope)
|
||||
}
|
||||
if _, err := s.scanCtl.ScanAll(ctx, task.ExecutionTriggerManual, true); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
|
@ -119,7 +140,14 @@ func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation
|
|||
return s.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage(message))
|
||||
}
|
||||
|
||||
if _, err := s.createOrUpdateScanAllSchedule(ctx, req.Schedule.Type, req.Schedule.Cron, nil); err != nil {
|
||||
cbParams := map[string]any{
|
||||
// the operator of schedule job is harbor-jobservice
|
||||
"operator": secret.JobserviceUser,
|
||||
}
|
||||
if scope != nil {
|
||||
cbParams["scope"] = scope
|
||||
}
|
||||
if _, err := s.scheduler.Schedule(ctx, job.ScanAllVendorType, 0, req.Schedule.Type, req.Schedule.Cron, scan.ScanAllCallback, cbParams, nil); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +161,17 @@ func (s *scanAllAPI) UpdateScanAllSchedule(ctx context.Context, params operation
|
|||
}
|
||||
req := params.Schedule
|
||||
|
||||
// parse optional scope header
|
||||
var scope any
|
||||
if params.HTTPRequest != nil {
|
||||
if h := params.HTTPRequest.Header.Get(scan.ScopeHeader); h != "" {
|
||||
var tmp map[string]any
|
||||
if err := json.Unmarshal([]byte(h), &tmp); err == nil {
|
||||
scope = tmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Schedule.Type == ScheduleManual {
|
||||
return s.SendError(ctx, errors.BadRequestError(nil).WithMessagef("fail to update scan all schedule as wrong schedule type: %s", req.Schedule.Type))
|
||||
}
|
||||
|
@ -147,7 +186,19 @@ func (s *scanAllAPI) UpdateScanAllSchedule(ctx context.Context, params operation
|
|||
err = s.scheduler.UnScheduleByID(ctx, schedule.ID)
|
||||
}
|
||||
} else {
|
||||
_, err = s.createOrUpdateScanAllSchedule(ctx, req.Schedule.Type, req.Schedule.Cron, schedule)
|
||||
// update with new cron and optional scope by re-scheduling
|
||||
if schedule != nil {
|
||||
if err := s.scheduler.UnScheduleByID(ctx, schedule.ID); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
}
|
||||
cbParams := map[string]any{
|
||||
"operator": secret.JobserviceUser,
|
||||
}
|
||||
if scope != nil {
|
||||
cbParams["scope"] = scope
|
||||
}
|
||||
_, err = s.scheduler.Schedule(ctx, job.ScanAllVendorType, 0, req.Schedule.Type, req.Schedule.Cron, scan.ScanAllCallback, cbParams, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue