This commit is contained in:
Soumya Raikwar 2025-10-02 08:49:04 +05:30 committed by GitHub
commit d07f1d6084
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 286 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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