Files
TREK/sonar-project.properties
T
Maurice 2c2d008dc2 fix: clear every open Sonar security issue, and half the reliability ones (#2037)
* fix(security): mint idempotency keys properly, and resolve the extractor by path

Three findings out of a sweep of every open Sonar security issue. The other 49
turned out to be false positives and are documented in sonar-project.properties
rather than changed — each entry scoped to a file, never a whole rule, so a
future Math.random() on an auth path still fails.

The idempotency key is the one that can lose data. Both producers fell back to
Math.random when crypto.randomUUID was missing, and randomUUID needs a secure
context — TREK's own quickstart is http://localhost:3000 and LAN installs run on
http://192.168.x.x, so on the deployments the README documents the fallback is
what actually runs. The axios one was
`Math.random().toString(36).slice(2)`, which is not length-stable: 0.5 yields a
single character and 0 yields the empty string, which the interceptor reads as
"no key" and skips deduplication for entirely. Two writes sharing a key collapse
into one — the second gets the first one's cached response and the edit is gone
with no error anywhere. Both now come from utils/randomId, which has the
getRandomValues rung that works in insecure contexts too.

The extractor probe stored the bare binary name, so every extraction re-resolved
it through whatever PATH held at that moment. The official image sets
KITINERARY_EXTRACTOR_PATH and never reaches that branch, but a bare-metal install
without it ran an unqualified name from a PATH it does not control. It now walks
the search path itself and keeps the absolute path of the file it verified;
execFileSync also drops the /bin/sh hop the old probe needed. PATH comes through
app-config because process.env is walled off in src/.

* fix: four tests and sorts that were not doing what they claimed

Sonar reported 43 high-impact reliability issues. Reading all of them, four are
real; the rest are cosmetic or would be made worse by the suggested change.

budget.test.ts hand-built `paid: true`, but both emitters send `paid: body.paid
? 1 : 0` and the handler stores the value verbatim — the test asserted a payload
the server never sends. The line below it already used a number.

admin.controller.test.ts had a `.resolves` assertion with no await. The promise
was dropped, so that line could not fail the test.

The two to-do category lists sorted with a bare .sort(), which is byte order:
every accented name files behind the whole ASCII range, so in a German trip
"Übernachtung" lands after "Zelt". TREK ships 23 locales, so this is real data.
One side effect worth knowing: katColor picks a category's dot colour by its
index, so re-sorting can move which category shows which colour — it already
moves whenever a category is added, and the colour carries no meaning.

Deliberately not changed: the two `| 0` truncations (Math.trunc is not
equivalent — `| 0` wraps at 32 bits and these carry millisecond timestamps), the
two .map() callbacks whose second parameter is declared and used, and the sha1
in thumbnail.service (its own comment says a swap would orphan every existing
thumbnail).

* refactor: give every button a type, and prefer the Number statics

Two mechanical sweeps, no behaviour change intended in either.

A <button> with no type attribute is type="submit" per the HTML spec. Outside a
form that does nothing today, but it turns into a surprise the moment the markup
is wrapped in one, which is what Sonar's S9011 is warning about. 870 buttons get
an explicit type="button". Two things were left alone on purpose: buttons that
take a {...spread}, because the spread may carry a type and the merge order
decides which wins, and anything inside a <form> — there the answer is a
judgement call, and checking all eleven form files found every real submit button
already carrying type="submit".

The Number sweep covers only the three globals that are exactly equivalent to
their Number twin: parseInt, parseFloat and NaN. isNaN is NOT in it — the global
coerces its argument, so isNaN('abc') is true where Number.isNaN('abc') is false,
and renaming it would change what the code does. It reaches server and plugin-sdk
as well as the client; both copies of egress-policy.ts got the identical edit, so
the mirror the SDK's parity test guards stays intact.
2026-08-25 15:22:38 +02:00

148 lines
7.9 KiB
Properties

sonar.projectKey=TREK
sonar.projectName=TREK
sonar.sourceEncoding=UTF-8
# Sources — one entry per workspace package (plugin-sdk is standalone but lives in-repo)
sonar.sources=client/src,server/src,shared/src,plugin-sdk/src
# Tests — dedicated test dirs per package, plus co-located client tests in src
# and co-located shared schema/sanitize/i18n specs in shared/src
sonar.tests=server/tests,client/tests,client/e2e,client/src,shared/src,plugin-sdk/test
sonar.test.inclusions=\
server/tests/**/*.test.ts,\
client/tests/**/*.test.ts,\
client/tests/**/*.test.tsx,\
client/e2e/**/*.spec.ts,\
client/src/**/*.test.ts,\
client/src/**/*.test.tsx,\
shared/src/**/*.spec.ts,\
plugin-sdk/test/**/*.test.ts
# Keep generated output and test files out of source analysis
sonar.exclusions=\
**/node_modules/**,\
**/dist/**,\
**/build/**,\
**/coverage/**,\
client/dev-dist/**,\
**/*.test.ts,\
**/*.test.tsx,\
**/*.spec.ts,\
**/*.d.ts
# Coverage — lcov paths relative to repo root, one per package
# (each package's vitest config emits lcov to <pkg>/coverage/).
sonar.javascript.lcov.reportPaths=\
server/coverage/lcov.info,\
client/coverage/lcov.info,\
shared/coverage/lcov.info,\
plugin-sdk/coverage/lcov.info
# Infrastructure/bootstrap files plus generated/data files the vitest configs
# exclude from instrumentation (a file absent from lcov would otherwise count
# as 0% covered here — keep these in sync with each package's vitest.config.ts)
sonar.coverage.exclusions=\
server/src/index.ts,\
server/src/config.ts,\
server/src/db/database.ts,\
server/src/db/migrations.ts,\
server/src/db/seeds.ts,\
server/src/demo/**,\
server/src/nest/plugins/runtime/plugin-host-entry.ts,\
client/src/main.tsx,\
client/src/types.ts,\
shared/src/plugin-permissions.ts,\
shared/src/i18n/*/**,\
plugin-sdk/src/generated/**,\
plugin-sdk/src/lucide-icon-names.ts
# Locale files are large near-identical literal objects — keep them out of duplication metrics
sonar.cpd.exclusions=shared/src/i18n/**
# Locale files are UI copy — strings like "Password", "API key", "token" are labels,
# not credentials, and the secret detectors false-positive on them constantly.
# Ignore the secrets analyzer and the hard-coded-credential rules on i18n only.
sonar.issue.ignore.multicriteria=i18nsec1,i18nsec2,i18nsec3,rand1,rand2,rand3,rand4,rand5,cli1,hash1,hash2,hash3,hash4,ns1,ns2,frame1
sonar.issue.ignore.multicriteria.i18nsec1.ruleKey=secrets:*
sonar.issue.ignore.multicriteria.i18nsec1.resourceKey=shared/src/i18n/**
sonar.issue.ignore.multicriteria.i18nsec2.ruleKey=typescript:S2068
sonar.issue.ignore.multicriteria.i18nsec2.resourceKey=shared/src/i18n/**
sonar.issue.ignore.multicriteria.i18nsec3.ruleKey=typescript:S6418
sonar.issue.ignore.multicriteria.i18nsec3.resourceKey=shared/src/i18n/**
# S2245 (Math.random in a security context) — scoped per file, never globally, so
# a future Math.random() on an auth, token or crypto path still fails the gate.
# The two places where the value DID reach something that matters — the two
# idempotency-key producers — were fixed instead of listed here; they now share
# client/src/utils/randomId.ts.
#
# Studio + studioStore: element and spread ids inside a photo-book document. The
# whole document is stored and fetched as one blob scoped by journey id, so an
# element id is never a URL, a lookup key or an authorization subject. Worst case
# of a collision is documented in studioStore itself: two elements would share a
# selection.
sonar.issue.ignore.multicriteria.rand1.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.rand1.resourceKey=client/src/components/Studio/**
sonar.issue.ignore.multicriteria.rand2.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.rand2.resourceKey=client/src/store/studioStore.ts
# LoginWorld: canvas animation parameters — arc endpoints, flight speed, spawn
# delays. Nothing crosses a trust boundary; nothing leaves the browser.
sonar.issue.ignore.multicriteria.rand3.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.rand3.resourceKey=client/src/pages/login/LoginWorld.tsx
# placesSession: a Google Places session token. It is a BILLING correlator, not a
# credential — Google groups requests by (API key, token) for pricing. Suggestions
# derive from the typed input, never from the token, so a guessed value cannot
# pull another user's results. The Math.random rung is also the third fallback,
# reached only where Web Crypto is absent entirely.
sonar.issue.ignore.multicriteria.rand4.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.rand4.resourceKey=client/src/utils/placesSession.ts
# Cost-split rows: React list keys for ephemeral ticket rows, dropped before the
# payload is serialised (see writeTicketItems) so they never reach the API or DB.
sonar.issue.ignore.multicriteria.rand5.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.rand5.resourceKey=client/src/components/Budget/CostsPanel.tsx
sonar.issue.ignore.multicriteria.cli1.ruleKey=typescript:S2245
sonar.issue.ignore.multicriteria.cli1.resourceKey=client/src/mobile/screens/trip/sheets/MCostSheet.tsx
# S4790 (weak hash) — every one of these derives a cache key or a filename, never
# a token, signature or integrity check, and the digest never reaches a client:
# the public URLs carry the plain placeId/photoId. Every genuine security hash in
# the server is already sha256 (backup codes, tokens, PKCE, the plugin audit
# chain). Swapping these would only orphan existing cached files — thumbnail.ts
# says so in its own comment.
sonar.issue.ignore.multicriteria.hash1.ruleKey=typescript:S4790
sonar.issue.ignore.multicriteria.hash1.resourceKey=server/src/nest/memories/**
sonar.issue.ignore.multicriteria.hash2.ruleKey=typescript:S4790
sonar.issue.ignore.multicriteria.hash2.resourceKey=server/src/nest/place-photos/**
sonar.issue.ignore.multicriteria.hash3.ruleKey=typescript:S4790
sonar.issue.ignore.multicriteria.hash3.resourceKey=server/src/nest/place-enrichment/**
# S4036 (OS command via PATH) — the plugin SDK is a CLI a plugin author runs on
# their own machine, and reaching git/npm through the developer's own PATH is the
# point of it. Every invocation passes an argv array, so nothing is shell-parsed.
# The one server-side site was NOT suppressed: it now resolves its binary to an
# absolute path (see kitinerary-extractor.service.ts).
sonar.issue.ignore.multicriteria.hash4.ruleKey=typescript:S4036
sonar.issue.ignore.multicriteria.hash4.resourceKey=plugin-sdk/src/cli/**
# S5332 (clear-text protocol) — neither site opens a connection.
# gpx-export: the GPX and XSI namespace URIs plus the schemaLocation pair. A
# namespace is an exact identifier fixed by the schema, so rewriting it to
# https:// would produce GPX that other tools reject.
sonar.issue.ignore.multicriteria.ns1.ruleKey=typescript:S5332
sonar.issue.ignore.multicriteria.ns1.resourceKey=server/src/nest/places/gpx-export.helpers.ts
# host-surface: 'http://x.invalid' is a throwaway base handed to new URL() so a
# relative path can be parsed — the very next line rejects the value if it
# resolved anywhere else. The string is part of the guard, not a destination, and
# .invalid is reserved by RFC 2606 precisely so it can never resolve.
sonar.issue.ignore.multicriteria.ns2.ruleKey=typescript:S5332
sonar.issue.ignore.multicriteria.ns2.resourceKey=server/src/nest/plugins/host/rpc/host-surface.rpc.ts
# S2819 (postMessage target origin) — the plugin frame is embedded with
# sandbox="allow-scripts allow-forms" and deliberately WITHOUT allow-same-origin,
# so the document inside has an opaque origin. There is no origin string that
# would match it: '*' is the only value that works, which makes the rule
# unsatisfiable here rather than unsatisfied. The receiving side authenticates on
# event.source instead.
sonar.issue.ignore.multicriteria.frame1.ruleKey=typescript:S2819
sonar.issue.ignore.multicriteria.frame1.resourceKey=client/src/components/Plugins/PluginFrame.tsx