feat(api): deterministic scan worker + AUD0 wire + tenant binding
CI (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / contracts (pull_request) Successful in 15s
CI (SIAX Cloud) / contracts (push) Successful in 17s
CI (SIAX Cloud) / quality (push) Successful in 1m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m10s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / contracts (pull_request) Successful in 15s
CI (SIAX Cloud) / contracts (push) Successful in 17s
CI (SIAX Cloud) / quality (push) Successful in 1m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m10s
- scan worker: atomic claim (FOR UPDATE SKIP LOCKED), Playwright-core + system chromium capture → typed EvidenceRecords (runtime-html/dom/computed-style/ stylesheet/asset/network-request/screenshot), confidence=measured - AUD0 emitter (fire-and-forget, advisory): c0py.registry_created/.scan_created/ .scan_completed/.scan_failed, tenant_id = Zitadel resourceowner (org) - tenant binding: resourceowner claim from introspection (deny-by-default 403), migration 002 tenant_id on registries+scans, all queries tenant+owner scoped - evidence gaps stay explicit (screenshot miss → no screenshot record) - 30/30 tests, canonical validator OK
This commit is contained in:
@@ -26,26 +26,28 @@ const ROW = {
|
||||
};
|
||||
|
||||
describe("registries service", () => {
|
||||
it("listRegistries filters by owner", async () => {
|
||||
it("listRegistries filters by owner and tenant", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [ROW] }) });
|
||||
const out = await listRegistries(db, "svc-user");
|
||||
const out = await listRegistries(db, "svc-user", "org-1");
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toMatchObject({ id: ROW.id, name: "example", createdAt: "2026-09-16T10:00:00.000Z" });
|
||||
expect(db.queries[0].values).toEqual(["svc-user"]);
|
||||
expect(db.queries[0].values).toEqual(["svc-user", "org-1"]);
|
||||
expect(db.queries[0].text).toContain("owner_sub = $1");
|
||||
expect(db.queries[0].text).toContain("tenant_id = $2");
|
||||
});
|
||||
|
||||
it("createRegistry inserts with owner and config jsonb", async () => {
|
||||
it("createRegistry inserts with owner, tenant and config jsonb", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [ROW] }) });
|
||||
const out = await createRegistry(db, "svc-user", { name: "example", url: "https://example.com" });
|
||||
const out = await createRegistry(db, "svc-user", "org-1", { name: "example", url: "https://example.com" });
|
||||
expect(out.id).toBe(ROW.id);
|
||||
expect(db.queries[0].values?.[0]).toBe("svc-user");
|
||||
expect(db.queries[0].values?.[1]).toBe("org-1");
|
||||
expect(db.queries[0].text).toContain("RETURNING");
|
||||
});
|
||||
|
||||
it("getRegistry returns null when not owner (fail-closed)", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
const out = await getRegistry(db, "other-user", ROW.id);
|
||||
const out = await getRegistry(db, "other-user", "org-1", ROW.id);
|
||||
expect(out).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -54,34 +56,36 @@ describe("scans service", () => {
|
||||
const SCAN_ROW = {
|
||||
...ROW,
|
||||
registry_id: ROW.id,
|
||||
tenant_id: "org-1",
|
||||
status: "pending",
|
||||
result: {},
|
||||
updated_at: ROW.created_at,
|
||||
};
|
||||
|
||||
it("createScan enforces registry ownership in SQL and returns null otherwise", async () => {
|
||||
it("createScan enforces registry ownership+tenant in SQL and returns null otherwise", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
const out = await createScan(db, "svc-user", ROW.id);
|
||||
const out = await createScan(db, "svc-user", "org-1", ROW.id);
|
||||
expect(out).toBeNull();
|
||||
expect(db.queries[0].text).toContain("owner_sub = $1");
|
||||
expect(db.queries[0].text).toContain("tenant_id = $2");
|
||||
});
|
||||
|
||||
it("createScan returns scan for owned registry", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [SCAN_ROW] }) });
|
||||
const out = await createScan(db, "svc-user", ROW.id);
|
||||
const out = await createScan(db, "svc-user", "org-1", ROW.id);
|
||||
expect(out).toMatchObject({ id: SCAN_ROW.id, registryId: SCAN_ROW.id, status: "pending" });
|
||||
});
|
||||
|
||||
it("listScans supports optional registryId filter", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [SCAN_ROW] }) });
|
||||
await listScans(db, "svc-user");
|
||||
expect(db.queries[0].text).not.toContain("registry_id = $2");
|
||||
await listScans(db, "svc-user", SCAN_ROW.id);
|
||||
expect(db.queries[1].values).toEqual(["svc-user", SCAN_ROW.id]);
|
||||
await listScans(db, "svc-user", "org-1");
|
||||
expect(db.queries[0].text).not.toContain("registry_id = $3");
|
||||
await listScans(db, "svc-user", "org-1", SCAN_ROW.id);
|
||||
expect(db.queries[1].values).toEqual(["svc-user", "org-1", SCAN_ROW.id]);
|
||||
});
|
||||
|
||||
it("getScan returns null for foreign scan", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
expect(await getScan(db, "other-user", SCAN_ROW.id)).toBeNull();
|
||||
expect(await getScan(db, "other-user", "org-1", SCAN_ROW.id)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user