From 412d14f49f5905a73c1991c8a0ab00bdf6bffb01 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Mon, 29 Sep 2025 10:19:39 +0530 Subject: [PATCH 1/5] interrogation services: add selective scan-all scope support via X-Scan-All-Scope header; propagate scope into execution and filter artifacts by project IDs or repositories; add simple UI to choose projects for schedule Signed-off-by: SoumyaRaikwar --- WARP.md | 136 ++++++++++ src/controller/scan/base_controller.go | 70 +++++ src/controller/scan/callback.go | 39 +++ src/controller/scan/scope.go | 52 ++++ .../vulnerability/scanAll.api.repository.ts | 14 +- .../vulnerability/scanAll.service.ts | 8 +- .../vulnerability-config.component.html | 10 + .../vulnerability-config.component.ts | 32 ++- src/server/v2.0/handler/scan_all.go | 241 ++++++++++++++++++ 9 files changed, 588 insertions(+), 14 deletions(-) create mode 100644 WARP.md create mode 100644 src/controller/scan/scope.go diff --git a/WARP.md b/WARP.md new file mode 100644 index 00000000000..af58bd9df4f --- /dev/null +++ b/WARP.md @@ -0,0 +1,136 @@ +# WARP.md + +This file provides guidance to WARP (warp.dev) when working with code in this repository. + +Project: Harbor (monorepo for building Harbor images, binaries, and installers) + +Key repos/docs referenced in-repo: +- README.md: badges, install links, and architecture links +- Makefile: entrypoint for build, lint, packaging, and local compose orchestration +- src/ (Go, main services and libraries) +- make/ (Docker build context, photon-based images, compose, and scripts) +- tests/ (integration/Robot assets, compose for tests) + +Common commands +- Build everything (compile Go, generate API, build Docker images, prepare config) + make install + # Equivalent to: make compile build prepare start + +- Compile Go binaries with Dockerized toolchain (golang image) only + make compile + # Subtargets: + make compile_core + make compile_jobservice + make compile_registryctl + make compile_standalone_db_migrator + +- Build Docker images (from make/photon/*) + make build + # Parameters you may override (examples): + # VERSIONTAG=dev|vX.Y.Z, BASEIMAGETAG=dev, BUILD_BASE=true|false, PULL_BASE_FROM_DOCKERHUB=true|false + # GOBUILDIMAGE=golang:1.24.6, NODEBUILDIMAGE=node:16.18.0, IMAGENAMESPACE=goharbor + +- Start and stop local compose stack (uses make/docker-compose.yml) + make start + make down + make restart + +- Generate and validate API/mocks (required before some builds/lint) + make lint_apis + make gen_apis + make gen_mocks + make mocks_check + +- Lint (golangci-lint; installs separately per comment in Makefile) + make lint + # Ensure $(go env GOPATH)/bin/golangci-lint exists or install as per Makefile comment + +- Vulnerability scan for Go deps (requires govulncheck installed) + make govulncheck + +- Go checks bundle + make go_check + # runs: gen_apis, mocks_check, misspell, commentfmt, lint + +- Package installers + # Online installer + make package_online PKGVERSIONTAG= [REGISTRYSERVER=host/ REGISTRYPROJECTNAME=project] + # Offline installer (saves built images into tarball) + make package_offline PKGVERSIONTAG= + +- Push built images to registry + make pushimage REGISTRYSERVER= REGISTRYUSER= REGISTRYPASSWORD=****** DEVFLAG=false + +- Clean + make clean # usage info for clean targets + make cleanall # remove binaries, images, compose files, configs, packages + make cleanbinary # remove compiled binaries + make cleanimage # remove Harbor images + make cleanbaseimage # remove base images + +- Run tests + # Unit/integration Go tests live under src; use standard go test, typically within src + (cd src && go test ./...) + + # Test compose and Robot-related flows are under tests/ + # Example helpers available: + tests/integration.sh + tests/startuptest.sh + tests/swaggerchecker.sh + # Single Go package test example: + (cd src && go test ./pkg/trace -run TestSampler -v) + +Notes on environment and prerequisites +- Docker and docker-compose are required for build/start/package flows (README lists minimum versions). +- Build largely orchestrated via photon-based Dockerfiles in make/photon; builds run in containers to avoid host toolchain drift. +- VERSION holds the release version (current: contents of VERSION file; e.g., v2.15.0). VERSIONTAG defaults to dev for images. +- Some targets use network access to fetch swagger/mocks tool images and dependencies. +- Setting GEN_TLS=1 when running make prepare can generate internal TLS certs via prepare image. + +High-level architecture +Harbor in this repo builds a multi-service system packaged as containers and orchestrated with docker-compose or Helm (chart is external). The main runtime services and their in-repo code live under src/: +- core (src/core): + - Web/API service implementing Harbor business logic and REST APIs (v2.0 swagger at api/v2.0/swagger.yaml with generated server under src/server/v2.0) + - Controllers, middlewares, auth, session mgmt, and Beego-based components +- jobservice (src/jobservice): + - Asynchronous job orchestration built on Redis (gocraft/work fork) + - Handles replication, GC, scan, notifications, exports, system artifact cleanup, etc. + - Components: API server, controller, scheduler, logger, reaper, Redis-backed queue; supports hooks and status web UI endpoints +- registryctl (src/registryctl): + - Sidecar/controller for upstream distribution registry (patched) for access control and cleanup; interacts with core and registry +- portal (src/portal): + - UI frontend; built via Node image, artifacts bundled into portal container +- common libs (src/lib, src/pkg, src/common): + - Cross-cutting libraries: config, http, errors, cache/redis wrappers, metrics, tracing (OpenTelemetry), icon, etc. + - Domain packages under src/pkg (retention, scanner, p2p preheat, scheduler, task, queuestatus, etc.) +- migration (src/migration): + - DB and data model migrations +- server (src/server/v2.0): + - Swagger-generated API server glue for core +- cmd (src/cmd): + - Auxiliary binaries, e.g., standalone DB migrator, exporter + +Build and image pipeline +- make compile_* builds Go binaries inside a golang container, embedding version info via -ldflags with src/pkg/version (GitCommit and ReleaseVersion from VERSION file and current commit). +- make build delegates to make/photon/Makefile to build base images and service images: core, jobservice, registry, registryctl, nginx, portal, db, redis, exporter, and prepare. +- make prepare renders config (harbor.yml) and compose files via the prepare image and make/ scripts; can generate internal TLS if GEN_TLS is set. +- make start/up uses docker-compose.yml under make/ to bring up the composed services. + +API and codegen +- Swagger spec: api/v2.0/swagger.yaml +- Lint spec: make lint_apis (Spectral via tools/spectral Docker image) +- Generate server stubs: make gen_apis (OpenAPI generator via tools/swagger Docker image; outputs to src/server/v2.0) +- Mocks: make gen_mocks (mockery via tools/mockery Docker image); make mocks_check ensures mocks are up to date + +Testing strategy (in-repo signals) +- Go unit/integration: standard go test under src/ (golangci config at src/.golangci.yaml) +- Robot/Selenium style UI tests and helpers under tests/, with supporting docker-compose.test.yml +- Helper scripts in tests/: integration.sh, startuptest.sh, swaggerchecker.sh, etc. + +Service secrets and configuration +- make prepare and prepare image manage assembling harbor.yml, secrets, and compose files under make/; many config knobs are parameterized in Helm chart (external) and values test data under src/pkg/chart/testdata/ for reference. + +Working tips specific to this repo +- Always run gen_apis and gen_mocks (or simply make go_check) after editing swagger or interfaces in src/pkg to keep generated code/mocks in sync; CI checks for drift. +- Use containerized build targets (default) to avoid local Go/Node version mismatches. Override GOBUILDIMAGE/NODEBUILDIMAGE only if necessary. +- For local dev on Go packages: run go test ./... from src/ to avoid pulling the entire monorepo module graph at top-level (module root is src/ per src/go.mod with replace to ../). diff --git a/src/controller/scan/base_controller.go b/src/controller/scan/base_controller.go index 3bce22dbde0..93ae93058b9 100644 --- a/src/controller/scan/base_controller.go +++ b/src/controller/scan/base_controller.go @@ -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"` diff --git a/src/controller/scan/callback.go b/src/controller/scan/callback.go index 211b2363829..bb96ba9075f 100644 --- a/src/controller/scan/callback.go +++ b/src/controller/scan/callback.go @@ -63,6 +63,45 @@ func init() { } func scanAllCallback(ctx context.Context, param string) error { + if param != "" { + params := make(map[string]any) + if err := json.Unmarshal([]byte(param), ¶ms); err != nil { + return err + } + + 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) + return err +} if param != "" { params := make(map[string]any) if err := json.Unmarshal([]byte(param), ¶ms); err != nil { diff --git a/src/controller/scan/scope.go b/src/controller/scan/scope.go new file mode 100644 index 00000000000..6e532f288a2 --- /dev/null +++ b/src/controller/scan/scope.go @@ -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 +} \ No newline at end of file diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.api.repository.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.api.repository.ts index 4270fca1277..d678ecc074a 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.api.repository.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.api.repository.ts @@ -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; + abstract postSchedule(param, scopeHeader?: string): Observable; - abstract putSchedule(param): Observable; + abstract putSchedule(param, scopeHeader?: string): Observable; abstract getSchedule(): Observable; } @@ -31,15 +31,15 @@ export class ScanApiDefaultRepository extends ScanApiRepository { super(); } - public postSchedule(param): Observable { +public postSchedule(param, scopeHeader?: string): Observable { 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 { +public putSchedule(param, scopeHeader?: string): Observable { 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))); } diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.service.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.service.ts index ab90763a33b..f5263c20753 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.service.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/scanAll.service.ts @@ -40,7 +40,7 @@ export class ScanAllRepoService { return this.scanApiRepository.getSchedule(); } - public postSchedule(type, cron): Observable { + public postSchedule(type, cron, scopeHeader?: string): Observable { 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 { + public putSchedule(type, cron, scopeHeader?: string): Observable { 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 { return this.http diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html index 4a6731dba55..a9650c8994a 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html @@ -19,6 +19,16 @@ (inputvalue)="saveSchedule($event)"> +
+ +
+ + +
Leave empty to scan all projects.
+
+
diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts index 34b500b9800..df33b61f14f 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts @@ -24,6 +24,8 @@ import { VULNERABILITY_SCAN_STATUS, } from '../../../../shared/units/utils'; import { DatePipe } from '@angular/common'; +import { Project } from '../../../../../../ng-swagger-gen/models/project'; +import { ProjectService } from '../../../../shared/services'; import { errorHandler } from '../../../../shared/units/shared.utils'; import { ScanAllService } from '../../../../../../ng-swagger-gen/services/scan-all.service'; @@ -65,7 +67,8 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { private scanningService: ScanAllRepoService, private newScanAllService: ScanAllService, private errorHandlerEntity: ErrorHandler, - private translate: TranslateService + private translate: TranslateService, + private projectService: ProjectService ) {} get scanningMetrics(): ScanningMetrics { @@ -177,9 +180,19 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { } } + // Scope selection for selective scanning + projects: Project[] = []; + selectedProjectIds: number[] = []; + 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) || []; + }); } ngOnDestroy() { @@ -346,6 +359,17 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { }; } + private buildScopeHeader(): string | undefined { + if (this.selectedProjectIds && this.selectedProjectIds.length) { + try { + return JSON.stringify({ project_ids: this.selectedProjectIds }); + } catch (e) { + return undefined; + } + } + return undefined; + } + saveSchedule(cron: string): void { let schedule = this.schedule; if ( @@ -353,8 +377,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 +395,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 diff --git a/src/server/v2.0/handler/scan_all.go b/src/server/v2.0/handler/scan_all.go index 0e0d7388127..e01de6638fb 100644 --- a/src/server/v2.0/handler/scan_all.go +++ b/src/server/v2.0/handler/scan_all.go @@ -16,6 +16,7 @@ package handler import ( "context" + "encoding/json" "fmt" "strings" @@ -90,6 +91,144 @@ 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() + } + + if req.Schedule.Type == ScheduleManual { + execution, err := s.getLatestScanAllExecution(ctx, task.ExecutionTriggerManual) + if err != nil { + return s.SendError(ctx, err) + } + + if execution != nil && execution.IsOnGoing() { + message := fmt.Sprintf("a previous scan all job aleady exits, its status is %s", execution.Status) + 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) + } + } else { + schedule, err := s.getScanAllSchedule(ctx) + if err != nil { + return s.SendError(ctx, err) + } + + if schedule != nil { + message := "fail to set schedule for scan all as always had one, please delete it firstly then to re-schedule" + return s.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage(message)) + } + + 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) + } + } + + return operation.NewCreateScanAllScheduleCreated() +} + if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil { + return s.SendError(ctx, err) + } + + 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() + } + + if req.Schedule.Type == ScheduleManual { + execution, err := s.getLatestScanAllExecution(ctx, task.ExecutionTriggerManual) + if err != nil { + return s.SendError(ctx, err) + } + + if execution != nil && execution.IsOnGoing() { + message := fmt.Sprintf("a previous scan all job aleady exits, its status is %s", execution.Status) + 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) + } + } else { + schedule, err := s.getScanAllSchedule(ctx) + if err != nil { + return s.SendError(ctx, err) + } + + if schedule != nil { + message := "fail to set schedule for scan all as always had one, please delete it firstly then to re-schedule" + return s.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage(message)) + } + + 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) + } + } + + return operation.NewCreateScanAllScheduleCreated() +} + if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil { + return s.SendError(ctx, err) + } + + req := params.Schedule + if req.Schedule.Type == ScheduleNone { return operation.NewCreateScanAllScheduleCreated() } @@ -133,6 +272,108 @@ 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)) + } + + schedule, err := s.getScanAllSchedule(ctx) + if err != nil { + return s.SendError(ctx, err) + } + + if req.Schedule.Type == ScheduleNone { + if schedule != nil { + err = s.scheduler.UnScheduleByID(ctx, schedule.ID) + } + } else { + // 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 { + return s.SendError(ctx, err) + } + + return operation.NewUpdateScanAllScheduleOK() +} + if err := s.requireAccess(ctx, rbac.ActionUpdate); err != nil { + return s.SendError(ctx, err) + } + 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)) + } + + schedule, err := s.getScanAllSchedule(ctx) + if err != nil { + return s.SendError(ctx, err) + } + + if req.Schedule.Type == ScheduleNone { + if schedule != nil { + err = s.scheduler.UnScheduleByID(ctx, schedule.ID) + } + } else { + // 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 { + return s.SendError(ctx, err) + } + + return operation.NewUpdateScanAllScheduleOK() +} + if err := s.requireAccess(ctx, rbac.ActionUpdate); err != nil { + return s.SendError(ctx, err) + } + req := params.Schedule + 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)) } From ea4352a28192a7745eef0b4fe139db01bbd57e83 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Mon, 29 Sep 2025 11:22:46 +0530 Subject: [PATCH 2/5] portal: add repository multi-select for selective scan-all scope; header sends repositories if chosen Signed-off-by: SoumyaRaikwar --- src/controller/scan/callback.go | 14 -- .../vulnerability-config.component.html | 9 +- .../vulnerability-config.component.ts | 41 +++- src/server/v2.0/handler/scan_all.go | 190 ------------------ 4 files changed, 43 insertions(+), 211 deletions(-) diff --git a/src/controller/scan/callback.go b/src/controller/scan/callback.go index bb96ba9075f..3a1682ebcd4 100644 --- a/src/controller/scan/callback.go +++ b/src/controller/scan/callback.go @@ -102,20 +102,6 @@ func scanAllCallback(ctx context.Context, param string) error { _, err := scanCtl.ScanAll(ctx, task.ExecutionTriggerSchedule, true) return err } - if param != "" { - params := make(map[string]any) - if err := json.Unmarshal([]byte(param), ¶ms); err != nil { - return err - } - - if op, ok := params["operator"].(string); ok { - ctx = context.WithValue(ctx, operator.ContextKey{}, op) - } - } - - _, err := scanCtl.ScanAll(ctx, task.ExecutionTriggerSchedule, true) - return err -} func scanTaskStatusChange(ctx context.Context, taskID int64, status string) (err error) { logger := log.G(ctx).WithFields(log.Fields{"task_id": taskID, "status": status}) diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html index a9650c8994a..81af71726d4 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.html @@ -23,11 +23,18 @@
-
Leave empty to scan all projects.
+
+ + +
If repositories selected, they take precedence over project selection.
+
diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts index df33b61f14f..fb5828c8689 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts @@ -25,6 +25,8 @@ import { } from '../../../../shared/units/utils'; import { DatePipe } from '@angular/common'; import { Project } from '../../../../../../ng-swagger-gen/models/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'; @@ -68,7 +70,8 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { private newScanAllService: ScanAllService, private errorHandlerEntity: ErrorHandler, private translate: TranslateService, - private projectService: ProjectService + private projectService: ProjectService, + private newRepoService: NewRepositoryService ) {} get scanningMetrics(): ScanningMetrics { @@ -183,6 +186,8 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { // Scope selection for selective scanning projects: Project[] = []; selectedProjectIds: number[] = []; + repositories: NewRepository[] = []; + selectedRepositories: string[] = []; ngOnInit(): void { this.getScanText(); @@ -193,6 +198,8 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy { .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() { @@ -359,13 +366,35 @@ 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 { - if (this.selectedProjectIds && this.selectedProjectIds.length) { + // Prefer repo list if specified, else fall back to projects + if (this.selectedRepositories && this.selectedRepositories.length) { try { - return JSON.stringify({ project_ids: this.selectedProjectIds }); - } catch (e) { - return undefined; - } + 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; } diff --git a/src/server/v2.0/handler/scan_all.go b/src/server/v2.0/handler/scan_all.go index e01de6638fb..7ef8dbba24a 100644 --- a/src/server/v2.0/handler/scan_all.go +++ b/src/server/v2.0/handler/scan_all.go @@ -154,117 +154,6 @@ func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation return operation.NewCreateScanAllScheduleCreated() } - if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil { - return s.SendError(ctx, err) - } - - 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() - } - - if req.Schedule.Type == ScheduleManual { - execution, err := s.getLatestScanAllExecution(ctx, task.ExecutionTriggerManual) - if err != nil { - return s.SendError(ctx, err) - } - - if execution != nil && execution.IsOnGoing() { - message := fmt.Sprintf("a previous scan all job aleady exits, its status is %s", execution.Status) - 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) - } - } else { - schedule, err := s.getScanAllSchedule(ctx) - if err != nil { - return s.SendError(ctx, err) - } - - if schedule != nil { - message := "fail to set schedule for scan all as always had one, please delete it firstly then to re-schedule" - return s.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage(message)) - } - - 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) - } - } - - return operation.NewCreateScanAllScheduleCreated() -} - if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil { - return s.SendError(ctx, err) - } - - req := params.Schedule - - if req.Schedule.Type == ScheduleNone { - return operation.NewCreateScanAllScheduleCreated() - } - - if req.Schedule.Type == ScheduleManual { - execution, err := s.getLatestScanAllExecution(ctx, task.ExecutionTriggerManual) - if err != nil { - return s.SendError(ctx, err) - } - - if execution != nil && execution.IsOnGoing() { - message := fmt.Sprintf("a previous scan all job aleady exits, its status is %s", execution.Status) - return s.SendError(ctx, errors.ConflictError(nil).WithMessage(message)) - } - - if _, err := s.scanCtl.ScanAll(ctx, task.ExecutionTriggerManual, true); err != nil { - return s.SendError(ctx, err) - } - } else { - schedule, err := s.getScanAllSchedule(ctx) - if err != nil { - return s.SendError(ctx, err) - } - - if schedule != nil { - message := "fail to set schedule for scan all as always had one, please delete it firstly then to re-schedule" - return s.SendError(ctx, errors.PreconditionFailedError(nil).WithMessage(message)) - } - - if _, err := s.createOrUpdateScanAllSchedule(ctx, req.Schedule.Type, req.Schedule.Cron, nil); err != nil { - return s.SendError(ctx, err) - } - } - - return operation.NewCreateScanAllScheduleCreated() -} func (s *scanAllAPI) UpdateScanAllSchedule(ctx context.Context, params operation.UpdateScanAllScheduleParams) middleware.Responder { if err := s.requireAccess(ctx, rbac.ActionUpdate); err != nil { @@ -318,85 +207,6 @@ func (s *scanAllAPI) UpdateScanAllSchedule(ctx context.Context, params operation return operation.NewUpdateScanAllScheduleOK() } - if err := s.requireAccess(ctx, rbac.ActionUpdate); err != nil { - return s.SendError(ctx, err) - } - 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)) - } - - schedule, err := s.getScanAllSchedule(ctx) - if err != nil { - return s.SendError(ctx, err) - } - - if req.Schedule.Type == ScheduleNone { - if schedule != nil { - err = s.scheduler.UnScheduleByID(ctx, schedule.ID) - } - } else { - // 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 { - return s.SendError(ctx, err) - } - - return operation.NewUpdateScanAllScheduleOK() -} - if err := s.requireAccess(ctx, rbac.ActionUpdate); err != nil { - return s.SendError(ctx, err) - } - req := params.Schedule - - 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)) - } - - schedule, err := s.getScanAllSchedule(ctx) - if err != nil { - return s.SendError(ctx, err) - } - - if req.Schedule.Type == ScheduleNone { - if schedule != nil { - err = s.scheduler.UnScheduleByID(ctx, schedule.ID) - } - } else { - _, err = s.createOrUpdateScanAllSchedule(ctx, req.Schedule.Type, req.Schedule.Cron, schedule) - } - - if err != nil { - return s.SendError(ctx, err) - } - - return operation.NewUpdateScanAllScheduleOK() -} func (s *scanAllAPI) GetScanAllSchedule(ctx context.Context, _ operation.GetScanAllScheduleParams) middleware.Responder { if err := s.requireAccess(ctx, rbac.ActionRead); err != nil { From 4118acf9922d811f764193afa1661aa1dfa51fb9 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Mon, 29 Sep 2025 11:50:53 +0530 Subject: [PATCH 3/5] portal: fix Project type import in vulnerability config\n\nResolve TS2322 by importing the local app Project model instead of the swagger-generated Project type. The ProjectService returns the app model (creation_time: string | Date), which mismatched the swagger model (creation_time: string). This unblocks Angular build for the selective scan scope UI. Signed-off-by: SoumyaRaikwar --- .../vulnerability/vulnerability-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts index fb5828c8689..52011559f5e 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability/vulnerability-config.component.ts @@ -24,7 +24,7 @@ import { VULNERABILITY_SCAN_STATUS, } from '../../../../shared/units/utils'; import { DatePipe } from '@angular/common'; -import { Project } from '../../../../../../ng-swagger-gen/models/project'; +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'; From df87dbdbfc220a229be1ee3342c396ad4e22fe66 Mon Sep 17 00:00:00 2001 From: Soumya Raikwar <164396577+SoumyaRaikwar@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:08:15 +0530 Subject: [PATCH 4/5] Delete WARP.md Signed-off-by: Soumya Raikwar <164396577+SoumyaRaikwar@users.noreply.github.com> --- WARP.md | 136 -------------------------------------------------------- 1 file changed, 136 deletions(-) delete mode 100644 WARP.md diff --git a/WARP.md b/WARP.md deleted file mode 100644 index af58bd9df4f..00000000000 --- a/WARP.md +++ /dev/null @@ -1,136 +0,0 @@ -# WARP.md - -This file provides guidance to WARP (warp.dev) when working with code in this repository. - -Project: Harbor (monorepo for building Harbor images, binaries, and installers) - -Key repos/docs referenced in-repo: -- README.md: badges, install links, and architecture links -- Makefile: entrypoint for build, lint, packaging, and local compose orchestration -- src/ (Go, main services and libraries) -- make/ (Docker build context, photon-based images, compose, and scripts) -- tests/ (integration/Robot assets, compose for tests) - -Common commands -- Build everything (compile Go, generate API, build Docker images, prepare config) - make install - # Equivalent to: make compile build prepare start - -- Compile Go binaries with Dockerized toolchain (golang image) only - make compile - # Subtargets: - make compile_core - make compile_jobservice - make compile_registryctl - make compile_standalone_db_migrator - -- Build Docker images (from make/photon/*) - make build - # Parameters you may override (examples): - # VERSIONTAG=dev|vX.Y.Z, BASEIMAGETAG=dev, BUILD_BASE=true|false, PULL_BASE_FROM_DOCKERHUB=true|false - # GOBUILDIMAGE=golang:1.24.6, NODEBUILDIMAGE=node:16.18.0, IMAGENAMESPACE=goharbor - -- Start and stop local compose stack (uses make/docker-compose.yml) - make start - make down - make restart - -- Generate and validate API/mocks (required before some builds/lint) - make lint_apis - make gen_apis - make gen_mocks - make mocks_check - -- Lint (golangci-lint; installs separately per comment in Makefile) - make lint - # Ensure $(go env GOPATH)/bin/golangci-lint exists or install as per Makefile comment - -- Vulnerability scan for Go deps (requires govulncheck installed) - make govulncheck - -- Go checks bundle - make go_check - # runs: gen_apis, mocks_check, misspell, commentfmt, lint - -- Package installers - # Online installer - make package_online PKGVERSIONTAG= [REGISTRYSERVER=host/ REGISTRYPROJECTNAME=project] - # Offline installer (saves built images into tarball) - make package_offline PKGVERSIONTAG= - -- Push built images to registry - make pushimage REGISTRYSERVER= REGISTRYUSER= REGISTRYPASSWORD=****** DEVFLAG=false - -- Clean - make clean # usage info for clean targets - make cleanall # remove binaries, images, compose files, configs, packages - make cleanbinary # remove compiled binaries - make cleanimage # remove Harbor images - make cleanbaseimage # remove base images - -- Run tests - # Unit/integration Go tests live under src; use standard go test, typically within src - (cd src && go test ./...) - - # Test compose and Robot-related flows are under tests/ - # Example helpers available: - tests/integration.sh - tests/startuptest.sh - tests/swaggerchecker.sh - # Single Go package test example: - (cd src && go test ./pkg/trace -run TestSampler -v) - -Notes on environment and prerequisites -- Docker and docker-compose are required for build/start/package flows (README lists minimum versions). -- Build largely orchestrated via photon-based Dockerfiles in make/photon; builds run in containers to avoid host toolchain drift. -- VERSION holds the release version (current: contents of VERSION file; e.g., v2.15.0). VERSIONTAG defaults to dev for images. -- Some targets use network access to fetch swagger/mocks tool images and dependencies. -- Setting GEN_TLS=1 when running make prepare can generate internal TLS certs via prepare image. - -High-level architecture -Harbor in this repo builds a multi-service system packaged as containers and orchestrated with docker-compose or Helm (chart is external). The main runtime services and their in-repo code live under src/: -- core (src/core): - - Web/API service implementing Harbor business logic and REST APIs (v2.0 swagger at api/v2.0/swagger.yaml with generated server under src/server/v2.0) - - Controllers, middlewares, auth, session mgmt, and Beego-based components -- jobservice (src/jobservice): - - Asynchronous job orchestration built on Redis (gocraft/work fork) - - Handles replication, GC, scan, notifications, exports, system artifact cleanup, etc. - - Components: API server, controller, scheduler, logger, reaper, Redis-backed queue; supports hooks and status web UI endpoints -- registryctl (src/registryctl): - - Sidecar/controller for upstream distribution registry (patched) for access control and cleanup; interacts with core and registry -- portal (src/portal): - - UI frontend; built via Node image, artifacts bundled into portal container -- common libs (src/lib, src/pkg, src/common): - - Cross-cutting libraries: config, http, errors, cache/redis wrappers, metrics, tracing (OpenTelemetry), icon, etc. - - Domain packages under src/pkg (retention, scanner, p2p preheat, scheduler, task, queuestatus, etc.) -- migration (src/migration): - - DB and data model migrations -- server (src/server/v2.0): - - Swagger-generated API server glue for core -- cmd (src/cmd): - - Auxiliary binaries, e.g., standalone DB migrator, exporter - -Build and image pipeline -- make compile_* builds Go binaries inside a golang container, embedding version info via -ldflags with src/pkg/version (GitCommit and ReleaseVersion from VERSION file and current commit). -- make build delegates to make/photon/Makefile to build base images and service images: core, jobservice, registry, registryctl, nginx, portal, db, redis, exporter, and prepare. -- make prepare renders config (harbor.yml) and compose files via the prepare image and make/ scripts; can generate internal TLS if GEN_TLS is set. -- make start/up uses docker-compose.yml under make/ to bring up the composed services. - -API and codegen -- Swagger spec: api/v2.0/swagger.yaml -- Lint spec: make lint_apis (Spectral via tools/spectral Docker image) -- Generate server stubs: make gen_apis (OpenAPI generator via tools/swagger Docker image; outputs to src/server/v2.0) -- Mocks: make gen_mocks (mockery via tools/mockery Docker image); make mocks_check ensures mocks are up to date - -Testing strategy (in-repo signals) -- Go unit/integration: standard go test under src/ (golangci config at src/.golangci.yaml) -- Robot/Selenium style UI tests and helpers under tests/, with supporting docker-compose.test.yml -- Helper scripts in tests/: integration.sh, startuptest.sh, swaggerchecker.sh, etc. - -Service secrets and configuration -- make prepare and prepare image manage assembling harbor.yml, secrets, and compose files under make/; many config knobs are parameterized in Helm chart (external) and values test data under src/pkg/chart/testdata/ for reference. - -Working tips specific to this repo -- Always run gen_apis and gen_mocks (or simply make go_check) after editing swagger or interfaces in src/pkg to keep generated code/mocks in sync; CI checks for drift. -- Use containerized build targets (default) to avoid local Go/Node version mismatches. Override GOBUILDIMAGE/NODEBUILDIMAGE only if necessary. -- For local dev on Go packages: run go test ./... from src/ to avoid pulling the entire monorepo module graph at top-level (module root is src/ per src/go.mod with replace to ../). From 665193f8eaa966ff2d3d4266ce100a88fc9b1617 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Sun, 18 Jan 2026 20:39:59 +0530 Subject: [PATCH 5/5] feat(scan): implement selective image scanning scope Signed-off-by: SoumyaRaikwar --- src/controller/scan/base_controller_test.go | 45 +++++++++++++++++++++ src/controller/scan/scope.go | 30 +++++++------- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/controller/scan/base_controller_test.go b/src/controller/scan/base_controller_test.go index 93235fbc717..87c1a146bd7 100644 --- a/src/controller/scan/base_controller_test.go +++ b/src/controller/scan/base_controller_test.go @@ -657,6 +657,51 @@ func (suite *ControllerTestSuite) TestScanAll() { } } +func (suite *ControllerTestSuite) TestScanAllWithScope() { + executionID := int64(2) + scope := &ScanAllScope{ + ProjectIDs: []int64{1}, + Repositories: []string{"library/test"}, + } + + // Reset mock to avoid pollution + suite.execMgr.ExpectedCalls = nil + suite.execMgr.Calls = nil + + // Prepare context with scope + ctx := WithScanAllScope(context.TODO(), scope) + + // Mock expectations + // Verify that Create is called with extraAttrs containing the scope + var capturedExtra map[string]any + suite.execMgr.On("Create", ctx, "SCAN_ALL", int64(0), "MANUAL", mock.Anything).Run(func(args mock.Arguments) { + capturedExtra = args.Get(4).(map[string]any) + }).Return(executionID, nil).Once() + + // Other necessary mocks for flow to complete + mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return([]*scan.Report{{UUID: "uuid"}}, nil).Once() + mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return([]*types.Policy{}).Once() + mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once() + suite.execMgr.On("Get", mock.Anything, executionID).Return(&task.Execution{ID: executionID}, nil).Times(2) + mock.OnAnything(suite.accessoryMgr, "List").Return([]accessoryModel.Accessory{}, nil).Once() + mock.OnAnything(suite.artifactCtl, "List").Return([]*artifact.Artifact{}, nil).Once() + suite.taskMgr.On("Count", ctx, q.New(q.KeyWords{"execution_id": executionID})).Return(int64(0), nil).Once() + mock.OnAnything(suite.execMgr, "UpdateExtraAttrs").Return(nil).Once() + suite.execMgr.On("MarkDone", mock.Anything, executionID, mock.Anything).Return(nil).Once() + suite.cache.On("Contains", ctx, scanAllStoppedKey(executionID)).Return(false).Once() + + _, err := suite.c.ScanAll(ctx, "MANUAL", false) + suite.NoError(err) + + // Verify scope was passed + assert.NotNil(suite.T(), capturedExtra) + if capturedExtra != nil { + s, ok := capturedExtra["scope"] + assert.True(suite.T(), ok) + assert.Equal(suite.T(), scope, s) + } +} + func (suite *ControllerTestSuite) TestStopScanAll() { mockExecID := int64(100) // mock error case diff --git a/src/controller/scan/scope.go b/src/controller/scan/scope.go index 6e532f288a2..5fc0880d39a 100644 --- a/src/controller/scan/scope.go +++ b/src/controller/scan/scope.go @@ -23,8 +23,8 @@ const ScopeHeader = "X-Scan-All-Scope" // 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"` + ProjectIDs []int64 `json:"project_ids,omitempty"` + Repositories []string `json:"repositories,omitempty"` } // scopeCtxKey is the context key type for storing scope in context @@ -33,20 +33,20 @@ 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) + 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 -} \ No newline at end of file + v := ctx.Value(scopeCtxKey{}) + if v == nil { + return nil + } + if s, ok := v.(*ScanAllScope); ok { + return s + } + return nil +}