Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.6.0-rc.13] β€” 2026-08-08

### Security

- Pinned `js-yaml` to 3.15.1 in the e2e workspace (override; transitive dependency) for [GHSA-5p4m-2wfm-xmqj](https://github.com/advisories/GHSA-5p4m-2wfm-xmqj) (CVE-2026-59870 backport gap: quadratic CPU consumption in `!!omap` resolution).
- Pinned `nanoid` to 3.3.18 (override; transitive dependency of `postcss` in the root, app, apps/demo, apps/web, and ui workspaces, and of `artillery` in the e2e workspace) for [GHSA-2v37-7h3g-55p8](https://github.com/advisories/GHSA-2v37-7h3g-55p8) (CVE-2026-67213) and, in the e2e workspace which was still on 3.3.12, also [GHSA-28wg-ghj8-5hjv](https://github.com/advisories/GHSA-28wg-ghj8-5hjv) (CVE-2026-67214).
- **`node:24-alpine` base image bumped to Node 24.19.0**, replacing the Node 24.18.0 image shipped in rc.12. Picks up Node's July 29 2026 security release, fixing 3 HIGH (CVE-2026-56846, CVE-2026-56848, CVE-2026-58043) + 5 MEDIUM CVEs that landed in 24.18.1 ([#682](https://github.com/CodesWhat/drydock/pull/682)).
- **Vendored `aquasec/trivy` build-stage pin bumped from 0.72.0 to 0.73.0**, resolving 4 HIGH / 6 MEDIUM CVEs in its vendored Go dependencies: go-git ([CVE-2026-71556](https://github.com/advisories/CVE-2026-71556)), `x/text` ([CVE-2026-56852](https://github.com/advisories/CVE-2026-56852)), grpc ([GHSA-hrxh-6v49-42gf](https://github.com/advisories/GHSA-hrxh-6v49-42gf)), oras-go ([CVE-2026-50151](https://github.com/advisories/CVE-2026-50151), [CVE-2026-50163](https://github.com/advisories/CVE-2026-50163)), and the Go stdlib ([CVE-2026-39822](https://github.com/advisories/CVE-2026-39822)) ([#682](https://github.com/CodesWhat/drydock/pull/682)).

### Fixed

- **Icon bundle no longer silently drops referenced icons at image build time** ([#683](https://github.com/CodesWhat/drydock/pull/683)). The bundle is regenerated from the locked `@iconify-json` packages during every Docker image build, but the extractor only looked up plain icon entries β€” `lucide:history` (the Audit navigation icon in the Lucide icon theme) became an alias in lucide 1.2.121 and vanished from shipped images, rendering blank. The extractor now resolves alias chains, and references that never existed in the locked collections are fixed: `iconoir:history` β†’ `iconoir:clock-rotate-right`, `iconoir:gitlab` β†’ `iconoir:gitlab-full`, `iconoir:stack` β†’ `iconoir:multiple-pages`, and the Font Awesome brand glyphs (GitHub/GitLab/Google/Microsoft registry icons) gained the previously missing `@iconify-json/fa6-brands` package. A new test asserts every icon referenced in `icons.ts` exists in the committed bundle.
- **Star History chart is now self-hosted** ([#671](https://github.com/CodesWhat/drydock/issues/671)). The homepage card and README embed rendered a broken image after api.star-history.com's global outage (their GitHub tokens rate-limited; starchart.cc also failing). A new `/api/star-history` route on the website fetches stargazer timestamps from the GitHub API server-side (optional `GITHUB_TOKEN`, edge-cached six hours with stale-while-revalidate, short-lived fallback SVG on fetch failure) and renders the chart in the site's own palette for both themes; the README uses a `<picture>` element with theme-matched variants. No third-party chart service remains in the path, and `api.star-history.com` is dropped from the site's CSP `img-src`.
- **Digest-update comparison no longer anchors on an arbitrary `RepoDigests[0]` entry** ([#669](https://github.com/CodesWhat/drydock/issues/669)). A local Docker image can carry multiple `repo@digest` entries for one Image ID (pull/retag accumulation, no ordering guarantee); `getRepoDigest` blindly took index 0, so a stale or foreign-repo entry landing first anchored the whole digest-update pipeline to the wrong manifest and produced a persistent digest-update false positive that survived applying the update. `getOrderedRepoDigests` (`app/watchers/providers/docker/docker-helpers.ts`) now returns every RepoDigests entry whose repo component matches the container's own image reference, ordered, falling back to the full list only when nothing matches; the container model gained an optional `image.digest.repoDigests` field carrying that ordered list, re-derived from the live Docker image inspect on every discovery/refresh cycle. `handleDigestWatch` (`app/watchers/providers/docker/image-comparison.ts`) now walks that candidate list β€” a cheap raw-value check first, then a normalize-and-compare registry call per remaining candidate, skipping anchors whose manifest lookup fails β€” and re-anchors `digest.repo` to whichever candidate actually matched, so a store already poisoned with a stale `digest.repo` self-heals on its own. A genuine same-tag republish (no candidate matches) still flags an update exactly as before; if every candidate fails to normalize, the failure now propagates instead of being silently coerced into a false "no update".

## [1.6.0-rc.12] β€” 2026-08-04

### Changed
Expand Down Expand Up @@ -2326,7 +2341,8 @@ Remaining upstream-only changes (not ported β€” not applicable to drydock):
| Fix codeberg tests | Covered by drydock's own tests |
| Update changelog | Upstream-specific |

[Unreleased]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.12...HEAD
[Unreleased]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.13...HEAD
[1.6.0-rc.13]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.12...v1.6.0-rc.13
[1.6.0-rc.12]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.11...v1.6.0-rc.12
[1.6.0-rc.11]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.10...v1.6.0-rc.11
[1.6.0-rc.10]: https://github.com/CodesWhat/drydock/compare/v1.6.0-rc.9...v1.6.0-rc.10
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# checkov:skip=CKV_DOCKER_3: entrypoint uses su-exec for runtime privilege drop
# Trivy publishes a multi-arch image and installs its binary at this path.
# Pin the image index so every target architecture resolves reproducibly.
FROM aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f AS trivy-bin
FROM aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c AS trivy-bin

# Common Stage
FROM node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS base
FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS base
Comment on lines +4 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 2 'aquasec/trivy@sha256:' Dockerfile app/configuration

Repository: CodesWhat/drydock

Length of output: 2149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== configuration symbol usages =="
rg -n -C 2 'DEFAULT_TRIVY_WORKER_IMAGE|DEFAULT_SECURITY_\w+WORKER|trivyWorker|trivy.*worker|workerImage' app packages .github Dockerfile 2>/dev/null || true

echo "== files mentioning trivy =="
rg -n -i 'trivy' app .github Dockerfile package.json 2>/dev/null || true

Repository: CodesWhat/drydock

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git status/stat =="
git status --short
git diff --stat

echo "== Dockerfile relevant lines =="
sed -n '1,20p' Dockerfile

echo "== app/configuration relevant lines =="
sed -n '1,120p' app/configuration/index.ts
sed -n '680,820p' app/configuration/index.test.ts
sed -n '1,35p' app/configuration/dockerfile-defaults.test.ts

Repository: CodesWhat/drydock

Length of output: 12478


🌐 Web query:

aquasec trivy image Dockerfile pin SHA 7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c sha256 cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f

πŸ’‘ Result:

The SHA-256 digests you provided correspond to specific versions of the official Aquasec Trivy container image: - The digest 7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c is the index digest for Trivy version 0.73.0 [1][2]. - The digest cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f is the index digest for Trivy version 0.72.0 [3]. To pin a container image in a Dockerfile by its SHA-256 digest, you must use the @ symbol followed by the full digest instead of a tag [4][5]. This ensures the build always uses the exact same image version [6]. For example, to pin your Dockerfile to Trivy v0.73.0, use the following syntax: FROM aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c Trivy itself supports referencing images by digest in its CLI commands as well, using the same syntax [4][5]. For example: trivy image aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== release-cut workflow Trivy image context =="
sed -n '680,715p' .github/workflows/release-cut.yml

echo "== release-cut retry test context =="
sed -n '265,290p' .github/tests/release-cut-retry-workflow.test.ts

echo "== exact pinned Trivy occurrences in tracked files =="
python3 - <<'PY'
from pathlib import Path
pins = [
    "aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c",
    "aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f",
]
for path in Path('.').rglob('*'):
    if path.is_file() and not path.startswith(('.git', 'node_modules', 'dist', 'build')):
        try:
            text = path.read_text(errors='ignore')
        except Exception:
            continue
        for pin in pins:
            if pin in text:
                print(f"{path}: {pin}")
PY

Repository: CodesWhat/drydock

Length of output: 2980


Synchronize the Trivy image pin across config, release, and tests.

The Dockerfile now builds from aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c, while DEFAULT_TRIVY_WORKER_IMAGE, .github/workflows/release-cut.yml, and matching tests still use aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f. Update those references if the new digest is the intended release pin. If the release and runtime images are intentionally independent, add a comment documenting that contract.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 4 - 7, Synchronize the Trivy image digest used by
DEFAULT_TRIVY_WORKER_IMAGE, .github/workflows/release-cut.yml, and the
corresponding tests with the Dockerfile’s aquasec/trivy pin when they represent
the same release image; otherwise document the intentional independence between
release and runtime pins with a comment.

WORKDIR /home/node/app

LABEL maintainer="CodesWhat"
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</div>

<p align="center">
<a href="https://github.com/CodesWhat/drydock/releases"><img src="https://img.shields.io/badge/version-1.6.0--rc.12-blue" alt="Version"></a>
<a href="https://github.com/CodesWhat/drydock/releases"><img src="https://img.shields.io/badge/version-1.6.0--rc.13-blue" alt="Version"></a>
<a href="https://github.com/orgs/CodesWhat/packages/container/package/drydock"><img src="https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-informational?logo=linux&logoColor=white" alt="Multi-arch"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-C9A227" alt="License AGPL-3.0"></a>
<br>
Expand Down Expand Up @@ -178,6 +178,17 @@ See the [Quick Start guide](https://getdrydock.com/docs/quickstart) for Docker C
<h2 align="center" id="recent-updates">πŸ†• Recent Updates</h2>

<details open>
<summary><strong>v1.6.0-rc.13 highlights</strong></summary>

- **Digest comparison anchors on repo-matched candidates** β€” `getOrderedRepoDigests` filters a container's `RepoDigests` to entries whose repo component matches its own image reference before comparing, instead of trusting an arbitrary index-0 entry; a store already poisoned with a stale anchor self-heals. ([#670](https://github.com/CodesWhat/drydock/pull/670))
- **`nanoid` pinned to 3.3.18** across the root, app, apps/demo, apps/web, ui, and e2e workspaces (transitive override) for CVE-2026-67213 and, in e2e, CVE-2026-67214. ([#673](https://github.com/CodesWhat/drydock/pull/673))
- **Star History chart is self-hosted** β€” a new same-origin `/api/star-history` route replaces the third-party embed that went down in a global outage, edge-cached with a fallback SVG on fetch failure. ([#672](https://github.com/CodesWhat/drydock/pull/672))
- **Base-image CVE sweep** β€” `node:24-alpine` bumped to Node 24.19.0 and the vendored `aquasec/trivy` build-stage pin bumped to 0.73.0, clearing HIGH/MEDIUM CVEs in both. ([#682](https://github.com/CodesWhat/drydock/pull/682))
- **Icon bundle alias resolution** β€” the build-time icon extractor follows iconify alias chains and gains the missing Font Awesome brands collection, so renamed icons (like the Lucide-theme Audit icon) no longer ship as blank glyphs; a guard test pins every referenced icon into the bundle. ([#683](https://github.com/CodesWhat/drydock/pull/683))

</details>

<details>
<summary><strong>v1.6.0-rc.12 highlights</strong></summary>

- **Security dependency refresh** β€” `brace-expansion` 5.0.9 (app/UI/e2e, CVE-2026-69152), `ip-address` 10.3.1 (app runtime, CVE-2026-54272/-69192/-69198), and `fast-uri` 4.1.2 (app/UI, CVE-2026-18446). ([#659](https://github.com/CodesWhat/drydock/pull/659))
Expand Down Expand Up @@ -409,8 +420,11 @@ High-level themes only β€” see [CHANGELOG.md](CHANGELOG.md) for per-release deta
<a id="star-history"></a>

<div align="center">
<a href="https://star-history.com/#CodesWhat/drydock&Date">
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=CodesWhat/drydock&type=Date" />
<a href="https://github.com/CodesWhat/drydock/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://getdrydock.com/api/star-history?theme=dark">
<img alt="Star History Chart" src="https://getdrydock.com/api/star-history?theme=light" />
</picture>
</a>
</div>

Expand Down
10 changes: 9 additions & 1 deletion app/configuration/dockerfile-defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ describe('Dockerfile release defaults', () => {
expect(dockerfile).toMatch(/FROM base AS release\s+ENV DD_LOG_FORMAT=text/u);
});

test('release image builds from the digest-pinned Node base image', () => {
const dockerfile = fs.readFileSync(new URL('../../Dockerfile', import.meta.url), 'utf8');

expect(dockerfile).toContain(
'FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS base',
);
});

test('release image copies Trivy from the digest-pinned multi-arch image', () => {
const dockerfile = fs.readFileSync(new URL('../../Dockerfile', import.meta.url), 'utf8');

expect(dockerfile).toContain(
'FROM aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f AS trivy-bin',
'FROM aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c AS trivy-bin',
);
expect(dockerfile).toContain('COPY --from=trivy-bin /usr/local/bin/trivy /usr/local/bin/trivy');
expect(dockerfile).not.toContain('alpine/edge/testing');
Expand Down
8 changes: 8 additions & 0 deletions app/model/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export interface ContainerImage {
watch: boolean;
value?: string;
repo?: string;
// Ordered candidate digests whose repo component matches this container's
// own image reference, re-derived from the live Docker image inspect on
// every discovery/refresh cycle (#669). Optional/additive β€” old stored
// containers lack it and fall back to single-anchor comparison using
// `repo` alone. See `getOrderedRepoDigests` in docker-helpers.ts and
// `handleDigestWatch` in image-comparison.ts.
repoDigests?: string[];
};
// True when the live Docker image inspect had no RepoDigests (built locally
// or `docker load`ed) β€” derived once at discovery/refresh time, independent
Expand Down Expand Up @@ -437,6 +444,7 @@ const schema = joi.object({
watch: joi.boolean().default(false),
value: joi.string(),
repo: joi.string(),
repoDigests: joi.array().items(joi.string()),
})
.required(),
isLocalImage: joi.boolean(),
Expand Down
6 changes: 3 additions & 3 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"@babel/core": "7.29.7",
"form-data": "4.0.6",
"protobufjs": "7.6.5",
"undici": "8.9.0"
"undici": "8.9.0",
"nanoid": "3.3.18"
},
"devDependencies": {
"@fast-check/vitest": "0.4.1",
Expand Down
75 changes: 75 additions & 0 deletions app/watchers/providers/docker/docker-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
getFirstConfigNumber,
getFirstConfigString,
getImageForRegistryLookup,
getImageReferenceCandidates,
getImageReferenceCandidatesFromPattern,
getImgsetSpecificity,
getInspectValueByPath,
getOldContainers,
getOrderedRepoDigests,
getRawContainerName,
getRepoDigest,
getResolvedImgsetConfiguration,
Expand Down Expand Up @@ -298,6 +300,79 @@ describe('docker helper extraction module', () => {
expect(isContainerToWatch('', true)).toBe(true);
});

describe('getOrderedRepoDigests (#669)', () => {
test('returns undefined when RepoDigests is empty or undefined', () => {
expect(getOrderedRepoDigests({ RepoDigests: [] })).toBeUndefined();
expect(getOrderedRepoDigests({} as any)).toBeUndefined();
});

test('returns the full list, in order, when no reference candidates are supplied', () => {
expect(
getOrderedRepoDigests({
RepoDigests: ['acme/service@sha256:one', 'other/service@sha256:two'],
}),
).toEqual(['sha256:one', 'sha256:two']);
});

test('prefers the entry matching the container repo when a foreign-repo entry comes first (#669)', () => {
const referenceCandidates = getImageReferenceCandidates('acme/service', 'ghcr.io');

const result = getOrderedRepoDigests(
{
RepoDigests: ['unrelated/other-image@sha256:foreign', 'ghcr.io/acme/service@sha256:mine'],
},
referenceCandidates,
);

expect(result).toEqual(['sha256:mine']);
});

test('returns every matching entry in original order when several entries match', () => {
const referenceCandidates = getImageReferenceCandidates('acme/service', 'ghcr.io');

const result = getOrderedRepoDigests(
{
RepoDigests: [
'ghcr.io/acme/service@sha256:first',
'unrelated/other-image@sha256:foreign',
'ghcr.io/acme/service@sha256:second',
],
},
referenceCandidates,
);

expect(result).toEqual(['sha256:first', 'sha256:second']);
});

test('falls back to the full list, in original order, when no entry matches any candidate', () => {
const referenceCandidates = getImageReferenceCandidates('acme/service', 'ghcr.io');

const result = getOrderedRepoDigests(
{
RepoDigests: ['unrelated/other-image@sha256:foreign', 'another/one@sha256:also-foreign'],
},
referenceCandidates,
);

expect(result).toEqual(['sha256:foreign', 'sha256:also-foreign']);
});

test('ignores malformed RepoDigests entries lacking an "@" separator', () => {
expect(getOrderedRepoDigests({ RepoDigests: ['malformed-entry'] })).toBeUndefined();
});

test('ignores entries with an empty repo or digest component', () => {
expect(getOrderedRepoDigests({ RepoDigests: ['acme/service@'] })).toBeUndefined();
expect(getOrderedRepoDigests({ RepoDigests: ['@sha256:orphan'] })).toBeUndefined();
// A well-formed entry still wins over malformed siblings.
expect(
getOrderedRepoDigests({
RepoDigests: ['acme/service@', '@sha256:orphan', 'acme/service@sha256:good'],
}),
).toEqual(['sha256:good']);
});
});

test('digest watch defaults require a meaningful current tag', () => {
expect(
isDigestToWatch(
Expand Down
Loading
Loading