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:
| Binary | Directory | Format | Output |
|---|---|---|---|
docker-git-processor | osv/ | OSV 1.5.0 JSON | CVEMetadata + child tables (source=docker) |
enrich-docker-vex | vex/ | OpenVEX 0.2.0 JSON | CVEMetadataReferences (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
CVEAffectedrows (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
| Column | Value |
|---|---|
cveId | from statement.vulnerability.name |
url | VEX 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 status | defaultStatus 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
| Event | Method | Triggers Slack alert |
|---|---|---|
task.started | notifier.Started(name) | No |
task.completed | notifier.Completed(name, stats) | No |
task.errored | notifier.Errored(name, stats, err) | Yes |
task.no_work | notifier.NoWork(name, reason) | No |
task.overtime | automatic (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:
| Processor | Log 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 Field | CVEMetadata / child table | Notes |
|---|---|---|
id | cveId | already a CVE ID in DHI files |
"docker" | source | constant |
aliases | CVEAlias | GHSA and other aliases |
summary | CVEDescription (type=summary) | |
details | CVEDescription (type=description) | |
published | datePublished | Unix seconds |
modified | dateUpdated | Unix seconds |
severity[].score | CVEMetric | CVSS v2/v3/v4 |
affected[].package.ecosystem | CVEAffected.collectionURL | "DHI" → https://dhi.docker.com |
affected[].package.name | CVEAffected.packageName | |
affected[].versions / ranges | CVEAffectedVersion | |
references[].url | CVEMetadataReferences | type derived from references[].type |
| SHA1 of file content | sourceFileHash | per-file incremental resume key |
Shared-table side effects
Beyond the CVE tables above, the shared store path
(processor.storeAdvisory → db.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
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
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:
PullOrClonefetches latest commits; returns new HEAD SHAGetTrackerretrieves the previously stored SHA- If SHA matches and
--forcenot set →notifier.NoWork→ exit 0 DetectChangedFilescollects the files touched in the lastprocessor.IncrementalSincewindow (3 days) — it is a time-windowgit log --since=… --name-only --diff-filter=ADM, not a<tracker-sha>..HEADdiff, so the tracker SHA only decides whether to run, never which files to considerFilterPathsrestricts processing to that changed set. When the window yields nothing (or history is too shallow to diff)DetectChangedFilesreturns nil and the run falls back to a full scan- On completion,
UpsertTrackerstores 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
| Flag | Default | Binary | Description |
|---|---|---|---|
--force | false | both | Reprocess all files regardless of git SHA or file hash |
--batch-size | 200 | docker-git-processor | Files per transaction batch |
--repo | /data/docker-hardened-image-advisories | both | Path to advisory repo clone |
--data-dir | "" (auto: repo/osv or repo/vex) | both | Override data directory |
Files
| Path | Purpose |
|---|---|
cmd/docker-git-processor/main.go | OSV processor entry point |
cmd/enrich-docker-vex/main.go | VEX enrichment entry point |
internal/openvex/types.go | OpenVEX 0.2.0 type definitions |
internal/notify/notify.go | SNS lifecycle events + overtime monitoring |
internal/osv/mapper.go | OSV mapper — "DHI" ecosystem entry |
internal/processor/pipeline.go | Shared Run() pipeline (reused by docker-git-processor) |
internal/db/cvereference.go | InsertReferences() — bulk insert with ON CONFLICT DO NOTHING |
terraform/go-schedules.tf | ECS 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).
See the S3 Persistence Contract for the full reason taxonomy.