diff --git a/.github/workflows/issue-416-error-redaction-fix.yml b/.github/workflows/issue-416-error-redaction-fix.yml deleted file mode 100644 index a02978a9d..000000000 --- a/.github/workflows/issue-416-error-redaction-fix.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Issue 416 Backup Error Redaction Fix - -on: - push: - branches: - - feat/issue-416-backup-destinations - paths: - - .github/workflows/issue-416-error-redaction-fix.yml - -permissions: - contents: write - -jobs: - fix-and-verify: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - ref: feat/issue-416-backup-destinations - - uses: pnpm/action-setup@v5 - with: - version: 10.22.0 - - uses: actions/setup-node@v5 - with: - node-version: 24.4.0 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Apply guarded root-cause fix - run: | - python - <<'PY' - from pathlib import Path - - redact = Path('packages/server/src/utils/backups/redact.ts') - text = redact.read_text() - marker = '\n};\n' - addition = '''\n\nexport const getSafeRcloneErrorMessage = (error: unknown): string =>\n\tredactRcloneCredentials(\n\t\terror instanceof Error ? error.message : String(error),\n\t);\n''' - if 'getSafeRcloneErrorMessage' not in text: - pos = text.rfind(marker) - if pos == -1: - raise SystemExit('redact helper insertion point not found') - pos += len(marker) - text = text[:pos] + addition + text[pos:] - redact.write_text(text) - - files = [ - 'packages/server/src/utils/backups/postgres.ts', - 'packages/server/src/utils/backups/mysql.ts', - 'packages/server/src/utils/backups/mariadb.ts', - 'packages/server/src/utils/backups/mongo.ts', - 'packages/server/src/utils/backups/libsql.ts', - 'packages/server/src/utils/backups/compose.ts', - ] - for name in files: - p = Path(name) - text = p.read_text() - if 'getSafeRcloneErrorMessage' not in text: - anchor = 'import {\n\tgetBackupCommand,' - if anchor not in text: - raise SystemExit(f'{name}: utils import anchor not found') - text = text.replace(anchor, 'import { getSafeRcloneErrorMessage } from "./redact";\nimport {\n\tgetBackupCommand,', 1) - - catch = '\t} catch (error) {\n' - if text.count(catch) != 1: - raise SystemExit(f'{name}: expected exactly one catch, got {text.count(catch)}') - if 'const safeErrorMessage = getSafeRcloneErrorMessage(error);' not in text: - text = text.replace(catch, catch + '\t\tconst safeErrorMessage = getSafeRcloneErrorMessage(error);\n', 1) - - text = text.replace( - '\t\tconsole.log(error);\n', - '\t\tconsole.error("Backup error:", safeErrorMessage);\n', - 1, - ) - old_error = '\t\t\t// @ts-ignore\n\t\t\terrorMessage: error?.message || "Error message not provided",' - if old_error not in text: - raise SystemExit(f'{name}: notification error message anchor not found') - text = text.replace(old_error, '\t\t\terrorMessage: safeErrorMessage,', 1) - if '\t\tthrow error;\n' not in text: - raise SystemExit(f'{name}: rethrow anchor not found') - text = text.replace('\t\tthrow error;\n', '\t\tthrow new Error(safeErrorMessage);\n', 1) - p.write_text(text) - - web = Path('packages/server/src/utils/backups/web-server.ts') - text = web.read_text() - text = text.replace( - 'import { redactRcloneCredentials } from "./redact";', - 'import { getSafeRcloneErrorMessage, redactRcloneCredentials } from "./redact";', - 1, - ) - old = '''\t\tconst safeErrorMessage = redactRcloneCredentials(\n\t\t\terror instanceof Error ? error.message : String(error),\n\t\t);''' - if old not in text: - raise SystemExit('web-server safe message anchor not found') - text = text.replace(old, '\t\tconst safeErrorMessage = getSafeRcloneErrorMessage(error);', 1) - if '\t\tthrow error;\n' not in text: - raise SystemExit('web-server rethrow anchor not found') - text = text.replace('\t\tthrow error;\n', '\t\tthrow new Error(safeErrorMessage);\n', 1) - web.write_text(text) - - test = Path('apps/dokploy/__test__/backups/redact-credentials.test.ts') - text = test.read_text() - text = text.replace( - 'import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact";', - 'import {\n\tgetSafeRcloneErrorMessage,\n\tredactRcloneCredentials,\n} from "@dokploy/server/utils/backups/redact";', - 1, - ) - insertion = '''\n\tit("should sanitize rclone credentials from propagated backup errors", () => {\n\t\tconst error = new Error(\n\t\t\t"Command failed: rclone rcat --s3-access-key-id=AKIA-LEAK --s3-secret-access-key=s3-secret --ftp-pass=ftp-secret --sftp-pass=sftp-secret --sftp-key-file-pass=key-pass :ftp:backups/file.gz",\n\t\t);\n\t\tconst safe = getSafeRcloneErrorMessage(error);\n\n\t\tfor (const secret of [\n\t\t\t"AKIA-LEAK",\n\t\t\t"s3-secret",\n\t\t\t"ftp-secret",\n\t\t\t"sftp-secret",\n\t\t\t"key-pass",\n\t\t]) {\n\t\t\texpect(safe).not.toContain(secret);\n\t\t}\n\t\texpect(safe.match(/\\[REDACTED\\]/g)?.length).toBe(5);\n\t});\n''' - if 'should sanitize rclone credentials from propagated backup errors' not in text: - idx = text.rfind('\n});') - if idx == -1: - raise SystemExit('redaction test insertion point not found') - text = text[:idx] + insertion + text[idx:] - test.write_text(text) - PY - - name: Format changed files - run: | - pnpm exec biome check --write \ - packages/server/src/utils/backups/redact.ts \ - packages/server/src/utils/backups/postgres.ts \ - packages/server/src/utils/backups/mysql.ts \ - packages/server/src/utils/backups/mariadb.ts \ - packages/server/src/utils/backups/mongo.ts \ - packages/server/src/utils/backups/libsql.ts \ - packages/server/src/utils/backups/compose.ts \ - packages/server/src/utils/backups/web-server.ts \ - apps/dokploy/__test__/backups/redact-credentials.test.ts - - name: Security regression test - run: pnpm --filter=dokploy exec vitest --config __test__/vitest.config.ts __test__/backups/redact-credentials.test.ts __test__/utils/backups.test.ts __test__/utils/issue-416-path-safety.test.ts --run - - name: Typecheck - run: pnpm typecheck - - name: Server build - run: pnpm server:build - - name: Verify all backup error sinks sanitize and rethrow safe messages - run: | - python - <<'PY' - from pathlib import Path - files = [ - 'postgres.ts','mysql.ts','mariadb.ts','mongo.ts','libsql.ts','compose.ts','web-server.ts' - ] - for f in files: - s=Path('packages/server/src/utils/backups', f).read_text() - assert 'getSafeRcloneErrorMessage(error)' in s, f - assert 'throw new Error(safeErrorMessage);' in s, f - assert 'errorMessage: error?.message' not in s, f - assert '\t\tconsole.log(error);' not in s, f - print('all backup error propagation paths use sanitized messages') - PY - - name: Commit verified source and test changes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add \ - packages/server/src/utils/backups/redact.ts \ - packages/server/src/utils/backups/postgres.ts \ - packages/server/src/utils/backups/mysql.ts \ - packages/server/src/utils/backups/mariadb.ts \ - packages/server/src/utils/backups/mongo.ts \ - packages/server/src/utils/backups/libsql.ts \ - packages/server/src/utils/backups/compose.ts \ - packages/server/src/utils/backups/web-server.ts \ - apps/dokploy/__test__/backups/redact-credentials.test.ts - git commit -m "fix: redact backup provider credentials from propagated errors" - git push origin HEAD:feat/issue-416-backup-destinations