Docker Hardened Images Processor

Overview

Docker publishes a public advisory repository at https://github.com/docker-hardened-images/advisories.git containing two distinct data types, each processed by a dedicated binary:

BinaryDirectoryFormatOutput
docker-git-processorosv/OSV 1.5.0 JSONCVEMetadata + child tables (source=docker)
enrich-docker-vexvex/OpenVEX 0.2.0 JSONCVEMetadataReferences (advisory + mitigation refs) + CVEAffected (VEX status per Docker image product)

Both processors share a single docker-git-data container image stage that bakes the cloned advisory repository into the image at build time (~5 MB). Both run daily via EventBridge: OSV at 05:00 UTC, VEX enrichment at 05:30 UTC (after the OSV processor has written the CVEMetadata rows that VEX references).

Validated data volumes (production run 2026-04-01):

  • 945 OSV files → 812 unique CVEs upserted (multiple packages share the same CVE ID)
  • 394 VEX documents → 279 advisory reference URLs + 255 CVEAffected rows (191 unaffected, 64 unknown)
  • 44 affected-status statements skipped via FK path (CVEs present in VEX but absent from OSV files)

Data Source

  • Publisher: Docker, Inc.
  • Repository: https://github.com/docker-hardened-images/advisories.git
  • Repository size: ~5 MB (baked into container image at build time)
  • Update frequency: Daily
  • Advisory formats: OSV 1.5.0 (osv/) + OpenVEX 0.2.0 (vex/)
  • Coverage: CVEs affecting Docker Hardened Images (curated subset of upstream CVEs)

OSV Directory (osv/)

Organised by package name: osv/<package>/CVE-YYYY-NNNNN.json. One file per (package, CVE) pair. There may be multiple files for the same CVE (different packages), all deduped by (cveId, source) on upsert into CVEMetadata.

IDs in the OSV id field are already CVE identifiers (unlike GHSA advisories where the primary ID is a GHSA key and the CVE appears in aliases).

{
  "schema_version": "1.5.0",
  "id": "CVE-2021-31957",
  "aliases": ["GHSA-mcwm-2wmc-6hv4"],
  "summary": "ASP.NET Core Denial of Service Vulnerability",
  "details": "...",
  "published": "2021-06-08T17:03:11Z",
  "modified": "2025-08-25T10:57:34Z",
  "affected": [
    {
      "package": {
        "ecosystem": "DHI",
        "name": "aspnetcore",
        "purl": "pkg:dhi/aspnetcore"
      },
      "database_specific": {
        "source_ecosystem": "binary",
        "source_package": "aspnetcorev2_inprocess.dll"
      }
    }
  ],
  "references": [
    { "type": "WEB", "url": "https://github.com/dotnet/aspnetcore/security/advisories/GHSA-mcwm-2wmc-6hv4" }
  ]
}

The DHI ecosystem identifier maps to collection URL https://dhi.docker.com in internal/osv/mapper.go.

VEX Directory (vex/)

Organised by image name: vex/<image>/dhi-<image>.vex.json. One document per Docker Hardened Image. Each document contains multiple VEX statements — one per CVE assessed for that image — with an exploitability status and optional justification.

