From f9ca38a8e05b96074995f0fa1a89baed9cedf7f8 Mon Sep 17 00:00:00 2001 From: SMOSTAFAH1 Date: Wed, 4 Nov 2026 19:16:38 +0000 Subject: [PATCH 1/4] fix(security): resolve SonarCloud code-scanning alerts - electron rest proxy: normalise the upstream path, drop __proto__ headers - desktop app protocol: BaaS relay refuses targets off the configured origin - curl downloads (scripts, Dockerfiles): HTTPS-only (--proto '=https' --tlsv1.2) - ide-sandbox: npm install -g --ignore-scripts - vault42-recover-dumps: sha256sum instead of md5sum for tree comparison - smoke tests: message origin check; os.tmpdir() instead of /tmp - justified NOSONAR on genuine false positives (seeded demo RNG, egress deny-lists, local-network http, demo seed credentials, maintainer CLIs) --- apps/osionos-desktop/docker/build.Dockerfile | 2 +- apps/osionos-electron/build-native.sh | 4 +- apps/osionos-electron/desktop-app-protocol.js | 8 +- apps/osionos-electron/desktop-backend.js | 2 +- apps/osionos-electron/native/firstrun.mjs | 3 +- apps/osionos-electron/native/restProxy.mjs | 10 +- .../native/supervisor-run.mjs | 3 +- .../osionos-electron/test/smoke/pty-smoke.mjs | 8 +- .../test/smoke/terminal-page-smoke.mjs | 2 +- archive/wiki-2026-09/docker-compose.yml | 2 +- archive/wiki-2026-09/translate_doc.py | 2 +- .../docker/osionos/ide-egress-proxy/proxy.mjs | 12 +- .../docker/osionos/ide-sandbox/Dockerfile | 8 +- .../docker/osionos/ide-socket-proxy/proxy.mjs | 2 +- models/seeds.sql | 4 +- scripts/lib/grobase-link-target.sh | 6 +- scripts/reencrypt-mounts.sh | 2 +- scripts/vault42-recover-dumps.sh | 2 +- tools/seeds/gourmand_app_env.py | 6 +- tools/seeds/seed_agency.py | 120 +++++++++--------- tools/seeds/seed_agency_wiki.py | 6 +- 21 files changed, 113 insertions(+), 101 deletions(-) diff --git a/apps/osionos-desktop/docker/build.Dockerfile b/apps/osionos-desktop/docker/build.Dockerfile index d5d24c745..d22c89f81 100644 --- a/apps/osionos-desktop/docker/build.Dockerfile +++ b/apps/osionos-desktop/docker/build.Dockerfile @@ -31,7 +31,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Node 22 for the osionos (Vite) frontend build + create-tauri-app. -RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ +RUN curl -fsSL --proto '=https' --tlsv1.2 https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* \ && corepack enable diff --git a/apps/osionos-electron/build-native.sh b/apps/osionos-electron/build-native.sh index bfa91d817..1636b7135 100755 --- a/apps/osionos-electron/build-native.sh +++ b/apps/osionos-electron/build-native.sh @@ -37,13 +37,13 @@ cp models/*.sql "$RT/models/" echo "[4/5] acquire binaries (cached in $CACHE): embedded postgres $PG_VER + postgrest $PGRST_VER" if [ ! -f "$CACHE/pg-$PG_VER.txz" ]; then - curl -fsSL -o "$CACHE/pg.jar" "https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-linux-amd64/${PG_VER}/embedded-postgres-binaries-linux-amd64-${PG_VER}.jar" + curl -fsSL --proto '=https' --tlsv1.2 -o "$CACHE/pg.jar" "https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-linux-amd64/${PG_VER}/embedded-postgres-binaries-linux-amd64-${PG_VER}.jar" rm -rf "$CACHE/pgx"; mkdir -p "$CACHE/pgx"; ( cd "$CACHE/pgx" && unzip -oq "../pg.jar" ) mv "$CACHE/pgx"/postgres-linux-*.txz "$CACHE/pg-$PG_VER.txz"; rm -rf "$CACHE/pgx" "$CACHE/pg.jar" fi tar -xJf "$CACHE/pg-$PG_VER.txz" -C "$RT/pgsql" if [ ! -f "$CACHE/postgrest-$PGRST_VER" ]; then - curl -fsSL -o "$CACHE/pgrst.tar.xz" "https://github.com/PostgREST/postgrest/releases/download/${PGRST_VER}/postgrest-${PGRST_VER}-linux-static-x64.tar.xz" + curl -fsSL --proto '=https' --tlsv1.2 -o "$CACHE/pgrst.tar.xz" "https://github.com/PostgREST/postgrest/releases/download/${PGRST_VER}/postgrest-${PGRST_VER}-linux-static-x64.tar.xz" tar -xJf "$CACHE/pgrst.tar.xz" -C "$CACHE"; mv "$CACHE/postgrest" "$CACHE/postgrest-$PGRST_VER"; rm -f "$CACHE/pgrst.tar.xz" fi cp "$CACHE/postgrest-$PGRST_VER" "$RT/bin/postgrest"; chmod +x "$RT/bin/postgrest" diff --git a/apps/osionos-electron/desktop-app-protocol.js b/apps/osionos-electron/desktop-app-protocol.js index 22d83a8a9..bdb413833 100644 --- a/apps/osionos-electron/desktop-app-protocol.js +++ b/apps/osionos-electron/desktop-app-protocol.js @@ -67,9 +67,15 @@ function isRegularFile(file) { // (seen as net::ERR_UNEXPECTED). It never throws: a failure is a 502. async function relayToBaas(net, baasTarget, request, url) { const target = baasTarget + url.pathname.slice("/__baas".length) + url.search; + // The relay only ever talks to the configured BaaS: refuse anything that resolves elsewhere. + try { + if (new URL(target).origin !== new URL(baasTarget).origin) return new Response(JSON.stringify({ error: "baas proxy refused" }), { status: 400, headers: baasResponseHeaders("application/json") }); + } catch { + return new Response(JSON.stringify({ error: "baas proxy refused" }), { status: 400, headers: baasResponseHeaders("application/json") }); + } const headers = new Headers(); for (const name of ["content-type", "accept", "x-baas-api-key", "apikey", "authorization"]) { - const value = request.headers.get(name); + const value = request.headers.get(name); // NOSONAR S5144: target restricted to the configured local upstream if (value) headers.set(name, value); } const init = { method: request.method, headers }; diff --git a/apps/osionos-electron/desktop-backend.js b/apps/osionos-electron/desktop-backend.js index 51b6f83f1..65ad2c14e 100644 --- a/apps/osionos-electron/desktop-backend.js +++ b/apps/osionos-electron/desktop-backend.js @@ -24,7 +24,7 @@ function bootSuite() { ? ["compose", "-f", localCompose, "--profile", "local", "up", "-d"] : ["compose", "--profile", "dev", "up", "-d"]; try { - const child = spawn("docker", args, { cwd, detached: true, stdio: "ignore" }); + const child = spawn("docker", args, { cwd, detached: true, stdio: "ignore" }); // NOSONAR S4036: fixed tool name, PATH is the dev/CI image child.on("error", () => {}); // docker missing / no compose -> the app just shows offline child.unref(); } catch { /* ignore */ } diff --git a/apps/osionos-electron/native/firstrun.mjs b/apps/osionos-electron/native/firstrun.mjs index 24b39235f..98ee7e1dc 100644 --- a/apps/osionos-electron/native/firstrun.mjs +++ b/apps/osionos-electron/native/firstrun.mjs @@ -15,6 +15,7 @@ import { randomBytes, createHmac } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { tmpdir } from "node:os"; const HERE = dirname(fileURLToPath(import.meta.url)); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -159,7 +160,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { superUser: process.env.PGSUPERUSER || "postgres", superPass: process.env.PGSUPERPASS || "postgres", migrationsDir: process.env.OSIONOS_MIGRATIONS_DIR || join(HERE, "..", "..", "..", "models"), - dataDir: process.env.OSIONOS_DATA_DIR || "/tmp/osio-native", + dataDir: process.env.OSIONOS_DATA_DIR || join(tmpdir(), "osio-native"), }; const { bootstrapped } = await firstRun(cfg); console.log(`[firstrun] ${bootstrapped ? "bootstrapped schema" : "schema already present"}; secrets in ${cfg.dataDir}/secrets.json`); diff --git a/apps/osionos-electron/native/restProxy.mjs b/apps/osionos-electron/native/restProxy.mjs index 9f90b2951..ac8970ad4 100644 --- a/apps/osionos-electron/native/restProxy.mjs +++ b/apps/osionos-electron/native/restProxy.mjs @@ -18,10 +18,14 @@ export function startRestProxy({ listenPort, routes }) { const url = req.url || "/"; const route = compiled.find((r) => url === r.prefix || url.startsWith(`${r.prefix}/`) || url.startsWith(`${r.prefix}?`)); if (!route) { res.writeHead(404).end('{"error":"no route"}'); return; } - const path = url.slice(route.prefix.length) || "/"; - const t = route.target; + // Keep the request on the route's own host: one leading slash, no scheme-relative "//host". + const path = `/${(url.slice(route.prefix.length) || "/").replace(/^\/+/, "")}`; + const t = route.target; // NOSONAR S5144: target restricted to the configured local upstream + // Copy own header entries only (a "__proto__" header must not reach the options object). + const headers = Object.fromEntries(Object.entries(req.headers).filter(([name]) => name !== "__proto__")); + headers.host = t.host; const upstream = http.request( - { hostname: t.hostname, port: t.port, path, method: req.method, headers: { ...req.headers, host: t.host } }, + { hostname: t.hostname, port: t.port, path, method: req.method, headers }, (up) => { res.writeHead(up.statusCode || 502, up.headers); up.pipe(res); }, ); upstream.on("error", () => { if (!res.headersSent) res.writeHead(502); res.end('{"error":"rest proxy upstream failed"}'); }); diff --git a/apps/osionos-electron/native/supervisor-run.mjs b/apps/osionos-electron/native/supervisor-run.mjs index b6037f347..f780ab98d 100644 --- a/apps/osionos-electron/native/supervisor-run.mjs +++ b/apps/osionos-electron/native/supervisor-run.mjs @@ -8,9 +8,10 @@ import { startSuite, DEFAULT_PORTS } from "./supervisor.mjs"; import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { tmpdir } from "node:os"; const RT = "/rt"; -const dataDir = process.env.OSIO_DATA || "/tmp/osio-data"; +const dataDir = process.env.OSIO_DATA || join(tmpdir(), "osio-data"); // Use the BUNDLED binaries (zonky postgres + postgrest) so the test exercises the // real shippable bundle — not a fuller system Postgres that happens to have psql. const PGBIN = process.env.PGBIN || `${RT}/pgsql/bin`; diff --git a/apps/osionos-electron/test/smoke/pty-smoke.mjs b/apps/osionos-electron/test/smoke/pty-smoke.mjs index 3b12133bd..7281f6bfb 100644 --- a/apps/osionos-electron/test/smoke/pty-smoke.mjs +++ b/apps/osionos-electron/test/smoke/pty-smoke.mjs @@ -23,7 +23,7 @@ async function check(name, fn) { } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const assert = (ok, msg) => { if (!ok) throw new Error(msg); }; -const ps = () => execFileSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" }).trim().split("\n") +const ps = () => execFileSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" }).trim().split("\n") // NOSONAR S4036: fixed tool name, PATH is the dev/CI image .map((l) => l.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/)).filter(Boolean).map(([, pid, ppid, args]) => ({ pid: +pid, ppid: +ppid, args })); const hostPid = () => ps().find((p) => /--type=utility/.test(p.args) && /node\.mojom\.NodeService/.test(p.args))?.pid ?? null; const shellsOf = (pid) => ps().filter((p) => p.ppid === pid).length; @@ -42,8 +42,8 @@ page.on("console", (m) => { const open = (cols = 80, rows = 24) => page.evaluate(async ([c, r]) => { if (!window.__ports) { Object.assign(window, { __ports: {}, __out: {}, __exit: {}, __portExit: {} }); - window.addEventListener("message", (e) => { - if (e.source !== window || e.data?.type !== "osionos-pty-port") return; + window.addEventListener("message", (e) => { // NOSONAR S2819: same-window message, source is checked + if (e.origin !== window.location.origin || e.source !== window || e.data?.type !== "osionos-pty-port") return; const { id } = e.data; const port = e.ports[0]; window.__ports[id] = port; @@ -120,7 +120,7 @@ try { const server = http.createServer((_q, s) => { s.writeHead(200, { "content-type": "text/html" }); s.end("m"); }).listen(47199, "127.0.0.1"); try { const child = app.waitForEvent("window", { timeout: 10000 }); - await page.evaluate(() => { window.open("http://127.0.0.1:47199/?node=m-abcdefghijklmnop"); }); + await page.evaluate(() => { window.open("http://127.0.0.1:47199/?node=m-abcdefghijklmnop"); }); // NOSONAR S5148: test-only window the harness controls const win = await child; // Electron opens the child on about:blank, then navigates it. await win.waitForURL(/127\.0\.0\.1:47199/, { timeout: 10000 }); diff --git a/apps/osionos-electron/test/smoke/terminal-page-smoke.mjs b/apps/osionos-electron/test/smoke/terminal-page-smoke.mjs index 4c4b92cc4..e0783ee6f 100644 --- a/apps/osionos-electron/test/smoke/terminal-page-smoke.mjs +++ b/apps/osionos-electron/test/smoke/terminal-page-smoke.mjs @@ -16,7 +16,7 @@ async function check(name, fn) { } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const assert = (ok, msg) => { if (!ok) throw new Error(msg); }; -const ps = () => execFileSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" }).trim().split("\n") +const ps = () => execFileSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" }).trim().split("\n") // NOSONAR S4036: fixed tool name, PATH is the dev/CI image .map((l) => l.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/)).filter(Boolean).map(([, pid, ppid, args]) => ({ pid: +pid, ppid: +ppid, args })); const hostPid = () => ps().find((p) => /--type=utility/.test(p.args) && /node\.mojom\.NodeService/.test(p.args))?.pid ?? null; const shells = () => { const h = hostPid(); return h ? ps().filter((p) => p.ppid === h).length : 0; }; diff --git a/archive/wiki-2026-09/docker-compose.yml b/archive/wiki-2026-09/docker-compose.yml index 76287d16d..3548d20a0 100644 --- a/archive/wiki-2026-09/docker-compose.yml +++ b/archive/wiki-2026-09/docker-compose.yml @@ -46,7 +46,7 @@ services: environment: NODE_ENV: production DATA_API_PORT: 3001 - DATABASE_URL: postgresql://${POSTGRES_USER:-transcendence}:${POSTGRES_PASSWORD:-transcendence}@db:5432/${POSTGRES_DB:-transcendence} + DATABASE_URL: postgresql://${POSTGRES_USER:-transcendence}:${POSTGRES_PASSWORD:-transcendence}@db:5432/${POSTGRES_DB:-transcendence} # NOSONAR S6698: archived compose, local-only default MONGO_URL: mongodb://mongo:27017/${POSTGRES_DB:-transcendence} CORS_ORIGINS: http://localhost:${FRONTEND_PORT:-8080} healthcheck: diff --git a/archive/wiki-2026-09/translate_doc.py b/archive/wiki-2026-09/translate_doc.py index 12b892d75..81519c62b 100644 --- a/archive/wiki-2026-09/translate_doc.py +++ b/archive/wiki-2026-09/translate_doc.py @@ -153,7 +153,7 @@ def translate_text(text: str, src: str, tgt: str, tally: Tally, label: str = "") def translate_markdown(src_path: Path, dst_path: Path, src: str, tgt: str, tally: Tally) -> None: """Translate Markdown while leaving fenced code blocks untouched.""" - raw = src_path.read_text(encoding="utf-8") + raw = src_path.read_text(encoding="utf-8") # NOSONAR S8707: maintainer CLI, the path is the operator's own argument lines = raw.splitlines(keepends=False) segments: list[tuple[str, str]] = [] diff --git a/infrastructure/docker/osionos/ide-egress-proxy/proxy.mjs b/infrastructure/docker/osionos/ide-egress-proxy/proxy.mjs index ad702c81c..ad54533f7 100644 --- a/infrastructure/docker/osionos/ide-egress-proxy/proxy.mjs +++ b/infrastructure/docker/osionos/ide-egress-proxy/proxy.mjs @@ -211,13 +211,13 @@ if (isMain && process.argv.includes("--selfcheck")) { expect("rfc1918 192.168 denied", denied4("192.168.1.1"), true); expect("loopback denied", denied4("127.0.0.1"), true); expect("cgnat denied", denied4("100.64.0.1"), true); - expect("public allowed", denied4("8.8.8.8"), false); - expect("public cloudflare", denied4("1.1.1.1"), false); + expect("public allowed", denied4("8.8.8.8"), false); // NOSONAR S1313: egress deny-list, the addresses are the point + expect("public cloudflare", denied4("1.1.1.1"), false); // NOSONAR S1313: egress deny-list, the addresses are the point expect("v6 loopback denied", denied6("::1"), true); - expect("v6 ula denied", denied6("fd00:ec2::254"), true); - expect("v6 linklocal denied", denied6("fe80::1"), true); - expect("v6 mapped metadata denied", denied6("::ffff:169.254.169.254"), true); - expect("v6 public allowed", denied6("2606:4700:4700::1111"), false); + expect("v6 ula denied", denied6("fd00:ec2::254"), true); // NOSONAR S1313: egress deny-list, the addresses are the point + expect("v6 linklocal denied", denied6("fe80::1"), true); // NOSONAR S1313: egress deny-list, the addresses are the point + expect("v6 mapped metadata denied", denied6("::ffff:169.254.169.254"), true); // NOSONAR S1313: egress deny-list, the addresses are the point + expect("v6 public allowed", denied6("2606:4700:4700::1111"), false); // NOSONAR S1313: egress deny-list, the addresses are the point if (fail.length) { process.stderr.write(`selfcheck FAIL:\n${fail.join("\n")}\n`); process.exit(1); } process.stdout.write("selfcheck OK: egress allow/deny tables correct\n"); process.exit(0); diff --git a/infrastructure/docker/osionos/ide-sandbox/Dockerfile b/infrastructure/docker/osionos/ide-sandbox/Dockerfile index 30a90af86..d0af0e1b5 100644 --- a/infrastructure/docker/osionos/ide-sandbox/Dockerfile +++ b/infrastructure/docker/osionos/ide-sandbox/Dockerfile @@ -43,7 +43,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # java → jdtls-launch (wrapper that starts Eclipse JDT LS on stdio) # Global, so a `docker exec` LSP relay finds them on PATH. Major-pinned (a dev # sandbox toolchain, not a project dependency under the release-age policy). -RUN npm install -g --no-audit --no-fund \ +RUN npm install -g --ignore-scripts --no-audit --no-fund \ typescript@5 typescript-language-server@4 pyright@1 \ && npm cache clean --force @@ -81,7 +81,7 @@ RUN ln -s /usr/local/go/bin/go /usr/local/bin/go \ # Temurin JRE 21.0.12.1+1 (built 2026-08-19) — Eclipse JDT LS requires Java 21 # runtime (eclipse.jdt.ls README line 45). User `java` stays 17 from default-jdk. # sha256: 2413149700df0f7d440500a84a8f764c535f21e5a5e87d38328b64eec2c5b500 -RUN curl -fsSL "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12.1%2B1/OpenJDK21U-jre_x64_linux_hotspot_21.0.12.1_1.tar.gz" \ +RUN curl -fsSL --proto '=https' --tlsv1.2 "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12.1%2B1/OpenJDK21U-jre_x64_linux_hotspot_21.0.12.1_1.tar.gz" \ -o /tmp/jre21.tar.gz \ && echo "2413149700df0f7d440500a84a8f764c535f21e5a5e87d38328b64eec2c5b500 /tmp/jre21.tar.gz" | sha256sum -c - \ && mkdir -p /opt/jre21 \ @@ -90,7 +90,7 @@ RUN curl -fsSL "https://github.com/adoptium/temurin21-binaries/releases/download # Eclipse JDT LS 1.61.0 (built 2026-09-03; 1.62.0 is too new under the 14-day # release-age rule). sha256: 338e7e73d61836651ba2453919a0d34fa763eb4e7c03342092309bffb8934c64 -RUN curl -fsSL "https://download.eclipse.org/jdtls/milestones/1.61.0/jdt-language-server-1.61.0-202609031315.tar.gz" \ +RUN curl -fsSL --proto '=https' --tlsv1.2 "https://download.eclipse.org/jdtls/milestones/1.61.0/jdt-language-server-1.61.0-202609031315.tar.gz" \ -o /tmp/jdtls.tar.gz \ && echo "338e7e73d61836651ba2453919a0d34fa763eb4e7c03342092309bffb8934c64 /tmp/jdtls.tar.gz" | sha256sum -c - \ && mkdir -p /opt/jdtls \ @@ -108,7 +108,7 @@ RUN set -- /opt/jdtls/plugins/org.eclipse.equinox.launcher_*.jar; \ # with no checksum let any upstream re-publish land in the image unchecked. ARG RUST_ANALYZER_VERSION=2026-09-28 ARG RUST_ANALYZER_SHA256=23f711d86b5f826e22886f01d7355dc01e0f4c1357dafa29710a95b903b48c85 -RUN curl -fsSL "https://github.com/rust-lang/rust-analyzer/releases/download/${RUST_ANALYZER_VERSION}/rust-analyzer-x86_64-unknown-linux-gnu.gz" \ +RUN curl -fsSL --proto '=https' --tlsv1.2 "https://github.com/rust-lang/rust-analyzer/releases/download/${RUST_ANALYZER_VERSION}/rust-analyzer-x86_64-unknown-linux-gnu.gz" \ -o /tmp/rust-analyzer.gz \ && echo "${RUST_ANALYZER_SHA256} /tmp/rust-analyzer.gz" | sha256sum -c - \ && gunzip -c /tmp/rust-analyzer.gz > /usr/local/bin/rust-analyzer \ diff --git a/infrastructure/docker/osionos/ide-socket-proxy/proxy.mjs b/infrastructure/docker/osionos/ide-socket-proxy/proxy.mjs index c39870f83..5f3a39c98 100644 --- a/infrastructure/docker/osionos/ide-socket-proxy/proxy.mjs +++ b/infrastructure/docker/osionos/ide-socket-proxy/proxy.mjs @@ -162,7 +162,7 @@ function forward(clientReq, clientRes, payload, body) { delete headers["content-encoding"]; headers["content-length"] = String(payload.length); } - const upstream = http.request( + const upstream = http.request( // NOSONAR S5144: target restricted to the configured local upstream { socketPath: DOCKER_SOCK, method: clientReq.method, path: clientReq.url, headers }, (upRes) => { audit.record(clientReq.method, clientReq.url, { status: upRes.statusCode, headers: clientReq.headers, body }); diff --git a/models/seeds.sql b/models/seeds.sql index 45e563b2a..7407bc0b5 100644 --- a/models/seeds.sql +++ b/models/seeds.sql @@ -24,7 +24,7 @@ INSERT INTO seed_constants ( campaign_started_at, user_login_activity ) VALUES ( - '$2b$12$Y6jhLOeIm2SNmZZDqt9LsOZbtu2I2GQBMN23w0gcfVjHV3MWtKHhG', + '$2b$12$Y6jhLOeIm2SNmZZDqt9LsOZbtu2I2GQBMN23w0gcfVjHV3MWtKHhG', -- NOSONAR S8215: demo seed credential, not a live secret 'light', 'dark', 'email_verify', @@ -121,7 +121,7 @@ WITH seed_activities (user_id, activity_type, activity_data, created_at) AS ( (3, 'password_reset_request', '{"ip": "10.0.0.4", "source": "mobile"}'::jsonb, TIMESTAMP '2026-05-03 14:10:00'), (4, 'account_creation', '{"signup_method": "email", "campaign": "google_ads"}'::jsonb, NULL), (5, NULL, '{"ip": "192.168.1.12", "device": "Firefox/Windows", "location": "Paris, France"}'::jsonb, TIMESTAMP '2026-05-03 10:22:00'), - (6, 'email_verified', '{"verify_token": "abc123xyz_verify_token_6"}'::jsonb, TIMESTAMP '2026-05-02 09:05:00'), + (6, 'email_verified', '{"verify_token": "abc123xyz_verify_token_6"}'::jsonb, TIMESTAMP '2026-05-02 09:05:00'), -- NOSONAR S6418: fake demo token (7, NULL, '{"ip": "172.16.0.21", "device": "Safari/iPhone", "location": "Berlin, Germany"}'::jsonb, TIMESTAMP '2026-05-03 11:12:00'), (8, 'settings_change', '{"notifications": false, "source": "web"}'::jsonb, TIMESTAMP '2026-05-02 14:00:00'), (9, NULL, '{"ip": "192.168.1.42", "device": "Chrome/Mac", "location": "London, UK"}'::jsonb, TIMESTAMP '2026-05-03 13:18:00'), diff --git a/scripts/lib/grobase-link-target.sh b/scripts/lib/grobase-link-target.sh index 362c574de..a6437ebaa 100644 --- a/scripts/lib/grobase-link-target.sh +++ b/scripts/lib/grobase-link-target.sh @@ -137,12 +137,12 @@ resolve_auto() { resolve_host() { # a bare host or alias: ssh first, then Kong's plain port, then https if remote_has_grobase "$1"; then printf 'ssh %s\n' "$1" - elif url_reachable "http://$1:8000"; then - printf 'direct http://%s:8000\n' "$1" + elif url_reachable "http://$1:8000"; then # NOSONAR S5332: plain http on the local docker network / dev loopback + printf 'direct http://%s:8000\n' "$1" # NOSONAR S5332: plain http on the local docker network / dev loopback elif url_reachable "https://$1"; then printf 'direct https://%s\n' "$1" else - die "$1: ssh shows no mini-baas-kong there and neither http://$1:8000 nor https://$1 answers from a container" + die "$1: ssh shows no mini-baas-kong there and neither http://$1:8000 nor https://$1 answers from a container" # NOSONAR S5332: plain http on the local docker network / dev loopback fi } # relay_cafile -> the CA file the relay verifies Kong with, as the relay sees it: the copy up diff --git a/scripts/reencrypt-mounts.sh b/scripts/reencrypt-mounts.sh index 13a7c89ee..f229d6868 100644 --- a/scripts/reencrypt-mounts.sh +++ b/scripts/reencrypt-mounts.sh @@ -135,7 +135,7 @@ dsn_for() { ;; osionos-iot) # dynamodb-local takes static fake credentials by design. - printf 'dynamodb://local?endpoint=http://dynamodb-local:8000®ion=us-east-1&access_key=fake&secret_key=fake' + printf 'dynamodb://local?endpoint=http://dynamodb-local:8000®ion=us-east-1&access_key=fake&secret_key=fake' # NOSONAR S5332: plain http on the local docker network / dev loopback ;; *) return 1 ;; esac diff --git a/scripts/vault42-recover-dumps.sh b/scripts/vault42-recover-dumps.sh index e5d9347ee..adcaec4b5 100755 --- a/scripts/vault42-recover-dumps.sh +++ b/scripts/vault42-recover-dumps.sh @@ -207,7 +207,7 @@ list_rev() { distinct_trees() { local listing for listing in "$@"; do - [ ! -s "$listing" ] || sort "$listing" | md5sum + [ ! -s "$listing" ] || sort "$listing" | sha256sum done | sort -u | wc -l } diff --git a/tools/seeds/gourmand_app_env.py b/tools/seeds/gourmand_app_env.py index 8d14ea6f3..b2686b12d 100755 --- a/tools/seeds/gourmand_app_env.py +++ b/tools/seeds/gourmand_app_env.py @@ -14,7 +14,7 @@ if not db_id: sys.exit("GOURMAND_DB_ID is required (source .gourmand-tenant.env)") mode = os.stat(path).st_mode & 0o777 if path.exists() else 0o600 -lines = path.read_text().splitlines() if path.exists() else [] +lines = path.read_text().splitlines() if path.exists() else [] # NOSONAR S8707: maintainer CLI, the path is the operator's own argument idx = next((i for i, line in enumerate(lines) if line.startswith("VITE_BAAS_LIVE_MOUNTS=")), None) mounts = json.loads(lines[idx].split("=", 1)[1]) if idx is not None else [] @@ -25,6 +25,6 @@ lines[idx] = line else: lines.append(line) -path.write_text("\n".join(lines) + "\n") -os.chmod(path, mode) +path.write_text("\n".join(lines) + "\n") # NOSONAR S2083: maintainer CLI, the path is the operator's own argument +os.chmod(path, mode) # NOSONAR S8707: maintainer CLI, the path is the operator's own argument print(f"VITE_BAAS_LIVE_MOUNTS now lists gourmand-db {db_id}") diff --git a/tools/seeds/seed_agency.py b/tools/seeds/seed_agency.py index 1d4eab496..183fb7f44 100644 --- a/tools/seeds/seed_agency.py +++ b/tools/seeds/seed_agency.py @@ -65,9 +65,9 @@ def q(value) -> str: def ts(base: datetime, max_days: int) -> str: - return (base + timedelta(days=random.randint(0, max_days), - hours=random.randint(0, 23), - minutes=random.randint(0, 59))).strftime("%Y-%m-%d %H:%M:%S+00") + return (base + timedelta(days=random.randint(0, max_days), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + hours=random.randint(0, 23), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + minutes=random.randint(0, 59))).strftime("%Y-%m-%d %H:%M:%S+00") # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context OPS = ["Nightfall", "Cobalt Ledger", "Silent Harbor", "Glass Orchid", "Iron Veil", @@ -147,15 +147,15 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: # cases ────────────────────────────────────────────────────────────────── cases = [] for i in range(1, 41): - status = random.choice(CASE_STATUS) + status = random.choice(CASE_STATUS) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context opened = ts(BASE - timedelta(days=400), 380) closed = ts(BASE, 200) if status in ("closed", "cold") else None cases.append([i, f"BIA-{2025 + (i % 2)}-{i:03d}", f"Operation {OPS[i - 1]}", - status, random.choice(PRIORITIES), random.choice(CLASSIF), - round(random.uniform(8_000, 250_000), 2), random.choice(inv_names), - random.choice(CLIENTS), opened, closed, - f"Investigation into {random.choice(['suspected money laundering', 'asset concealment', 'procurement fraud', 'sanctions evasion', 'art-market fraud', 'insurance fraud', 'corporate espionage'])} " - f"involving {random.choice(['offshore structures', 'a shipping network', 'shell companies', 'a freeport vault', 'crypto mixers', 'trade mis-invoicing'])}.", + status, random.choice(PRIORITIES), random.choice(CLASSIF), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + round(random.uniform(8_000, 250_000), 2), random.choice(inv_names), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(CLIENTS), opened, closed, # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + f"Investigation into {random.choice(['suspected money laundering', 'asset concealment', 'procurement fraud', 'sanctions evasion', 'art-market fraud', 'insurance fraud', 'corporate espionage'])} " # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + f"involving {random.choice(['offshore structures', 'a shipping network', 'shell companies', 'a freeport vault', 'crypto mixers', 'trade mis-invoicing'])}.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context owner]) emit("cases", ["id", "code", "title", "status", "priority", "classification", "budget", "lead_investigator", "client", "opened_at", "closed_at", "summary", "owner_id"], cases) @@ -165,17 +165,17 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: used = set() for i in range(1, 61): while True: - name = f"{random.choice(FIRST)} {random.choice(LAST)}" + name = f"{random.choice(FIRST)} {random.choice(LAST)}" # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context if name not in used: used.add(name) break - alias = random.choice([None, None, f"“{random.choice(['The Banker', 'Marquis', 'Cashmere', 'Doc', 'The Courier', 'Magpie', 'Halcyon', 'Brick'])}”"]) - dob = (datetime(1955, 1, 1) + timedelta(days=random.randint(0, 14_000))).strftime("%Y-%m-%d") - ssn = f"{random.randint(100, 999)}-{random.randint(10, 99)}-{random.randint(1000, 9999)}" - subjects.append([i, random.randint(1, 40), name, alias, ssn, - random.choice(["CH", "DE", "RU", "FR", "IT", "NL", "AE", "MT", "PA", "CY"]), - random.choice(RISK), random.choice(OCCUP), dob, - random.choice([None, "Known to travel under secondary passport.", + alias = random.choice([None, None, f"“{random.choice(['The Banker', 'Marquis', 'Cashmere', 'Doc', 'The Courier', 'Magpie', 'Halcyon', 'Brick'])}”"]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + dob = (datetime(1955, 1, 1) + timedelta(days=random.randint(0, 14_000))).strftime("%Y-%m-%d") # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + ssn = f"{random.randint(100, 999)}-{random.randint(10, 99)}-{random.randint(1000, 9999)}" # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + subjects.append([i, random.randint(1, 40), name, alias, ssn, # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(["CH", "DE", "RU", "FR", "IT", "NL", "AE", "MT", "PA", "CY"]), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(RISK), random.choice(OCCUP), dob, # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice([None, "Known to travel under secondary passport.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "Frequent contact with case principals.", "Cooperative witness — handle with care.", "Surveillance authorized by client mandate."]), @@ -186,26 +186,26 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: # locations ────────────────────────────────────────────────────────────── locations = [] for i in range(1, 51): - city, country, lat, lng = random.choice(CITIES) - kind = random.choice(LOC_KINDS) + city, country, lat, lng = random.choice(CITIES) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + kind = random.choice(LOC_KINDS) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context locations.append([i, f"{kind.title()} — {city} #{i}", kind, - f"{random.randint(1, 220)} {random.choice(['Quai', 'Strasse', 'Avenue', 'Laan', 'Via', 'Rua'])} {random.choice(LAST)}", + f"{random.randint(1, 220)} {random.choice(['Quai', 'Strasse', 'Avenue', 'Laan', 'Via', 'Rua'])} {random.choice(LAST)}", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context city, country, round(lat + random.uniform(-0.05, 0.05), 6), round(lng + random.uniform(-0.05, 0.05), 6), - random.random() < 0.3, owner]) + random.random() < 0.3, owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("locations", ["id", "label", "kind", "address", "city", "country", "lat", "lng", "surveillance_active", "owner_id"], locations) # evidence ─────────────────────────────────────────────────────────────── evidence = [] for i in range(1, 121): - kind = random.choice(EV_KINDS) - evidence.append([i, random.randint(1, 40), kind, - f"{kind.title()} recovered during {random.choice(['site visit', 'records request', 'surveillance op', 'source meeting', 'forensic imaging'])}.", - f"BIA-EV-{i:04d} sealed; custody log {random.randint(2, 6)} entries", - random.randint(1, 50), random.choice([p['name'] for p in people]), - ts(BASE - timedelta(days=300), 290), random.random() < 0.92, owner]) + kind = random.choice(EV_KINDS) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + evidence.append([i, random.randint(1, 40), kind, # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + f"{kind.title()} recovered during {random.choice(['site visit', 'records request', 'surveillance op', 'source meeting', 'forensic imaging'])}.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + f"BIA-EV-{i:04d} sealed; custody log {random.randint(2, 6)} entries", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.randint(1, 50), random.choice([p['name'] for p in people]), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + ts(BASE - timedelta(days=300), 290), random.random() < 0.92, owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("evidence", ["id", "case_id", "kind", "description", "chain_of_custody", "storage_location_id", "collected_by", "collected_at", "integrity_verified", "owner_id"], evidence) @@ -213,9 +213,9 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: # leads ────────────────────────────────────────────────────────────────── leads = [] for i in range(1, 81): - leads.append([i, random.randint(1, 40), random.choice([None, random.randint(1, 60)]), - random.choice(LEAD_SRC), random.choice(CRED), random.choice(LEAD_STATUS), - random.choice(["Subject seen meeting unknown male at marina.", + leads.append([i, random.randint(1, 40), random.choice([None, random.randint(1, 60)]), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(LEAD_SRC), random.choice(CRED), random.choice(LEAD_STATUS), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(["Subject seen meeting unknown male at marina.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "Invoice totals diverge from customs declaration.", "New shell entity registered with shared director.", "Repeated structuring just under reporting threshold.", @@ -229,56 +229,56 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: # transactions ─────────────────────────────────────────────────────────── txns = [] for i in range(1, 151): - flagged = random.random() < 0.35 - amount = round(random.choice([random.uniform(900, 9_900), + flagged = random.random() < 0.35 # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + amount = round(random.choice([random.uniform(900, 9_900), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context random.uniform(9_000, 9_999), random.uniform(10_000, 480_000)]), 2) - txns.append([i, random.randint(1, 60), amount, random.choice(CURRENCIES), - random.choice(COUNTERPARTIES), - f"{random.choice(['CH', 'MT', 'AE', 'VG', 'PA'])}{random.randint(10**10, 10**11 - 1)}", - flagged, ts(BASE - timedelta(days=365), 360), random.choice(METHODS), owner]) + txns.append([i, random.randint(1, 60), amount, random.choice(CURRENCIES), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(COUNTERPARTIES), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + f"{random.choice(['CH', 'MT', 'AE', 'VG', 'PA'])}{random.randint(10**10, 10**11 - 1)}", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + flagged, ts(BASE - timedelta(days=365), 360), random.choice(METHODS), owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("transactions", ["id", "subject_id", "amount", "currency", "counterparty", "account_ref", "flagged", "executed_at", "method", "owner_id"], txns) # vehicles ─────────────────────────────────────────────────────────────── vehicles = [] for i in range(1, 31): - make, model = random.choice(MAKES) - plate = f"{random.choice(['ZH', 'GE', 'HH', 'B', 'NL', 'F'])}-{random.randint(1000, 99999)}" - vehicles.append([i, random.choice([None, random.randint(1, 60)]), plate, make, model, - random.choice(COLORS), random.randint(2012, 2025), - random.choice([None, random.randint(1, 50)]), owner]) + make, model = random.choice(MAKES) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + plate = f"{random.choice(['ZH', 'GE', 'HH', 'B', 'NL', 'F'])}-{random.randint(1000, 99999)}" # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + vehicles.append([i, random.choice([None, random.randint(1, 60)]), plate, make, model, # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(COLORS), random.randint(2012, 2025), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice([None, random.randint(1, 50)]), owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("vehicles", ["id", "owner_subject_id", "plate", "make", "model", "color", "year", "last_seen_location_id", "owner_id"], vehicles) # communications ───────────────────────────────────────────────────────── comms = [] for i in range(1, 201): - comms.append([i, random.randint(1, 60), random.choice(CHANNELS), - random.choice([f"{random.choice(FIRST)} {random.choice(LAST)}", + comms.append([i, random.randint(1, 60), random.choice(CHANNELS), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice([f"{random.choice(FIRST)} {random.choice(LAST)}", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "unknown number", "unregistered SIM", - random.choice(COUNTERPARTIES)]), + random.choice(COUNTERPARTIES)]), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context ts(BASE - timedelta(days=200), 195), - random.choice(["Arranged meeting; location referenced obliquely.", + random.choice(["Arranged meeting; location referenced obliquely.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "Discussed 'paperwork' for upcoming shipment.", "Payment confirmation — amount matches txn pattern.", "Subject anxious about 'the audit'.", "Travel plans: short-notice flight, cash ticket.", "Mentioned contact at the freeport by first name.", "Routine personal call — no case relevance."]), - random.choice(COMM_CLASS), random.randint(1, 40), owner]) + random.choice(COMM_CLASS), random.randint(1, 40), owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("communications", ["id", "subject_id", "channel", "counterparty", "intercepted_at", "summary", "classification", "case_id", "owner_id"], comms) # reports ──────────────────────────────────────────────────────────────── reports = [] for i in range(1, 41): - author = random.choice([p["name"] for p in people if p["dept"] in ("analysis", "investigations", "forensics")]) - reports.append([i, i, f"{random.choice(['Interim', 'Surveillance', 'Financial', 'Forensic', 'Closing'])} report — Operation {OPS[i - 1]}", - author, random.choice(REPORT_STATUS), random.choice(CLASSIF), + author = random.choice([p["name"] for p in people if p["dept"] in ("analysis", "investigations", "forensics")]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + reports.append([i, i, f"{random.choice(['Interim', 'Surveillance', 'Financial', 'Forensic', 'Closing'])} report — Operation {OPS[i - 1]}", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + author, random.choice(REPORT_STATUS), random.choice(CLASSIF), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context ts(BASE - timedelta(days=100), 95), f"Findings summary for Operation {OPS[i - 1]}: " - f"{random.choice(['fund flows traced through three jurisdictions', 'subject network mapped, two new principals identified', 'evidence chain complete and verified', 'surveillance window produced actionable pattern-of-life', 'ledger reconstruction shows systematic skimming'])}.", + f"{random.choice(['fund flows traced through three jurisdictions', 'subject network mapped, two new principals identified', 'evidence chain complete and verified', 'surveillance window produced actionable pattern-of-life', 'ledger reconstruction shows systematic skimming'])}.", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context owner]) emit("reports", ["id", "case_id", "title", "author", "status", "classification", "published_at", "body", "owner_id"], reports) @@ -286,11 +286,11 @@ def emit(table: str, cols: list[str], rows: list[list]) -> None: # assignments ──────────────────────────────────────────────────────────── assignments = [] for i in range(1, 101): - assignments.append([i, ((i - 1) % 40) + 1, random.choice(emails), - random.choice(ROLES_ON_CASE), round(random.uniform(4, 160), 1), + assignments.append([i, ((i - 1) % 40) + 1, random.choice(emails), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.choice(ROLES_ON_CASE), round(random.uniform(4, 160), 1), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context round(random.uniform(45, 180), 2), - (BASE - timedelta(days=random.randint(10, 300))).strftime("%Y-%m-%d"), - random.random() < 0.7, owner]) + (BASE - timedelta(days=random.randint(10, 300))).strftime("%Y-%m-%d"), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + random.random() < 0.7, owner]) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context emit("assignments", ["id", "case_id", "employee_email", "role_on_case", "hours", "hourly_rate", "started_at", "active", "owner_id"], assignments) @@ -307,21 +307,21 @@ def edge(frm: str, to: str, etype: str, label: str, directed: bool = True) -> No seen_pairs = set() for _ in range(30): # subject ↔ subject associations - a, b = random.randint(1, 60), random.randint(1, 60) + a, b = random.randint(1, 60), random.randint(1, 60) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context if a == b or (min(a, b), max(a, b)) in seen_pairs: continue seen_pairs.add((min(a, b), max(a, b))) edge(nid("subjects", a), nid("subjects", b), "associate", - random.choice(["business partner", "family", "frequent contact", + random.choice(["business partner", "family", "frequent contact", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "co-director", "courier for", "introduced by informant"]), directed=False) for _ in range(25): # subject → location patterns of life - edge(nid("subjects", random.randint(1, 60)), nid("locations", random.randint(1, 50)), - "frequents", random.choice(["weekly visits", "registered address", + edge(nid("subjects", random.randint(1, 60)), nid("locations", random.randint(1, 50)), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + "frequents", random.choice(["weekly visits", "registered address", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "meeting spot", "storage access", "owns via proxy"])) for _ in range(25): # cross-case discoveries: case → subject outside its FK chain - edge(nid("cases", random.randint(1, 40)), nid("subjects", random.randint(1, 60)), - "involves", random.choice(["person of interest", "witness", "beneficial owner", + edge(nid("cases", random.randint(1, 40)), nid("subjects", random.randint(1, 60)), # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context + "involves", random.choice(["person of interest", "witness", "beneficial owner", # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "intermediary", "unconfirmed link"])) emit("edges", ["id", '"from"', '"to"', "type", "label", "directed", "owner_id"], edges) diff --git a/tools/seeds/seed_agency_wiki.py b/tools/seeds/seed_agency_wiki.py index 5c7052db3..7b380f78b 100644 --- a/tools/seeds/seed_agency_wiki.py +++ b/tools/seeds/seed_agency_wiki.py @@ -1192,12 +1192,12 @@ def T(channel, *msgs): events = [] # (msg_uuid, channel_key, author, text, minutes_ago) n_threads = len(THREADS) for ti, (chan, msgs) in enumerate(THREADS): - base = MIN_14D - int((ti + 1) * (MIN_14D - 90) / (n_threads + 1)) + rng.randint(-40, 40) + base = MIN_14D - int((ti + 1) * (MIN_14D - 90) / (n_threads + 1)) + rng.randint(-40, 40) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context offset = max(base, 75) for mi, (author, text) in enumerate(msgs): mid = uid(f"msg:{chan}:{ti}:{mi}") events.append((mid, chan, author, text, offset)) - offset -= rng.randint(2, 6) + offset -= rng.randint(2, 6) # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context # reactions: (msg uuid5 key parts, reactor, emoji) REACTIONS = [ @@ -1334,7 +1334,7 @@ def T(channel, *msgs): for i, u in enumerate(users): chat.append( "INSERT INTO public.osionos_feed_likes (page_id, user_id, created_at) " - f"VALUES ('{pid}', '{P(u)}', now() - interval '{rng.randint(600, 9000)} minutes') " + f"VALUES ('{pid}', '{P(u)}', now() - interval '{rng.randint(600, 9000)} minutes') " # NOSONAR S2245: seeded RNG: reproducible demo data, not a security context "ON CONFLICT DO NOTHING;") like_count += 1 for i, (pkey, author, text, minutes) in enumerate(COMMENTS): From cbea2b64e1cb031a02ac14c35421128ae7b8fbd3 Mon Sep 17 00:00:00 2001 From: SMOSTAFAH1 Date: Wed, 4 Nov 2026 19:24:05 +0000 Subject: [PATCH 2/4] ci(security): pin GitHub Actions to full commit SHAs and harden installs - every third-party action pinned to its commit SHA (tag kept as comment) - npm ci --ignore-scripts where lifecycle scripts are not needed - HTTPS-only curl downloads; govulncheck pinned instead of @latest --- .github/workflows/baas-ci.yml | 8 ++-- .github/workflows/baas-cli-publish.yml | 10 ++--- .github/workflows/baas-release.yml | 24 +++++------ .github/workflows/drawnosaurus-edge.yml | 2 +- .github/workflows/fresh-machine.yml | 10 ++--- .github/workflows/gitleaks.yml | 2 +- .github/workflows/graph-studio-contract.yml | 4 +- .github/workflows/graph-studio.yml | 2 +- .github/workflows/mini-baas-security.yml | 44 ++++++++++----------- .github/workflows/models-grants.yml | 6 +-- .github/workflows/supply-chain.yml | 8 ++-- 11 files changed, 60 insertions(+), 60 deletions(-) diff --git a/.github/workflows/baas-ci.yml b/.github/workflows/baas-ci.yml index 362ddedf7..cd8bff7ec 100644 --- a/.github/workflows/baas-ci.yml +++ b/.github/workflows/baas-ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule diff --git a/.github/workflows/baas-cli-publish.yml b/.github/workflows/baas-cli-publish.yml index 9e4aea831..0fb759a82 100644 --- a/.github/workflows/baas-cli-publish.yml +++ b/.github/workflows/baas-cli-publish.yml @@ -48,7 +48,7 @@ jobs: name: Build · smoke-test · pack (never publishes) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -58,7 +58,7 @@ jobs: run: | git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "20" cache: npm @@ -86,7 +86,7 @@ jobs: || { echo "::error::the publishable tarball would not include the baas CLI"; exit 1; } tar -tzf ./*.tgz | grep -E 'package/dist/index\.js' \ || { echo "::error::the publishable tarball would not include the SDK entrypoint"; exit 1; } - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: npm-tarball path: apps/grobase/sdks/js/*.tgz @@ -104,7 +104,7 @@ jobs: contents: read id-token: write # npm provenance steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -114,7 +114,7 @@ jobs: run: | git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "20" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/baas-release.yml b/.github/workflows/baas-release.yml index cdf8019a4..9de211749 100644 --- a/.github/workflows/baas-release.yml +++ b/.github/workflows/baas-release.yml @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest needs: version steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -89,7 +89,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -99,14 +99,14 @@ jobs: run: | git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 # Docker Hub — where the images go (public by default on push). - - uses: docker/login-action@v4 + - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} # GHCR — layer-cache push only (cache-to refs in docker-bake.hcl). - - uses: docker/login-action@v4 + - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -136,7 +136,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -146,8 +146,8 @@ jobs: run: | git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v4 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} @@ -180,7 +180,7 @@ jobs: > "binocle-${ed}-${VERSION}-linux-amd64.tar.gz.sha256" done sha256sum binocle-*-"${VERSION}"-linux-amd64.tar.gz > checksums.txt - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: binocle-binaries path: | @@ -197,7 +197,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -207,7 +207,7 @@ jobs: run: | git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: name: binocle-binaries path: release-assets/ @@ -219,7 +219,7 @@ jobs: sed -E 's#^VERSION="\$\{BINOCLE_VERSION:-[^}]*\}"#VERSION="${BINOCLE_VERSION:-'"$VER"'}"#' \ apps/grobase/scripts/ci/install.sh > release-assets/install.sh grep '^VERSION=' release-assets/install.sh - - uses: softprops/action-gh-release@v3 + - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: name: "Grobase BaaS ${{ github.ref_name }}" prerelease: ${{ needs.version.outputs.prerelease == 'true' }} diff --git a/.github/workflows/drawnosaurus-edge.yml b/.github/workflows/drawnosaurus-edge.yml index 718f1b0c1..0e1657c46 100644 --- a/.github/workflows/drawnosaurus-edge.yml +++ b/.github/workflows/drawnosaurus-edge.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: nginx from mirror.gcr.io under its public.ecr.aws name # Same reason as graph-studio.yml: public.ecr.aws rate-limits anonymous pulls per runner IP. run: | diff --git a/.github/workflows/fresh-machine.yml b/.github/workflows/fresh-machine.yml index b1a1d5cd4..7b500e4ba 100644 --- a/.github/workflows/fresh-machine.yml +++ b/.github/workflows/fresh-machine.yml @@ -91,7 +91,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: grobase + opposite-osiris + osionos app + scripts checkout (vault-restore.sh, the web/gateway Dockerfiles, the bridge image closure and the vault42 CLI are under test) run: | git config --global --add url."https://github.com/".insteadOf "git@github.com:" @@ -160,7 +160,7 @@ jobs: git config --global --get-all url."https://github.com/".insteadOf - name: git clone (root only — submodules are next, explicitly) - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 1 @@ -270,8 +270,8 @@ jobs: msc=$(docker exec mini-baas-mssql sh -lc '/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -h-1 -W -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM finance.sys.tables"' 2>/dev/null | tr -d '[:space:]') echo "mssql finance tables = ${msc:-}"; gt0 "$msc" || { echo "::error::mssql finance empty"; fail=1; } # 5 dynamodb (table count) - ddc=$(docker run --rm --network mini-baas_mini-baas -e AWS_ACCESS_KEY_ID=local -e AWS_SECRET_ACCESS_KEY=local -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli dynamodb list-tables --endpoint-url http://mini-baas-dynamodb-local:8000 --output text --query 'length(TableNames)' 2>/dev/null | tr -d '[:space:]') - echo "dynamodb tables = ${ddc:-}"; gt0 "$ddc" || { echo "::error::dynamodb empty"; fail=1; } + ddc=$(docker run --rm --network mini-baas_mini-baas -e AWS_ACCESS_KEY_ID=local -e AWS_SECRET_ACCESS_KEY=local -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli dynamodb list-tables --endpoint-url http://mini-baas-dynamodb-local:8000 --output text --query 'length(TableNames)' 2>/dev/null | tr -d '[:space:]') # NOSONAR S5332: docker-network endpoint inside the CI runner + echo "dynamodb tables = ${ddc:-}"; gt0 "$ddc" || { echo "::error::dynamodb empty"; fail=1; } # NOSONAR S5332: docker-network endpoint inside the CI runner # 6 minio (object count) mnc=$(docker exec mini-baas-minio sh -c 'find /data -type f -not -path "*/.minio.sys/*" 2>/dev/null | wc -l' 2>/dev/null | tr -d '[:space:]') echo "minio objects = ${mnc:-}"; gt0 "$mnc" || { echo "::error::minio objects empty"; fail=1; } @@ -351,7 +351,7 @@ jobs: - name: Upload the e2e results if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: e2e-test-results path: tests/e2e/test-results diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index d92ba3d67..21c2a00cb 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -19,6 +19,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: make secrets-scan run: make secrets-scan diff --git a/.github/workflows/graph-studio-contract.yml b/.github/workflows/graph-studio-contract.yml index 7ebcbffe6..0126ffa11 100644 --- a/.github/workflows/graph-studio-contract.yml +++ b/.github/workflows/graph-studio-contract.yml @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: fetch graph_render at the gitlink (the submodule is update = none) run: | pin="$(git ls-tree HEAD apps/graph_render | awk '$2 == "commit" { print $3 }')" @@ -73,7 +73,7 @@ jobs: # tests/e2e/Dockerfile's base, pinned by digest (v1.63.0-noble = @playwright/test in the lockfile) PW_IMAGE: mcr.microsoft.com/playwright:v1.63.0-noble@sha256:eff16c30e6f3f4af0a03fa4b706120d5e9b0891c344a27d64559aff5900a4a27 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: fetch the pinned pack by digest (no docker) and verify it run: tests/graph-studio-contract/fetch-pack.sh "$RUNNER_TEMP/pack" # C7 (c7-hierarchy.spec.ts) runs osionos's own graph adapter, not a copy of it. diff --git a/.github/workflows/graph-studio.yml b/.github/workflows/graph-studio.yml index 38c942783..a20a225c9 100644 --- a/.github/workflows/graph-studio.yml +++ b/.github/workflows/graph-studio.yml @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: osionos checkout (the build context, with its nested packages) run: | git config --global url."https://github.com/".insteadOf "git@github.com:" diff --git a/.github/workflows/mini-baas-security.yml b/.github/workflows/mini-baas-security.yml index 681687ea1..a14dd69e5 100644 --- a/.github/workflows/mini-baas-security.yml +++ b/.github/workflows/mini-baas-security.yml @@ -33,7 +33,7 @@ jobs: security-events: write timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} fetch-depth: 0 @@ -57,7 +57,7 @@ jobs: - name: Upload SARIF to GitHub Security tab if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4 with: sarif_file: semgrep.sarif category: semgrep @@ -80,7 +80,7 @@ jobs: - dir: apps/grobase/sdks/js tool: npm steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -90,7 +90,7 @@ jobs: git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase apps/opposite-osiris apps/calendar apps/mail apps/osionos/app - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "22" @@ -118,7 +118,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -132,7 +132,7 @@ jobs: if: env.SNYK_TOKEN == '' run: echo "::notice::SNYK_TOKEN not configured — skipping Snyk" - - uses: snyk/actions/setup@master + - uses: snyk/actions/setup@11b673cfb4b0dc0d860a1323caa199d54950dcce # master if: env.SNYK_TOKEN != '' - name: Snyk test @@ -149,7 +149,7 @@ jobs: security-events: write timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -160,7 +160,7 @@ jobs: git submodule update --init apps/grobase apps/opposite-osiris apps/calendar apps/mail apps/osionos/app - name: Trivy filesystem scan - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@8f28faec26515256b6d22074d1d2a8986f4cbd87 # master with: scan-type: fs scan-ref: . @@ -181,7 +181,7 @@ jobs: - name: Upload Trivy filesystem SARIF if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4 with: sarif_file: trivy-fs.sarif category: trivy-fs @@ -205,7 +205,7 @@ jobs: # when the build actually succeeded, so a Docker Hub outage can't red the security gate. - name: Trivy image scan if: steps.build_img.outcome == 'success' - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@8f28faec26515256b6d22074d1d2a8986f4cbd87 # master with: image-ref: mini-baas/query-router:ci severity: HIGH,CRITICAL @@ -216,7 +216,7 @@ jobs: - name: Upload Trivy image SARIF if: steps.build_img.outcome == 'success' - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4 with: sarif_file: trivy-image.sarif category: trivy-image @@ -229,7 +229,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} fetch-depth: 0 @@ -241,7 +241,7 @@ jobs: git submodule update --init apps/grobase apps/opposite-osiris apps/calendar apps/mail apps/osionos/app - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@main + uses: trufflesecurity/trufflehog@93633ac70c28603931c365b6ef530cab878f4286 # main with: path: ./ base: ${{ github.event.pull_request.base.sha || github.event.before }} @@ -257,7 +257,7 @@ jobs: security-events: write timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -274,7 +274,7 @@ jobs: run: | set -euo pipefail VER=8.30.1 - curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" \ + curl -fsSL --proto '=https' --tlsv1.2 "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" \ | sudo tar -xz -C /usr/local/bin gitleaks gitleaks version @@ -291,7 +291,7 @@ jobs: - name: Upload gitleaks SARIF if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4 with: sarif_file: gitleaks.sarif category: gitleaks @@ -310,7 +310,7 @@ jobs: - apps/grobase/src/data-plane-router - apps/grobase/infra/docker/services/realtime/realtime-agnostic steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -367,7 +367,7 @@ jobs: contents: read timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -377,7 +377,7 @@ jobs: git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "git@github.com:" git submodule update --init apps/grobase apps/opposite-osiris apps/calendar apps/mail apps/osionos/app - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: # Minor-version floor + check-latest so this always resolves to the newest # patch (never re-pinned to a stale cached patch like go1.25.11, which @@ -397,7 +397,7 @@ jobs: - name: Install govulncheck run: | set -euo pipefail - go install golang.org/x/vuln/cmd/govulncheck@latest + go install golang.org/x/vuln/cmd/govulncheck@v1.9.0 "$(go env GOPATH)/bin/govulncheck" -version - name: govulncheck (reachability-based, blocking) @@ -419,7 +419,7 @@ jobs: # "Run workflow" with run_dast=true. if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_dast }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out submodules (non-recursive) @@ -460,7 +460,7 @@ jobs: - name: Upload ZAP artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: zap-baseline path: apps/grobase/artifacts/security/zap-baseline.* diff --git a/.github/workflows/models-grants.yml b/.github/workflows/models-grants.yml index d0a41739e..b8d327937 100644 --- a/.github/workflows/models-grants.yml +++ b/.github/workflows/models-grants.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out grobase submodule @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Verify the publish cascade on a throwaway database run: sh scripts/verify/publish-cascade.sh @@ -75,6 +75,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Verify the member attributes table on a throwaway database run: bash scripts/verify/member-attributes.sh diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index 001604eac..e4fa58c4e 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -19,7 +19,7 @@ jobs: name: npm frozen installs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} # Init only the frontend submodules we install here (not the heavy grobase @@ -32,7 +32,7 @@ jobs: git submodule update --init \ apps/mail apps/calendar apps/osionos/app - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "22" @@ -63,7 +63,7 @@ jobs: name: pnpm frozen installs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.SUBMODULES_TOKEN || github.token }} - name: Check out osionos submodule @@ -75,7 +75,7 @@ jobs: # path into the grobase submodule, so init grobase (non-recursive) too. git submodule update --init apps/grobase apps/opposite-osiris apps/osionos/app - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "22" From a4457b06c769d02551f108cca365622792c8147d Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Hashemian Date: Wed, 4 Nov 2026 20:57:17 +0100 Subject: [PATCH 3/4] fix(models-grants): seed ai_context_items + page_comments under their CHECKs p5_erase_canary failed on both fresh and legacy: generic p5_insert could not satisfy ai_context_items_kind_check (IN without ::text casts) or osionos_page_comments_decided_check. Widen p5_literal to match plain IN ('a','b') and force valid kind/scope/slug/body values plus NULL decided_* columns. --- .../verify/models-grants/account-checks.sql | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/verify/models-grants/account-checks.sql b/scripts/verify/models-grants/account-checks.sql index a1c72fa37..e688fab4d 100644 --- a/scripts/verify/models-grants/account-checks.sql +++ b/scripts/verify/models-grants/account-checks.sql @@ -2048,10 +2048,20 @@ ROLLBACK; -- ===== P5-6-a: account data map, erase function, export jobs ===== CREATE OR REPLACE FUNCTION pg_temp.p5_literal(p_rel regclass, p_col text) RETURNS text LANGUAGE sql AS $$ - SELECT (regexp_match(pg_get_constraintdef(c.oid), '''([^'']*)''::'))[1] + -- Prefer the first quoted literal from a column CHECK. + -- Handles both forms Postgres emits: + -- CHECK (kind IN ('skill', 'rule', 'command')) + -- CHECK ((kind = ANY (ARRAY['skill'::text, 'rule'::text, ...]))) + SELECT COALESCE( + (regexp_match(pg_get_constraintdef(c.oid), '''([^'']+)''::'))[1], + (regexp_match(pg_get_constraintdef(c.oid), '''([^'']+)'''))[1] + ) FROM pg_constraint c WHERE c.conrelid = p_rel AND c.contype = 'c' - AND pg_get_constraintdef(c.oid) ~ ('\(\(?' || p_col || ' (= ANY|IN|= .)|\(' || p_col || '\)::text = ANY') + AND ( + pg_get_constraintdef(c.oid) ~ ('\(\(?' || p_col || ' (= ANY|IN|= .)|\(' || p_col || '\)::text = ANY') + OR pg_get_constraintdef(c.oid) ~ ('\y' || p_col || '\y.*\yIN\y') + ) LIMIT 1 $$; @@ -2095,6 +2105,17 @@ BEGIN WHEN 'osionos_messages.ai_run_id' THEN CASE WHEN p_col = 'ai_requested_by' THEN 'gen_random_uuid()' ELSE 'NULL' END WHEN 'osionos_messages.ai_requested_by' THEN 'NULL' WHEN 'osionos_messages.author_id' THEN CASE WHEN p_col = 'ai_requested_by' THEN 'public.osionos_ai_bot_author()' END + -- ai_context_items: kind/scope/slug CHECKs reject random text; force valid values. + WHEN 'ai_context_items.kind' THEN '''skill''' + WHEN 'ai_context_items.scope' THEN '''user''' + WHEN 'ai_context_items.slug' THEN '''p5-'' || translate(left(md5(random()::text), 8), ''0189'', ''abcd'')' + WHEN 'ai_context_items.name' THEN '''p5 skill''' + WHEN 'ai_context_items.description' THEN '''p5 canary skill description''' + WHEN 'ai_context_items.body' THEN '''p5 canary skill body''' + -- page comments: decided_* CHECK requires decided_at/decided_by both null or both set + WHEN 'osionos_page_comments.decided_by' THEN 'NULL' + WHEN 'osionos_page_comments.decided_at' THEN 'NULL' + WHEN 'osionos_page_comments.decision' THEN 'NULL' ELSE NULL END; CONTINUE WHEN v_force IS NULL AND NOT v_iscol AND a.attname <> p_col AND a.attname IS DISTINCT FROM p_col2 AND (a.atthasdef OR NOT a.attnotnull); v_typ := a.typ; From 87e9e8703776918d2f02c72563f69356acea32fd Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Hashemian Date: Wed, 4 Nov 2026 21:02:29 +0100 Subject: [PATCH 4/4] fix(models-grants): pair decided_by seed with status=accepted + short emoji decided_check requires (status=open) iff both decided_* are null. When the canary seeds decided_by as the user column, force status=accepted and decided_at=now(). Reactions emoji CHECK is char_length 1..16; force a short literal. --- scripts/verify/models-grants/account-checks.sql | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/verify/models-grants/account-checks.sql b/scripts/verify/models-grants/account-checks.sql index e688fab4d..0ab317858 100644 --- a/scripts/verify/models-grants/account-checks.sql +++ b/scripts/verify/models-grants/account-checks.sql @@ -2112,10 +2112,16 @@ BEGIN WHEN 'ai_context_items.name' THEN '''p5 skill''' WHEN 'ai_context_items.description' THEN '''p5 canary skill description''' WHEN 'ai_context_items.body' THEN '''p5 canary skill body''' - -- page comments: decided_* CHECK requires decided_at/decided_by both null or both set - WHEN 'osionos_page_comments.decided_by' THEN 'NULL' - WHEN 'osionos_page_comments.decided_at' THEN 'NULL' - WHEN 'osionos_page_comments.decision' THEN 'NULL' + -- page comments (discussions migration): + -- decided_check: (status = 'open') = (decided_by IS NULL AND decided_at IS NULL) + -- When the canary seeds decided_by as the user column, pair it with accepted + decided_at. + -- Otherwise keep the row open with null decided_*. + WHEN 'osionos_page_comments.kind' THEN '''comment''' + WHEN 'osionos_page_comments.status' THEN CASE WHEN p_col = 'decided_by' THEN '''accepted''' ELSE '''open''' END + WHEN 'osionos_page_comments.decided_at' THEN CASE WHEN p_col = 'decided_by' THEN 'now()' ELSE 'NULL' END + WHEN 'osionos_page_comments.decided_by' THEN CASE WHEN p_col = 'decided_by' THEN NULL ELSE 'NULL' END + -- reactions: emoji CHECK is char_length 1..16; random text is longer + WHEN 'osionos_page_comment_reactions.emoji' THEN '''ok''' ELSE NULL END; CONTINUE WHEN v_force IS NULL AND NOT v_iscol AND a.attname <> p_col AND a.attname IS DISTINCT FROM p_col2 AND (a.atthasdef OR NOT a.attnotnull); v_typ := a.typ;