diff --git a/.claude/skills/check-results-service/SKILL.md b/.claude/skills/check-results-service/SKILL.md index f79904ebff..5caf3db7e7 100644 --- a/.claude/skills/check-results-service/SKILL.md +++ b/.claude/skills/check-results-service/SKILL.md @@ -52,6 +52,36 @@ Notes: - Empty array = "no data" (source not bound/connected, or never really ran). Never throws for "no results". +## Person-scoped results: the shape contract + +Checks about PEOPLE (employee access, 2FA/MFA, training) follow a standard emission shape — +this section is the canonical definition of it: + +- **One row per person** — never a single aggregate row with a roster buried in `evidence`. +- **`resourceType: 'user'`** (exactly this string). +- **`resourceId` = the person's email, lowercased + trimmed.** Fallback `username || id` + only when the provider genuinely exposes no email (such rows won't join to members — + acceptable, still visible in evidence views). +- **`evidence`** carries what the provider knows: `email`, `name`, `role`, `isAdmin`, + `status`, `lastLogin`, plus a `checkedAt` timestamp. +- **Access/inventory rows always emit as pass** (having access is information, not a + violation); compliance-gate checks (2FA, training) pass/fail per person; error paths + (bad creds, missing scopes) stay org-level rows. + +So a feature joining check results to org members does exactly this — no parsing, no AI: + +```ts +const rows = await checkResults.getLatestResultsForTask({ + organizationId, taskTemplateId, sourceSlug, resourceType: 'user', +}); +const forMember = rows.filter((r) => r.resourceId === member.email.toLowerCase()); +``` + +If a source returns zero `'user'` rows, its check hasn't been normalized to the standard +yet (or genuinely has no per-person data) — render that as "no per-person data from this +source", and fix the CHECK to emit the shape above. Never work around it by parsing +aggregate evidence in the feature. + ## The envelope you get back ```ts diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/employee-access.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/employee-access.test.ts new file mode 100644 index 0000000000..a25eca1f9a --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/employee-access.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'bun:test'; +import { employeeAccessCheck } from '../checks/employee-access'; +import type { CheckContext, CheckResult, CheckVariableValues } from '../../../types'; +import type { GoogleWorkspaceUser } from '../types'; + +const makeUser = (overrides: Partial & { primaryEmail: string }): GoogleWorkspaceUser => ({ + id: `id_${overrides.primaryEmail}`, + name: { givenName: 'Test', familyName: 'User', fullName: 'Test User' }, + isAdmin: false, + isDelegatedAdmin: false, + isEnrolledIn2Sv: true, + isEnforcedIn2Sv: true, + suspended: false, + archived: false, + creationTime: '2024-01-01T00:00:00Z', + lastLoginTime: '2026-01-01T00:00:00Z', + orgUnitPath: '/', + ...overrides, +}); + +async function runCheck( + users: GoogleWorkspaceUser[], + variables: CheckVariableValues = {}, +): Promise<{ passed: CheckResult[]; failed: CheckResult[] }> { + const passed: CheckResult[] = []; + const failed: CheckResult[] = []; + + const ctx: CheckContext = { + accessToken: 'tok', + credentials: {}, + variables, + connectionId: 'conn_1', + organizationId: 'org_1', + metadata: {}, + log: () => {}, + pass: (result) => { + passed.push(result as CheckResult); + }, + fail: (result) => { + failed.push(result as CheckResult); + }, + fetch: (async (path: string): Promise => { + if (path.includes('/roles')) { + return { items: [{ roleId: 'r1', roleName: 'Groups Admin' }] } as unknown as T; + } + if (path.includes('/roleassignments')) { + return { + items: users + .filter((u) => u.isDelegatedAdmin) + .map((u) => ({ roleId: 'r1', assignedTo: u.id })), + } as unknown as T; + } + if (path.includes('/users')) { + return { kind: 'k', users } as unknown as T; + } + throw new Error(`Unexpected fetch: ${path}`); + }) as CheckContext['fetch'], + fetchAllPages: (async () => []) as CheckContext['fetchAllPages'], + graphql: (async () => ({})) as CheckContext['graphql'], + } as CheckContext; + + await employeeAccessCheck.run(ctx); + return { passed, failed }; +} + +describe('employeeAccessCheck per-user emission', () => { + it('emits one user row per person, keyed by lowercased email', async () => { + const users = [ + makeUser({ primaryEmail: 'Admin@Example.com', isAdmin: true }), + makeUser({ primaryEmail: 'person@example.com' }), + ]; + + const { passed, failed } = await runCheck(users); + + expect(failed).toHaveLength(0); + expect(passed).toHaveLength(2); + expect(passed.every((r) => r.resourceType === 'user')).toBe(true); + expect(passed.map((r) => r.resourceId).sort()).toEqual([ + 'admin@example.com', + 'person@example.com', + ]); + }); + + it('carries role details in each row evidence', async () => { + const users = [ + makeUser({ primaryEmail: 'admin@example.com', isAdmin: true }), + makeUser({ primaryEmail: 'delegated@example.com', isDelegatedAdmin: true }), + makeUser({ primaryEmail: 'person@example.com' }), + ]; + + const { passed } = await runCheck(users); + + const byEmail = new Map(passed.map((r) => [r.resourceId, r])); + expect((byEmail.get('admin@example.com')?.evidence as { role: string }).role).toBe( + 'Super Admin', + ); + expect((byEmail.get('delegated@example.com')?.evidence as { role: string }).role).toBe( + 'Delegated Admin', + ); + expect((byEmail.get('person@example.com')?.evidence as { role: string }).role).toBe('User'); + expect((byEmail.get('person@example.com')?.evidence as { email: string }).email).toBe( + 'person@example.com', + ); + }); + + it('excludes suspended users by default (same filter as employee sync)', async () => { + const users = [ + makeUser({ primaryEmail: 'active@example.com' }), + makeUser({ primaryEmail: 'gone@example.com', suspended: true }), + ]; + + const { passed } = await runCheck(users); + + expect(passed.map((r) => r.resourceId)).toEqual(['active@example.com']); + }); + + it('emits a single org-level summary row when no users match the filters', async () => { + const users = [makeUser({ primaryEmail: 'gone@example.com', suspended: true })]; + + const { passed, failed } = await runCheck(users); + + expect(failed).toHaveLength(0); + expect(passed).toHaveLength(1); + expect(passed[0].resourceType).toBe('organization'); + expect(passed[0].resourceId).toBe('google-workspace'); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts index 6f9c87fa61..c80ff73ccf 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts @@ -176,27 +176,42 @@ export const employeeAccessCheck: IntegrationCheck = { }; }); - // Group users by role for summary + // Group users by role for the summary log const superAdmins = activeUsers.filter((u) => u.isAdmin); const delegatedAdmins = activeUsers.filter((u) => u.isDelegatedAdmin && !u.isAdmin); - const regularUsers = activeUsers.filter((u) => !u.isAdmin && !u.isDelegatedAdmin); - - // Pass with the full employee list as evidence - ctx.pass({ - title: 'Employee Access List', - resourceType: 'organization', - resourceId: 'google-workspace', - description: `Retrieved ${activeUsers.length} employees from Google Workspace (${superAdmins.length} super admins, ${delegatedAdmins.length} delegated admins, ${regularUsers.length} regular users)`, - evidence: { - totalUsers: activeUsers.length, - superAdminCount: superAdmins.length, - delegatedAdminCount: delegatedAdmins.length, - regularUserCount: regularUsers.length, - reviewedAt: new Date().toISOString(), - employees: employeeList, - }, - }); - ctx.log('Google Workspace Employee Access check complete'); + const checkedAt = new Date().toISOString(); + + // No users after filtering is still a completed review — emit one org-level + // row so the run never stores zero results (which would read as "no evidence"). + if (employeeList.length === 0) { + ctx.pass({ + title: 'Employee Access List', + resourceType: 'organization', + resourceId: 'google-workspace', + description: `No active users matched the configured filters (${allUsers.length} total user records inspected)`, + evidence: { totalUsers: 0, inspectedUsers: allUsers.length, checkedAt }, + }); + ctx.log('Google Workspace Employee Access check complete: 0 users after filtering'); + return; + } + + // One row per person (resourceType 'user', resourceId = lowercased email) so + // person-scoped features can join results to org members by email. Access is + // an inventory, not a violation — every person row emits as pass; error paths + // keep their org-level rows. + for (const employee of employeeList) { + ctx.pass({ + title: 'Employee Access', + resourceType: 'user', + resourceId: employee.email.toLowerCase().trim(), + description: `${employee.name} has access to Google Workspace as ${employee.role}`, + evidence: { ...employee, checkedAt }, + }); + } + + ctx.log( + `Google Workspace Employee Access check complete: ${employeeList.length} users (${superAdmins.length} super admins, ${delegatedAdmins.length} delegated admins)`, + ); }, };