Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .claude/skills/check-results-service/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GoogleWorkspaceUser> & { 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 <T,>(path: string): Promise<T> => {
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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,
);
},
};
Loading