feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @file security.test.js
|
||||
* @description Tests the network-exposure hardening (GHSA-gr74-4xfh-6jw9):
|
||||
* loopback-by-default bind, Host-header allowlist (anti DNS-rebinding),
|
||||
* loopback-only CORS, and the optional bearer-token gate on /api/* + WebSocket.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
const { describe, it, afterEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const sec = require("../lib/security");
|
||||
|
||||
const ENV_KEYS = ["DASHBOARD_HOST", "DASHBOARD_ALLOWED_HOSTS", "DASHBOARD_TOKEN"];
|
||||
afterEach(() => {
|
||||
for (const k of ENV_KEYS) delete process.env[k];
|
||||
});
|
||||
|
||||
function mockRes() {
|
||||
return {
|
||||
statusCode: 0,
|
||||
body: null,
|
||||
status(c) {
|
||||
this.statusCode = c;
|
||||
return this;
|
||||
},
|
||||
json(b) {
|
||||
this.body = b;
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveHost", () => {
|
||||
it("defaults to loopback (127.0.0.1)", () => {
|
||||
assert.equal(sec.resolveHost(), "127.0.0.1");
|
||||
});
|
||||
it("honors an explicit DASHBOARD_HOST opt-in", () => {
|
||||
process.env.DASHBOARD_HOST = "0.0.0.0";
|
||||
assert.equal(sec.resolveHost(), "0.0.0.0");
|
||||
assert.equal(sec.isLoopbackHostname("0.0.0.0"), true); // treated as loopback-equiv for Host checks
|
||||
});
|
||||
});
|
||||
|
||||
describe("Host allowlist (DNS-rebinding defense)", () => {
|
||||
it("allows loopback Host headers", () => {
|
||||
assert.equal(sec.isHostAllowed("localhost:4820"), true);
|
||||
assert.equal(sec.isHostAllowed("127.0.0.1:4820"), true);
|
||||
assert.equal(sec.isHostAllowed("[::1]:4820"), true);
|
||||
assert.equal(sec.isHostAllowed(""), true); // missing Host (HTTP/1.0 / local tooling)
|
||||
});
|
||||
it("rejects a rebound attacker Host", () => {
|
||||
assert.equal(sec.isHostAllowed("evil.example"), false);
|
||||
assert.equal(sec.isHostAllowed("attacker.example:4820"), false);
|
||||
});
|
||||
it("permits operator-allowlisted hostnames", () => {
|
||||
process.env.DASHBOARD_ALLOWED_HOSTS = "dash.internal, 192.168.1.50";
|
||||
assert.equal(sec.isHostAllowed("dash.internal:4820"), true);
|
||||
assert.equal(sec.isHostAllowed("192.168.1.50:4820"), true);
|
||||
assert.equal(sec.isHostAllowed("evil.example"), false);
|
||||
});
|
||||
it("hostGuard middleware 403s a disallowed Host", () => {
|
||||
const res = mockRes();
|
||||
let nexted = false;
|
||||
sec.hostGuard({ headers: { host: "evil.example" } }, res, () => (nexted = true));
|
||||
assert.equal(nexted, false);
|
||||
assert.equal(res.statusCode, 403);
|
||||
assert.equal(res.body.error.code, "EBADHOST");
|
||||
});
|
||||
it("hostGuard middleware allows loopback", () => {
|
||||
let nexted = false;
|
||||
sec.hostGuard({ headers: { host: "localhost:4820" } }, mockRes(), () => (nexted = true));
|
||||
assert.equal(nexted, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CORS", () => {
|
||||
const allowed = (origin) =>
|
||||
new Promise((resolve) => sec.corsOptions().origin(origin, (_e, ok) => resolve(ok)));
|
||||
|
||||
it("allows same-origin / no-Origin (curl, the server's own client)", async () => {
|
||||
assert.equal(await allowed(undefined), true);
|
||||
});
|
||||
it("allows loopback origins", async () => {
|
||||
assert.equal(await allowed("http://localhost:5173"), true);
|
||||
assert.equal(await allowed("http://127.0.0.1:4820"), true);
|
||||
});
|
||||
it("refuses cross-origin pages", async () => {
|
||||
assert.equal(await allowed("https://evil.example"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("token gate (optional, opt-in)", () => {
|
||||
it("is a no-op when DASHBOARD_TOKEN is unset (default)", () => {
|
||||
let nexted = false;
|
||||
sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, mockRes(), () => (nexted = true));
|
||||
assert.equal(nexted, true);
|
||||
});
|
||||
|
||||
it("rejects a missing/invalid token when configured", () => {
|
||||
process.env.DASHBOARD_TOKEN = "s3cret";
|
||||
const res = mockRes();
|
||||
let nexted = false;
|
||||
sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, res, () => (nexted = true));
|
||||
assert.equal(nexted, false);
|
||||
assert.equal(res.statusCode, 401);
|
||||
assert.equal(res.body.error.code, "EUNAUTHORIZED");
|
||||
|
||||
const res2 = mockRes();
|
||||
sec.tokenGuard(
|
||||
{ path: "/stats", headers: { "x-dashboard-token": "wrong" }, query: {} },
|
||||
res2,
|
||||
() => {}
|
||||
);
|
||||
assert.equal(res2.statusCode, 401);
|
||||
});
|
||||
|
||||
it("accepts a correct token via header, bearer, or query", () => {
|
||||
process.env.DASHBOARD_TOKEN = "s3cret";
|
||||
const ok = (req) => {
|
||||
let nexted = false;
|
||||
sec.tokenGuard(req, mockRes(), () => (nexted = true));
|
||||
return nexted;
|
||||
};
|
||||
assert.equal(
|
||||
ok({ path: "/stats", headers: { "x-dashboard-token": "s3cret" }, query: {} }),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
ok({ path: "/stats", headers: { authorization: "Bearer s3cret" }, query: {} }),
|
||||
true
|
||||
);
|
||||
assert.equal(ok({ path: "/stats", headers: {}, query: { token: "s3cret" } }), true);
|
||||
});
|
||||
|
||||
it("exempts health, docs, and local hook ingestion even when a token is set", () => {
|
||||
process.env.DASHBOARD_TOKEN = "s3cret";
|
||||
const ok = (path) => {
|
||||
let nexted = false;
|
||||
sec.tokenGuard({ path, headers: {}, query: {} }, mockRes(), () => (nexted = true));
|
||||
return nexted;
|
||||
};
|
||||
assert.equal(ok("/health"), true);
|
||||
assert.equal(ok("/openapi.json"), true);
|
||||
assert.equal(ok("/hooks/event"), true);
|
||||
assert.equal(ok("/sessions/abc"), false); // still gated
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket auth", () => {
|
||||
it("allows any upgrade when no token is configured", () => {
|
||||
assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), true);
|
||||
});
|
||||
it("requires a matching ?token= when configured", () => {
|
||||
process.env.DASHBOARD_TOKEN = "s3cret";
|
||||
assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=s3cret", headers: {} }), true);
|
||||
assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=nope", headers: {} }), false);
|
||||
assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), false);
|
||||
assert.equal(
|
||||
sec.isWebSocketAuthorized({ url: "/ws", headers: { "x-dashboard-token": "s3cret" } }),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user