The document @id is a hash-based URL (not image-name-based):

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://dhi.docker.com/vex/vex-768b642da0b2d31fdb37c17381f2fb00",
  "author": "Docker Hardened Images <dhi@docker.com>",
  "role": "Document Creator",
  "timestamp": "2025-06-17T12:21:41.904Z",
  "last_updated": "2026-03-25T09:19:54.156Z",
  "version": 70,
  "statements": [
    {
      "@id": "bc6d545d-1c0c-43a7-acb1-8e43a688e407",
      "vulnerability": {
        "name": "CVE-2025-70873"
      },
      "timestamp": "2026-03-25T09:19:54.156Z",
      "products": [
        { "@id": "pkg:deb/debian/libsqlite3-0" },
        { "@id": "pkg:docker/dhi/activemq-artemis",
          "subcomponents": [ { "@id": "pkg:deb/debian/libsqlite3-0" } ]
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_present"
    }
  ]
}

VEX status values: not_affected, affected, fixed, under_investigation.


Processors

1. docker-git-processor — OSV Advisory Processor

Purpose: Import CVE advisories from the osv/ directory into CVEMetadata and related tables using the standard git+OSV pipeline.

Source constant: source = "docker"

Walk config: files matching CVE-*.json (prefix CVE-, suffix .json), recursive

CVE ID derivation: The OSV file id field is already a CVE ID. MapAdvisory calls ExtractCVEID(adv.Aliases) first; since DHI aliases are GHSA-prefixed and don’t match the CVE pattern, it falls back to adv.ID which is the CVE identifier directly. No custom MapFunc is needed.

Processing flow:

PullOrClone(repo) → read HEAD SHA
  → GetTracker("docker_advisory")
    → SHA unchanged + no --force → notifier.NoWork → exit 0
  → DetectChangedFiles (incremental git diff)
  → processor.Run(WalkConfig{FilePrefix:"CVE-", FileSuffix:".json"}, ParseJSON:true)
    → per batch of 200 files → BEGIN TX
      → per file: ReadFile → SHA1 hash → skip if unchanged
        → ParseJSON → MapAdvisory → UpsertCVEMetadata + child rows
      → COMMIT
  → UpsertTracker("docker_advisory", headSHA, cvesUpserted)
  → notifier.Completed / notifier.Errored

2. enrich-docker-vex — OpenVEX Enrichment Processor

Purpose: For every VEX document in the vex/ directory, record the document’s @id URL as an "advisory" reference on each CVE mentioned in its statements. This surfaces Docker’s VEX assessment URL in the CVE’s reference list, following the same pattern as suse-csaf-processor.

Source constant: cveSource = "docker" (matches CVEMetadata rows from docker-git-processor)

Walk config: files matching *.vex.json (suffix .vex.json), recursive

Processing flow:

PullOrClone(repo) → read HEAD SHA
  → GetTracker("docker_vex")
    → SHA unchanged + no --force → notifier.NoWork → exit 0
  → DetectChangedFiles (incremental git diff)
  → WalkAdvisories(vex/, FileSuffix:".vex.json")
  → FilterPaths (if incremental)
  → per file:
      ReadFile → json.Unmarshal → openvex.Document
      collectVexItems(doc) → deduplicate (cveID, docURL) pairs
      BEGIN TX
        → per unique (cveID, docURL):
            SAVEPOINT sp_vex
            writeVexItem(tx, item):
              InsertReferences([advisory URL, optional mitigation ref])
              UpsertAffected (if Docker image product found in statement.products)
            → FK violation (CVE not in CVEMetadata) → ROLLBACK TO SAVEPOINT → skip
            → success → RELEASE SAVEPOINT
      COMMIT
      → notifier.RecordError on file-level failures
  → UpsertTracker("docker_vex", headSHA, refsInserted)
  → notifier.Completed / notifier.Errored

VEX Data — Storage in Detail

enrich-docker-vex stores three types of data per VEX statement, all using existing schema tables.

1. Advisory reference — CVEMetadataReferences (type=advisory)

The VEX document’s @id URL links the CVE to Docker’s published VEX assessment document.

INSERT INTO "CVEMetadataReferences"
    ("uuid", "cveId", "source", "url", "type", "referenceSource", "title", "createdAt")
VALUES ($1, $2, 'docker', $3, 'advisory', 'Docker', NULL, $4)
ON CONFLICT DO NOTHING
ColumnValue
cveIdfrom statement.vulnerability.name
urlVEX document @id (e.g. https://dhi.docker.com/vex/vex-768b642...)
type"advisory"

2. Mitigation reference — CVEMetadataReferences (type=mitigation)

When a statement has status="affected" and a non-empty action_statement, the remediation guidance is stored as a second reference row. The title field carries the action text.

INSERT INTO "CVEMetadataReferences"
    ("uuid", "cveId", "source", "url", "type", "referenceSource", "title", "createdAt")
VALUES ($1, $2, 'docker', $3, 'mitigation', 'Docker', $4, $5)
ON CONFLICT DO NOTHING
-- where $3 = VEX document @id, $4 = action_statement text

Example title: "Resolving requires upstream dependency updates; security fixes are available in aircompressor-v3"

3. Affected product status — CVEAffected (containerType=adp)

For each statement that names a Docker image product (PURL starting with pkg:docker/dhi/ or pkg:oci/), a CVEAffected row records the image’s exploitability status:

INSERT INTO "CVEAffected" ("uuid","cveId","source","containerType","adpOrgId",
    "collectionURL","packageName","affectedHash","defaultStatus","createdAt")
VALUES ($1,$2,'docker','adp','docker','https://dhi.docker.com',$3,$4,$5,$6)
ON CONFLICT ("cveId","source","containerType","affectedHash") DO UPDATE SET
    "defaultStatus" = EXCLUDED."defaultStatus", ...
VEX statusdefaultStatus stored
not_affected"unaffected"
fixed"unaffected"
affected"affected"
under_investigation"unknown"

affectedHash = MD5 of ""|##|""|##|"https://dhi.docker.com"|##|<packageName>.

Deduplication

collectVexItems deduplicates by (cveID, docURL) using map[string]struct{} keyed on cveID + "\x00" + doc.ID. One writeVexItem call per unique pair writes all three rows (advisory ref, optional mitigation ref, optional CVEAffected) inside a single savepoint scope.

FK constraint and savepoints

CVEMetadataReferences.cveId and CVEAffected.cveId both have foreign keys into CVEMetadata. CVEs present in VEX documents but absent from the OSV files (no source=docker row in CVEMetadata) trigger FK violations. A PostgreSQL FK violation aborts the current transaction — all subsequent inserts in the same TX fail with SQLSTATE 25P02. To prevent cascade failures, every writeVexItem call is wrapped in a savepoint:

tx.Exec(ctx, "SAVEPOINT sp_vex")
n, err := writeVexItem(ctx, tx, item, doc.ID, logger)
if err != nil {
    tx.Exec(ctx, "ROLLBACK TO SAVEPOINT sp_vex")
    tx.Exec(ctx, "RELEASE SAVEPOINT sp_vex")
    // FK violation → log at DEBUG, skip this CVE, continue
    continue
}
tx.Exec(ctx, "RELEASE SAVEPOINT sp_vex")

Only the failing CVE is skipped; the transaction remains open for remaining CVEs.

Relationship to existing schema

CVEMetadata (cveId, source="docker")
    ├── CVEMetadataReferences          type="advisory"
    │       url  = VEX document @id
    ├── CVEMetadataReferences          type="mitigation"  (only when status=affected + action_statement)
    │       url   = VEX document @id
    │       title = action_statement text
    └── CVEAffected                    containerType="adp", adpOrgId="docker"
            packageName = pkg:docker/dhi/<image>  (or pkg:oci/<image>)
            defaultStatus = "affected" | "unaffected" | "unknown"

Error Alerting

Infrastructure

Both processors publish lifecycle events to SNS via the notify package. The pipeline is:

ECS task → notify.Publish → SNS topic (vdb-processor-events)
                                      → Lambda (slack_notifier)
                                              → Slack channel

SNS_TOPIC_ARN is injected via local.go_task_environment in terraform/go-schedules.tf, which is passed as the environment block of both ECS task modules. No additional Terraform configuration is needed — both modules use the same shared environment map as every other go processor.

Events published

EventMethodTriggers Slack alert
task.startednotifier.Started(name)No
task.completednotifier.Completed(name, stats)No
task.errorednotifier.Errored(name, stats, err)Yes
task.no_worknotifier.NoWork(name, reason)No
task.overtimeautomatic (background goroutine)Yes

Slack message contents

The SNS message published on task.errored includes:

{
  "event": "task.errored",
  "processor": "docker-git-processor",
  "timestamp": "2026-04-01T05:02:00Z",
  "region": "ap-southeast-2",
  "error": "<error text>\n<accumulated per-file error details>",
  "stats": { "stored": 800, "unchanged": 0, "errored": 12, "duration": "360.1s" },
  "deeplinks": {
    "cloudwatch": "https://ap-southeast-2.console.aws.amazon.com/cloudwatch/...",
    "ecs": "https://ap-southeast-2.console.aws.amazon.com/ecs/v2/clusters/vdb-scheduler/tasks?..."
  }
}

The Lambda formats this into a Slack message with the processor name, error summary, stats block, and clickable links to CloudWatch logs and the ECS task list.

Overtime monitoring

If EXPECTED_DURATION_MINUTES is set (30 for OSV, 20 for VEX), the notifier starts a background goroutine that fires task.overtime if the processor has not completed within that window. On overtime the context is cancelled, the processor stops processing new files at the next soft-deadline check, skips the tracker update (allowing the next run to resume from scratch), and exits 0. The overtime Slack alert includes expected and elapsed fields.

EXPECTED_DURATION_MINUTES=30  →  soft deadline = now + 20min   (docker-git-processor)
EXPECTED_DURATION_MINUTES=20  →  soft deadline = now + 10min   (enrich-docker-vex)
                               →  overtime SNS event at the full budget
                               →  context cancel → graceful exit

Both binaries compute the margin as a fixed EXPECTED_DURATION_MINUTES - 10 rather than via internal/rundeadline. On the VEX task’s 20-minute budget that reserves half the run — conservative, not incorrect.

One caveat on the closing event

enrich-docker-vex publishes task.completed unconditionally and then, if any file failed, also publishes task.errored before exiting 1. A run with per-file failures therefore emits both events. docker-git-processor emits one or the other.

Per-item error accumulation

notifier.RecordError(msg) accumulates detail strings during processing. At the end of the loop, if any file failed to parse or enrich, notifier.Errored is called instead of notifier.Completed, and all accumulated messages are appended to the Slack alert:

"error": "3 files failed to process\nread vex/nginx/dhi-nginx.vex.json: permission denied\n..."

Local runs (no SNS)

When SNS_TOPIC_ARN is not set, notify.New returns a no-op notifier. All notify calls are silently ignored. No AWS credentials are needed for local development.

CloudWatch log group

Each processor writes structured JSON logs to:

ProcessorLog group
docker-git-processor/ecs/vdb-scheduler/docker-git-processor
enrich-docker-vex/ecs/vdb-scheduler/enrich-docker-vex

The CloudWatch deeplink in every Slack message points directly to the relevant log group.


Data Mapping — OSV to CVEMetadata

OSV FieldCVEMetadata / child tableNotes
idcveIdalready a CVE ID in DHI files
"docker"sourceconstant
aliasesCVEAliasGHSA and other aliases
summaryCVEDescription (type=summary)
detailsCVEDescription (type=description)
publisheddatePublishedUnix seconds
modifieddateUpdatedUnix seconds
severity[].scoreCVEMetricCVSS v2/v3/v4
affected[].package.ecosystemCVEAffected.collectionURL"DHI"https://dhi.docker.com
affected[].package.nameCVEAffected.packageName
affected[].versions / rangesCVEAffectedVersion
references[].urlCVEMetadataReferencestype derived from references[].type
SHA1 of file contentsourceFileHashper-file incremental resume key

Shared-table side effects

Beyond the CVE tables above, the shared store path (processor.storeAdvisorydb.EnrichAffectedWithDependency) also writes, for every affected[] entry: Dependency, DependencyRegistry, PackageVersion, PackageVersionCVE, and GitHubRepoDependency when the advisory names a GitHub repository. CVEAlias edges are always written through db.InsertAliases — including the same-cveId cross-source edges that link this docker row to every other source holding the same CVE — even when the advisory lists no aliases. One additional CVEMetric per record is derived locally by cvss.DeriveV4FromDescription and stored with containerType="vulnetix", metricType="cvssV4_0".

AI enrichment

docker-git-processor passes an aienrich.Enricher into processor.Run, so on ECS (where PIX_INFERENCE_ENABLED and the AI-Gateway credentials are set) each stored record gets the shared post-batch passes — affected routines, ATT&CK mapping, CWE inference, TreeSitter queries — written outside the batch transaction. Local just runs are inference-free unless AIENRICH=true.


Architecture Diagram

graph TD subgraph "Container Image (shared docker-git-data stage)" REPO[/data/docker-hardened-image-advisories
git clone --depth=1 at image build time] OSV_DIR[osv/ — CVE-*.json per package
OSV 1.5.0] VEX_DIR[vex/ — dhi-*.vex.json per image
OpenVEX 0.2.0] REPO --> OSV_DIR REPO --> VEX_DIR end subgraph "docker-git-processor" GIT1[PullOrClone → incremental pull] TRACKER1[GetTracker docker_advisory] WALK1[WalkAdvisories
CVE-*.json recursive] PIPE[processor.Run
batch=200] PARSE[ParseJSON → MapAdvisory] STORE1[UpsertCVEMetadata
+ CVEDescription
+ CVEMetric
+ CVEAffected
+ CVEAlias
+ CVEMetadataReferences] NOTIFY1[notify.Completed
or notify.Errored] TRACKER_UP1[UpsertTracker docker_advisory] end subgraph "enrich-docker-vex" GIT2[PullOrClone → incremental pull] TRACKER2[GetTracker docker_vex] WALK2[WalkAdvisories
*.vex.json recursive] PARSE2[json.Unmarshal
openvex.Document] DEDUP[deduplicate cveID+docURL] SP[SAVEPOINT per CVE
skip FK violations] REF[InsertReferences
CVEMetadataReferences
type=advisory] NOTIFY2[notify.Completed
or notify.Errored] TRACKER_UP2[UpsertTracker docker_vex] end subgraph "Alert Pipeline" SNS[SNS: vdb-processor-events] LAMBDA[Lambda: slack_notifier] SLACK[Slack: #alerts
task.errored + task.overtime only] end subgraph "Database" CVEMETA[(CVEMetadata
source=docker)] REFS[(CVEMetadataReferences
source=docker)] TRACKER[(BulkDataDumpTracker
docker_advisory / docker_vex)] end OSV_DIR --> GIT1 --> TRACKER1 --> WALK1 --> PIPE --> PARSE --> STORE1 --> TRACKER_UP1 --> NOTIFY1 VEX_DIR --> GIT2 --> TRACKER2 --> WALK2 --> PARSE2 --> DEDUP --> SP --> REF --> TRACKER_UP2 --> NOTIFY2 NOTIFY1 --> SNS NOTIFY2 --> SNS SNS --> LAMBDA --> SLACK STORE1 --> CVEMETA STORE1 --> REFS REF --> REFS TRACKER_UP1 --> TRACKER TRACKER_UP2 --> TRACKER

Deployment

flowchart TD GHA[GitHub Actions
go-ecr-deploy.yml] -->|ARM64 image push| ECR[ECR: go-processors
go-docker-git-processor-sha-xxx
go-enrich-docker-vex-sha-xxx] ECR --> TASKDEF1[ECS Task Definition
go-docker-git-processor] ECR --> TASKDEF2[ECS Task Definition
go-enrich-docker-vex] TASKDEF1 --> EB1[EventBridge Schedule
cron 0 5 * * ? *
05:00 UTC daily] TASKDEF2 --> EB2[EventBridge Schedule
cron 30 5 * * ? *
05:30 UTC daily] EB1 -->|trigger| FARGATE1[ECS Fargate ARM64
docker-git-processor
256 CPU / 512 MB
EXPECTED_DURATION=30m] EB2 -->|trigger| FARGATE2[ECS Fargate ARM64
enrich-docker-vex
256 CPU / 512 MB
EXPECTED_DURATION=20m] FARGATE1 -->|task.started/completed/errored/overtime| SNS[SNS: vdb-processor-events] FARGATE2 -->|task.started/completed/errored/overtime| SNS SNS --> LAMBDA[Lambda: slack_notifier] LAMBDA -->|errored + overtime only| SLACK[Slack alert] FARGATE1 --> CW1[CloudWatch
/ecs/vdb-scheduler/docker-git-processor] FARGATE2 --> CW2[CloudWatch
/ecs/vdb-scheduler/enrich-docker-vex] FARGATE1 --> RDS[(RDS PostgreSQL
via cf-hyperdrive proxy)] FARGATE2 --> RDS

Schedule ordering

docker-git-processor runs first (05:00 UTC) to ensure CVEMetadata source=docker rows exist before enrich-docker-vex (05:30 UTC) inserts CVEMetadataReferences referencing them. The 30-minute gap covers the expected ~6 minute OSV processing window (945 files, based on production timing).


Incremental Processing

Both processors use git-diff incremental strategy on subsequent daily runs:

  1. PullOrClone fetches latest commits; returns new HEAD SHA
  2. GetTracker retrieves the previously stored SHA
  3. If SHA matches and --force not set → notifier.NoWork → exit 0
  4. DetectChangedFiles collects the files touched in the last processor.IncrementalSince window (3 days) — it is a time-window git log --since=… --name-only --diff-filter=ADM, not a <tracker-sha>..HEAD diff, so the tracker SHA only decides whether to run, never which files to consider
  5. FilterPaths restricts processing to that changed set. When the window yields nothing (or history is too shallow to diff) DetectChangedFiles returns nil and the run falls back to a full scan
  6. On completion, UpsertTracker stores the new SHA

At container build time, git clone --depth=1 bakes the initial state. In ECS, PullOrClone pulls at processor.IncrementalDepth (depth 1); on such a shallow clone git log --name-only reports the whole tree, which is why the practical behaviour is a full scan guarded by the per-file sourceFileHash skip set rather than a true incremental diff. If the tracker update is skipped (overtime/context cancel), the next run repeats the scan.


Flags

FlagDefaultBinaryDescription
--forcefalsebothReprocess all files regardless of git SHA or file hash
--batch-size200docker-git-processorFiles per transaction batch
--repo/data/docker-hardened-image-advisoriesbothPath to advisory repo clone
--data-dir"" (auto: repo/osv or repo/vex)bothOverride data directory

Files

PathPurpose
cmd/docker-git-processor/main.goOSV processor entry point
cmd/enrich-docker-vex/main.goVEX enrichment entry point
internal/openvex/types.goOpenVEX 0.2.0 type definitions
internal/notify/notify.goSNS lifecycle events + overtime monitoring
internal/osv/mapper.goOSV mapper — "DHI" ecosystem entry
internal/processor/pipeline.goShared Run() pipeline (reused by docker-git-processor)
internal/db/cvereference.goInsertReferences() — bulk insert with ON CONFLICT DO NOTHING
terraform/go-schedules.tfECS task modules + go_task_environment with SNS_TOPIC_ARN

Local Development

# OSV processor — go run against local advisory clone (uses .env)
cd /path/to/vdb-manager
set -a; source .env; set +a
cd scripts/go-processors && go run ./cmd/docker-git-processor \
  --repo=/home/chris/GitHub/docker-hardened-image-advisories --force=true

# OSV processor — against production DB
set -a; source .env.production; set +a
cd scripts/go-processors && go run ./cmd/docker-git-processor \
  --repo=/home/chris/GitHub/docker-hardened-image-advisories --force=true

# VEX enrichment — go run against local advisory clone
set -a; source .env.production; set +a
cd scripts/go-processors && go run ./cmd/enrich-docker-vex \
  --repo=/home/chris/GitHub/docker-hardened-image-advisories --force=true

# Container builds (mirrors ECS ARM64 image)
just docker-git-processor
just enrich-docker-vex

# Verify OSV data
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM \"CVEMetadata\" WHERE source = 'docker';"

# Verify VEX references (advisory URLs only)
psql "$DATABASE_URL" -c "
  SELECT COUNT(*) AS vex_refs,
         COUNT(DISTINCT \"cveId\") AS cves_with_vex
  FROM \"CVEMetadataReferences\"
  WHERE source = 'docker' AND url LIKE 'https://dhi.docker.com/vex/%';
"

# Reference type breakdown
psql "$DATABASE_URL" -c "
  SELECT type, COUNT(*) AS n
  FROM \"CVEMetadataReferences\"
  WHERE source = 'docker' AND \"referenceSource\" = 'Docker'
  GROUP BY type ORDER BY n DESC;
"

# Check tracker state
psql "$DATABASE_URL" -c "
  SELECT source, \"sha256\", \"totalCVEs\", to_timestamp(\"lastProcessedAt\" / 1000) AS last_run
  FROM \"BulkDataDumpTracker\"
  WHERE source IN ('docker_advisory', 'docker_vex');
"

S3 Persistence — per binary

The generated compliance block at the end of this page derives its quarantine prefix from this page’s filename (docker-processor). The prefix the code actually writes is the cmd/ directory name, docker-git-processor — the paths below are authoritative.

docker-git-processor is compliant. It builds an uploader from S3_BUCKET_NAME via s3client.NewFromEnv and passes s3client.PipelineHooks(uploader, "docker", "docker-git-processor") into processor.Run, so every OSV file is archived on success and quarantined on failure (both after the batch transaction commits):

  • Archive: docker/files/{sha256}/{repo-relative path}
  • Quarantine: failed-feeds/docker-git-processor/{YYYY-MM-DD}/{reason}/{path}
  • Reasons in use: parse-error, store-error

Uploads are skipped only when S3_BUCKET_NAME is unset (local development).

enrich-docker-vex does not yet archive or quarantine its VEX payloads — see the S3 Persistence Contract and the compliance matrix.

S3 Persistence

  • Archive path: docker/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/docker-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Failure reasons emitted: parse-error, store-error

Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).

flowchart LR SRC[Source feed] --> PROC[docker-processor] PROC -->|success| ARCHIVE[("S3: docker/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/docker-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.