Skip to content

CI/CD Integration

The HarborGuard API is what you wire into CI. The pattern is always the same: trigger a scan with POST /api/scans, poll GET /api/scans/{scanId} until it leaves IN_PROGRESS, then read the compliance grade and decide whether to ship.

Authentication

Generate an API key in Settings -> API Keys. Pick the lowest role that works:

RoleCan scanCan read scansCan mutate orgs
viewernoyesno
developeryesyesno
adminyesyesyes

For CI, developer is right.

Send the key as either header:

x-api-key: hg_live_...
Authorization: Bearer hg_live_...

Rate limit on POST /api/scans is 10 requests per minute per key.

Trigger a scan

POST /api/scans
Content-Type: application/json
x-api-key: hg_live_...
 
{
  "image": "docker.io/myorg/api:sha-abc1234",
  "scanners": ["trivy", "grype", "syft"]
}

Response (HTTP 202):

{
  "scanId": "scan-lq8a4f2x-9k3p1m",
  "status": "PENDING",
  "message": "Scan queued for execution",
  "url": "/dashboard/scans/scan-lq8a4f2x-9k3p1m"
}

scanners must be a non-empty subset of ["trivy", "grype", "syft", "dockle", "osv", "dive"].

Poll for completion

GET /api/scans/scan-lq8a4f2x-9k3p1m
x-api-key: hg_live_...

Status moves PENDING -> IN_PROGRESS -> COMPLETED (or FAILED). When COMPLETED, the response includes:

{
  "data": {
    "id": "scan-lq8a4f2x-9k3p1m",
    "status": "COMPLETED",
    "compliance": "B",
    "counts": { "critical": 0, "high": 2, "medium": 14, "low": 31 }
  }
}

Gate on compliance (the grade) and/or on the raw counts if you want stricter rules than the grade encodes.

Pipeline examples

name: container-scan
on:
  push:
    branches: [main]
 
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger HarborGuard scan
        id: trigger
        env:
          HG_API_KEY: ${{ secrets.HG_API_KEY }}
          IMAGE: docker.io/myorg/api:sha-${{ github.sha }}
        run: |
          resp=$(curl -fsS -X POST https://harborguard.co/api/scans \
            -H "x-api-key: $HG_API_KEY" \
            -H "content-type: application/json" \
            -d "{\"image\":\"$IMAGE\",\"scanners\":[\"trivy\",\"grype\",\"syft\"]}")
          echo "scan_id=$(echo "$resp" | jq -r .scanId)" >> "$GITHUB_OUTPUT"
 
      - name: Wait for scan to complete
        id: wait
        env:
          HG_API_KEY: ${{ secrets.HG_API_KEY }}
          SCAN_ID: ${{ steps.trigger.outputs.scan_id }}
        run: |
          for i in $(seq 1 60); do
            resp=$(curl -fsS https://harborguard.co/api/scans/$SCAN_ID \
              -H "x-api-key: $HG_API_KEY")
            status=$(echo "$resp" | jq -r .data.status)
            echo "[$i] status=$status"
            if [ "$status" = "COMPLETED" ] || [ "$status" = "FAILED" ]; then
              echo "$resp" > scan.json
              echo "status=$status" >> "$GITHUB_OUTPUT"
              echo "grade=$(echo "$resp" | jq -r .data.compliance)" >> "$GITHUB_OUTPUT"
              exit 0
            fi
            sleep 10
          done
          echo "scan did not complete in 10 minutes" >&2
          exit 1
 
      - name: Gate on grade
        env:
          STATUS: ${{ steps.wait.outputs.status }}
          GRADE: ${{ steps.wait.outputs.grade }}
          MIN_GRADE: B
        run: |
          if [ "$STATUS" != "COMPLETED" ]; then
            echo "scan failed"; exit 1
          fi
          # Allow A and B; fail C and D.
          case "$GRADE" in
            A|B) echo "grade $GRADE passes (min $MIN_GRADE)";;
            *)   echo "grade $GRADE does not meet minimum $MIN_GRADE"; exit 1;;
          esac

Tightening the gate

Grade-based gating is coarse. For finer control, read counts directly:

critical=$(jq -r .data.counts.critical scan.json)
high=$(jq -r .data.counts.high scan.json)
[ "$critical" -gt 0 ] && { echo "criticals present"; exit 1; }
[ "$high" -gt 5 ]    && { echo "$high highs (max 5)"; exit 1; }

Failure modes to handle

  • HTTP 429 - rate limit. Back off and retry.
  • HTTP 402 - your org's plan scan limit was hit. Upgrade or wait.
  • status: FAILED - check logs on the scan detail page; common causes are private-registry pull failures and unreachable images.
  • Polling timeout - large images can exceed 10 minutes. Tune the loop.

On this page