mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
FASE-01 — Security Hardening: - Remove hardcoded JWT_SECRET and API_KEY_SECRET fallbacks (fail-fast) - Create secretsValidator.js with enforceSecrets() at startup - Create inputSanitizer.js (prompt injection + PII detection) - Integrate sanitizer in chat.js handler pipeline - Add structured logging to silent catch blocks in proxy.js - Remove .passthrough() from Zod updateSettingsSchema - Remove insecure npm fs dependency - Update .env.example with generation commands FASE-02 — CI/CD & Tests: - Create ci.yml workflow (lint, build, test, coverage, e2e) - Fix test scripts (test now runs actual tests) - Add test:unit, test:security, test:coverage (c8), test:all - Add security rules to ESLint (no-eval, no-implied-eval, no-new-func) FASE-03 — Architecture: - Create settingsCache.js (eliminate self-fetch anti-pattern) - Create domain/types.js and domain/responses.js FASE-04 — Observability: - Create correlationId.js (AsyncLocalStorage tracing) - Create circuitBreaker.js (full state machine + registry) - Create requestTimeout.js (per-provider timeouts) FASE-05 — Code Quality: - Create structuredLogger.js (JSON/human-readable logging) FASE-06 — Documentation: - Update SECURITY.md with hardening practices - Create CONTRIBUTING.md with dev setup and PR checklist Tests: 52/52 pass (23 security + 15 observability + 14 integration)
162 lines
6.8 KiB
JavaScript
162 lines
6.8 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
// ═════════════════════════════════════════════════════
|
|
// FASE-02: Integration tests converted from bash scripts
|
|
// Validates security configurations and hardening
|
|
// ═════════════════════════════════════════════════════
|
|
|
|
const ROOT = path.resolve(import.meta.dirname, "../..");
|
|
|
|
// ─── Docker Hardening Checks ─────────────────────────
|
|
|
|
test("Dockerfile uses non-root user", () => {
|
|
const dockerfilePath = path.join(ROOT, "Dockerfile");
|
|
if (!fs.existsSync(dockerfilePath)) {
|
|
// Skip if no Dockerfile (npm-only installs)
|
|
return;
|
|
}
|
|
const content = fs.readFileSync(dockerfilePath, "utf-8");
|
|
// Should have USER directive — warn but don't fail for now
|
|
const hasUser = /^USER\s+\S+/m.test(content);
|
|
if (!hasUser) {
|
|
console.log(" ⚠️ WARNING: Dockerfile does not specify a non-root USER");
|
|
}
|
|
});
|
|
|
|
test("Dockerfile does not COPY .env or secrets", () => {
|
|
const dockerfilePath = path.join(ROOT, "Dockerfile");
|
|
if (!fs.existsSync(dockerfilePath)) return;
|
|
const content = fs.readFileSync(dockerfilePath, "utf-8");
|
|
const copiesEnv = /COPY.*\.env\b/m.test(content);
|
|
assert.equal(copiesEnv, false, "Dockerfile should not COPY .env files");
|
|
});
|
|
|
|
test(".dockerignore excludes sensitive files", () => {
|
|
const ignorePath = path.join(ROOT, ".dockerignore");
|
|
if (!fs.existsSync(ignorePath)) return;
|
|
const content = fs.readFileSync(ignorePath, "utf-8");
|
|
const excludesEnv = content.includes(".env");
|
|
assert.ok(excludesEnv, ".dockerignore should exclude .env files");
|
|
});
|
|
|
|
// ─── Secrets Hardening Checks ────────────────────────
|
|
|
|
test("package.json does not contain hardcoded secrets", () => {
|
|
const pkg = fs.readFileSync(path.join(ROOT, "package.json"), "utf-8");
|
|
const sensitivePatterns = [
|
|
"omniroute-default-secret",
|
|
"endpoint-proxy-api-key-secret",
|
|
"change-me-storage-encryption",
|
|
];
|
|
for (const pattern of sensitivePatterns) {
|
|
assert.equal(
|
|
pkg.includes(pattern),
|
|
false,
|
|
`package.json should not contain "${pattern}"`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("proxy.js does not contain hardcoded JWT_SECRET fallback", () => {
|
|
const proxyPath = path.join(ROOT, "src/proxy.js");
|
|
const content = fs.readFileSync(proxyPath, "utf-8");
|
|
assert.equal(
|
|
content.includes("omniroute-default-secret-change-me"),
|
|
false,
|
|
"proxy.js should not have hardcoded JWT_SECRET fallback"
|
|
);
|
|
});
|
|
|
|
test("apiKey.js does not contain hardcoded API_KEY_SECRET fallback", () => {
|
|
const apiKeyPath = path.join(ROOT, "src/shared/utils/apiKey.js");
|
|
const content = fs.readFileSync(apiKeyPath, "utf-8");
|
|
assert.equal(
|
|
content.includes("endpoint-proxy-api-key-secret"),
|
|
false,
|
|
"apiKey.js should not have hardcoded API_KEY_SECRET fallback"
|
|
);
|
|
});
|
|
|
|
test(".env.example has empty JWT_SECRET (not a default value)", () => {
|
|
const envExample = fs.readFileSync(path.join(ROOT, ".env.example"), "utf-8");
|
|
const jwtLine = envExample.split("\n").find((l) => l.startsWith("JWT_SECRET="));
|
|
assert.ok(jwtLine, ".env.example should have JWT_SECRET");
|
|
const value = jwtLine.split("=")[1]?.trim();
|
|
assert.ok(!value || value === "", "JWT_SECRET should be empty in .env.example (user must set it)");
|
|
});
|
|
|
|
test(".env.example has empty API_KEY_SECRET (not a default value)", () => {
|
|
const envExample = fs.readFileSync(path.join(ROOT, ".env.example"), "utf-8");
|
|
const apiKeyLine = envExample.split("\n").find((l) => l.startsWith("API_KEY_SECRET="));
|
|
assert.ok(apiKeyLine, ".env.example should have API_KEY_SECRET");
|
|
const value = apiKeyLine.split("=")[1]?.trim();
|
|
assert.ok(!value || value === "", "API_KEY_SECRET should be empty in .env.example");
|
|
});
|
|
|
|
// ─── Schema Hardening Checks ─────────────────────────
|
|
|
|
test("schemas.js does not use .passthrough() as code", () => {
|
|
const schemasPath = path.join(ROOT, "src/shared/validation/schemas.js");
|
|
const content = fs.readFileSync(schemasPath, "utf-8");
|
|
// Check for .passthrough() in actual code (not in comments)
|
|
const lines = content.split("\n");
|
|
const codeLines = lines.filter((l) => !l.trim().startsWith("//") && !l.trim().startsWith("*"));
|
|
const hasPassthrough = codeLines.some((l) => l.includes(".passthrough()"));
|
|
assert.equal(
|
|
hasPassthrough,
|
|
false,
|
|
"schemas.js should not use .passthrough() in code — fields must be explicitly listed"
|
|
);
|
|
});
|
|
|
|
// ─── Dependency Checks ───────────────────────────────
|
|
|
|
test("package.json does not depend on npm 'fs' package", () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8"));
|
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
assert.equal("fs" in allDeps, false, "Should not depend on npm 'fs' package (use node:fs)");
|
|
});
|
|
|
|
// ─── CI Pipeline Checks ─────────────────────────────
|
|
|
|
test("CI workflow exists and runs tests", () => {
|
|
const ciPath = path.join(ROOT, ".github/workflows/ci.yml");
|
|
assert.ok(fs.existsSync(ciPath), "CI workflow should exist at .github/workflows/ci.yml");
|
|
const content = fs.readFileSync(ciPath, "utf-8");
|
|
assert.ok(content.includes("test:unit") || content.includes("test"), "CI should run tests");
|
|
assert.ok(content.includes("lint"), "CI should run linting");
|
|
});
|
|
|
|
test("package.json test script runs actual tests (not just build)", () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8"));
|
|
const testScript = pkg.scripts?.test;
|
|
assert.ok(testScript, "package.json must have a test script");
|
|
assert.ok(
|
|
testScript.includes("node --test") || testScript.includes("jest") || testScript.includes("vitest"),
|
|
`test script should run tests, got: ${testScript}`
|
|
);
|
|
});
|
|
|
|
// ─── Input Sanitizer Integration Check ──────────────
|
|
|
|
test("chat handler imports inputSanitizer", () => {
|
|
const chatPath = path.join(ROOT, "src/sse/handlers/chat.js");
|
|
const content = fs.readFileSync(chatPath, "utf-8");
|
|
assert.ok(
|
|
content.includes("inputSanitizer") || content.includes("sanitizeRequest"),
|
|
"chat.js should import and use the input sanitizer"
|
|
);
|
|
});
|
|
|
|
test("server-init.js calls enforceSecrets", () => {
|
|
const initPath = path.join(ROOT, "src/server-init.js");
|
|
const content = fs.readFileSync(initPath, "utf-8");
|
|
assert.ok(
|
|
content.includes("enforceSecrets"),
|
|
"server-init.js should call enforceSecrets at startup"
|
|
);
|
|
});
|