Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
56e8942
Disable periodic snapshots (forked repo doesn't need) (#10)
wtfiwtz Jul 23, 2025
5fe7c2f
Merge branch 'master' of https://github.com/getredash/redash
wtfiwtz May 14, 2026
2c075a8
Fix python libs
wtfiwtz May 13, 2026
f69c14c
npm fixes
wtfiwtz May 13, 2026
1b1ce12
More fixes
wtfiwtz May 13, 2026
0686f1d
Frontend build fixes
wtfiwtz May 14, 2026
2e891a4
Update to authlib 1.3.x
wtfiwtz May 14, 2026
5fb23e6
Fix tests
wtfiwtz May 14, 2026
afd8278
authlib to 1.7.2
wtfiwtz May 14, 2026
a8efbbb
Clean up after cherry-pick
wtfiwtz May 14, 2026
360e6c5
Fixes from docker scout
wtfiwtz May 14, 2026
030b3f7
Werkzeug 3.x on Flask 2.x
wtfiwtz May 14, 2026
5d75b28
fix: resolve Flask 3.0 compatibility issues and test deadlocks
wtfiwtz May 14, 2026
c5a12da
fix: complete Flask 3.0 upgrade with all test fixes
wtfiwtz May 14, 2026
b92f6d2
fix: downgrade webpack-manifest-plugin to 5.0.1 for CommonJS compatib…
wtfiwtz May 14, 2026
8a0b422
fix: update dependencies and resolve JWT test issues
wtfiwtz May 15, 2026
60953a5
Update axios to 1.15.2 and minimatch to fix critical vulnerabilities
wtfiwtz May 15, 2026
6fc8ad8
fix: resolve remaining scan findings via pnpm overrides
wtfiwtz May 15, 2026
1ba008d
fix: override lodash.template to 4.18.1 (CVE-2026-4800)
wtfiwtz May 15, 2026
25e0cfa
Minimise PR changes
wtfiwtz May 18, 2026
6b486c4
Clean up Dockerfile
wtfiwtz May 19, 2026
195f5f5
AI code review: restore retry for curl
wtfiwtz May 19, 2026
570d09b
Cleanup excess documentation
wtfiwtz May 19, 2026
709a4b0
More cleanup and restore talisman
wtfiwtz May 19, 2026
4e5b2e7
Reinstate Talisman
wtfiwtz May 19, 2026
08111b5
Re-instate Flask plugins
wtfiwtz May 19, 2026
8cb2911
Remove testing overrides
wtfiwtz May 19, 2026
b4445ce
Re-instate skipped JWT test
wtfiwtz May 19, 2026
51bfbb3
Simplify test_authentication.py
wtfiwtz May 19, 2026
1ef189a
Replace advocate (deprecated) with champion (pre-release)
wtfiwtz May 19, 2026
318e7b1
Fix up monkeypatching of pytest
wtfiwtz May 19, 2026
7a763f1
chore: remove deadlock debugging scaffolding and simplify test-mode o…
wtfiwtz May 19, 2026
fb78419
chore: simplify factories.py and fix test collection/session issues
wtfiwtz May 19, 2026
c941b98
Simplify code for SQLAlchemy 1.4 only
wtfiwtz May 19, 2026
ef6c0b0
PR review fixes
wtfiwtz May 19, 2026
3bacd9b
Other vulnerability fixes (Github Advanced Security)
wtfiwtz May 19, 2026
f8b99d1
Trixie security updates applied
wtfiwtz May 20, 2026
a1e7fbd
Switch to trixie and other minor version bumps
wtfiwtz Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:24-bookworm AS frontend-builder
FROM node:24-trixie AS frontend-builder

RUN npm install --global pnpm@10.30.3

Expand Down Expand Up @@ -39,14 +39,29 @@ RUN --mount=type=cache,id=pnpm-store,target=/frontend/.cache/pnpm,uid=1001,gid=1
fi
EOF

FROM python:3.13-slim-bookworm
FROM python:3.13-slim-trixie

EXPOSE 5000

RUN useradd --create-home redash

# Ubuntu packages
# Add Debian trixie-security and trixie-updates repositories so we get the latest
# security fixes and stable point updates at build time.
# trixie-proposed-updates is kept for opt-in pre-release fixes already in flight.
RUN set -eux; \
printf 'deb http://deb.debian.org/debian-security trixie-security main\n' \
> /etc/apt/sources.list.d/trixie-security.list; \
printf 'deb http://deb.debian.org/debian trixie-updates main\n' \
> /etc/apt/sources.list.d/trixie-updates.list; \
printf 'deb http://deb.debian.org/debian trixie-proposed-updates main\n' \
> /etc/apt/sources.list.d/trixie-proposed-updates.list

# Apply security archive first (forces -t trixie-security so the security pocket wins),
# then a general upgrade for stable point updates, then install build dependencies.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y -t trixie-security upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get -y -t trixie-updates upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: trixie-proposed-updates is globally enabled but claimed to be opt-in; the unpinned apt-get -y upgrade can auto-select pre-release packages from it, reducing build reproducibility.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Dockerfile, line 64:

<comment>`trixie-proposed-updates` is globally enabled but claimed to be opt-in; the unpinned `apt-get -y upgrade` can auto-select pre-release packages from it, reducing build reproducibility.</comment>

<file context>
@@ -45,8 +45,23 @@ EXPOSE 5000
 RUN apt-get update && \
+  DEBIAN_FRONTEND=noninteractive apt-get -y -t trixie-security upgrade && \
+  DEBIAN_FRONTEND=noninteractive apt-get -y -t trixie-updates upgrade && \
+  DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \
   apt-get install -y --no-install-recommends \
   pkg-config \
</file context>

apt-get install -y --no-install-recommends \
pkg-config \
curl \
Expand Down Expand Up @@ -80,7 +95,7 @@ ARG databricks_odbc_driver_url=https://databricks-bi-artifacts.s3.us-east-2.amaz
RUN <<EOF
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl https://packages.microsoft.com/config/debian/12/prod.list > /etc/apt/sources.list.d/mssql-release.list
curl https://packages.microsoft.com/config/debian/13/prod.list > /etc/apt/sources.list.d/mssql-release.list
apt-get update
ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18
apt-get clean
Expand All @@ -97,10 +112,12 @@ EOF

WORKDIR /app

ENV POETRY_VERSION=2.1.4
ENV POETRY_VERSION=2.4.1
ENV POETRY_HOME=/etc/poetry
ENV POETRY_VIRTUALENVS_CREATE=false
RUN curl -sSL --retry 3 --retry-delay 5 https://install.python-poetry.org | python3 -

RUN python3 -m pip install --no-cache-dir --upgrade "pip>=26.1" "setuptools>=78.1.1" "wheel>=0.46.2" \
&& curl -sSL --retry 3 --retry-delay 5 https://install.python-poetry.org | python3 -

# Avoid crashes, including corrupted cache artifacts, when building multi-platform images with GitHub Actions.
RUN /etc/poetry/bin/poetry cache clear pypi --all
Expand Down
8 changes: 1 addition & 7 deletions client/.babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,7 @@
],
"plugins": [
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-object-assign",
[
"babel-plugin-transform-builtin-extend",
{
"globals": ["Error"]
}
]
"@babel/plugin-transform-object-assign"
],
"env": {
"test": {
Expand Down
6 changes: 3 additions & 3 deletions client/app/components/dashboards/TextboxDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toString } from "lodash";
import { markdown } from "markdown";
import { marked } from "marked";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { useDebouncedCallback } from "use-debounce";
Expand All @@ -20,11 +20,11 @@ function TextboxDialog({ dialog, isNew, ...props }) {

useEffect(() => {
setText(props.text);
setPreview(markdown.toHTML(props.text));
setPreview(marked.parse(props.text || ""));
}, [props.text]);

const [updatePreview] = useDebouncedCallback(() => {
setPreview(markdown.toHTML(text));
setPreview(marked.parse(text || ""));
}, 200);

const handleInputChange = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { markdown } from "markdown";
import { marked } from "marked";
import Menu from "antd/lib/menu";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
import TextboxDialog from "@/components/dashboards/TextboxDialog";
Expand Down Expand Up @@ -32,7 +32,7 @@ function TextboxWidget(props) {

return (
<Widget {...props} menuOptions={canEdit ? TextboxMenuOptions : null} className="widget-text">
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">{markdown.toHTML(text || "")}</HtmlContent>
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">{marked.parse(text || "")}</HtmlContent>
</Widget>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { compact, isEmpty, invoke, map } from "lodash";
import { markdown } from "markdown";
import { marked } from "marked";
import cx from "classnames";
import Menu from "antd/lib/menu";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
Expand Down Expand Up @@ -107,7 +107,7 @@ function VisualizationWidgetHeader({
</p>
{!isEmpty(widget.getQuery().description) && (
<HtmlContent className="text-muted markdown query--description">
{markdown.toHTML(widget.getQuery().description || "")}
{marked.parse(widget.getQuery().description || "")}
</HtmlContent>
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/app/pages/queries/VisualizationEmbed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { find, has } from "lodash";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import moment from "moment";
import { markdown } from "markdown";
import { marked } from "marked";

import Button from "antd/lib/button";
import Dropdown from "antd/lib/dropdown";
Expand Down Expand Up @@ -40,7 +40,7 @@ function VisualizationEmbedHeader({ queryName, queryDescription, visualization }
<VisualizationName visualization={visualization} /> {queryName}
{queryDescription && (
<small>
<HtmlContent className="markdown text-muted">{markdown.toHTML(queryDescription || "")}</HtmlContent>
<HtmlContent className="markdown text-muted">{marked.parse(queryDescription || "")}</HtmlContent>
</small>
)}
</h3>
Expand Down
169 changes: 107 additions & 62 deletions client/cypress/cypress.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-disable import/no-extraneous-dependencies, no-console */
const { find } = require("lodash");
const axios = require("axios");
const { execSync } = require("child_process");
const { get, post } = require("request").defaults({ jar: true });
const { seedData } = require("./seed-data");
const fs = require("fs");
var Cookie = require("request-cookies").Cookie;
const { seedData } = require("./seed-data");

let cypressConfigBaseUrl;
try {
Expand All @@ -14,31 +12,76 @@ try {

const baseUrl = process.env.CYPRESS_baseUrl || cypressConfigBaseUrl || "http://localhost:5001";

function seedDatabase(seedValues) {
get(baseUrl + "/login", (_, { headers }) => {
const request = seedValues.shift();
const data = request.type === "form" ? { formData: request.data } : { json: request.data };

if (headers["set-cookie"]) {
const cookies = headers["set-cookie"].map((cookie) => new Cookie(cookie));
const csrfCookie = find(cookies, { key: "csrf_token" });
if (csrfCookie) {
if (request.type === "form") {
data["formData"] = { ...data["formData"], csrf_token: csrfCookie.value };
} else {
data["headers"] = { "X-CSRFToken": csrfCookie.value };
}
}
}
// Minimal cookie jar (avoids deprecated `request` / `request-cookies` packages).
function parseSetCookieHeader(setCookieHeaders) {
const jar = {};
if (!setCookieHeaders) return jar;
const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
for (const header of headers) {
const [pair] = header.split(";");
const idx = pair.indexOf("=");
if (idx === -1) continue;
const name = pair.slice(0, idx).trim();
const value = pair.slice(idx + 1).trim();
if (name) jar[name] = value;
}
return jar;
}

post(baseUrl + request.route, data, (err, response) => {
const result = response ? response.statusCode : err;
console.log("POST " + request.route + " - " + result);
if (seedValues.length) {
seedDatabase(seedValues);
}
function cookieJarToHeader(jar) {
return Object.entries(jar)
.map(([k, v]) => `${k}=${v}`)
.join("; ");
}

function buildFormBody(data) {
return Object.entries(data)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
}

async function seedDatabase(seedValues) {
let cookieJar = {};
try {
const loginResp = await axios.get(`${baseUrl}/login`, {
maxRedirects: 0,
validateStatus: () => true,
});
});
cookieJar = parseSetCookieHeader(loginResp.headers["set-cookie"]);
} catch (err) {
console.log(`GET /login failed: ${err.message}`);
}

const csrfToken = cookieJar.csrf_token;

for (const request of seedValues) {
const isForm = request.type === "form";
const headers = { Cookie: cookieJarToHeader(cookieJar) };

let body;
if (isForm) {
const formData = csrfToken ? { ...request.data, csrf_token: csrfToken } : { ...request.data };
body = buildFormBody(formData);
headers["Content-Type"] = "application/x-www-form-urlencoded";
} else {
body = request.data;
headers["Content-Type"] = "application/json";
if (csrfToken) headers["X-CSRFToken"] = csrfToken;
}

try {
const response = await axios.post(`${baseUrl}${request.route}`, body, {
headers,
maxRedirects: 0,
validateStatus: () => true,
});
console.log(`POST ${request.route} - ${response.status}`);
const newCookies = parseSetCookieHeader(response.headers["set-cookie"]);
cookieJar = { ...cookieJar, ...newCookies };
} catch (err) {
console.log(`POST ${request.route} - ${err.message}`);
}
}
}

function buildServer() {
Expand Down Expand Up @@ -75,38 +118,40 @@ function runCypressCI() {

const command = process.argv[2] || "all";

switch (command) {
case "build":
buildServer();
break;
case "start":
startServer();
if (!process.argv.includes("--skip-db-seed")) {
seedDatabase(seedData);
}
break;
case "db-seed":
seedDatabase(seedData);
break;
case "run":
execSync("cypress run", { stdio: "inherit" });
break;
case "open":
execSync("cypress open", { stdio: "inherit" });
break;
case "run-ci":
runCypressCI();
break;
case "stop":
stopServer();
break;
case "all":
startServer();
seedDatabase(seedData);
execSync("cypress run", { stdio: "inherit" });
stopServer();
break;
default:
console.log("Usage: pnpm run cypress [build|start|db-seed|open|run|stop]");
break;
}
(async () => {
switch (command) {
case "build":
buildServer();
break;
case "start":
startServer();
if (!process.argv.includes("--skip-db-seed")) {
await seedDatabase(seedData);
}
break;
case "db-seed":
await seedDatabase(seedData);
break;
case "run":
execSync("cypress run", { stdio: "inherit" });
break;
case "open":
execSync("cypress open", { stdio: "inherit" });
break;
case "run-ci":
runCypressCI();
break;
case "stop":
stopServer();
break;
case "all":
startServer();
await seedDatabase(seedData);
execSync("cypress run", { stdio: "inherit" });
stopServer();
break;
default:
console.log("Usage: pnpm run cypress [build|start|db-seed|open|run|stop]");
break;
}
})();
Loading