docs(backend): say what CANARYTOKENS_API_URL is for and what empty costs - #1938
Merged
Merged
Conversation
…e to write The lab's whole premise is that a theme is reviewed against real captured data rather than fixtures -- both previous design refreshes were run that way and that is what shipped. The harness that made it possible went with the Go dashboard in cb77cdf, so the pattern branding/design-lab/ documents has not been executable since. The Go version got its safety from constructing the server with nil write-services. frontend-next has no such handle: it reaches data over HTTP through two bases, and one of them is write-capable by definition. So the guarantee is made at that seam instead -- BACKEND_URL a gate forwarding GET/HEAD, refusing the rest with 405 BACKEND_MOUNTED_URL nothing at all; every request 503s -- which is stronger than what it replaces, because it is enforced per request rather than by remembering to pass nil. That is not a formality. The Rust tier really exposes stores.rs's generic_delete, preferences.rs's put, the honeyfs implant writer and the canarytoken minter, so a reviewer clicking through a variant would otherwise delete real documents and mint real tokens against live infrastructure. lab.test.mjs drives the actual harness against a recording stand-in backend and asserts a write never arrives; it runs in CI, because the guarantee is the point of the tool and a silent regression would be indistinguishable from it working. Variant stylesheets are symlinked into the dev server's public/ tree, so a variant layers on the vendored theme.css with no rebuild and no change to the shipped vite config -- and saving the source file is enough to see it. Ports are the ones the lab has always used, so the review log's links keep resolving. Verified end to end against the homeserver's real backend: the page renders cowrie, dionaea, wordpot, galah and suricata with live states, carries both stylesheets in the right order, and the gate refuses a delete without it reaching Elasticsearch. Booting it also surfaced that routeShape.test.ts was being scanned as a route file and warned on every dev-server start; renamed to the generator's ignore prefix, which vitest's own include pattern is unaffected by. Closes #1828
…t have Chasing #1566's "near-duplicate rows" on /ml-anomalies turned up why they looked the way they did: the list was not in any order at all. Store pages sort with `unmapped_type` set, so a store whose index does not exist yet returns an empty list instead of an error. The same setting means a field the documents do not have sorts every hit as null rather than failing -- and nothing anywhere says so. Three stores shipped that way: ml-anomalies asked for `timestamp` where the worker writes `@timestamp`, auth-events asked for `last_seen` where Keycloak writes `@timestamp`, and static-analysis asked for `Analysis.GeneratedUTC`, which its documents do not carry in any spelling -- they hold only Analysis and Fingerprint, and no time field whatsoever. Measured on the live index, the first page of /ml-anomalies read 20:58, 20:58, 20:59, 20:59, 20:59, 20:57, 20:57, 20:58. The newest document was third by luck. Worse than the disorder: an all-null sort is one enormous tie, and `from`/`size` over a tie leaves the order inside it undefined. Paging could hand back the same document twice and never show another. So every store sort now carries `_doc` as a tiebreak -- which the numeric sorts needed anyway, since campaigns-by-score and attackers-by-events tie constantly -- and each declares its field's real type, because an `unmapped_type` that disagrees with the mapping is the same silent no-op for keyword sorts. CI cannot see Elasticsearch, so scripts/check-store-sorts.py is the guard: it checks all 23 declared sort fields against a live mapping dump. Run against the homeserver it passes, and re-introducing either bug class fails it with the reason. With the order fixed, the duplicates #1566 actually reported are adjacent for the first time, so they can be folded. 66% of the 7,305 live anomalies sit in an (address, same second) bucket with siblings; the worst single burst is 48 rows for one IP inside one second, every one scoring exactly 0.8000. Consecutive rows from one address in one second scoring within 0.01 now collapse to one row badged with what it stands for. Acknowledging a folded row acknowledges all of it -- otherwise it would return as open on the next refresh and the button would look broken while behaving exactly as written -- and the status badge reports "3 of 48 open" rather than speaking only for whichever row represents the run. Folding is deliberately page-local and says so in the badge: doing it in the Rust tier would change the shape of the shared /api/v1/store endpoint for every other consumer, and a burst longer than one page still splits at the boundary. Also verified while here, and already correct: page-title casing is uniformly sentence case across all 27 headers, and /campaigns renders 7 columns rather than 15 -- the rest are behind `detail: true`. Closes #1566
… into Leaflet sizes its tile grid once, against whatever the container measured at construction time, and the map is built before the card has settled at its final width. Measured at an 1854px viewport: six 256px tile columns laid out from the wrong origin for a 1442px container, leaving a 92px strip of bare card down the right-hand edge. It reads as a rendering fault rather than as ocean, which is how #1565 reported it. invalidateSize() re-measures and recomputes the grid. A ResizeObserver repeats that whenever the card changes width, because a fixed zoom covers a fixed pixel width and the card's width is not fixed -- a window resize puts the strip straight back otherwise. Verified on a rendered page against live data, at two widths. Before, at 1442px: tiles ended 92px short on the right. After: every edge overflows the container (-75 left, -19 right, -179 top, -69 bottom) and the whole world is still in frame, Australia and southern Africa included. Narrowed to a 488px card the observer holds it (-552/-496/-249/-139). Fitting the world to *cover* the card was the other candidate and is worse: it zooms to 3 and crops the southern hemisphere, trading data for coverage. This keeps zoom 2 and simply stops lying to leaflet about how big the card is. Closes #1565
Every other honeytoken component was live and healthy -- the Canarytokens platform, the adapter, the fired-event pipeline, the store, the page -- while token creation answered 503 "not configured", because this one variable was never set on the host. It was never set because this file described it as "optional, defaults to '' if unset" and nothing more, while the variable directly below it got a paragraph explaining where the service lives and why. HONEYFS_IMPLANT_URL had already been mis-set once, in the sibling stack's env where nothing read it, and the fix for that was the explanation it now carries. So this one gets the same: where the service is published, that it is WireGuard-only, and that leaving it empty disables token creation quietly rather than loudly. Refs #1487
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every honeytoken component was live and healthy — the self-hosted Canarytokens platform, the adapter, the fired-event pipeline, the store, the page — while token creation answered
503 canarytoken creation is not configured on this host.CANARYTOKENS_API_URLhad never been set on the host, so Compose interpolated the empty default andcanarytokens::createshort-circuited.Nothing about that is visible from the dashboard: the tokens page lists, the types endpoint answers, the platform serves. Only creation is off.
It was never set because this file described it as
optional, defaults to '' if unsetand nothing more — whileHONEYFS_IMPLANT_URL, directly below, carries a paragraph explaining where the service lives and why. That paragraph exists because that variable had already been mis-set once, in the sibling stack's env where nothing read it. This gives the same treatment to the one that hadn't had its turn yet.Fixed live and verified end to end
The variable is now set on the homeserver and the backend recreated. All five items of #1487 exercised against the live fleet:
POST /api/v1/canarytokens→ 200, real token id + URL, persisted todashboard-canarytokens-v1(6 rows, newest first)fs.pickleat/opt/backup/…, which is what makes it visible tols/catrather than just bytes on diskPOST /credentials/{id}/rotate→ 200, new password,rotated_atset — and the file on disk carries the rotated value with a matching mtimeGET /api/v1/credentialsreturns target, real path, username and current passwordPOST /credentials/{id}/link-token→ 200,linked_token_idsetConfirmed cowrie itself reads the implanted file from its honeyfs mount and returns the rotated contents.
Worth noting for anyone repeating this:
fs.pickle's node format puts contents at index 7. Walking it at the wrong index reports the file as absent when it is indexed correctly — the first check here said "NOT FOUND" for exactly that reason.Refs #1487