added db guard changes to prevent deletion
This commit is contained in:
1
.env
1
.env
@@ -7,6 +7,7 @@ DATABASE_URL=postgres://skymoney_app:RicearoniSkyMoney124521!@postgres:5432/skym
|
||||
BACKUP_DATABASE_URL=postgres://skymoney_app:RicearoniSkyMoney124521!@127.0.0.1:5432/skymoney
|
||||
RESTORE_DATABASE_URL=postgres://skymoney_app:RicearoniSkyMoney124521!@127.0.0.1:5432/skymoney_restore_test
|
||||
ADMIN_DATABASE_URL=postgres://skymoney_app:RicearoniSkyMoney124521!@127.0.0.1:5432/postgres
|
||||
TEST_DATABASE_URL=postgres://skymoney_app:RicearoniSkyMoney124521!@postgres:5432/skymoney_test
|
||||
|
||||
APP_ORIGIN=https://skymoneybudget.com
|
||||
CORS_ORIGINS=https://skymoneybudget.com
|
||||
|
||||
@@ -19,10 +19,13 @@ DATABASE_URL=postgres://skymoney_app:change-me@postgres:5432/skymoney
|
||||
BACKUP_DATABASE_URL=postgres://skymoney_app:change-me@127.0.0.1:5432/skymoney
|
||||
RESTORE_DATABASE_URL=postgres://skymoney_app:change-me@127.0.0.1:5432/skymoney_restore_test
|
||||
ADMIN_DATABASE_URL=postgres://postgres:change-me@127.0.0.1:5432/postgres
|
||||
TEST_DATABASE_URL=postgres://skymoney_app:change-me@127.0.0.1:5432/skymoney_test
|
||||
EXPECTED_PROD_DB_HOST=postgres
|
||||
EXPECTED_PROD_DB_NAME=skymoney
|
||||
EXPECTED_BACKUP_DB_HOST=127.0.0.1
|
||||
EXPECTED_BACKUP_DB_NAME=skymoney
|
||||
PROTECTED_DB_NAMES=skymoney,postgres,template0,template1
|
||||
REQUIRE_TEST_DB_NAME=1
|
||||
PROD_DB_VOLUME_NAME=skymoney_pgdata
|
||||
ALLOW_EMPTY_PROD_VOLUME=0
|
||||
ARCHIVE_EXISTING_RESTORE_DB=1
|
||||
|
||||
@@ -46,9 +46,21 @@ jobs:
|
||||
cd api
|
||||
npm ci
|
||||
|
||||
- name: Guard TEST_DATABASE_URL target
|
||||
env:
|
||||
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
|
||||
EXPECTED_PROD_DB_NAME: skymoney
|
||||
PROTECTED_DB_NAMES: skymoney,postgres,template0,template1
|
||||
REQUIRE_TEST_DB_NAME: "1"
|
||||
run: |
|
||||
chmod +x ./scripts/validate-test-db-target.sh
|
||||
bash ./scripts/validate-test-db-target.sh
|
||||
|
||||
- name: Run OWASP security suite (DB-backed)
|
||||
env:
|
||||
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
|
||||
PROTECTED_DB_NAMES: skymoney,postgres,template0,template1
|
||||
REQUIRE_TEST_DB_NAME: "1"
|
||||
run: |
|
||||
cd api
|
||||
SECURITY_DB_TESTS=1 npx vitest run -c vitest.security.config.ts
|
||||
|
||||
@@ -30,11 +30,48 @@ function resolveDatabaseUrl(): string {
|
||||
if (dbUrl) return dbUrl.replace("@postgres:", "@127.0.0.1:");
|
||||
}
|
||||
|
||||
return "postgres://app:app@127.0.0.1:5432/skymoney";
|
||||
return "postgres://app:app@127.0.0.1:5432/skymoney_test";
|
||||
}
|
||||
|
||||
function parseDbName(url: string): string {
|
||||
const parsed = new URL(url);
|
||||
const dbName = parsed.pathname.replace(/^\/+/, "");
|
||||
if (!dbName) throw new Error(`DATABASE_URL has no database name: ${url}`);
|
||||
return dbName;
|
||||
}
|
||||
|
||||
function assertSafeDbTarget(url: string): void {
|
||||
const requireTestDbName = process.env.REQUIRE_TEST_DB_NAME === "1";
|
||||
const protectedNamesRaw =
|
||||
process.env.PROTECTED_DB_NAMES ??
|
||||
process.env.EXPECTED_PROD_DB_NAME ??
|
||||
"skymoney,postgres,template0,template1";
|
||||
const protectedNames = new Set(
|
||||
protectedNamesRaw
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
const dbName = parseDbName(url);
|
||||
|
||||
if (protectedNames.has(dbName)) {
|
||||
throw new Error(
|
||||
`Refusing to run DB tests against protected database '${dbName}'. ` +
|
||||
"Set TEST_DATABASE_URL to a dedicated test database."
|
||||
);
|
||||
}
|
||||
|
||||
if (requireTestDbName && !/(test|ci|sandbox|staging|shadow|tmp)/i.test(dbName)) {
|
||||
throw new Error(
|
||||
`Refusing to run DB tests against '${dbName}' because it does not look like a test database. ` +
|
||||
"Set REQUIRE_TEST_DB_NAME=0 only for intentional local exceptions."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || "test";
|
||||
process.env.DATABASE_URL = resolveDatabaseUrl();
|
||||
assertSafeDbTarget(process.env.DATABASE_URL);
|
||||
process.env.PORT = process.env.PORT || "8081";
|
||||
process.env.HOST ??= "127.0.0.1";
|
||||
process.env.CORS_ORIGIN = process.env.CORS_ORIGIN || "";
|
||||
|
||||
@@ -31,4 +31,16 @@ describe("A03 Software Supply Chain Failures", () => {
|
||||
|
||||
expect(deployWorkflow).toMatch(/uses:\s*actions\/checkout@v\d+\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
it("guards DB-backed security tests from targeting production database", () => {
|
||||
const repoRoot = resolve(__dirname, "..", "..");
|
||||
const securityWorkflow = readFileSync(
|
||||
resolve(repoRoot, ".gitea/workflows/security.yml"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
expect(securityWorkflow).toContain("name: Guard TEST_DATABASE_URL target");
|
||||
expect(securityWorkflow).toContain("bash ./scripts/validate-test-db-target.sh");
|
||||
expect(securityWorkflow).toContain("TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
||||
setupFiles: dbSecurityTestsEnabled ? ["tests/setup.ts"] : [],
|
||||
env: {
|
||||
NODE_ENV: "test",
|
||||
DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney",
|
||||
DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney_test",
|
||||
AUTH_DISABLED: "1",
|
||||
SEED_DEFAULT_BUDGET: "1",
|
||||
JWT_SECRET: "test-jwt-secret-32-chars-min-abcdef",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Production DB Recovery and Safety Runbook
|
||||
|
||||
Last updated: March 2, 2026
|
||||
Last updated: March 10, 2026
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -191,6 +191,8 @@ psql "postgres://<admin-user>:<admin-pass>@127.0.0.1:5432/skymoney" \
|
||||
4. Deploy workflow runs `scripts/guard-prod-volume.sh` and blocks deploy when prod volume is missing/empty.
|
||||
5. Deploy workflow runs pre-migration `scripts/backup.sh`.
|
||||
6. Deploy workflow uses `prisma migrate deploy` only.
|
||||
7. Security DB workflow runs `scripts/validate-test-db-target.sh` and refuses protected DB names (`skymoney`, `postgres`, `template*`).
|
||||
8. DB-backed test runtime (`api/tests/setup.ts`) refuses protected DB targets before any `deleteMany` cleanup runs.
|
||||
|
||||
### Intentional rebuild override
|
||||
|
||||
|
||||
47
scripts/validate-test-db-target.sh
Normal file
47
scripts/validate-test-db-target.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${TEST_DATABASE_URL:-}" ]]; then
|
||||
echo "TEST_DATABASE_URL is required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXPECTED_PROD_DB_NAME="${EXPECTED_PROD_DB_NAME:-skymoney}"
|
||||
PROTECTED_DB_NAMES="${PROTECTED_DB_NAMES:-$EXPECTED_PROD_DB_NAME,postgres,template0,template1}"
|
||||
REQUIRE_TEST_DB_NAME="${REQUIRE_TEST_DB_NAME:-1}"
|
||||
|
||||
extract_db() {
|
||||
local url="$1"
|
||||
sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+/([^?]+).*$#\1#' <<< "$url"
|
||||
}
|
||||
|
||||
TEST_DB_NAME="$(extract_db "$TEST_DATABASE_URL")"
|
||||
if [[ "$TEST_DB_NAME" == "$TEST_DATABASE_URL" || -z "$TEST_DB_NAME" ]]; then
|
||||
echo "Unable to parse TEST_DATABASE_URL database name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${DATABASE_URL:-}" && "$TEST_DATABASE_URL" == "$DATABASE_URL" ]]; then
|
||||
echo "TEST_DATABASE_URL must not equal DATABASE_URL."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a protected <<< "$PROTECTED_DB_NAMES"
|
||||
for name in "${protected[@]}"; do
|
||||
trimmed="$(echo "$name" | xargs)"
|
||||
if [[ -n "$trimmed" && "$TEST_DB_NAME" == "$trimmed" ]]; then
|
||||
echo "Refusing to run DB security tests against protected database '$TEST_DB_NAME'."
|
||||
echo "Set TEST_DATABASE_URL to a dedicated test database (for example: skymoney_test)."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$REQUIRE_TEST_DB_NAME" == "1" ]]; then
|
||||
if ! [[ "$TEST_DB_NAME" =~ (test|ci|sandbox|staging|shadow|tmp) ]]; then
|
||||
echo "Refusing TEST_DATABASE_URL db '$TEST_DB_NAME': name must include test/ci/sandbox/staging/shadow/tmp."
|
||||
echo "If intentional, set REQUIRE_TEST_DB_NAME=0 for this run."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "TEST_DATABASE_URL target check passed (db=$TEST_DB_NAME)."
|
||||
@@ -11,6 +11,8 @@ Use this after every deploy (staging and production).
|
||||
- `ALLOW_INSECURE_AUTH_FOR_DEV=false`
|
||||
4. Test DB preflight (for DB-backed suites):
|
||||
- `TEST_DATABASE_URL` points to a reachable PostgreSQL instance.
|
||||
- `TEST_DATABASE_URL` database name is not `skymoney` and is clearly test-only (for example `skymoney_test`).
|
||||
- `bash ./scripts/validate-test-db-target.sh` passes before DB-backed suites run.
|
||||
- Example quick check:
|
||||
```bash
|
||||
echo "$TEST_DATABASE_URL"
|
||||
|
||||
Reference in New Issue
Block a user