docs(release): the documents say what 1.1.1 does, and the package manifests verify the file they name

Statements the release reviews of 1.1.1 found untrue at this commit, and two
gaps in the release tooling.

- Threat model: the version reviewed; an installed build contacts
  git.kof22.com for updates; the binaries are signed (the accepted risks named
  them unsigned) and T1 with the Scoop part of T2 is what is accepted; T2 and
  T11 say what ADR 0009 changed.
- SECURITY.md names 1.1.x as the supported line.
- README: the beta is a separate app with its own accounts; parallelism is
  under Sync options, after loading; the buttons have the names on screen; the
  app is built unpackaged, with no Developer Mode. CONTRIBUTING the same.
  docs/CLI.md shows a current version.
- RELEASING.md: on which forge the packaging commit lands; the check that the
  server answers from outside comes before anything is tagged; a version that
  moves the updater is rehearsed with two prereleases read from that server;
  the engine package is numbered by its own public surface.
- Update-PackageManifests.ps1 downloads the file the manifests will name and
  compares its SHA-256 with the built one's, instead of asking only whether
  the URL answers; and writes the Scoop manifest with the repository's line
  endings.
- One vault test stored an entry in the credential namespace the installed
  app uses. It uses two test scopes now (#161).

Closes #183

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
james.maesandClaude Fable 5.2 committed 2026-11-11 00:04:02 -06:00
1 parent 6696e37049
commit 821425f931
8 files changed
+98 -54

No files matched your search

+25 -7
View File
@@ -31,7 +31,8 @@ param(
[string] $Sha256,
# Skips the check that the download is on the server (offline use, a rehearsal).
# Skips the check that the download is on the server and is the file that was hashed
# (offline use, a rehearsal).
[switch] $SkipDownloadCheck
)
@@ -83,18 +84,34 @@ $url = "https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v$Version/gclo
################################################################################
## The manifests name the download on git.kof22.com (ADR 0009). A package that ##
## points at a file that is not there yet fails for everyone who installs it, ##
## so the release must be on that server before its manifests are written. ##
## points at a file that is not there yet, or that is not the file whose hash ##
## the manifest carries, fails for everyone who installs it. So the release ##
## must be on that server, with the built file, before its manifests are ##
## written: the file is downloaded and its hash compared. ##
################################################################################
if(-not $SkipDownloadCheck)
{
$served = Join-Path ([System.IO.Path]::GetTempPath()) "gclo-manifest-check-$([System.Guid]::NewGuid().ToString('N')).zip"
try
{
$null = Invoke-WebRequest -Uri $url -Method Head -MaximumRedirection 5 -TimeoutSec 60 -ErrorAction Stop
try
{
Invoke-WebRequest -Uri $url -OutFile $served -MaximumRedirection 5 -TimeoutSec 300 -ErrorAction Stop
}
catch
{
throw "$url does not answer ($($_.Exception.Message)). Publish the release and its assets on git.kof22.com first, then run this again."
}
$servedSha256 = (Get-FileHash -LiteralPath $served -Algorithm SHA256).Hash.ToLowerInvariant()
if($servedSha256 -ne $Sha256)
{
throw "$url serves a file whose SHA-256 is $servedSha256; the manifests would name $Sha256. The asset on git.kof22.com is not the file that was built: replace it with the release's own, then run this again."
}
}
catch
finally
{
throw "$url does not answer ($($_.Exception.Message)). Publish the release and its assets on git.kof22.com first, then run this again."
Remove-Item -LiteralPath $served -Force -ErrorAction SilentlyContinue
}
}
@@ -106,7 +123,8 @@ $scoop = Get-Content -LiteralPath $scoopPath -Raw | ConvertFrom-Json
$scoop.version = $Version
$scoop.architecture.'64bit'.url = $url
$scoop.architecture.'64bit'.hash = $Sha256
$scoopJson = ($scoop | ConvertTo-Json -Depth 10) -replace '\\u0026', '&'
# (ConvertTo-Json ends its lines the platform's way; the repository's text is LF.)
$scoopJson = (($scoop | ConvertTo-Json -Depth 10) -replace '\\u0026', '&') -replace "`r`n", "`n"
Write-FileText -Path $scoopPath -Text ($scoopJson + "`n")
Write-Progress-Line "Scoop manifest -> $Version"
+4 -7
View File
@@ -25,18 +25,16 @@ Prerequisites:
These are the commands CI runs; run them from the repository root before pushing:
```powershell
# Build (packaged, as F5 in Visual Studio does). Zero warnings: -warnaserror is the gate.
# Build. Zero warnings: -warnaserror is the gate. The app is always built unpackaged and
# self-contained (gclo.csproj fixes WindowsPackageType=None): no MSIX deployment, no package identity.
dotnet build gclo.slnx -p:Platform=x64 -warnaserror
# Unpackaged build: runs without MSIX deployment or package identity. Must keep working.
dotnet build gclo.slnx -p:Platform=x64 -p:WindowsPackageType=None
# Tests (engine + view models, and the CLI), with coverage as CI measures it
dotnet test gclo.Engine.Tests --settings coverage.runsettings --collect:"XPlat Code Coverage"
dotnet test gclo.Cli.Tests --settings coverage.runsettings --collect:"XPlat Code Coverage"
# UI end-to-end tests (build the app first, then drive the real exe)
dotnet build gclo/gclo.csproj -p:Platform=x64 -p:WindowsPackageType=None
dotnet build gclo/gclo.csproj -p:Platform=x64
dotnet test gclo.UiTests/gclo.UiTests.csproj
# Format and style gate
@@ -81,8 +79,7 @@ pre-commit install --hook-type pre-commit --hook-type commit-msg
- Tests stay offline: engine tests use local fixture repositories and the fakes in
`gclo.Engine.Tests/Fakes.cs`; UI tests use the offline fixture token. No network, no
real tokens.
- Unpackaged mode keeps working: no APIs that require package identity without a
fallback.
- The app has no package identity: no APIs that require one.
- User-visible changes update the docs (`README.md`, `docs/CLI.md`) in the same pull
request; a change to the attack surface updates
[the threat model](docs/security/threat-model.md); an expensive-to-reverse decision
+9 -11
View File
@@ -33,7 +33,7 @@ All builds are published on the project's [**Releases** page](https://git.kof22.
Plain version tags (`v1.2.3`) are **stable** releases; pre-release tags (`v1.2.3-beta.1`) publish to the **dev** channel, and an installed app only ever updates within its own channel.
> **Upgrading from a beta?** Channels don't cross: a dev/beta install never auto-updates to a stable release (its **Check for updates** only looks at the dev channel). To move to stable, install `gclo-stable-Setup.exe` once — it takes over the existing install and self-updates within the stable channel from then on.
> **Upgrading from a beta?** Channels don't cross: a dev/beta install never auto-updates to a stable release (its **Check for updates** only looks at the dev channel). The beta is a separate app, **gclo (dev)**, with its own accounts and settings. To move to stable, install `gclo-stable-Setup.exe` beside it, add your accounts again, then uninstall the beta; the stable app updates itself within the stable channel from then on.
**Package managers (CLI):**
@@ -96,15 +96,15 @@ On launch, gclo shows a brief branded splash, then the **Quick Sync** connect ca
1. **Paste your token first.** After a moment, the **Organization or account** dropdown fills with everything the token can sync — your personal account is listed first, then your organizations alphabetically. If the token cannot list organizations (a fine-grained PAT, or a classic PAT without `read:org`), the status line says so; the dropdown is editable, so just type the name.
2. **Pick the organization or account** from the dropdown.
3. **Choose a target folder** — type a path or click **Browse...**. Tick **Create a `<org>` subfolder** to keep several organizations under one root; the live preview line underneath always shows exactly where repositories will land (e.g. `C:\src\acme\my-repo`).
4. **Set parallelism** if you like (1–64, default 8): how many git operations run at once.
5. **Press Load repos.** The table fills with every repository of the organization — name, status, default branch, and an Archived marker — all selected. Click a column header to sort by it (click again to flip, a third time to return to the default order); use the funnel on any column to filter by name, status, branch, or archived state.
4. **Press Load repositories.** The table fills with every repository of the organization — name, status, default branch, and an Archived marker — all selected. Click a column header to sort by it (click again to flip, a third time to return to the default order); use the funnel on any column to filter by name, status, branch, or archived state.
<p align="center">
<img src="docs/images/gclo-repos.png" width="820" alt="Loaded repository table: 59 repositories all selected, with sortable, filterable Name / Status / Branch / Archived columns and a Sync 59 repos button" />
</p>
6. **Uncheck anything you don't want.** The header checkbox selects or clears every row at once.
7. **Press Sync selected.** Each row moves through its states live — Queued, Cloning (with a per-row progress bar and transfer percentage), Pulling, Done, Failed, or Canceled — while the overall progress bar and completed/total counter track the run, which ends in a summary (`Finished: 3 cloned, 41 updated, 1 failed, 0 canceled of 45.`). By default the table follows the sync, so what is happening is always in view: repositories in progress are on top, then the failed ones, then the finished ones with the latest first, then the ones still waiting. When the sync is over, the failed ones come first and the rest are in name order. A column you sorted by yourself stays as you set it.
5. **Uncheck anything you don't want.** The header checkbox selects or clears every row at once.
6. **Set parallelism** if you like: the gear button (**Sync options**) in the toolbar has **Parallel clones** (1–64, default 8), how many git operations run at once.
7. **Press Sync** (the button names how many repositories are selected: **Sync 45 repos**). Each row moves through its states live — Queued, Cloning (with a per-row progress bar and transfer percentage), Pulling, Done, Failed, or Canceled — while the overall progress bar and completed/total counter track the run, which ends in a summary (`Finished: 3 cloned, 41 updated, 1 failed, 0 canceled of 45.`). By default the table follows the sync, so what is happening is always in view: repositories in progress are on top, then the failed ones, then the finished ones with the latest first, then the ones still waiting. When the sync is over, the failed ones come first and the rest are in name order. A column you sorted by yourself stays as you set it.
<p align="center">
<img src="docs/images/gclo-results.png" width="820" alt="Finished sync: every row Done, with the summary 58 cloned, 0 updated, 1 failed, 0 canceled of 59" />
@@ -201,12 +201,10 @@ Requirements: Windows 11 (or Windows 10 1809+), the [.NET 10 SDK](https://dotnet
git clone https://git.kof22.com/KofTwentyTwo/gclo.git
cd gclo
# Desktop app (packaged build)
# Everything, the desktop app included: a plain exe under gclo\bin\x64\Debug\ (the app is always
# built unpackaged and self-contained; there is no MSIX deployment)
dotnet build gclo.slnx -p:Platform=x64
# Desktop app, unpackaged — a plain exe under gclo\bin\x64\Debug\, no MSIX deployment
dotnet build gclo.slnx -p:Platform=x64 -p:WindowsPackageType=None
# CLI only (plain net10.0 — also builds on Linux/macOS)
dotnet run --project gclo.Cli -- sync --org contoso --target C:\src\contoso
@@ -214,13 +212,13 @@ dotnet run --project gclo.Cli -- sync --org contoso --target C:\src\contoso
dotnet test
```
To F5-debug the packaged app in Visual Studio, open `gclo.slnx`, set `gclo` as the startup project, select the `x64` platform, and make sure Windows **Developer Mode** is enabled (Settings > System > For developers) — packaged deployment requires it.
To debug the app in Visual Studio, open `gclo.slnx`, set `gclo` as the startup project, select the `x64` platform, and press F5. Nothing is deployed as a package, so Windows Developer Mode is not needed.
## Project layout
| Project | Description |
| --- | --- |
| `gclo` | WinUI 3 packaged desktop app — XAML, dialogs, pickers, and update plumbing only; no business logic |
| `gclo` | WinUI 3 desktop app (unpackaged) — XAML, dialogs, pickers, and update plumbing only; no business logic |
| `gclo.Cli` | Cross-platform command-line head over the engine ([docs/CLI.md](docs/CLI.md)) |
| `gclo.Engine` | Core sync engine: `OrgSyncEngine`, Octokit-based listers, LibGit2Sharp git client, Windows path validation |
| `gclo.ViewModels` | UI-framework-free view models and settings persistence, shared presentation logic |
+1 -1
View File
@@ -43,7 +43,7 @@ Reporters are credited in the advisory unless they ask not to be.
| Version | Security fixes |
| --- | --- |
| 1.0.x (the latest release) | Yes |
| 1.1.x (the latest release) | Yes |
| Older | No; upgrade to the latest release |
Only the latest MINOR of the latest MAJOR receives fixes; a line stops receiving
+2 -2
View File
@@ -574,7 +574,7 @@ done
```console
$ gclo --version
1.0.0 (a893741d2)
1.1.1 (a893741d2)
```
Released builds print the semantic version and the short commit hash; local builds print the next-release version with a `-dev` suffix (e.g. `1.0.1-dev (<hash>)`).
Released builds print the semantic version and the short commit hash; local builds print the next-release version with a `-dev` suffix (e.g. `1.1.2-dev (<hash>)`).
+29 -7
View File
@@ -188,7 +188,12 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
`autoupdate` entries keyed on `SHA256SUMS`, so `scoop update` works as
soon as the manifest on `main` names the new version — which is why the
`packages` job opens its pull request against `main` (three generated files,
nothing else). Merge it.
nothing else). The workflow runs on GitHub and opens that pull request on the
GitHub copy, which takes no changes of its own. Scoop reads the bucket from
`main` on `git.kof22.com`, so the same three files are committed there through
a pull request: run `Update-PackageManifests.ps1` by hand (below) on a branch
and merge it on Gitea. Until then Chocolatey can already hold the new version
while Scoop still names the old one.
- **Chocolatey** needs a one-time account on chocolatey.org and its API key as
`CHOCO_API_KEY`. The first version goes through Chocolatey's moderation queue
(expect a few days); later versions are usually auto-approved. Users then run
@@ -218,13 +223,19 @@ does on GitHub:
because their feed file is not in it). A stable release copied with the flag is
invisible to stable installs.
- **Before the package manifests are written.** `Update-PackageManifests.ps1`
refuses to write a manifest whose download does not answer, so the `packages`
job fails until the copy is done; run it again afterwards.
downloads the CLI zip from git.kof22.com and refuses to write a manifest when it
does not answer or is not, by its SHA-256, the file that was built; so the
`packages` job fails until the copy is done and right. Run it again afterwards.
- **winget by hand.** The workflow's winget step still submits the download on
GitHub; the manual command above names git.kof22.com. Until the pipeline moves,
submit a version to winget by hand.
- **No more than nine prereleases newer than the latest stable release.** The
updater reads the ten newest releases and picks by channel; a longer run of betas
hides the stable release from stable installs. Delete old prereleases.
- **From the network the users are on.** Check the release page and one download
from outside the local network.
from outside the local network, without signing in. For the server as such this
is checked before anything is tagged (step 2 below): an install that has updated
to a version that reads git.kof22.com looks nowhere else.
Releases on Gitea can be edited and their assets replaced; nothing there is
immutable. Treat a published version as final anyway (see "A broken release").
@@ -239,16 +250,27 @@ immutable. Treat a published version as final anyway (see "A broken release").
review (`docs/security/threat-model.md`).
2. **Make sure `main` is green** and the `release` environment holds every
token the table above lists for the kind of release you are cutting.
token the table above lists for the kind of release you are cutting. Check,
from a machine outside the local network and without signing in, that
`https://git.kof22.com/KofTwentyTwo/gclo/releases` and one asset of an earlier
release answer. If they do not, stop: the updater of the version you are about
to publish reads only that server.
3. **Rehearse on a prerelease.** Tag `vX.Y.Z-beta.N` first: it runs the whole
pipeline except the stable-only publishers, on the `dev` channel. Install
`gclo-dev-Setup.exe` on a clean machine, run a Quick Sync, and let an
installed earlier prerelease self-update to it. Only then tag the stable
installed earlier prerelease self-update to it. Copy each prerelease to
git.kof22.com first ("The release on git.kof22.com"): the self-update is read
from there, and it is tried from a machine outside the local network. A
version that changes where updates are read (1.1.1 did) needs two prereleases
for this, one to install and one to be updated to. Only then tag the stable
version.
4. **Pick the version.** Follow semver: breaking change → major, new feature →
minor, fix → patch.
minor, fix → patch. The `gclo.Engine` package is judged by its own public
surface (`git diff <last tag>..HEAD -- gclo.Engine`): when a public member
changed and the app still ships as a patch, leave `NUGET_PUBLISH` off for that
release, or number the release for the package, and say which in the notes.
5. **Tag and push** (the `protect-release-tags` ruleset lets only the
repository admin create a `v*` tag):
+13 -7
View File
@@ -4,7 +4,7 @@
review it in every MINOR/MAJOR release checklist (K22-SEC-30).
Method: data-flow diagram + STRIDE per trust boundary. -->
- **Version reviewed:** v1.0.1 · **Last reviewed:** 2026-10-09
- **Version reviewed:** v1.1.1 · **Last reviewed:** 2026-11-10
## 1. What the product is and does
@@ -14,8 +14,10 @@ account into a local folder: it lists repositories through the GitHub REST API,
clones the missing ones and fast-forwards the existing ones over HTTPS with a
personal access token (PAT) the user supplies. It runs as the user, with the user's
privileges, on the user's machine, and has access to that PAT, the target folder, and
(for the WSL recovery) the default WSL distribution. It sends nothing anywhere except
GitHub.
(for the WSL recovery) the default WSL distribution. It contacts GitHub (the REST API
and git over HTTPS, with the token) and, in an installed build, the project's release
server `git.kof22.com` to look for updates: anonymously, with no token and nothing
about the user's accounts or repositories. It sends nothing anywhere else.
## 2. Actors and components
@@ -84,7 +86,7 @@ flowchart LR
| # | Boundary / component | Category | Threat | Likelihood | Impact | Mitigation | Status |
| --- | --- | --- | --- | --- | --- | --- | --- |
| T1 | Update feed | Tampering | Attacker serves a malicious update | Low | Critical | HTTPS to the project's releases on `git.kof22.com` only (ADR 0009); Velopack verifies each package against the size and hash in the feed. The feed and the packages come from the same server, and the updater does not check an Authenticode signature or an attestation: whoever controls that server, the repository's release permission, or the server's TLS certificate can deliver a package that installs without a warning. The mitigations are therefore the server's own: who may publish a release there, how its certificate and DNS are held. Releases are not immutable on Gitea. | Accepted for 1.1.1; depends on the server's access control |
| T2 | Package managers / downloads | Tampering | Modified installer or CLI zip on a mirror | Low | High | `SHA256SUMS`; Scoop and Chocolatey manifests pin the hash; "Verifying a release" instructions | Mitigated |
| T2 | Package managers / downloads | Tampering | Modified installer or CLI zip on a mirror | Low | High | `SHA256SUMS`; Scoop and Chocolatey manifests pin the hash; "Verifying a release" instructions; the binaries are Authenticode-signed. Since ADR 0009 the Scoop manifest, the zip it names and `SHA256SUMS` are all served by `git.kof22.com`: whoever controls that server, its TLS certificate or its DNS can change the file and the hash together, which is the position of T1 (release permission alone does not change the manifest on `main`). Chocolatey's hash is computed in CI from the built zip and held at chocolatey.org, independently of that server. | Mitigated against a modified copy elsewhere; for Scoop, accepted against the release server (as T1) |
| T3 | Git remotes | Tampering / EoP | Repository with Windows-invalid or traversal paths (`..`, reserved names, case collisions) writes outside or corrupts the target | Medium | High | Pre-checkout validation of every incoming path, which also refuses a `.git` segment in any spelling Windows opens as that folder; `RepositoryPathResolver` containment; recovery by rename/skip or clone in WSL; `core.longpaths`. The recovery materializes the tree itself, so every write and delete it makes goes through `WorkingTree`: inside the working directory, never in `.git`, never through a link, and an existing file is replaced rather than written through (#141). Residual: the link check and the write are two steps, so a link swapped in between them by something already running as the user is not caught | Mitigated |
| T4 | Git remotes | Tampering | Hostile `.git` config or hooks in a pre-existing target repository | Low | High | gclo runs no hooks (libgit2, no shell); only fetch and fast-forward; `gclo.checkoutpending` marks incomplete trees; a folder is fetched only when its `origin` names the listed repository, and the remote contacted is the listed URL, not origin's spelling of it; a `url.<base>.insteadOf` rewrite in the folder's own configuration is refused, and so is its `core.sshCommand` when the fetch is not HTTPS (`RemoteUrl`, #139). The user's global git configuration still applies, as it does to git itself. The recovery file `.git\gclo-recovery.json` is validated when it is loaded: a listed path that is not in the working tree fails the row instead of being deleted (#141) | Mitigated for the remote, the transport program, and the recovery file; other `.git` content of an existing folder is still trusted |
| T5 | PAT handling | Information disclosure | Token leaks to disk, logs, process listings, or UI | Medium | Critical | Memory-only in Quick Sync; Credential Manager otherwise; accounts may reference the default token (`TokenSource`), resolved at use time through one resolver and never copied; logs record lengths; no `--token`; WSL via env only; secret scanning + gitleaks + CodeQL in CI | Mitigated |
@@ -93,7 +95,7 @@ flowchart LR
| T8 | GitHub API / remotes | Spoofing | A MITM impersonates GitHub | Low | Critical | HTTPS with system trust store (Octokit, libgit2); no HTTP fallback: an existing folder whose origin is `http://` or SSH is fetched through the listed HTTPS URL instead (unless the user's own global configuration rewrites that URL) | Mitigated |
| T9 | WSL recovery | Elevation of privilege | Hostile org/repo names injected into the shell script, or used to leave `~/gclo` | Low | High | Script fed on stdin with positional parameters; organization and repository each validated as one plain name inside `WslCloner` (and the organization again before any listing request); the URL must be HTTPS and is passed after `--`; no string interpolation into the script | Mitigated |
| T10 | Logs and crash output | Information disclosure | Exception text carries a token or private path | Low | Medium | Crash net logs messages only; log review in incident handling; tokens never in exception messages (tests) | Mitigated |
| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release | Low | Critical | Build runs in the standards' shared release workflow (SLSA Build L3 provenance, `--signer-repo KofTwentyTwo/standards`), SHA-pinned actions, no caches, read-only tokens, publishing tokens only in the tag-restricted `release` environment, `protect-release-tags`, required gates re-run on the tag | Mitigated |
| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release, or a file is changed on its way to the release server | Low | Critical | Until a pipeline on `git.kof22.com` exists, a release's assets are copied there by hand from the GitHub release (docs/RELEASING.md): that step is outside the attested pipeline, and nothing the app checks ties a file on that server to the provenance (`Update-PackageManifests.ps1` does compare the CLI zip served there with the built one's hash before it writes the manifests). For the build itself: Build runs in the standards' shared release workflow (SLSA Build L3 provenance, `--signer-repo KofTwentyTwo/standards`), SHA-pinned actions, no caches, read-only tokens, publishing tokens only in the tag-restricted `release` environment, `protect-release-tags`, required gates re-run on the tag | Mitigated |
| T12 | Repository | Repudiation | Unattributed change reaches `main` | Low | High | Signed, signed-off commits; squash-only through gated pull requests; no bypass actors | Mitigated |
| T14 | PAT handling | Information disclosure | The token is offered to a host other than the one the repository was listed on: an existing folder whose `origin` points elsewhere, or a server named by the repository's own configuration during the WSL clone (for example an LFS endpoint) | Low | Critical | The git credential callback answers only a remote whose URL is HTTPS on the listed URL's host and port, and refuses every other remote with a clear error (a redirect that the listed host itself issues is followed by libgit2; that host already holds the token); a folder whose origin names another repository fails before any request; the WSL credential helper is scoped to the listed host (`credential.https://<host>.helper`) and an existing WSL destination is pulled only when its stored origin is the listed URL (#139) | Mitigated |
| T15 | Logs and terminal | Spoofing / Tampering | Text from a repository or a server (a file path may contain a newline, an escape sequence, or a character that reorders or hides text) forges an entry in the activity log or drives the terminal the CLI prints to | Low | Medium | `DisplayText` shows control, separator and format characters (direction marks and overrides, invisible characters, tag characters) and unpaired surrogates as visible `\uXXXX` text: every log message is one line, and an exception's lines are indented under their entry so none can start like one. The CLI writes every line through one place, `Terminal`, which also shows any character the console's code page cannot represent in that form, because a legacy code page turns some printable characters into control bytes; a test fails if any other CLI file writes to the console. The JSON modes rely on the JSON encoder (#143) | Mitigated for the log and the CLI. Not covered: the desktop app's own surfaces (row error text, the path-recovery dialog), where a newline is harmless but a direction override in a path the user is asked to approve is not; and text that merely looks like something else (look-alike letters, an escape written out literally) |
@@ -104,8 +106,11 @@ flowchart LR
- T6 and T13: code running as the same Windows user can read Credential Manager
entries and edit the data directory; that is the platform's boundary and is stated
in SECURITY.md.
- Windows binaries are not yet Authenticode-signed: standards exception EX-0002 (#57).
Compensated by immutable releases, `SHA256SUMS`, and attestations.
- T1, and T2 for Scoop: the updater and the Scoop manifest trust the release server
`git.kof22.com` for both the file and its hash (ADR 0009). Releases there are not
immutable, and the copy of a release's assets to that server is a manual step
(T11). The binaries are Authenticode-signed (ADR 0008), which a user can check and
Windows shows at install, but the updater does not check it.
## 8. Review log
@@ -123,3 +128,4 @@ flowchart LR
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | The project's home moved to `git.kof22.com`: the Help menu, the About dialog, the CLI's error text and the documentation link there, and the Scoop bucket is added from there (the manifest that pins the download URL and its hash now comes from that server over HTTPS; T2 is unchanged in kind). Help > Share hands a fixed text (what gclo is, where to download it, the sender's gclo version) to the system's mail handler as a `mailto:` link with no recipient, or shows it and copies it to the clipboard; nothing about the user, their accounts, or their repositories is in it, and the address it is built from must be a plain http(s) host and path (#163, #165). The update feed and the release downloads named in package manifests are still GitHub Releases at this revision. |
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | Working tree (#174; the T3 and T4 rows are unchanged, this adds to what they rely on). On Windows, a forced checkout into a folder whose tree is not finished (the first checkout of a clone that was empty, and the completion of a checkout that was cut off) is preceded by refusals that change nothing: a link on the way or at a destination (except, at a destination where the repository itself has a symbolic link, a link that leads where the repository's does), a file where a folder is needed (also on the way to a submodule), a folder that is not empty where a file is needed, a file at a destination that is not what the unfinished checkout wrote there, and anything but an empty folder where the repository has a submodule. "What the unfinished checkout wrote" is the content, or the beginning of it, of the commit the branch is on or of a commit recorded in `gclo.checkoutwrittenbefore`, as stored or as the checkout filters make it, carriage returns aside. An unfinished checkout is a marked folder that has no index, or that does not hold every file of its commit; a marked folder that has an index and every file is updated like any clone, without force. A completion is refused when the index names a file the commit does not have (somebody added it with git, and the forced checkout would remove it). An unfinished checkout is completed at the commit its branch is on, before the branch moves, except where the branch must move first (a tree Windows cannot write, or a history origin rewrote); the commit it leaves is then recorded, and a path recovery of that folder accepts a file that is, whole, what a recorded commit has. A repository that has local branches is not treated as an empty clone; one without follows origin's default branch only when its folder holds nothing but `.git`. The folder is marked unfinished before its branch is created. One git operation per folder at a time within a process (not across processes, and not across spellings of a path that only the file system can resolve). The WSL clone is made in `~/gclo/.gclo-tmp` and renamed into place when complete (`mv -T`, after a check that nothing is there). Not covered: a file created in the folder between the check and the checkout that follows it; a file of the user's at a destination that is empty, or holds the repository's content or the beginning of it, which is replaced; a link of the user's that leads exactly where the repository's symbolic link at that path does, which is replaced by the repository's; a marked folder whose index somebody's git wrote and whose last file was cut off, which is taken for finished (git shows that file as changed); files of a commit the branch left that the new commit does not have, which stay in the folder untracked; an unfinished clone in which a folder of an earlier commit became a file, or that was cut off by a power loss leaving files of zeros, which is refused until the folder is cleared; platforms other than Windows, where the checkout is forced as before. |
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | Update feed and release downloads moved from GitHub Releases to the project's releases on `git.kof22.com` (ADR 0009, #170): the in-app updater reads the feed with Velopack's Gitea source, anonymously over HTTPS, and the Scoop and Chocolatey manifests download from there. The updater verifies packages against the hashes in the feed, as before; it has never checked a signature or an attestation. The trust that was placed in GitHub's release hosting (account security, immutable releases) is now placed in a self-hosted server: whoever can publish a release there, or holds its TLS certificate or DNS, can deliver an update that installs without a warning. T1 is rewritten to say so and its status changed from mitigated to accepted. The manifests still pin a SHA-256 (T2). |
| 2026-11-10 | v1.1.1 | James Maes (@KofTwentyTwo) | Corrections from the release reviews (#183): the header names the version reviewed; section 1 says that an installed build contacts `git.kof22.com` for updates; the accepted risks no longer say the binaries are unsigned (they are signed, ADR 0008) and name T1 and the Scoop part of T2 instead; T2 and T11 say what ADR 0009 changed (the Scoop manifest and the file it names come from one server; the copy of release assets to that server is manual and outside the attested pipeline). |
+15 -12
View File
@@ -68,7 +68,7 @@ public sealed class VaultEdgeTests
[Fact]
public void ScopedVault_DoesNotSeeTheUnscopedEntry_AndCleansUpItsOwn()
public void AScopedVault_DoesNotSeeAnotherScopesEntry_AndCleansUpItsOwn()
{
if(!OperatingSystem.IsWindows())
{
@@ -76,24 +76,27 @@ public sealed class VaultEdgeTests
}
var id = Guid.NewGuid();
var unscoped = new CredentialManagerVault(isWindows: true, scope: null);
var scoped = new CredentialManagerVault(isWindows: true, scope: "0123456789abcdef");
// Two data roots, neither the one the installed app uses: a test stays out of
// that namespace (#161). That the default data root has no scope in its entry
// names is stated above, without storing anything.
CredentialManagerVault one = NewTestVault();
CredentialManagerVault other = NewTestVault();
try
{
unscoped.Store(id, "ghp_default_profile");
Assert.Null(scoped.TryRetrieve(id)); // another data root never sees the default profile's token
one.Store(id, "ghp_one_profile");
Assert.Null(other.TryRetrieve(id)); // another data root never sees this one's token
scoped.Store(id, "ghp_isolated");
Assert.Equal("ghp_isolated", scoped.TryRetrieve(id));
Assert.Equal("ghp_default_profile", unscoped.TryRetrieve(id)); // and vice versa
other.Store(id, "ghp_isolated");
Assert.Equal("ghp_isolated", other.TryRetrieve(id));
Assert.Equal("ghp_one_profile", one.TryRetrieve(id)); // and vice versa
}
finally
{
unscoped.Delete(id);
scoped.Delete(id);
one.Delete(id);
other.Delete(id);
}
Assert.Null(unscoped.TryRetrieve(id));
Assert.Null(scoped.TryRetrieve(id));
Assert.Null(one.TryRetrieve(id));
Assert.Null(other.TryRetrieve(id));
}