A standalone Go service that implements Harbor's pluggable scanner adapter spec (v1.2) and forwards vulnerability scanning to DevGuard.
Once registered in Harbor, it works exactly like Trivy — users click "Scan" in the Harbor UI, and results appear in both Harbor's vulnerability tab and DevGuard's dashboard with enriched risk intelligence.
Harbor Registry harbor-scanner-devguard DevGuard Platform
+----------------+ +------------------------+ +------------------+
| | POST | | POST | |
| User clicks |--/scan--> 1. Return 202 |--SBOM--> Vuln database |
| "Scan" in UI | | 2. Pull image (crane) | | CVSS + EPSS |
| |<-report- 3. SBOM (syft) |<-vulns- + VEX + Risk |
| | GET | 4. Sign SBOM (Cosign) | sync | |
| |/report| 5. Upload to DevGuard | | |
| | | 6. Fetch VEX document | | |
| | | 7. Translate report | | |
| | | 8. Create attestation | | |
+----------------+ +------------------------+ +------------------+
- Harbor calls
POST /api/v1/scanwith the artifact reference and registry credentials. - The adapter returns
202 Acceptedwith a scan ID immediately. - A background goroutine:
- Pulls the container image using the provided bearer token (used once, then dropped).
- Generates two SBOM formats from a single syft analysis: CycloneDX (uploaded to DevGuard, which prefers it) and SPDX (returned to Harbor, which requires it — see note below).
- Auto-creates the project and asset in DevGuard if they don't exist.
- Uploads the CycloneDX SBOM to DevGuard's
POST /api/v1/scan/endpoint (synchronous). - Fetches the VEX document from DevGuard for the scanned artifact.
- Translates DevGuard findings into a Harbor vulnerability report.
- Applies VEX filtering:
fixed,accepted, andfalsePositivefindings are excluded. - Enriches each vulnerability with
vendor_attributes: risk score, EPSS, VEX status, deep link. - Builds an SBOM report (returned to Harbor when requested via Accept header).
- Signs the SBOM with Cosign (ECDSA P-256) for cryptographic proof of authenticity.
- Creates a signed In-Toto scan attestation in DevGuard (proof that the scan happened).
- Harbor polls
GET /api/v1/scan/{id}/reportuntil the report is ready (302 while pending, 200 when done). - Harbor can request either the vulnerability report or the SBOM report depending on the
Acceptheader.
- Vulnerability scanning via DevGuard (CVSS + EPSS + risk scores)
- Dual-format SBOM generation from one syft pass: CycloneDX 1.6 (for DevGuard) and SPDX JSON (for Harbor)
- Cosign SBOM signing of the CycloneDX SBOM (ECDSA P-256 signature)
- VEX filtering (auto-exclude accepted/false-positive findings from Harbor report)
- VEX document export (fetched from DevGuard per scan)
- In-Toto scan attestations with Cosign signature (proof of scan created in DevGuard)
- Auto-provisioning of DevGuard projects and assets
- Async scan workflow (202 + polling per Harbor spec v1.2)
Harbor v2.12 hard-codes application/spdx+json as the only SBOM media type it will store as an OCI accessory artifact (harbor source). DevGuard, on the other hand, consumes CycloneDX. Rather than force a user to choose, the adapter generates the image SBOM once with syft and encodes it into both formats: CycloneDX goes to DevGuard, SPDX goes to Harbor. Same component list, both tools happy.
- Kubernetes admission control (Kyverno policy to block unscanned deploys)
- Policy-based scan pass/fail (fail scan if critical vulns exceed threshold)
- Webhook notifications (alert on new critical vulns)
| Trivy | DevGuard Adapter | |
|---|---|---|
| Vuln database | Local (downloaded to pod) | Remote (DevGuard API) |
| Enrichment | CVSS only | CVSS + EPSS + VEX + risk score |
| VEX support | Basic | Full (auto-filters accepted/false-positive) |
| VEX document export | No | Yes (fetched from DevGuard per scan) |
| SBOM return to Harbor | Yes | Yes (SPDX SBOM stored as OCI accessory + CycloneDX also available via Accept header) |
| Cosign SBOM signing | No | Yes (ECDSA P-256 signature) |
| Scan attestations | No | Yes (signed In-Toto attestation in DevGuard) |
| Asset tracking | None | DevGuard tracks vulns per org/project/asset |
| Dashboard | Harbor UI only | Harbor UI + DevGuard dashboard |
| Auto-provision | N/A | Creates projects/assets in DevGuard automatically |
harbor-scanner-devguard/
├── cmd/scanner-adapter/main.go # HTTP server entrypoint
├── pkg/
│ ├── config/config.go # Env var configuration
│ ├── harbor/types.go # Harbor OpenAPI v1.2 spec types
│ ├── devguard/
│ │ ├── types.go # DevGuard API response types
│ │ ├── client.go # API client + Cosign signing + VEX + attestations
│ │ └── client_test.go # httptest-based tests
│ ├── handler/
│ │ ├── handler.go # /metadata, /scan, /report (vuln+SBOM), /healthz
│ │ └── router.go # chi router + bearer auth middleware
│ ├── scanner/sbom.go # Image pull (crane) + dual-format SBOM generation (syft → CycloneDX + SPDX)
│ ├── translator/
│ │ ├── report.go # DevGuard -> Harbor translation + VEX + SBOM + attestations
│ │ └── report_test.go # Severity mapping, VEX, purl parsing tests
│ └── store/store.go # Thread-safe in-memory job state (vuln + SBOM)
├── deploy/helm/ # Helm chart (Deployment, Service, Secret, SA)
├── Dockerfile # Multi-stage, distroless final image
├── Makefile # build, test, lint, docker-build, run-local
└── go.mod
- Go 1.22+
- Docker (for building container images)
- A running DevGuard instance
- A DevGuard Personal Access Token (PAT) with
scan managescopes
All configuration is via environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
DEVGUARD_API_URL |
yes | -- | Base URL of the DevGuard instance |
DEVGUARD_API_TOKEN |
yes | -- | Hex-encoded ECDSA P-256 private key (PAT) |
DEVGUARD_ORG_SLUG |
yes | -- | DevGuard organization slug |
ADAPTER_BEARER_TOKEN |
no | -- | Shared secret for Harbor-to-adapter auth |
LISTEN_ADDR |
no | :8080 |
Address to bind the HTTP server |
ENABLE_SBOM_CAPABILITY |
no | true |
Advertise the SBOM capability in /metadata. Set to false when registering against Harbor < v2.11, which rejects scanner metadata containing the unknown sbom capability type. |
make build # Compile binary to bin/scanner-adapter
make test # Run unit tests with race detector
make lint # Run golangci-lint
make docker-build # Build Docker image
make run-local # Build and run locallymake testRuns tests for:
- Translator (
pkg/translator/report_test.go): severity mapping (all CVSS boundary values), VEX filtering (verifiesfixed/accepted/falsePositiveare excluded), vendor attributes population, purl parsing, empty response handling. - DevGuard client (
pkg/devguard/client_test.go): successful SBOM upload with request signing verification, server error handling, invalid URL/token handling.
Start the adapter with dummy env vars to verify it boots and responds:
DEVGUARD_API_URL=https://example.com \
DEVGUARD_API_TOKEN=abc123 \
DEVGUARD_ORG_SLUG=test-org \
make run-localIn another terminal:
# Health check — should return "ok"
curl http://localhost:8080/probe/healthz
# Metadata — should return scanner capabilities JSON
curl -s http://localhost:8080/api/v1/metadata | jq .cd devguard/
# Copy Kratos config (one-time fix for try-it setup)
sudo chown $USER:$USER kratos/
cp .kratos/identity.schema.json kratos/identity.schema.json
cp .kratos/gh-mapping.jsonnet kratos/gh-mapping.jsonnet
cp .kratos/kratos.docker-compose-try-it.yml kratos/kratos.yml
docker compose -f docker-compose-try-it.yaml up -d
# Wait for healthy: curl http://localhost:8080/api/v1/health
# DevGuard UI: http://localhost:3000cd /tmp
curl -sL https://github.com/goharbor/harbor/releases/download/v2.12.2/harbor-offline-installer-v2.12.2.tgz | tar xz
cd harbor
cp harbor.yml.tmpl harbor.yml
# Edit harbor.yml: set hostname=localhost, disable HTTPS
sudo ./install.sh
# Harbor UI at http://localhost (admin / Harbor12345)Sign up at http://localhost:3000, create an organization, then create a PAT via the DevGuard UI (Settings > Personal Access Tokens, scopes: scan manage).
Alternatively, generate a keypair manually:
openssl ecparam -name prime256v1 -genkey -noout 2>/dev/null | \
openssl ec -text -noout 2>/dev/null | \
grep -A3 "priv:" | tail -n+2 | tr -d ' :\n'
echo # This is your DEVGUARD_API_TOKENcd harbor-scanner-devguard/
export DEVGUARD_API_URL=http://localhost:8080
export DEVGUARD_API_TOKEN=<your-hex-private-key>
export DEVGUARD_ORG_SLUG=<your-org-slug>
export ADAPTER_BEARER_TOKEN=test-secret
export LISTEN_ADDR=:9090 # avoid conflict with DevGuard on 8080
make run-localGo to Administration > Interrogation Services > Scanners > + New Scanner:
- Name:
DevGuard - Endpoint:
http://172.17.0.1:9090(find your Docker bridge IP withip addr show docker0) - Authorization: Bearer
test-secret
Click Test Connection, save, set as default.
docker pull nginx:1.20
docker tag nginx:1.20 localhost/library/nginx:1.20
docker push localhost/library/nginx:1.20In Harbor UI: Projects > library > nginx > select image > Scan Vulnerability.
- Harbor UI: Vulnerability count and severity chart appear in the image row.
- DevGuard UI: Navigate to
http://localhost:3000/<org>/projects/library/assets/nginxfor enriched data. - Adapter logs should show the full pipeline:
scan accepted → pulling image → SBOM generated → uploading to DevGuard → VEX document fetched → SBOM signed with Cosign → signed attestation created → scan complete
Push any new image — no manual setup needed:
docker pull python:3.9-slim
docker tag python:3.9-slim localhost/library/python:3.9-slim
docker push localhost/library/python:3.9-slimScan from Harbor. The adapter auto-creates the python asset in DevGuard.
DIGEST=<digest-from-docker-push>
SCAN_ID=$(curl -s -X POST http://localhost:9090/api/v1/scan \
-H "Authorization: Bearer test-secret" \
-H "Content-Type: application/json" \
-d "{
\"registry\":{\"url\":\"http://localhost\",\"authorization\":\"Basic $(echo -n admin:Harbor12345 | base64)\"},
\"artifact\":{\"repository\":\"library/nginx\",\"digest\":\"$DIGEST\",\"mime_type\":\"application/vnd.docker.distribution.manifest.v2+json\"}
}" | jq -r '.id')
echo "Scan ID: $SCAN_ID"
sleep 15
# Vulnerability report
curl -s http://localhost:9090/api/v1/scan/$SCAN_ID/report \
-H "Authorization: Bearer test-secret" \
-H "Accept: application/vnd.security.vulnerability.report; version=1.1" \
| jq '{severity, vuln_count: (.vulnerabilities | length), first_vuln_attrs: .vulnerabilities[0].vendor_attributes}'
# SBOM report
curl -s http://localhost:9090/api/v1/scan/$SCAN_ID/report \
-H "Authorization: Bearer test-secret" \
-H "Accept: application/vnd.security.sbom.report+json; version=1.0" \
| jq '{media_type, scanner: .scanner.name, sbom_format: .sbom.bomFormat, components_count: (.sbom.components | length)}'Production setup is a one-time process with 3 steps. After that, everything is automatic.
- Log in to your DevGuard instance at
https://devguard.yourcompany.com - Go to Settings > Personal Access Tokens
- Click Create Token, set scopes to
scan manage - DevGuard gives you a hex-encoded token — save it securely
This is the DEVGUARD_API_TOKEN. You also need your organization slug (visible in the DevGuard URL, e.g. https://devguard.yourcompany.com/my-org/... means the slug is my-org).
helm install harbor-scanner-devguard ./deploy/helm \
--set devguard.apiURL=https://devguard.yourcompany.com \
--set devguard.apiToken=<your-pat-hex-key> \
--set devguard.orgSlug=my-org \
--set adapterBearerToken=<strong-random-secret> \
--set image.repository=your-registry.com/harbor-scanner-devguard \
--set image.tag=v1.0.0Or use an existing Kubernetes Secret (recommended for production):
kubectl create secret generic devguard-scanner-secret \
--from-literal=DEVGUARD_API_TOKEN=<your-pat-hex-key> \
--from-literal=ADAPTER_BEARER_TOKEN=<shared-secret>
helm install harbor-scanner-devguard ./deploy/helm \
--set devguard.apiURL=https://devguard.yourcompany.com \
--set devguard.orgSlug=my-org \
--set existingSecret=devguard-scanner-secret- Log in to Harbor as admin
- Go to Administration > Interrogation Services > Scanners > + New Scanner
- Fill in:
- Name:
DevGuard - Endpoint:
http://harbor-scanner-devguard:8080(in-cluster Service DNS) - Authorization: Bearer
<your ADAPTER_BEARER_TOKEN>
- Name:
- Click Test Connection, save, and set as default scanner
Once these 3 steps are done, everything is automatic:
- Push any image to Harbor — no manual project/asset setup needed in DevGuard
- Scan from Harbor UI — click "Scan Vulnerability" or enable auto-scan on push
- Projects and assets are auto-created in DevGuard by the adapter
- SBOMs are signed with Cosign and attestations are created in DevGuard
- Results appear in both Harbor's vulnerability tab and DevGuard's dashboard
- No database access, no CLI tools, no manual steps required for day-to-day use
To smoke-test the chart without a real cluster, use kind:
# 1. Create a local cluster
kind create cluster --name harbor-scanner
# 2. Build the image and load it into kind
docker build -t harbor-scanner-devguard:latest .
kind load docker-image harbor-scanner-devguard:latest --name harbor-scanner
# 3. Create namespace + secret (placeholder token is fine for smoke-testing)
kubectl create namespace scanner
kubectl -n scanner create secret generic devguard-creds \
--from-literal=DEVGUARD_API_TOKEN='placeholder-token' \
--from-literal=ADAPTER_BEARER_TOKEN='test-secret-123'
# 4. Install the chart
helm install scanner ./deploy/helm \
--namespace scanner \
--set image.repository=harbor-scanner-devguard \
--set image.tag=latest \
--set image.pullPolicy=Never \
--set devguard.apiURL=http://placeholder.devguard.local \
--set devguard.orgSlug=test-org \
--set existingSecret=devguard-creds
# 5. Verify the metadata endpoint
kubectl -n scanner port-forward svc/scanner-harbor-scanner-devguard 8080:8080 &
curl -s http://localhost:8080/probe/healthz # -> ok
curl -s http://localhost:8080/api/v1/metadata \
-H "Authorization: Bearer test-secret-123" | jq .scanner.name # -> "DevGuard"
# 6. Cleanup
helm -n scanner uninstall scanner
kubectl delete namespace scanner
kind delete cluster --name harbor-scannerThe pod will run (and /probe/healthz + /api/v1/metadata will respond), but actual scans require a real DevGuard endpoint + PAT.
The adapter maps Harbor artifacts to DevGuard assets:
| Harbor | DevGuard | Example |
|---|---|---|
| -- | Organization | DEVGUARD_ORG_SLUG env var |
| Project name (first path segment) | Project | library from library/nginx |
| Remaining path segments | Asset | nginx from library/nginx |
| Artifact digest | Asset version ref | sha256:abc123... |
Projects and assets are auto-created in DevGuard if they don't exist.
- The
registry.authorizationbearer token from Harbor is used once to pull the image, then immediately discarded. It is never logged, stored, or included in error messages. - SBOMs are cryptographically signed with ECDSA P-256 (Cosign-compatible) to prove authenticity.
- Scan attestations are signed and uploaded to DevGuard as In-Toto statements.
- DevGuard authentication uses HTTP Message Signatures (RFC 9421) with ECDSA P-256.
- The adapter optionally requires a shared bearer token for incoming requests from Harbor.
- The final Docker image uses distroless with a non-root user.
Apache 2.0