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

- 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:
2026-09-16 22:53:55 +02:00
parent 7fb6043e34
commit 12f6a65b80
13 changed files with 757 additions and 52 deletions
+10 -8
View File
@@ -1,6 +1,7 @@
export interface RegistryRow {
id: string;
owner_sub: string;
tenant_id: string;
name: string;
url: string;
config: unknown;
@@ -27,10 +28,10 @@ function toRegistry(row: Record<string, unknown>): {
};
}
export async function listRegistries(db: PoolLike, ownerSub: string) {
export async function listRegistries(db: PoolLike, ownerSub: string, tenantId: string) {
const res = await db.query(
"SELECT id, owner_sub, name, url, config, created_at FROM registries WHERE owner_sub = $1 ORDER BY created_at DESC",
[ownerSub],
"SELECT id, owner_sub, tenant_id, name, url, config, created_at FROM registries WHERE owner_sub = $1 AND tenant_id = $2 ORDER BY created_at DESC",
[ownerSub, tenantId],
);
return res.rows.map(toRegistry);
}
@@ -38,19 +39,20 @@ export async function listRegistries(db: PoolLike, ownerSub: string) {
export async function createRegistry(
db: PoolLike,
ownerSub: string,
tenantId: string,
input: { name: string; url: string; config?: unknown },
) {
const res = await db.query(
"INSERT INTO registries (owner_sub, name, url, config) VALUES ($1, $2, $3, $4::jsonb) RETURNING id, owner_sub, name, url, config, created_at",
[ownerSub, input.name, input.url, JSON.stringify(input.config ?? {})],
"INSERT INTO registries (owner_sub, tenant_id, name, url, config) VALUES ($1, $2, $3, $4, $5::jsonb) RETURNING id, owner_sub, tenant_id, name, url, config, created_at",
[ownerSub, tenantId, input.name, input.url, JSON.stringify(input.config ?? {})],
);
return toRegistry(res.rows[0]);
}
export async function getRegistry(db: PoolLike, ownerSub: string, id: string) {
export async function getRegistry(db: PoolLike, ownerSub: string, tenantId: string, id: string) {
const res = await db.query(
"SELECT id, owner_sub, name, url, config, created_at FROM registries WHERE owner_sub = $1 AND id = $2",
[ownerSub, id],
"SELECT id, owner_sub, tenant_id, name, url, config, created_at FROM registries WHERE owner_sub = $1 AND tenant_id = $2 AND id = $3",
[ownerSub, tenantId, id],
);
return res.rows[0] ? toRegistry(res.rows[0]) : null;
}