Skip to content

Aloui-Ikram/harbor-scanner-devguard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Harbor Scanner Adapter for DevGuard

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.

Architecture

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

Request Flow

  1. Harbor calls POST /api/v1/scan with the artifact reference and registry credentials.
  2. The adapter returns 202 Accepted with a scan ID immediately.
  3. 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, and falsePositive findings 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).
  4. Harbor polls GET /api/v1/scan/{id}/report until the report is ready (302 while pending, 200 when done).
  5. Harbor can request either the vulnerability report or the SBOM report depending on the Accept header.

Features

Core

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

Why two SBOM formats?

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.

Future Extensions

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

What Makes This Different from Trivy

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

Project Structure

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

Prerequisites

  • Go 1.22+
  • Docker (for building container images)
  • A running DevGuard instance
  • A DevGuard Personal Access Token (PAT) with scan manage scopes

Configuration

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.

Build

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 locally

Testing

Unit Tests

make test

Runs tests for:

  • Translator (pkg/translator/report_test.go): severity mapping (all CVSS boundary values), VEX filtering (verifies fixed/accepted/falsePositive are 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.

Smoke Test (no DevGuard/Harbor needed)

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

In 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 .

End-to-End Test (full pipeline)

1. Start DevGuard

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:3000

2. Start Harbor

cd /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)

3. Create DevGuard PAT

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_TOKEN

4. Start the adapter

cd 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-local

5. Register in Harbor

Go to Administration > Interrogation Services > Scanners > + New Scanner:

  • Name: DevGuard
  • Endpoint: http://172.17.0.1:9090 (find your Docker bridge IP with ip addr show docker0)
  • Authorization: Bearer test-secret

Click Test Connection, save, set as default.

6. Push and scan

docker pull nginx:1.20
docker tag nginx:1.20 localhost/library/nginx:1.20
docker push localhost/library/nginx:1.20

In Harbor UI: Projects > library > nginx > select image > Scan Vulnerability.

7. Verify results

  • Harbor UI: Vulnerability count and severity chart appear in the image row.
  • DevGuard UI: Navigate to http://localhost:3000/<org>/projects/library/assets/nginx for 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
    

8. Test auto-provisioning

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

Scan from Harbor. The adapter auto-creates the python asset in DevGuard.

9. Test SBOM return (via API)

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 Deployment

Production setup is a one-time process with 3 steps. After that, everything is automatic.

Step 1: Create a DevGuard PAT (one-time)

  1. Log in to your DevGuard instance at https://devguard.yourcompany.com
  2. Go to Settings > Personal Access Tokens
  3. Click Create Token, set scopes to scan manage
  4. 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).

Step 2: Deploy the adapter (one-time)

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.0

Or 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

Step 3: Register in Harbor (one-time)

  1. Log in to Harbor as admin
  2. Go to Administration > Interrogation Services > Scanners > + New Scanner
  3. Fill in:
    • Name: DevGuard
    • Endpoint: http://harbor-scanner-devguard:8080 (in-cluster Service DNS)
    • Authorization: Bearer <your ADAPTER_BEARER_TOKEN>
  4. Click Test Connection, save, and set as default scanner

After setup: fully automatic

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

Local Helm testing with kind

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

The pod will run (and /probe/healthz + /api/v1/metadata will respond), but actual scans require a real DevGuard endpoint + PAT.

Asset Mapping

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.

Security

  • The registry.authorization bearer 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.

License

Apache 2.0

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors