From 0962f865fd7cf82d343373e6a55ffb18f6ed735d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:56:31 +0000 Subject: [PATCH 01/21] Start squad working record for issue 363 --- specs/issue-363/log.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 specs/issue-363/log.md diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md new file mode 100644 index 0000000..519956a --- /dev/null +++ b/specs/issue-363/log.md @@ -0,0 +1,8 @@ +# Log: issue #363 + +One line per pipeline step: date and time of day (UTC), step, member, result. Lead decisions and escalations +are quoted in full. + +| Date | Step | Member | Result | +| ---- | ---- | ------ | ------ | +| 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | From 4f2341b40bf76319eb241ddb0fccddaa86508a23 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:01:37 +0000 Subject: [PATCH 02/21] Plan for issue 363 --- ...-is-validated-as-a-namespace-at-startup.md | 45 +++++ docs/decisions/README.md | 1 + specs/issue-363/log.md | 1 + specs/issue-363/plan.md | 163 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md create mode 100644 specs/issue-363/plan.md diff --git a/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md new file mode 100644 index 0000000..055e201 --- /dev/null +++ b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md @@ -0,0 +1,45 @@ +# 0018: The Docker Hub user name is validated as a Docker Hub namespace at startup + +- **Status:** Proposed +- **Date:** 2026-11-05 +- **Area:** Registries +- **Source:** Issue #363 +- **Supersedes:** — + +## Context + +`DockerUpdateGuard:DockerHub:UserName` is the namespace of the Docker Hub account listing: +`DockerHubClient.GetRepositoriesAsync` builds `v2/repositories/{name}/` with `Uri.EscapeDataString`, which leaves `.` +alone, and `System.Uri` removes a `.` segment and resolves a `..` segment, so `.` or `..` sent the listing, with the +account's bearer token, to another path of the Docker Hub web API. [0014](0014-registry-tag-names-and-digests-are-validated-where-they-are-read.md) +(option 19) left this configured value to the startup validation instead of a check in the request builder. The same +value is also the login identifier of the Docker Hub web API, the user part of the registry token request's basic +credentials and the Docker Scout login name. + +## Options considered + +1. **Reject only `.` and `..`** — closes the defect, but keeps values that are no Docker Hub name (`a/b`, `me:x`, + `a b`) and fail later at Docker Hub with a less clear message, and a `:` splits the basic credentials of the + registry token request. +2. **Hold the trimmed value to one path component of the Docker reference grammar, ASCII letters of either case, + bounded like a repository path** — every Docker Hub namespace fits it, no form of it is a dot segment, and a + configuration with an upper-case letter keeps starting. Chosen. +3. **Hold it to Docker's sign-up rule for a Docker ID (4 to 30 lower-case letters and digits)** — stricter than any + check the security property needs, and would stop deployments whose value has an upper-case letter or belongs to an + older account outside today's sign-up rule. +4. **Refuse a dot segment in `GetRepositoriesAsync` as well** — the validator also runs on every configuration reload + before the value is read, so the request builder sees no other value; [0014](0014-registry-tag-names-and-digests-are-validated-where-they-are-read.md) + rejected it for the same reason. + +## Decision + +A non-blank `DockerHub:UserName` must, after trimming, be one ASCII path component of the Docker reference grammar of +at most 255 characters; any other value fails the configuration validation, at startup and on reload. The rule and +its failure text are in [Registries](../areas/registries.md), *Configuration reload*. + +## Consequences + +- A configuration whose user name is an e-mail address or carries other characters stops starting; the failure names + the key and the expected form, never the value. Whether Docker Hub accepted such a value for login is not known; the + account listing never worked with it. +- Upper-case letters stay accepted although Docker Hub names are lower-case; how Docker Hub treats them is unchanged. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 06203bc..8c3a109 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -70,4 +70,5 @@ once in the [area document](../areas/README.md) of the record's area, and the re | [0015](0015-the-trivy-scan-target-follows-an-end-of-options-marker.md) | The Trivy scan target follows an end-of-options marker | Accepted | 2026-10-29 | | [0016](0016-the-bundled-trivy-skips-its-update-check-and-telemetry.md) | The bundled Trivy skips its update check and telemetry | Accepted | 2026-10-29 | | [0017](0017-product-prs-may-change-the-squad-settings-file.md) | Product PRs may change the squad settings file | Accepted | 2026-11-01 | +| [0018](0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md) | The Docker Hub user name is validated as a Docker Hub namespace at startup | Proposed | 2026-11-05 | diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 519956a..6327805 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -6,3 +6,4 @@ are quoted in full. | Date | Step | Member | Result | | ---- | ---- | ------ | ------ | | 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | +| 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · usage pending) | diff --git a/specs/issue-363/plan.md b/specs/issue-363/plan.md new file mode 100644 index 0000000..bbccf2f --- /dev/null +++ b/specs/issue-363/plan.md @@ -0,0 +1,163 @@ +# Plan: Validate `DockerHub:UserName` as a Docker Hub namespace at startup + +Source: Issue #363 +Status: Draft +Tier: security — the check confines which path of the Docker Hub web API the account listing, with the account's bearer token, reaches (a guard on a URL, `.squad/routing.md`, *Tiers*). +Security areas touched: Registry and Docker Hub calls + +## Problem / root cause + +`DockerHubAccountImageDiscoveryService.SynchronizeAccountImagesAsync` +(`src/DockerUpdateGuard/Images/DockerHubAccountImageDiscoveryService.cs:273-277`) passes the trimmed +`DockerHub:UserName` to `DockerHubClient.GetRepositoriesAsync`, which builds +`new Uri(apiBaseUri, $"v2/repositories/{Uri.EscapeDataString(accountName)}/?page_size=100")` +(`src/DockerUpdateGuard/DockerHub/DockerHubClient.cs:1246`). `EscapeDataString` leaves `.` alone and `System.Uri` +removes `.` and resolves `..` segments, so `..` requests `v2/?page_size=100` and `.` requests +`v2/repositories/?page_size=100`, authenticated. `DockerUpdateGuardOptionsValidator.ValidateDockerHubOptions` +(`src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs:93-119`) checks `Registry`, the ranges and +`ApiBaseUrl`, never `UserName`. + +## Verified facts + +| # | Claim | Evidence | Status | +| - | ----- | -------- | ------ | +| 1 | The account listing puts the user name into the path with `Uri.EscapeDataString` only | `DockerHubClient.cs:1246`; caller `DockerHubAccountImageDiscoveryService.cs:273-277` (trimmed value), the only caller (`grep GetRepositoriesAsync src/DockerUpdateGuard`) | confirmed | +| 2 | `.`/`..` in that segment address another path (issue claim) | `EscapeDataString` leaves `.` unescaped and `System.Uri` removes/resolves dot segments, also written as `%2E` (measured for #360: decision 0014, options 14-15; `docs/areas/registries.md`, *Request paths*) | confirmed | +| 3 | `UserName` is not validated today | `DockerUpdateGuardOptionsValidator.cs:93-119` | confirmed | +| 4 | The validator runs at startup and on every reload before a value is read | `ServiceCollectionExtensions.cs:53-56` (`IValidateOptions` + `ValidateOnStart`); `OptionsFactory.Create` runs every `IValidateOptions` when `IOptionsMonitor` rebuilds the value; `docs/areas/hosting.md`, *Configuration reload* | confirmed | +| 5 | Every consumer uses the value trimmed and treats blank as not set | `DockerHubClient.cs:682-685`, `1053-1060`; `DockerHubAccountImageDiscoveryService.cs:266-273`; `DockerScoutVulnerabilityProvider.cs:252`, `432`; `NavMenu.razor.cs:66`, `Dashboard.razor.cs:117` | confirmed | +| 6 | The values in repository configuration and test data fit the new rule | `appsettings.json` (`""`), `README.md` sample and `TestDevelopmentConfiguration.cs:24` (`dockerupdateguard`); validator tests' `CreateValidOptions` sets no user name | confirmed | +| 7 | `RegexOptions.IgnoreCase` and `ToLowerInvariant` fold `\u212A` (Kelvin sign) to `k`, which is why the grammar uses explicit `[a-zA-Z0-9]` classes | to verify: the `\u212Acme` row of AC1 fails red before the fix and passes after it, owner Tester (step 5) and Dev (step 6) | unverified | + +## Acceptance criteria + +- [ ] AC1: A non-blank `DockerHub:UserName` whose trimmed value (`string.Trim()`) is not one component of the + namespace grammar `\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z` of at most 255 characters fails + `DockerUpdateGuardOptionsValidator.Validate` with exactly + `'DockerUpdateGuard:DockerHub:UserName' must be a Docker Hub user name: letters and digits, optionally separated by '.', '_' or '-'`, + and no failure contains the configured value. Cases: `.`, `..`, ` .. `, `...`, `.acme`, `acme.`, `acme/..`, + `../acme`, `acme/x`, `%2e%2e`, `acme?x`, `acme#x`, `ac me`, `acme:pw`, `me@example.com`, `a___b`, `acme-`, + `\u212Acme` (Kelvin sign), `ac\u200Bme` (zero-width space), and 256 `a`. +- [ ] AC2: These values pass validation: `null`, `""`, `" "`, `acme`, `Acme`, ` acme `, `\u00A0acme\u00A0` + (no-break spaces, trimmed like the consumers trim), `my.org`, `my_org`, `my__org`, `my-org`, `my---org`, `a1b2`, + `a`, and 255 `a`. + +The C# escapes `\u212A`, `\u200B`, `\u00A0` are written as escapes in the test source. An AC1 row that lost its +backslash (`u212Acme`, `acu200Bme`) becomes a valid name, so the row fails red; the AC2 no-break-space row asserts +`char.IsWhiteSpace(value[0])` first, so a lost backslash there fails that guard. + +## Tests per criterion + +| AC | Test(s) | Test file | +| -- | ------- | --------- | +| AC1 | `DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFailure` (one `DataRow` per AC1 case; the failures are exactly the fixed text) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | +| AC2 | `DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess` (one `DataRow` per AC2 case) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | + +Both tests start from `CreateValidOptions()` with `CreateAtSignValidator()`, so AC1 asserts that the failures are +exactly the one fixed text (`CollectionAssert.AreEqual`), which also proves that no failure carries the value. The +255/256-character values are built in the test (`new string('a', n)`), not typed. + +## Approach + +1. `RegistryReferenceValidator` (`src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs`) gets a + `[GeneratedRegex(@"\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z", RegexOptions.CultureInvariant, 100)]` + `NamespaceExpression()` — explicit ASCII classes, no `IgnoreCase`, no lowercasing (fact 7) — and + `IsValidNamespace`, which checks non-empty, length `<= MaxRepositoryLength` (255) **before** the regex, then the + match. +2. `ValidateDockerHubOptions` adds, after the `Registry` rule: when `string.IsNullOrWhiteSpace(options.UserName) == + false` and `RegistryReferenceValidator.IsValidNamespace(options.UserName.Trim()) == false`, the AC1 failure. The + validator checks exactly the value the consumers send (fact 5). +3. No change to `DockerHubClient` (options considered, 4). + +Forms the consumer accepts (guard against bypasses): the value reaches `Uri.EscapeDataString` and then +`new Uri(base, relative)`. `EscapeDataString` escapes everything but `A-Za-z0-9-._~`, so `/`, `?`, `#`, `\`, `%`, +spaces and non-ASCII arrive encoded and stay inside the segment (`%2e` arrives as `%252e`). The only forms that leave +the segment are a segment exactly `.` or `..` (fact 2); `~` is not dangerous but outside the grammar. The grammar +rejects both dot forms (it starts and ends with `[a-zA-Z0-9]`), every character outside ASCII letters, digits, `.`, +`_`, `-`, and so every case of AC1. Surrounding white space is trimmed by validator and consumers alike, so ` .. ` +is checked as `..`. A value of more than 255 characters fails on its length before the regex runs, so the regex +timeout (100 ms) cannot throw out of the validator. + +## Options considered + +1. Reject only `.` and `..` — lost: keeps `/`, `:` and other non-names; a `:` splits the registry token request's + basic credentials (`DockerHubClient.cs:685`). +2. Namespace grammar, ASCII of either case, 255 bound — chosen. +3. Docker ID sign-up rule (4-30 lower-case letters and digits) — lost: stops configurations with upper-case letters + or older names for no security gain. +4. Also refuse a dot segment in `GetRepositoriesAsync` — lost: the validator covers startup and reload (fact 4), and + decision 0014 (option 19) already rejected the client check for this value. + +Recorded in `docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md`. + +## Affected projects and types + +| Project | Type / file | Change | +| ------- | ----------- | ------ | +| `DockerUpdateGuard` | `Images/Helper/RegistryReferenceValidator.cs` | `NamespaceExpression`, `IsValidNamespace` | +| `DockerUpdateGuard` | `Configuration/DockerUpdateGuardOptionsValidator.cs` | `UserName` rule in `ValidateDockerHubOptions` | + +## Signatures (for the Dev's skeleton) + +In `RegistryReferenceValidator` (internal static partial class), region *Regular expressions* and *Methods*: + +```csharp +private static partial Regex NamespaceExpression(); +public static bool IsValidNamespace([NotNullWhen(true)] string? namespaceName) +``` + +`ValidateDockerHubOptions(DockerHubOptions, List)` keeps its signature. + +## Test files + +- `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` + +Existing test call sites of a changed signature: none. Existing tests kept unchanged: none of them sets +`DockerHub:UserName` to a value outside the grammar (fact 6). + +Files no unit test can cover: none. + +## Areas + +- `docs/areas/registries.md`: the validation bullet of *Configuration reload* gains the `UserName` rule; the + *Known gaps* entry for #363 is removed. + +## Documentation updates + +Owner: Dev, once the code is green, against the code. + +- `docs/areas/registries.md` — *Configuration reload*, bullet "Validation of these keys at startup": add that a + non-blank `UserName`, trimmed, must be a Docker Hub user name (ASCII letters and digits, separated by `.`, `_`, + `__` or any run of `-`, at most 255 characters), with the AC1 failure text, at startup and on reload; link 0018. + *Known gaps*: delete the #363 entry. *Related decisions*: add 0018. +- `README.md` — `DockerUpdateGuard:DockerHub` table, `UserName` row: "Docker Hub user name (Docker ID) for + authenticated API requests; letters, digits and `.`, `_`, `-` between them, validated at startup". +- `docs/ARCHITECTURE.md` — *Security posture*, the bullet ending in record 0014: add that the configured Docker Hub + user name, the path segment of the account listing, must be a Docker Hub user name to pass validation, linking 0018. +- Sweep done: `docs/areas/hosting.md` (*Validation* delegates feature-key rules to their areas; the `@` rule list + is about URL keys only — no change), `docs/areas/vulnerabilities.md:32`, `docs/areas/web-ui.md:25,98,117` + (mention the key, no enumeration of rules — no change), `INSTALL.md:85`, `docs/.env.example`, compose files (no + rule text — no change). + +## Architecture check + +No guarantee of `.squad/project.md` changes: validation stays one `IValidateOptions` step at startup and reload; +integration clients keep their outcomes; layering unchanged (the grammar stays in `Images/Helper`, used by the +validator in the same project). `docs/ARCHITECTURE.md` update as above, owner Dev. + +## Security considerations + +- [ ] Limits: the 255-character bound counts UTF-16 characters of the trimmed configuration value, before any + escaping; no decoding happens. +- [ ] Exceptions reaching a message: none — the length bound precedes the regex, so `RegexMatchTimeoutException` + cannot occur on a bounded ASCII-class pattern; the failure text is fixed and carries no value. + +## Decision records + +- `docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md` (Proposed) + +## Out of scope / follow-ups + +- A check in `DockerHubClient.GetRepositoriesAsync` itself (option 4). +- How Docker Hub treats upper-case letters or an e-mail address as login identifier: not known, and the rule does + not rest on it (0018, *Consequences*). From 860ccf8e9b4bdbf4cea39042fdfbf8313ffc3fde Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:01:58 +0000 Subject: [PATCH 03/21] Log plan launch usage for issue 363 --- specs/issue-363/log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 6327805..8cfe554 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -6,4 +6,4 @@ are quoted in full. | Date | Step | Member | Result | | ---- | ---- | ------ | ------ | | 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | -| 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · usage pending) | +| 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | From 26d6ebbe19672657075fe6426559708e26c9196c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:02:57 +0000 Subject: [PATCH 04/21] Append challenge and security notes to plan --- specs/issue-363/log.md | 2 ++ specs/issue-363/plan.md | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 8cfe554..b4d33fa 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -7,3 +7,5 @@ are quoted in full. | ---- | ---- | ------ | ------ | | 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | | 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | +| 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | +| 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · usage pending) | diff --git a/specs/issue-363/plan.md b/specs/issue-363/plan.md index bbccf2f..290f09c 100644 --- a/specs/issue-363/plan.md +++ b/specs/issue-363/plan.md @@ -161,3 +161,19 @@ validator in the same project). `docs/ARCHITECTURE.md` update as above, owner De - A check in `DockerHubClient.GetRepositoriesAsync` itself (option 4). - How Docker Hub treats upper-case letters or an e-mail address as login identifier: not known, and the rule does not rest on it (0018, *Consequences*). + +## Challenge notes + +Devil's Advocate, mode plan: OBJECTIONS 0 major, 2 minor (appended verbatim). + +1. minor — Verified fact 5 is wrong for one consumer, so do not rely on it beyond the URL path. `DockerScoutVulnerabilityProvider.cs:252` sends `hubOptions.UserName!` untrimmed in the Docker Scout login body; it does not use the trimmed value. `MyImages.razor.cs:178` also shows the raw value. Instruction for the Dev and Reviewer: leave both unchanged, because they are not path-bearing and not part of #363. Do not write in the docs or in 0018 that every consumer trims the value. Neither the ARCHITECTURE bullet nor the registries.md bullet may claim this. Why minor: the security property only needs the account-listing path to be trimmed, and it is (`DockerHubAccountImageDiscoveryService.cs:273`). + +2. minor — The test section names `CollectionAssert.AreEqual`, but the sibling at-sign tests in `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` (around lines 862 and 900) use `Assert.AreSequenceEqual([...], ...)`. Instruction for the Tester: assert the exact single failure the way those tests do. Use `Assert.AreSequenceEqual` with the fixed AC1 text and an assertion message, so the new tests match their neighbours. Why minor: either assertion catches the defect; this is only about consistency in the file. + +## Security notes + +Security, mode plan: APPROVED, no blocking findings (non-blocking finding appended verbatim). + +1. Fact 5 says every consumer trims the value, but `DockerScoutVulnerabilityProvider.AuthenticateAsync` (`src/DockerUpdateGuard/Vulnerabilities/DockerScoutVulnerabilityProvider.cs:252`) sends `hubOptions.UserName!` untrimmed. It goes into a JSON body, not a URL path, so it has no security effect and the guard is not weakened. The Lead should correct the wording of fact 5 (or the decision draft's Context), not the code. Leading or trailing spaces sent to the Scout login are existing behavior and out of scope here. + +Fact 7 was measured by Security (a one-off .NET 10 app): `IgnoreCase` with `[a-z0-9]` matches the Kelvin sign and `ToLowerInvariant` folds it to `k`; the plan's pattern rejects it. The 255-character check precedes the regex, whose near-miss inputs ran in under 1 ms. From b70ad5288234660fcaf2113cba61da4cd1be3217 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:03:12 +0000 Subject: [PATCH 05/21] Log security review usage and skeleton in progress --- specs/issue-363/log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index b4d33fa..acc6e6e 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -8,4 +8,4 @@ are quoted in full. | 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | | 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | | 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | -| 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · usage pending) | +| 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | From fe2aa01c9ef1891ca9699403d4e3becb3fab6b4d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:06:03 +0000 Subject: [PATCH 06/21] Skeleton for namespace validation --- specs/issue-363/log.md | 1 + .../Helper/RegistryReferenceValidator.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index acc6e6e..bf88f4d 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -9,3 +9,4 @@ are quoted in full. | 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | | 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | | 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | +| 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · usage pending) | diff --git a/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs b/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs index 057b002..d808a83 100644 --- a/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs +++ b/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs @@ -69,6 +69,15 @@ internal static partial class RegistryReferenceValidator 100)] private static partial Regex DigestExpression(); + /// + /// Namespace grammar pattern + /// + /// Compiled regular expression + [GeneratedRegex(@"\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z", + RegexOptions.CultureInvariant, + 100)] + private static partial Regex NamespaceExpression(); + /// /// Registry grammar pattern /// @@ -103,6 +112,16 @@ public static bool HasDotSegment(string path) || string.Equals(segment, "..", StringComparison.Ordinal)); } + /// + /// Check whether a string is a valid Docker Hub namespace + /// + /// Namespace to check + /// True when the namespace is valid + public static bool IsValidNamespace([NotNullWhen(true)] string? namespaceName) + { + throw new InvalidOperationException("Skeleton: not implemented"); + } + /// /// Check whether a string is a valid registry host with an optional port /// From 9354c48ed2490b5524c03bd61302e4ddc7f02ee9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:06:17 +0000 Subject: [PATCH 07/21] Log skeleton usage --- specs/issue-363/log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index bf88f4d..3c0f8dd 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -9,4 +9,4 @@ are quoted in full. | 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | | 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | | 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | -| 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · usage pending) | +| 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | From e92da0f3ad4e582b3f64fad468ac5ec7e560e6b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:08:25 +0000 Subject: [PATCH 08/21] Add failing tests for Docker Hub user name validation --- .../DockerUpdateGuardOptionsValidatorTests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs index 1c97495..0217d10 100644 --- a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs @@ -818,6 +818,92 @@ public void DockerUpdateGuardOptionsValidatorValidateWithUnusableCertificateOnUn "An enabled instance must be checked for its certificate path whatever its transport"); } + /// + /// Verify a Docker Hub user name outside the namespace grammar fails with the fixed text only + /// + /// Configured Docker Hub user name; null stands for 256 letters + [TestMethod] + [DataRow(".")] + [DataRow("..")] + [DataRow(" .. ")] + [DataRow("...")] + [DataRow(".acme")] + [DataRow("acme.")] + [DataRow("acme/..")] + [DataRow("../acme")] + [DataRow("acme/x")] + [DataRow("%2e%2e")] + [DataRow("acme?x")] + [DataRow("acme#x")] + [DataRow("ac me")] + [DataRow("acme:pw")] + [DataRow("me@example.com")] + [DataRow("a___b")] + [DataRow("acme-")] + [DataRow("\u212Acme")] + [DataRow("ac\u200Bme")] + [DataRow(null)] + public void DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFailure(string? userName) + { + const string failureText = "'DockerUpdateGuard:DockerHub:UserName' must be a Docker Hub user name: letters and digits, optionally separated by '.', '_' or '-'"; + + var validator = CreateAtSignValidator(); + var options = CreateValidOptions(); + var value = userName ?? new string('a', 256); + + options.DockerHub.UserName = value; + + var validationResult = validator.Validate(Options.DefaultName, options); + var failures = validationResult.Failures?.ToArray() ?? []; + + Assert.IsTrue(validationResult.Failed, "A Docker Hub user name outside the namespace grammar must fail validation"); + Assert.AreSequenceEqual([failureText], + failures, + "The user name failure must be the only failure and carry the fixed text"); + Assert.DoesNotContain(message => message.Contains(value.Trim(), StringComparison.Ordinal), + failures, + "No failure may repeat the configured value"); + } + + /// + /// Verify a blank or valid Docker Hub user name passes validation + /// + /// Configured Docker Hub user name; "255" stands for 255 letters + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow("acme")] + [DataRow("Acme")] + [DataRow(" acme ")] + [DataRow("\u00A0acme\u00A0")] + [DataRow("my.org")] + [DataRow("my_org")] + [DataRow("my__org")] + [DataRow("my-org")] + [DataRow("my---org")] + [DataRow("a1b2")] + [DataRow("a")] + [DataRow("255")] + public void DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess(string? userName) + { + var validator = CreateAtSignValidator(); + var options = CreateValidOptions(); + var value = userName == "255" ? new string('a', 255) : userName; + + if (value is not null && value.StartsWith('\u00A0')) + { + Assert.IsTrue(char.IsWhiteSpace(value[0]), "The no-break space row must start with a white space character"); + } + + options.DockerHub.UserName = value; + + var validationResult = validator.Validate(Options.DefaultName, options); + + Assert.IsTrue(validationResult.Succeeded, + $"A blank or valid Docker Hub user name must pass validation, failures: {string.Join("; ", validationResult.Failures ?? [])}"); + } + /// /// Verify a Portainer base URL with an at sign outside its user info fails with the fixed text only /// From 3994e9a7e3c2655ffcb27f1cdc761c518769aa7c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:08:39 +0000 Subject: [PATCH 09/21] Log tests-first launch --- specs/issue-363/log.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 3c0f8dd..323382e 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -10,3 +10,4 @@ are quoted in full. | 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | | 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | | 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | +| 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | From 92caac43af2ba0d205372e61e4d0898a7f011daa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:08:49 +0000 Subject: [PATCH 10/21] Implement namespace validation (in progress) --- .../Configuration/DockerUpdateGuardOptionsValidator.cs | 6 ++++++ .../Images/Helper/RegistryReferenceValidator.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs b/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs index 97f13db..ced7715 100644 --- a/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs +++ b/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs @@ -103,6 +103,12 @@ private static void ValidateDockerHubOptions(DockerHubOptions options, List 300) { failures.Add($"'{DockerUpdateGuardOptions.SectionName}:DockerHub:RequestTimeoutSeconds' must be between 1 and 300"); diff --git a/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs b/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs index d808a83..1222a0d 100644 --- a/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs +++ b/src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs @@ -119,7 +119,9 @@ public static bool HasDotSegment(string path) /// True when the namespace is valid public static bool IsValidNamespace([NotNullWhen(true)] string? namespaceName) { - throw new InvalidOperationException("Skeleton: not implemented"); + return string.IsNullOrEmpty(namespaceName) == false + && namespaceName.Length <= MaxRepositoryLength + && NamespaceExpression().IsMatch(namespaceName); } /// From 9abef9713629dbab1d8c37d8d643232a4d8d576f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:10:33 +0000 Subject: [PATCH 11/21] Log implement launch --- specs/issue-363/log.md | 1 + .../Configuration/DockerUpdateGuardOptionsValidator.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 323382e..b698c22 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -11,3 +11,4 @@ are quoted in full. | 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | | 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | | 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | +| 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | diff --git a/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs b/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs index ced7715..4f005ba 100644 --- a/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs +++ b/src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using DockerUpdateGuard.Images.Helper; + using Microsoft.Extensions.Options; namespace DockerUpdateGuard.Configuration; From 63f391beb3ad5985e021a2c2ade5de6e41efab07 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:10:57 +0000 Subject: [PATCH 12/21] Clarify AC1 test assertion in plan --- specs/issue-363/log.md | 1 + specs/issue-363/plan.md | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index b698c22..64a901d 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -12,3 +12,4 @@ are quoted in full. | 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | | 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | | 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | +| 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · usage pending) | diff --git a/specs/issue-363/plan.md b/specs/issue-363/plan.md index 290f09c..8b9f99e 100644 --- a/specs/issue-363/plan.md +++ b/specs/issue-363/plan.md @@ -35,7 +35,8 @@ removes `.` and resolves `..` segments, so `..` requests `v2/?page_size=100` and namespace grammar `\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z` of at most 255 characters fails `DockerUpdateGuardOptionsValidator.Validate` with exactly `'DockerUpdateGuard:DockerHub:UserName' must be a Docker Hub user name: letters and digits, optionally separated by '.', '_' or '-'`, - and no failure contains the configured value. Cases: `.`, `..`, ` .. `, `...`, `.acme`, `acme.`, `acme/..`, + and no other failure, so no failure carries the configured value (the text does not depend on it; no separate + substring check, which the case `.` would fail against the text's own `'.'`). Cases: `.`, `..`, ` .. `, `...`, `.acme`, `acme.`, `acme/..`, `../acme`, `acme/x`, `%2e%2e`, `acme?x`, `acme#x`, `ac me`, `acme:pw`, `me@example.com`, `a___b`, `acme-`, `\u212Acme` (Kelvin sign), `ac\u200Bme` (zero-width space), and 256 `a`. - [ ] AC2: These values pass validation: `null`, `""`, `" "`, `acme`, `Acme`, ` acme `, `\u00A0acme\u00A0` @@ -54,7 +55,8 @@ backslash (`u212Acme`, `acu200Bme`) becomes a valid name, so the row fails red; | AC2 | `DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess` (one `DataRow` per AC2 case) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | Both tests start from `CreateValidOptions()` with `CreateAtSignValidator()`, so AC1 asserts that the failures are -exactly the one fixed text (`CollectionAssert.AreEqual`), which also proves that no failure carries the value. The +exactly the one fixed text (`Assert.AreSequenceEqual`), which also proves that no failure carries the value; the +test has no separate `Contains(value)` assertion. The 255/256-character values are built in the test (`new string('a', n)`), not typed. ## Approach From a8c9e252b4d524a738d3e0b556376b334b8db915 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:11:12 +0000 Subject: [PATCH 13/21] Log lead decision usage --- specs/issue-363/log.md | 2 +- .../DockerUpdateGuardOptionsValidatorTests.cs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 64a901d..ea25194 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -12,4 +12,4 @@ are quoted in full. | 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | | 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | | 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | -| 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · usage pending) | +| 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · 30,724 tokens · 7 tool uses · 23 s) | diff --git a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs index 0217d10..e568020 100644 --- a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs @@ -860,9 +860,6 @@ public void DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFail Assert.AreSequenceEqual([failureText], failures, "The user name failure must be the only failure and carry the fixed text"); - Assert.DoesNotContain(message => message.Contains(value.Trim(), StringComparison.Ordinal), - failures, - "No failure may repeat the configured value"); } /// From 06f150a759edbd8e1f394617e168989a7fd8d19f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:12:39 +0000 Subject: [PATCH 14/21] Log tester coverage round, docs in progress --- specs/issue-363/log.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index ea25194..f91f382 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -13,3 +13,4 @@ are quoted in full. | 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | | 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | | 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · 30,724 tokens · 7 tool uses · 23 s) | +| 2026-11-05 18:12 | 6 Coverage (test fix) | squad-tester | Dropped redundant assert; 35/35 DockerHubUserName tests green. Orchestrator: full suite 1043+21 green, Coverage gate PASS (new/changed 100% 6/6, overall 88.3%) (opus/medium · 70,800 tokens · 7 tool uses · 38 s) | From 5554e9db1f73cd2d2d1aa3b7a6b429a172bf0e25 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:15:11 +0000 Subject: [PATCH 15/21] Document Docker Hub user name validation --- README.md | 2 +- docs/ARCHITECTURE.md | 2 ++ docs/areas/registries.md | 13 +++++++++---- specs/issue-363/log.md | 2 ++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 156d21a..a8fa018 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ in a user name or password. | --- | --- | --- | --- | | `Registry` | `docker.io` | Yes | Registry host handled by the Docker Hub integration | | `ApiBaseUrl` | `https://hub.docker.com/` | No | Docker Hub API base address used for registries served by Docker Hub; a mirror or proxy must rewrite next-page links to its own origin, otherwise listings fail | -| `UserName` | none | No | Docker Hub username for authenticated API requests | +| `UserName` | none | No | Docker Hub user name (Docker ID) for authenticated API requests; letters, digits and `.`, `_`, `-` between them, validated at startup | | `Pat` | none | No | Docker Hub personal access token | | `RequestTimeoutSeconds` | `30` | No | Total time per outbound Docker Hub, OCI registry and .NET/nginx release-feed request, including its retries | | `MaxParallelRequests` | `4` | No | Maximum logical request parallelism for registry operations | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 12a707d..4b6d35e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -431,6 +431,8 @@ it links: or cut where they are display names; the registry request builders refuse `.` and `..` path segments of any reference or digest, and the OCI request builder a registry that is not a host with an optional port ([record 0014](decisions/0014-registry-tag-names-and-digests-are-validated-where-they-are-read.md)). + The configured Docker Hub user name, the path segment of the account listing, must be a Docker Hub user name to pass + validation ([record 0018](decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md)). - **Pagination stays on its origin.** `DockerHubClient`, `OciRegistryClient` and `DockerScoutVulnerabilityProvider` follow a next-page link only on the origin of the listing (`Infrastructure\PaginationLinkGuard`), so a remote diff --git a/docs/areas/registries.md b/docs/areas/registries.md index 937a5a4..90da783 100644 --- a/docs/areas/registries.md +++ b/docs/areas/registries.md @@ -285,7 +285,13 @@ values stored before the replacement existed, which stay until they are written origin of `ApiBaseUrl` (or of an absolute `DockerHub:Registry` that replaces it), fixed at startup like `ApiBaseUrl`, is the only one Docker Hub pagination follows ([Pagination links](#pagination-links)). - Validation of these keys at startup (an invalid value fails startup, see [Hosting](hosting.md)): `Registry` non-empty, `ApiBaseUrl` an absolute `http`/`https` URL, neither `ApiBaseUrl` nor an absolute `Registry` with an `@` - outside its user info ([Hosting](hosting.md), *Validation*), and the ranges above. + outside its user info ([Hosting](hosting.md), *Validation*), the ranges above, and a non-blank `UserName`, trimmed, + which must be a Docker Hub user name: ASCII letters and digits, separated by `.`, `_`, `__` or any run of `-`, at + most 255 characters. Anything else fails with + `'DockerUpdateGuard:DockerHub:UserName' must be a Docker Hub user name: letters and digits, optionally separated by '.', '_' or '-'` + (the failure never contains the value), at startup and on reload. The account listing, which puts the trimmed value + into its request path, thereby never receives `.` or `..` + ([0018](../decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md)). ## Limits and errors @@ -326,9 +332,6 @@ values stored before the replacement existed, which stay until they are written attestation manifest, and publication times come only from a top-level `created` field of the manifest, which most registries do not send, so their tags usually have no publication time. Tracked in [#277](https://github.com/LarsLaskowski/DockerUpdateGuard/issues/277). -- The configured `DockerHub:UserName` reaches the path of the account listing without a check, so `.` or `..` would - address another path of the Docker Hub web API. Tracked in - [#363](https://github.com/LarsLaskowski/DockerUpdateGuard/issues/363). ## Related decisions @@ -339,6 +342,8 @@ values stored before the replacement existed, which stay until they are written failure reasons, never an error's own text. - [0014](../decisions/0014-registry-tag-names-and-digests-are-validated-where-they-are-read.md): remote tag names, digests, image references and names are validated or bounded where they are read. +- [0018](../decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md): the Docker Hub user + name is validated as a namespace at startup. ## Not here diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index f91f382..bc024de 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -14,3 +14,5 @@ are quoted in full. | 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | | 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · 30,724 tokens · 7 tool uses · 23 s) | | 2026-11-05 18:12 | 6 Coverage (test fix) | squad-tester | Dropped redundant assert; 35/35 DockerHubUserName tests green. Orchestrator: full suite 1043+21 green, Coverage gate PASS (new/changed 100% 6/6, overall 88.3%) (opus/medium · 70,800 tokens · 7 tool uses · 38 s) | +| 2026-11-05 18:13 | 6 Implement (docs round) | squad-dev | Docs written: registries.md, README.md, ARCHITECTURE.md (sonnet/medium · 4,243 tokens · 4 tool uses · 23 s · continued from 6 Implement) | +| 2026-11-05 18:15 | 7 Code check | Orchestrator | Format check 0/393, build 0 warnings, tests green, coverage PASS, analyzer 0 diagnostics, scope-check PASS; code-officer not needed | From 19b0aa96e453447b6c862ac4e5c4eeb83326250e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:16:34 +0000 Subject: [PATCH 16/21] Log review round 1 --- specs/issue-363/log.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index bc024de..62a3493 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -16,3 +16,5 @@ are quoted in full. | 2026-11-05 18:12 | 6 Coverage (test fix) | squad-tester | Dropped redundant assert; 35/35 DockerHubUserName tests green. Orchestrator: full suite 1043+21 green, Coverage gate PASS (new/changed 100% 6/6, overall 88.3%) (opus/medium · 70,800 tokens · 7 tool uses · 38 s) | | 2026-11-05 18:13 | 6 Implement (docs round) | squad-dev | Docs written: registries.md, README.md, ARCHITECTURE.md (sonnet/medium · 4,243 tokens · 4 tool uses · 23 s · continued from 6 Implement) | | 2026-11-05 18:15 | 7 Code check | Orchestrator | Format check 0/393, build 0 warnings, tests green, coverage PASS, analyzer 0 diagnostics, scope-check PASS; code-officer not needed | +| 2026-11-05 18:16 | 8 Review round 1 | squad-reviewer | APPROVE on 5554e9d; 2 non-blocking: (1) AC2 no-break-space guard in test can never fail (conditional), (2) decision 0018 Decision sentence omits upper case (opus/medium · 48,314 tokens · 6 tool uses · 60 s) | +| 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | From 7580b620bbe05502d96682552c06c22b8297156f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:17:36 +0000 Subject: [PATCH 17/21] Lead edits to record and plan after review --- ...ub-user-name-is-validated-as-a-namespace-at-startup.md | 4 ++-- specs/issue-363/log.md | 1 + specs/issue-363/plan.md | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md index 055e201..734d032 100644 --- a/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md +++ b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md @@ -33,8 +33,8 @@ credentials and the Docker Scout login name. ## Decision -A non-blank `DockerHub:UserName` must, after trimming, be one ASCII path component of the Docker reference grammar of -at most 255 characters; any other value fails the configuration validation, at startup and on reload. The rule and +A non-blank `DockerHub:UserName` must, after trimming, be one path component of the Docker reference grammar, with +ASCII letters of either case, of at most 255 characters; any other value fails the configuration validation, at startup and on reload. The rule and its failure text are in [Registries](../areas/registries.md), *Configuration reload*. ## Consequences diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 62a3493..625b4a8 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -18,3 +18,4 @@ are quoted in full. | 2026-11-05 18:15 | 7 Code check | Orchestrator | Format check 0/393, build 0 warnings, tests green, coverage PASS, analyzer 0 diagnostics, scope-check PASS; code-officer not needed | | 2026-11-05 18:16 | 8 Review round 1 | squad-reviewer | APPROVE on 5554e9d; 2 non-blocking: (1) AC2 no-break-space guard in test can never fail (conditional), (2) decision 0018 Decision sentence omits upper case (opus/medium · 48,314 tokens · 6 tool uses · 60 s) | | 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | +| 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · usage pending) | diff --git a/specs/issue-363/plan.md b/specs/issue-363/plan.md index 8b9f99e..80fc3d8 100644 --- a/specs/issue-363/plan.md +++ b/specs/issue-363/plan.md @@ -25,7 +25,7 @@ removes `.` and resolves `..` segments, so `..` requests `v2/?page_size=100` and | 2 | `.`/`..` in that segment address another path (issue claim) | `EscapeDataString` leaves `.` unescaped and `System.Uri` removes/resolves dot segments, also written as `%2E` (measured for #360: decision 0014, options 14-15; `docs/areas/registries.md`, *Request paths*) | confirmed | | 3 | `UserName` is not validated today | `DockerUpdateGuardOptionsValidator.cs:93-119` | confirmed | | 4 | The validator runs at startup and on every reload before a value is read | `ServiceCollectionExtensions.cs:53-56` (`IValidateOptions` + `ValidateOnStart`); `OptionsFactory.Create` runs every `IValidateOptions` when `IOptionsMonitor` rebuilds the value; `docs/areas/hosting.md`, *Configuration reload* | confirmed | -| 5 | Every consumer uses the value trimmed and treats blank as not set | `DockerHubClient.cs:682-685`, `1053-1060`; `DockerHubAccountImageDiscoveryService.cs:266-273`; `DockerScoutVulnerabilityProvider.cs:252`, `432`; `NavMenu.razor.cs:66`, `Dashboard.razor.cs:117` | confirmed | +| 5 | The account listing uses the value trimmed and treats blank as not set (the Docker Scout login, `DockerScoutVulnerabilityProvider.cs:252`, sends it untrimmed in a JSON body, not a path) | `DockerHubClient.cs:682-685`, `1053-1060`; `DockerHubAccountImageDiscoveryService.cs:266-273`; `DockerScoutVulnerabilityProvider.cs:252`, `432`; `NavMenu.razor.cs:66`, `Dashboard.razor.cs:117` | confirmed | | 6 | The values in repository configuration and test data fit the new rule | `appsettings.json` (`""`), `README.md` sample and `TestDevelopmentConfiguration.cs:24` (`dockerupdateguard`); validator tests' `CreateValidOptions` sets no user name | confirmed | | 7 | `RegexOptions.IgnoreCase` and `ToLowerInvariant` fold `\u212A` (Kelvin sign) to `k`, which is why the grammar uses explicit `[a-zA-Z0-9]` classes | to verify: the `\u212Acme` row of AC1 fails red before the fix and passes after it, owner Tester (step 5) and Dev (step 6) | unverified | @@ -44,15 +44,15 @@ removes `.` and resolves `..` segments, so `..` requests `v2/?page_size=100` and `a`, and 255 `a`. The C# escapes `\u212A`, `\u200B`, `\u00A0` are written as escapes in the test source. An AC1 row that lost its -backslash (`u212Acme`, `acu200Bme`) becomes a valid name, so the row fails red; the AC2 no-break-space row asserts -`char.IsWhiteSpace(value[0])` first, so a lost backslash there fails that guard. +backslash (`u212Acme`, `acu200Bme`) becomes a valid name, so the row fails red; the AC2 no-break-space case is its own test, +which asserts `char.IsWhiteSpace(userName[0])` unconditionally first, so a lost backslash there fails that guard. ## Tests per criterion | AC | Test(s) | Test file | | -- | ------- | --------- | | AC1 | `DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFailure` (one `DataRow` per AC1 case; the failures are exactly the fixed text) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | -| AC2 | `DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess` (one `DataRow` per AC2 case) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | +| AC2 | `DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess` (one `DataRow` per AC2 case except the no-break-space one); `DockerUpdateGuardOptionsValidatorNoBreakSpaceDockerHubUserNameReturnsSuccess` (`const string userName = "\u00A0acme\u00A0";`, unconditional `Assert.IsTrue(char.IsWhiteSpace(userName[0]))` before validating) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | Both tests start from `CreateValidOptions()` with `CreateAtSignValidator()`, so AC1 asserts that the failures are exactly the one fixed text (`Assert.AreSequenceEqual`), which also proves that no failure carries the value; the From 2e1e9f853eabe79a2330c533bef942c5bb51737d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:17:49 +0000 Subject: [PATCH 18/21] Log approval round, tester delta in progress --- specs/issue-363/log.md | 2 +- .../DockerUpdateGuardOptionsValidatorTests.cs | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 625b4a8..1f61028 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -18,4 +18,4 @@ are quoted in full. | 2026-11-05 18:15 | 7 Code check | Orchestrator | Format check 0/393, build 0 warnings, tests green, coverage PASS, analyzer 0 diagnostics, scope-check PASS; code-officer not needed | | 2026-11-05 18:16 | 8 Review round 1 | squad-reviewer | APPROVE on 5554e9d; 2 non-blocking: (1) AC2 no-break-space guard in test can never fail (conditional), (2) decision 0018 Decision sentence omits upper case (opus/medium · 48,314 tokens · 6 tool uses · 60 s) | | 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | -| 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · usage pending) | +| 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · 45,781 tokens · 10 tool uses · 49 s) | diff --git a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs index e568020..dfe6c91 100644 --- a/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs @@ -873,7 +873,6 @@ public void DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFail [DataRow("acme")] [DataRow("Acme")] [DataRow(" acme ")] - [DataRow("\u00A0acme\u00A0")] [DataRow("my.org")] [DataRow("my_org")] [DataRow("my__org")] @@ -888,11 +887,6 @@ public void DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSucces var options = CreateValidOptions(); var value = userName == "255" ? new string('a', 255) : userName; - if (value is not null && value.StartsWith('\u00A0')) - { - Assert.IsTrue(char.IsWhiteSpace(value[0]), "The no-break space row must start with a white space character"); - } - options.DockerHub.UserName = value; var validationResult = validator.Validate(Options.DefaultName, options); @@ -901,6 +895,27 @@ public void DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSucces $"A blank or valid Docker Hub user name must pass validation, failures: {string.Join("; ", validationResult.Failures ?? [])}"); } + /// + /// Verify a Docker Hub user name surrounded by no-break spaces is trimmed and passes validation + /// + [TestMethod] + public void DockerUpdateGuardOptionsValidatorNoBreakSpaceDockerHubUserNameReturnsSuccess() + { + const string userName = "\u00A0acme\u00A0"; + + Assert.IsTrue(char.IsWhiteSpace(userName[0]), "The user name must start with a no-break space that counts as white space"); + + var validator = CreateAtSignValidator(); + var options = CreateValidOptions(); + + options.DockerHub.UserName = userName; + + var validationResult = validator.Validate(Options.DefaultName, options); + + Assert.IsTrue(validationResult.Succeeded, + $"A Docker Hub user name surrounded by no-break spaces must pass validation, failures: {string.Join("; ", validationResult.Failures ?? [])}"); + } + /// /// Verify a Portainer base URL with an at sign outside its user info fails with the fixed text only /// From f4a270956d519d804ccb6e7cc453e756ca7d37ed Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:20:42 +0000 Subject: [PATCH 19/21] Test no-break-space user name in its own test --- specs/issue-363/log.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 1f61028..4664cdd 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -19,3 +19,4 @@ are quoted in full. | 2026-11-05 18:16 | 8 Review round 1 | squad-reviewer | APPROVE on 5554e9d; 2 non-blocking: (1) AC2 no-break-space guard in test can never fail (conditional), (2) decision 0018 Decision sentence omits upper case (opus/medium · 48,314 tokens · 6 tool uses · 60 s) | | 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | | 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · 45,781 tokens · 10 tool uses · 49 s) | +| 2026-11-05 18:20 | 8 Fix round 1 (tester) | squad-tester | AC2 no-break-space guard moved to own unconditional test; gates re-run green (format, build, 1043+21 tests, coverage 100%/88.3%, analyzer 0, scope PASS) (opus/medium · 4,597 tokens · 5 tool uses · 37 s · continued from 6 Coverage (test fix)) | From dd3499c4f5100dc5cdd54a829d5e17c474c31d09 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:21:21 +0000 Subject: [PATCH 20/21] Accept decision 0018 --- ...cker-hub-user-name-is-validated-as-a-namespace-at-startup.md | 2 +- docs/decisions/README.md | 2 +- specs/issue-363/log.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md index 734d032..9063b35 100644 --- a/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md +++ b/docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md @@ -1,6 +1,6 @@ # 0018: The Docker Hub user name is validated as a Docker Hub namespace at startup -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-11-05 - **Area:** Registries - **Source:** Issue #363 diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 8c3a109..ecf1351 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -70,5 +70,5 @@ once in the [area document](../areas/README.md) of the record's area, and the re | [0015](0015-the-trivy-scan-target-follows-an-end-of-options-marker.md) | The Trivy scan target follows an end-of-options marker | Accepted | 2026-10-29 | | [0016](0016-the-bundled-trivy-skips-its-update-check-and-telemetry.md) | The bundled Trivy skips its update check and telemetry | Accepted | 2026-10-29 | | [0017](0017-product-prs-may-change-the-squad-settings-file.md) | Product PRs may change the squad settings file | Accepted | 2026-11-01 | -| [0018](0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md) | The Docker Hub user name is validated as a Docker Hub namespace at startup | Proposed | 2026-11-05 | +| [0018](0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md) | The Docker Hub user name is validated as a Docker Hub namespace at startup | Accepted | 2026-11-05 | diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md index 4664cdd..699b7e5 100644 --- a/specs/issue-363/log.md +++ b/specs/issue-363/log.md @@ -20,3 +20,5 @@ are quoted in full. | 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | | 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · 45,781 tokens · 10 tool uses · 49 s) | | 2026-11-05 18:20 | 8 Fix round 1 (tester) | squad-tester | AC2 no-break-space guard moved to own unconditional test; gates re-run green (format, build, 1043+21 tests, coverage 100%/88.3%, analyzer 0, scope PASS) (opus/medium · 4,597 tokens · 5 tool uses · 37 s · continued from 6 Coverage (test fix)) | +| 2026-11-05 18:21 | 8 Delta review (round 2) | squad-reviewer | APPROVE on f4a2709; both non-blocking findings resolved (opus/medium · 3,917 tokens · 3 tool uses · 14 s · continued) | +| 2026-11-05 18:21 | 9 PR approval | Orchestrator | Approved by checklist after Lead's NOT APPROVED condition met (delta clean, gates green on f4a2709); 0018 set to Accepted | From f76bbd54bc41f10f22fb76b9e1595f5e058d5afd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:21:52 +0000 Subject: [PATCH 21/21] Remove squad working record --- specs/issue-363/log.md | 24 ------ specs/issue-363/plan.md | 181 ---------------------------------------- 2 files changed, 205 deletions(-) delete mode 100644 specs/issue-363/log.md delete mode 100644 specs/issue-363/plan.md diff --git a/specs/issue-363/log.md b/specs/issue-363/log.md deleted file mode 100644 index 699b7e5..0000000 --- a/specs/issue-363/log.md +++ /dev/null @@ -1,24 +0,0 @@ -# Log: issue #363 - -One line per pipeline step: date and time of day (UTC), step, member, result. Lead decisions and escalations -are quoted in full. - -| Date | Step | Member | Result | -| ---- | ---- | ------ | ------ | -| 2026-11-05 17:56 | 1 Intake | Orchestrator | Issue #363 (bug, open, no comments); branch claude/amazing-mendel-mzdyfe off main 7e9393e | -| 2026-11-05 18:01 | 2 Plan | squad-lead | RESULT: DONE, tier security, plan.md + decision 0018; areas: Registry and Docker Hub calls (opus/medium · 127,819 tokens · 35 tool uses · 292 s) | -| 2026-11-05 18:02 | 2 Plan challenge | squad-devils-advocate | OBJECTIONS 0 major, 2 minor; appended verbatim to plan.md (opus/medium · 42,048 tokens · 8 tool uses · 42 s) | -| 2026-11-05 18:02 | 3 Plan security review | squad-security | APPROVED, 1 non-blocking note (fact 5 wording); appended verbatim (opus/medium · 37,734 tokens · 5 tool uses · 61 s) | -| 2026-11-05 18:06 | 4 Skeleton | squad-dev | RegistryReferenceValidator: NamespaceExpression + IsValidNamespace skeleton; build 0 warnings; ran git add -N and git reset (blocked) - lesson candidate (sonnet/medium · 77,760 tokens · 8 tool uses · 170 s) | -| 2026-11-05 18:08 | 5 Tests first | squad-tester | 2 tests (AC1 20 rows red, AC2 15 rows green) in DockerUpdateGuardOptionsValidatorTests; confirmed red by orchestrator; none deferred. Orchestrator launched it on the agent file's sonnet instead of opus (Launch model, tier security): logged as run; later Tester launches use opus, the reviewer checks the tests (sonnet/medium · 83,436 tokens · 14 tool uses · 109 s) | -| 2026-11-05 18:10 | 6 Implement | squad-dev | Code done, build 0 warnings; 1 of 1043 tests red: AC1 row '.' - substring check 'no failure contains the value' hits the fixed text (contains '.'); Dev disputes the test (sonnet/medium · 79,967 tokens · 9 tool uses · 103 s) | -| 2026-11-05 18:10 | 6 Decide (test dispute) | squad-lead | DECIDED: Tester drops the redundant Assert.DoesNotContain; AC1 wording clarified in plan.md (opus/medium · 30,724 tokens · 7 tool uses · 23 s) | -| 2026-11-05 18:12 | 6 Coverage (test fix) | squad-tester | Dropped redundant assert; 35/35 DockerHubUserName tests green. Orchestrator: full suite 1043+21 green, Coverage gate PASS (new/changed 100% 6/6, overall 88.3%) (opus/medium · 70,800 tokens · 7 tool uses · 38 s) | -| 2026-11-05 18:13 | 6 Implement (docs round) | squad-dev | Docs written: registries.md, README.md, ARCHITECTURE.md (sonnet/medium · 4,243 tokens · 4 tool uses · 23 s · continued from 6 Implement) | -| 2026-11-05 18:15 | 7 Code check | Orchestrator | Format check 0/393, build 0 warnings, tests green, coverage PASS, analyzer 0 diagnostics, scope-check PASS; code-officer not needed | -| 2026-11-05 18:16 | 8 Review round 1 | squad-reviewer | APPROVE on 5554e9d; 2 non-blocking: (1) AC2 no-break-space guard in test can never fail (conditional), (2) decision 0018 Decision sentence omits upper case (opus/medium · 48,314 tokens · 6 tool uses · 60 s) | -| 2026-11-05 18:16 | 8 Security diff | squad-security | APPROVED on 5554e9d; no findings (opus/medium · 40,357 tokens · 4 tool uses · 30 s) | -| 2026-11-05 18:17 | 9 PR approval | squad-lead | NOT APPROVED until Tester delta: AC2 no-break-space guard to own test; 0018 sentence + plan fact 5/AC2 note fixed by Lead; sonnet Tester slip accepted unfixed (step-12 lesson) (opus/medium · 45,781 tokens · 10 tool uses · 49 s) | -| 2026-11-05 18:20 | 8 Fix round 1 (tester) | squad-tester | AC2 no-break-space guard moved to own unconditional test; gates re-run green (format, build, 1043+21 tests, coverage 100%/88.3%, analyzer 0, scope PASS) (opus/medium · 4,597 tokens · 5 tool uses · 37 s · continued from 6 Coverage (test fix)) | -| 2026-11-05 18:21 | 8 Delta review (round 2) | squad-reviewer | APPROVE on f4a2709; both non-blocking findings resolved (opus/medium · 3,917 tokens · 3 tool uses · 14 s · continued) | -| 2026-11-05 18:21 | 9 PR approval | Orchestrator | Approved by checklist after Lead's NOT APPROVED condition met (delta clean, gates green on f4a2709); 0018 set to Accepted | diff --git a/specs/issue-363/plan.md b/specs/issue-363/plan.md deleted file mode 100644 index 80fc3d8..0000000 --- a/specs/issue-363/plan.md +++ /dev/null @@ -1,181 +0,0 @@ -# Plan: Validate `DockerHub:UserName` as a Docker Hub namespace at startup - -Source: Issue #363 -Status: Draft -Tier: security — the check confines which path of the Docker Hub web API the account listing, with the account's bearer token, reaches (a guard on a URL, `.squad/routing.md`, *Tiers*). -Security areas touched: Registry and Docker Hub calls - -## Problem / root cause - -`DockerHubAccountImageDiscoveryService.SynchronizeAccountImagesAsync` -(`src/DockerUpdateGuard/Images/DockerHubAccountImageDiscoveryService.cs:273-277`) passes the trimmed -`DockerHub:UserName` to `DockerHubClient.GetRepositoriesAsync`, which builds -`new Uri(apiBaseUri, $"v2/repositories/{Uri.EscapeDataString(accountName)}/?page_size=100")` -(`src/DockerUpdateGuard/DockerHub/DockerHubClient.cs:1246`). `EscapeDataString` leaves `.` alone and `System.Uri` -removes `.` and resolves `..` segments, so `..` requests `v2/?page_size=100` and `.` requests -`v2/repositories/?page_size=100`, authenticated. `DockerUpdateGuardOptionsValidator.ValidateDockerHubOptions` -(`src/DockerUpdateGuard/Configuration/DockerUpdateGuardOptionsValidator.cs:93-119`) checks `Registry`, the ranges and -`ApiBaseUrl`, never `UserName`. - -## Verified facts - -| # | Claim | Evidence | Status | -| - | ----- | -------- | ------ | -| 1 | The account listing puts the user name into the path with `Uri.EscapeDataString` only | `DockerHubClient.cs:1246`; caller `DockerHubAccountImageDiscoveryService.cs:273-277` (trimmed value), the only caller (`grep GetRepositoriesAsync src/DockerUpdateGuard`) | confirmed | -| 2 | `.`/`..` in that segment address another path (issue claim) | `EscapeDataString` leaves `.` unescaped and `System.Uri` removes/resolves dot segments, also written as `%2E` (measured for #360: decision 0014, options 14-15; `docs/areas/registries.md`, *Request paths*) | confirmed | -| 3 | `UserName` is not validated today | `DockerUpdateGuardOptionsValidator.cs:93-119` | confirmed | -| 4 | The validator runs at startup and on every reload before a value is read | `ServiceCollectionExtensions.cs:53-56` (`IValidateOptions` + `ValidateOnStart`); `OptionsFactory.Create` runs every `IValidateOptions` when `IOptionsMonitor` rebuilds the value; `docs/areas/hosting.md`, *Configuration reload* | confirmed | -| 5 | The account listing uses the value trimmed and treats blank as not set (the Docker Scout login, `DockerScoutVulnerabilityProvider.cs:252`, sends it untrimmed in a JSON body, not a path) | `DockerHubClient.cs:682-685`, `1053-1060`; `DockerHubAccountImageDiscoveryService.cs:266-273`; `DockerScoutVulnerabilityProvider.cs:252`, `432`; `NavMenu.razor.cs:66`, `Dashboard.razor.cs:117` | confirmed | -| 6 | The values in repository configuration and test data fit the new rule | `appsettings.json` (`""`), `README.md` sample and `TestDevelopmentConfiguration.cs:24` (`dockerupdateguard`); validator tests' `CreateValidOptions` sets no user name | confirmed | -| 7 | `RegexOptions.IgnoreCase` and `ToLowerInvariant` fold `\u212A` (Kelvin sign) to `k`, which is why the grammar uses explicit `[a-zA-Z0-9]` classes | to verify: the `\u212Acme` row of AC1 fails red before the fix and passes after it, owner Tester (step 5) and Dev (step 6) | unverified | - -## Acceptance criteria - -- [ ] AC1: A non-blank `DockerHub:UserName` whose trimmed value (`string.Trim()`) is not one component of the - namespace grammar `\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z` of at most 255 characters fails - `DockerUpdateGuardOptionsValidator.Validate` with exactly - `'DockerUpdateGuard:DockerHub:UserName' must be a Docker Hub user name: letters and digits, optionally separated by '.', '_' or '-'`, - and no other failure, so no failure carries the configured value (the text does not depend on it; no separate - substring check, which the case `.` would fail against the text's own `'.'`). Cases: `.`, `..`, ` .. `, `...`, `.acme`, `acme.`, `acme/..`, - `../acme`, `acme/x`, `%2e%2e`, `acme?x`, `acme#x`, `ac me`, `acme:pw`, `me@example.com`, `a___b`, `acme-`, - `\u212Acme` (Kelvin sign), `ac\u200Bme` (zero-width space), and 256 `a`. -- [ ] AC2: These values pass validation: `null`, `""`, `" "`, `acme`, `Acme`, ` acme `, `\u00A0acme\u00A0` - (no-break spaces, trimmed like the consumers trim), `my.org`, `my_org`, `my__org`, `my-org`, `my---org`, `a1b2`, - `a`, and 255 `a`. - -The C# escapes `\u212A`, `\u200B`, `\u00A0` are written as escapes in the test source. An AC1 row that lost its -backslash (`u212Acme`, `acu200Bme`) becomes a valid name, so the row fails red; the AC2 no-break-space case is its own test, -which asserts `char.IsWhiteSpace(userName[0])` unconditionally first, so a lost backslash there fails that guard. - -## Tests per criterion - -| AC | Test(s) | Test file | -| -- | ------- | --------- | -| AC1 | `DockerUpdateGuardOptionsValidatorInvalidDockerHubUserNameReturnsFailure` (one `DataRow` per AC1 case; the failures are exactly the fixed text) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | -| AC2 | `DockerUpdateGuardOptionsValidatorValidDockerHubUserNameReturnsSuccess` (one `DataRow` per AC2 case except the no-break-space one); `DockerUpdateGuardOptionsValidatorNoBreakSpaceDockerHubUserNameReturnsSuccess` (`const string userName = "\u00A0acme\u00A0";`, unconditional `Assert.IsTrue(char.IsWhiteSpace(userName[0]))` before validating) | `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` | - -Both tests start from `CreateValidOptions()` with `CreateAtSignValidator()`, so AC1 asserts that the failures are -exactly the one fixed text (`Assert.AreSequenceEqual`), which also proves that no failure carries the value; the -test has no separate `Contains(value)` assertion. The -255/256-character values are built in the test (`new string('a', n)`), not typed. - -## Approach - -1. `RegistryReferenceValidator` (`src/DockerUpdateGuard/Images/Helper/RegistryReferenceValidator.cs`) gets a - `[GeneratedRegex(@"\A[a-zA-Z0-9]+(?:(?:[._]|__|-+)[a-zA-Z0-9]+)*\z", RegexOptions.CultureInvariant, 100)]` - `NamespaceExpression()` — explicit ASCII classes, no `IgnoreCase`, no lowercasing (fact 7) — and - `IsValidNamespace`, which checks non-empty, length `<= MaxRepositoryLength` (255) **before** the regex, then the - match. -2. `ValidateDockerHubOptions` adds, after the `Registry` rule: when `string.IsNullOrWhiteSpace(options.UserName) == - false` and `RegistryReferenceValidator.IsValidNamespace(options.UserName.Trim()) == false`, the AC1 failure. The - validator checks exactly the value the consumers send (fact 5). -3. No change to `DockerHubClient` (options considered, 4). - -Forms the consumer accepts (guard against bypasses): the value reaches `Uri.EscapeDataString` and then -`new Uri(base, relative)`. `EscapeDataString` escapes everything but `A-Za-z0-9-._~`, so `/`, `?`, `#`, `\`, `%`, -spaces and non-ASCII arrive encoded and stay inside the segment (`%2e` arrives as `%252e`). The only forms that leave -the segment are a segment exactly `.` or `..` (fact 2); `~` is not dangerous but outside the grammar. The grammar -rejects both dot forms (it starts and ends with `[a-zA-Z0-9]`), every character outside ASCII letters, digits, `.`, -`_`, `-`, and so every case of AC1. Surrounding white space is trimmed by validator and consumers alike, so ` .. ` -is checked as `..`. A value of more than 255 characters fails on its length before the regex runs, so the regex -timeout (100 ms) cannot throw out of the validator. - -## Options considered - -1. Reject only `.` and `..` — lost: keeps `/`, `:` and other non-names; a `:` splits the registry token request's - basic credentials (`DockerHubClient.cs:685`). -2. Namespace grammar, ASCII of either case, 255 bound — chosen. -3. Docker ID sign-up rule (4-30 lower-case letters and digits) — lost: stops configurations with upper-case letters - or older names for no security gain. -4. Also refuse a dot segment in `GetRepositoriesAsync` — lost: the validator covers startup and reload (fact 4), and - decision 0014 (option 19) already rejected the client check for this value. - -Recorded in `docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md`. - -## Affected projects and types - -| Project | Type / file | Change | -| ------- | ----------- | ------ | -| `DockerUpdateGuard` | `Images/Helper/RegistryReferenceValidator.cs` | `NamespaceExpression`, `IsValidNamespace` | -| `DockerUpdateGuard` | `Configuration/DockerUpdateGuardOptionsValidator.cs` | `UserName` rule in `ValidateDockerHubOptions` | - -## Signatures (for the Dev's skeleton) - -In `RegistryReferenceValidator` (internal static partial class), region *Regular expressions* and *Methods*: - -```csharp -private static partial Regex NamespaceExpression(); -public static bool IsValidNamespace([NotNullWhen(true)] string? namespaceName) -``` - -`ValidateDockerHubOptions(DockerHubOptions, List)` keeps its signature. - -## Test files - -- `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` - -Existing test call sites of a changed signature: none. Existing tests kept unchanged: none of them sets -`DockerHub:UserName` to a value outside the grammar (fact 6). - -Files no unit test can cover: none. - -## Areas - -- `docs/areas/registries.md`: the validation bullet of *Configuration reload* gains the `UserName` rule; the - *Known gaps* entry for #363 is removed. - -## Documentation updates - -Owner: Dev, once the code is green, against the code. - -- `docs/areas/registries.md` — *Configuration reload*, bullet "Validation of these keys at startup": add that a - non-blank `UserName`, trimmed, must be a Docker Hub user name (ASCII letters and digits, separated by `.`, `_`, - `__` or any run of `-`, at most 255 characters), with the AC1 failure text, at startup and on reload; link 0018. - *Known gaps*: delete the #363 entry. *Related decisions*: add 0018. -- `README.md` — `DockerUpdateGuard:DockerHub` table, `UserName` row: "Docker Hub user name (Docker ID) for - authenticated API requests; letters, digits and `.`, `_`, `-` between them, validated at startup". -- `docs/ARCHITECTURE.md` — *Security posture*, the bullet ending in record 0014: add that the configured Docker Hub - user name, the path segment of the account listing, must be a Docker Hub user name to pass validation, linking 0018. -- Sweep done: `docs/areas/hosting.md` (*Validation* delegates feature-key rules to their areas; the `@` rule list - is about URL keys only — no change), `docs/areas/vulnerabilities.md:32`, `docs/areas/web-ui.md:25,98,117` - (mention the key, no enumeration of rules — no change), `INSTALL.md:85`, `docs/.env.example`, compose files (no - rule text — no change). - -## Architecture check - -No guarantee of `.squad/project.md` changes: validation stays one `IValidateOptions` step at startup and reload; -integration clients keep their outcomes; layering unchanged (the grammar stays in `Images/Helper`, used by the -validator in the same project). `docs/ARCHITECTURE.md` update as above, owner Dev. - -## Security considerations - -- [ ] Limits: the 255-character bound counts UTF-16 characters of the trimmed configuration value, before any - escaping; no decoding happens. -- [ ] Exceptions reaching a message: none — the length bound precedes the regex, so `RegexMatchTimeoutException` - cannot occur on a bounded ASCII-class pattern; the failure text is fixed and carries no value. - -## Decision records - -- `docs/decisions/0018-the-docker-hub-user-name-is-validated-as-a-namespace-at-startup.md` (Proposed) - -## Out of scope / follow-ups - -- A check in `DockerHubClient.GetRepositoriesAsync` itself (option 4). -- How Docker Hub treats upper-case letters or an e-mail address as login identifier: not known, and the rule does - not rest on it (0018, *Consequences*). - -## Challenge notes - -Devil's Advocate, mode plan: OBJECTIONS 0 major, 2 minor (appended verbatim). - -1. minor — Verified fact 5 is wrong for one consumer, so do not rely on it beyond the URL path. `DockerScoutVulnerabilityProvider.cs:252` sends `hubOptions.UserName!` untrimmed in the Docker Scout login body; it does not use the trimmed value. `MyImages.razor.cs:178` also shows the raw value. Instruction for the Dev and Reviewer: leave both unchanged, because they are not path-bearing and not part of #363. Do not write in the docs or in 0018 that every consumer trims the value. Neither the ARCHITECTURE bullet nor the registries.md bullet may claim this. Why minor: the security property only needs the account-listing path to be trimmed, and it is (`DockerHubAccountImageDiscoveryService.cs:273`). - -2. minor — The test section names `CollectionAssert.AreEqual`, but the sibling at-sign tests in `src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuardOptionsValidatorTests.cs` (around lines 862 and 900) use `Assert.AreSequenceEqual([...], ...)`. Instruction for the Tester: assert the exact single failure the way those tests do. Use `Assert.AreSequenceEqual` with the fixed AC1 text and an assertion message, so the new tests match their neighbours. Why minor: either assertion catches the defect; this is only about consistency in the file. - -## Security notes - -Security, mode plan: APPROVED, no blocking findings (non-blocking finding appended verbatim). - -1. Fact 5 says every consumer trims the value, but `DockerScoutVulnerabilityProvider.AuthenticateAsync` (`src/DockerUpdateGuard/Vulnerabilities/DockerScoutVulnerabilityProvider.cs:252`) sends `hubOptions.UserName!` untrimmed. It goes into a JSON body, not a URL path, so it has no security effect and the guard is not weakened. The Lead should correct the wording of fact 5 (or the decision draft's Context), not the code. Leading or trailing spaces sent to the Scout login are existing behavior and out of scope here. - -Fact 7 was measured by Security (a one-off .NET 10 app): `IgnoreCase` with `[a-z0-9]` matches the Kelvin sign and `ToLowerInvariant` folds it to `k`; the plan's pattern rejects it. The 255-character check precedes the regex, whose near-miss inputs ran in under 1 ms.