DevOS v1 + Phase 2: unified resource model, infra ops, website deployment, AI chat ops
- Monorepo: NestJS API (Fastify) + React web (antd/ProComponents) + shared packages - PostgreSQL/Prisma (unified resources, relations graph, audit, events, jobs) - Auth/RBAC/workspace isolation, credential vault (AES-256-GCM) - Servers/SSH, Docker discovery & lifecycle, Job Center - Websites & deployments (git provider credential clone, env, health verification) - Domains/DNS (Cloudflare adapter), certificates (TLS probe), monitoring - Automation (cron/event/webhook HMAC + notifications), AI Gateway, MCP (29 tools) - AI chat window (LLM function-calling + rules fallback, SSE streaming, approvals) - Notes: Vditor WYSIWYG markdown editor, non-blocking autosave - Tests: 35 unit/security tests; UI audit clean (desktop+mobile, deep links)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/** JWT 签发/校验单元测试(@devos/auth) */
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { signJwt, verifyJwt, hashPassword, verifyPassword } from "../packages/auth/src/index";
|
||||
|
||||
const SECRET = "test-secret";
|
||||
|
||||
describe("JWT", () => {
|
||||
test("签发并校验合法令牌", () => {
|
||||
const token = signJwt({ sub: "u1", email: "a@b.c", wid: "w1" }, SECRET, { nowSec: 1000, expiresInSec: 3600 });
|
||||
const result = verifyJwt(token, SECRET, 2000);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.payload.sub).toBe("u1");
|
||||
expect(result.payload.wid).toBe("w1");
|
||||
expect(result.payload.exp).toBe(4600);
|
||||
}
|
||||
});
|
||||
|
||||
test("过期令牌被拒绝", () => {
|
||||
const token = signJwt({ sub: "u1", email: "a@b.c", wid: null }, SECRET, { nowSec: 1000, expiresInSec: 100 });
|
||||
const result = verifyJwt(token, SECRET, 2000);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.reason).toBe("EXPIRED");
|
||||
});
|
||||
|
||||
test("篡改签名被拒绝", () => {
|
||||
const token = signJwt({ sub: "u1", email: "a@b.c", wid: null }, SECRET);
|
||||
const parts = token.split(".");
|
||||
const tampered = `${parts[0]}.${parts[1]}.AAAA${parts[2].slice(4)}`;
|
||||
expect(verifyJwt(tampered, SECRET).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("错误密钥被拒绝", () => {
|
||||
const token = signJwt({ sub: "u1", email: "a@b.c", wid: null }, SECRET);
|
||||
expect(verifyJwt(token, "other-secret").ok).toBe(false);
|
||||
});
|
||||
|
||||
test("alg 混淆攻击被拒绝(仅接受 HS256)", () => {
|
||||
const forgedHeader = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url");
|
||||
const payload = Buffer.from(JSON.stringify({ sub: "u1", exp: 9999999999 })).toString("base64url");
|
||||
const forged = `${forgedHeader}.${payload}.`;
|
||||
expect(verifyJwt(forged, SECRET).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("畸形令牌被拒绝", () => {
|
||||
expect(verifyJwt("abc", SECRET).ok).toBe(false);
|
||||
expect(verifyJwt("a.b.c", SECRET).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Password", () => {
|
||||
test("哈希并校验密码", async () => {
|
||||
const hash = await hashPassword("MyPassword123!");
|
||||
expect(hash).not.toContain("MyPassword123!");
|
||||
expect(await verifyPassword("MyPassword123!", hash)).toBe(true);
|
||||
expect(await verifyPassword("wrong", hash)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/** 安全回归测试(Phase A,对应 11-TEST-ACCEPTANCE 安全测试项) */
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
buildSignatureHeader,
|
||||
verifyWebhookSignature,
|
||||
SIGNATURE_TOLERANCE_MS,
|
||||
} from "../apps/api/src/common/security/webhook-signature";
|
||||
import { SecurityPolicyService, assertProductionSecrets } from "../apps/api/src/common/security/security-policy.service";
|
||||
import { ConfigService } from "../apps/api/src/common/config/config.service";
|
||||
|
||||
describe("webhook HMAC 签名", () => {
|
||||
const secret = "whsec-test";
|
||||
const body = JSON.stringify({ event: "deployment.failed" });
|
||||
|
||||
test("合法签名通过", () => {
|
||||
const header = buildSignatureHeader(secret, body);
|
||||
expect(verifyWebhookSignature(secret, header, body).ok).toBe(true);
|
||||
});
|
||||
|
||||
test("错误密钥被拒绝", () => {
|
||||
const header = buildSignatureHeader(secret, body);
|
||||
expect(verifyWebhookSignature("other", header, body).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("篡改请求体被拒绝", () => {
|
||||
const header = buildSignatureHeader(secret, body);
|
||||
expect(verifyWebhookSignature(secret, header, body + "x").ok).toBe(false);
|
||||
});
|
||||
|
||||
test("过期时间戳(重放)被拒绝", () => {
|
||||
const stale = Math.floor((Date.now() - SIGNATURE_TOLERANCE_MS - 60_000) / 1000);
|
||||
const header = `t=${stale},v1=${require("node:crypto").createHmac("sha256", secret).update(`${stale}.${body}`).digest("hex")}`;
|
||||
const result = verifyWebhookSignature(secret, header, body);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toContain("replay");
|
||||
});
|
||||
|
||||
test("缺失/畸形签名被拒绝", () => {
|
||||
expect(verifyWebhookSignature(secret, undefined, body).ok).toBe(false);
|
||||
expect(verifyWebhookSignature(secret, "t=abc,v1=zz", body).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("限流器", () => {
|
||||
test("窗口内超过上限被拒绝", () => {
|
||||
const svc = new SecurityPolicyService(new ConfigService());
|
||||
let allowed = 0;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (svc.checkRate("login:1.2.3.4")) allowed++;
|
||||
}
|
||||
expect(allowed).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
test("不同维度键相互隔离", () => {
|
||||
const svc = new SecurityPolicyService(new ConfigService());
|
||||
expect(svc.checkRate("login:5.5.5.5")).toBe(true);
|
||||
expect(svc.checkRate("login:6.6.6.6")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("注册开关", () => {
|
||||
test("生产模式下默认关闭", () => {
|
||||
const svc = new SecurityPolicyService(new ConfigService());
|
||||
// 本测试进程 NODE_ENV 非 production → 默认开启;显式环境由部署配置决定
|
||||
const verdict = svc.checkRegistration();
|
||||
expect(typeof verdict.allowed).toBe("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("生产 secret 校验", () => {
|
||||
test("拒绝已知开发默认 APP_SECRET", () => {
|
||||
const origEnv = process.env.NODE_ENV;
|
||||
const origSecret = process.env.APP_SECRET;
|
||||
try {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.APP_SECRET = "dev-devos-app-secret-change-me-please-0123456789";
|
||||
process.env.ENCRYPTION_KEY = "a".repeat(64);
|
||||
expect(() => new ConfigService()).toThrow(/APP_SECRET/);
|
||||
} finally {
|
||||
process.env.NODE_ENV = origEnv;
|
||||
process.env.APP_SECRET = origSecret;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/** Cron 解析器与脱敏工具单元测试 */
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parseCron, parseCronField, cronMatches } from "../apps/api/src/modules/automation/cron";
|
||||
import { maskSecrets, isDestructiveCommand, maskString } from "../apps/api/src/common/utils/mask";
|
||||
import { evaluateCondition } from "../apps/api/src/modules/automation/automation.service";
|
||||
import { parseDockerPsOutput, assertSafeContainerId } from "../apps/api/src/integrations/docker/docker.service";
|
||||
|
||||
describe("cron", () => {
|
||||
test("每分钟表达式匹配", () => {
|
||||
expect(cronMatches("* * * * *", new Date(2026, 8, 8, 10, 30))).toBe(true);
|
||||
});
|
||||
|
||||
test("固定分钟匹配", () => {
|
||||
expect(cronMatches("30 10 * * *", new Date(2026, 8, 8, 10, 30))).toBe(true);
|
||||
expect(cronMatches("30 10 * * *", new Date(2026, 8, 8, 10, 31))).toBe(false);
|
||||
});
|
||||
|
||||
test("步进表达式 */15", () => {
|
||||
expect(cronMatches("*/15 * * * *", new Date(2026, 8, 8, 10, 0))).toBe(true);
|
||||
expect(cronMatches("*/15 * * * *", new Date(2026, 8, 8, 10, 15))).toBe(true);
|
||||
expect(cronMatches("*/15 * * * *", new Date(2026, 8, 8, 10, 16))).toBe(false);
|
||||
});
|
||||
|
||||
test("区间与列表混合", () => {
|
||||
expect(cronMatches("1-5,10 9-17 * * 1-5", new Date(2026, 8, 8, 12, 3))).toBe(true); // 2026-09-08 是周二
|
||||
expect(cronMatches("1-5,10 9-17 * * 1-5", new Date(2026, 8, 8, 12, 7))).toBe(false);
|
||||
expect(cronMatches("* * * * 0,6", new Date(2026, 8, 8, 12, 0))).toBe(false); // 周二不匹配周末
|
||||
});
|
||||
|
||||
test("非法表达式安全返回 false", () => {
|
||||
expect(cronMatches("not a cron", new Date())).toBe(false);
|
||||
expect(cronMatches("* * * *", new Date())).toBe(false);
|
||||
});
|
||||
|
||||
test("字段越界抛错", () => {
|
||||
expect(() => parseCronField("61 * * * *", [0, 59])).toThrow();
|
||||
expect(() => parseCron("* * * * 9", new Date())).toThrow();
|
||||
});
|
||||
|
||||
test("parseCron 返回 5 个字段集合", () => {
|
||||
expect(parseCron("*/2 0-23 1,15 * SUN-MON".replace("SUN-MON", "0-1")).length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maskSecrets", () => {
|
||||
test("敏感键被遮蔽", () => {
|
||||
const out = maskSecrets({ password: "abc", token: "xyz", name: "keep" });
|
||||
expect(out.password).toBe("***");
|
||||
expect(out.token).toBe("***");
|
||||
expect(out.name).toBe("keep");
|
||||
});
|
||||
|
||||
test("嵌套对象递归遮蔽", () => {
|
||||
const out = maskSecrets({ a: { apiKey: "secret-value" }, list: [{ privateKey: "x" }] });
|
||||
expect((out.a as Record<string, unknown>).apiKey).toBe("***");
|
||||
expect((out.list as Record<string, unknown>[])[0].privateKey).toBe("***");
|
||||
});
|
||||
|
||||
test("内嵌 Bearer/sk- 值被遮蔽", () => {
|
||||
expect(maskString("Bearer abc.def.ghi")).toBe("Bearer ***");
|
||||
expect(maskString("key=sk-abc123")).toContain("***");
|
||||
});
|
||||
|
||||
test("深度限制与原语透传", () => {
|
||||
expect(maskSecrets(42)).toBe(42);
|
||||
expect(maskSecrets(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDestructiveCommand", () => {
|
||||
test("识别破坏性命令", () => {
|
||||
expect(isDestructiveCommand("rm -rf /")).toBe(true);
|
||||
expect(isDestructiveCommand("sudo rm -r /data")).toBe(true);
|
||||
expect(isDestructiveCommand("mkfs.ext4 /dev/sda1")).toBe(true);
|
||||
expect(isDestructiveCommand("dd if=/dev/zero of=/dev/sda")).toBe(true);
|
||||
expect(isDestructiveCommand("reboot")).toBe(true);
|
||||
expect(isDestructiveCommand("DROP TABLE users")).toBe(true);
|
||||
});
|
||||
|
||||
test("普通命令放行", () => {
|
||||
expect(isDestructiveCommand("docker ps")).toBe(false);
|
||||
expect(isDestructiveCommand("ls -la /var/log")).toBe(false);
|
||||
expect(isDestructiveCommand("git pull origin main")).toBe(false);
|
||||
expect(isDestructiveCommand("rm file.txt")).toBe(false); // 无 -r/-f 标志
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateCondition", () => {
|
||||
const payload = { branch: "main", nested: { count: 5 }, flag: true };
|
||||
test("eq / neq / contains", () => {
|
||||
expect(evaluateCondition({ path: "branch", op: "eq", value: "main" }, payload)).toBe(true);
|
||||
expect(evaluateCondition({ path: "branch", op: "neq", value: "dev" }, payload)).toBe(true);
|
||||
expect(evaluateCondition({ path: "branch", op: "contains", value: "ai" }, payload)).toBe(true);
|
||||
});
|
||||
test("gt / lt / exists", () => {
|
||||
expect(evaluateCondition({ path: "nested.count", op: "gt", value: 3 }, payload)).toBe(true);
|
||||
expect(evaluateCondition({ path: "nested.count", op: "lt", value: 3 }, payload)).toBe(false);
|
||||
expect(evaluateCondition({ path: "nested.count", op: "exists" }, payload)).toBe(true);
|
||||
expect(evaluateCondition({ path: "missing", op: "exists" }, payload)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDockerPsOutput", () => {
|
||||
test("解析 NDJSON 输出", () => {
|
||||
const line1 = JSON.stringify({ ID: "abc123", Names: "web,web1", Image: "nginx", State: "running", Status: "Up 2 hours", Ports: "0.0.0.0:80->80/tcp", Labels: "team=core" });
|
||||
const line2 = JSON.stringify({ ID: "def456", Names: "db", Image: "postgres", State: "exited", Status: "Exited (0)" });
|
||||
const result = parseDockerPsOutput(`${line1}\n${line2}\n`);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].name).toBe("web");
|
||||
expect(result[0].state).toBe("running");
|
||||
expect(result[0].labels["team"]).toBe("core");
|
||||
expect(result[1].state).toBe("exited");
|
||||
});
|
||||
|
||||
test("跳过噪音行", () => {
|
||||
expect(parseDockerPsOutput("WARN: something\n{bad json}\n").length).toBe(0);
|
||||
expect(parseDockerPsOutput("").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertSafeContainerId", () => {
|
||||
test("合法 ID 放行", () => {
|
||||
expect(assertSafeContainerId("abc123def456")).toBe("abc123def456");
|
||||
expect(assertSafeContainerId("my-container.name_1")).toBe("my-container.name_1");
|
||||
});
|
||||
test("注入被拒绝", () => {
|
||||
expect(() => assertSafeContainerId("abc; rm -rf /")).toThrow();
|
||||
expect(() => assertSafeContainerId("$(whoami)")).toThrow();
|
||||
expect(() => assertSafeContainerId("-oProxyCommand=evil")).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user