Skip to content

Feat/google workspace admin audit - #3569

Open
cyberhiker wants to merge 4 commits into
trycompai:mainfrom
windborne:feat/google-workspace-admin-audit
Open

cyberhiker wants to merge 4 commits into
trycompai:mainfrom
windborne:feat/google-workspace-admin-audit

Conversation

@cyberhiker

@cyberhiker cyberhiker commented Sep 10, 2026

Copy link
Copy Markdown

I did stuff with Claude to get the Google Workspace stuff working.


Summary by cubic

Adds Google Workspace admin audit checks and fixes a correctness bug where admin roles granted through groups were invisible to access reviews. Existing Google Workspace connections must reconnect to get the new OAuth scopes.

Google Workspace

  • Adds two checks over the admin audit log: admin-privilege-changes and admin-security-events.
  • Turns on the Google Workspace Admin integration in the catalog.
  • Expands group-assigned admin roles to members and records whether each grant is direct or via a group.
  • Shares one user-scoping filter between sync and checks, with new target_groups and target_domains variables; nested groups are skipped and an empty selected group excludes everyone.
  • Removes the now-dead sync-ou-filter since the API sync uses the shared filter.
  • Reports missing OAuth scopes as findings instead of failing the run or under-counting admin access.
  • Fixes the sync controller tests that failed on a missing DI provider.

Also included

  • Adds the CMMC Level 2 framework definition (110 practices) with an import script and generator.
  • Adds setup-comp.sh and start-comp.sh for local development.

Written for commit b164c43. Summary will update on new commits.

Review in cubic

cyberhiker and others added 3 commits September 9, 2026 20:18
…access review

Implements the admin-audit service (previously implemented: false) and fixes a
correctness bug in the access review, then removes the duplicated user-scoping
logic that made the two diverge.

Admin Audit service

Two checks over the Reports API admin audit log:
- admin-privilege-changes: role and privilege grants/revocations. Super admin
  grants escalate to high severity; changes by configured approved actors pass
  with evidence rather than failing.
- admin-security-events: admin console changes that weaken posture (2SV
  enforcement, OAuth API access, admin password reset), each with its own
  severity and remediation.

Fix: admin roles assigned to groups were invisible

employee-access treated every role assignment's assignedTo as a user id. Google
sets assigneeType: 'group' on group assignments, where assignedTo is a group id
from a different id space, so those matched no user and anyone holding admin
access through a group was absent from the access review entirely.

Group assignments are now expanded to their members, and each grant records
whether it was direct or via a group -- provenance matters because a role held
through a group is revoked by changing the group, not the user.

One user-scope filter for sync and checks

sync.controller.ts re-implemented the OU and include/exclude rules that
check-user-filter.ts already had, kept aligned only by comments. Both now share
one implementation, so the personnel list and the access review cannot disagree
about the population.

The shared filter is explicitly staged because the two callers legitimately
differ: employee sync needs suspended users in scope to drive offboarding,
while security checks exclude them. isGoogleWorkspaceUserInScope covers org
unit, group and domain; isGoogleWorkspaceUserSelectedBySyncTerms covers the
include/exclude selection; shouldIncludeGoogleWorkspaceUserForCheck composes
both plus the activeness rule. The OU logic is unchanged.

Group and domain filtering (Drata parity)

New target_groups and target_domains variables, applied to both sync and
checks. Direct group members only -- nested groups are skipped with a warning.
A selected group with no members excludes everyone rather than silently
disabling the filter.

New OAuth scopes

admin.directory.group.readonly, admin.directory.domain.readonly and
admin.reports.audit.readonly. Existing connections predate all three and must
reconnect for full function. Each degrades to an actionable finding naming the
missing permission rather than erroring the run, so an unresolvable group
assignment or unreadable audit log is reported instead of silently
under-counting who has admin access.

Test suite repair

sync-gws.controller.spec.ts could not instantiate SyncController --
GenericDeviceSyncService was missing from the test module, so all 20 tests
failed on a DI error and the offboarding coverage was dark. Adding the provider
also surfaced a stale assertion: reactivation clears offboardDate (97636c4)
but the test still expected the old payload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CMMC 2.0 Level 2 as an importable framework: 110 practices across 14 families
aligned to NIST SP 800-171 Rev 2, 36 control templates covering every
requirement, 14 policy templates with body content, and 25 task templates.

Identifiers use the CMMC practice format (AC.L2-3.1.1). Family counts match
the published structure: AC 22, AT 3, AU 9, CM 9, IA 11, IR 3, MA 6, MP 9,
PS 2, PE 6, RA 3, CA 4, SC 16, SI 7.

The payload matches ImportFrameworkDto, so the supported path is
POST /v1/framework-editor/framework/import. import-cmmc.ts is a fallback for
headless use, since that route sits behind PlatformAdminGuard and needs a
browser session; it mirrors FrameworkExportService.import() and refuses to run
if a framework of the same name already exists.

Requirement statements paraphrase NIST SP 800-171 Rev 2 and should be verified
against the official publication before use in a real assessment. The control
groupings, policies and tasks are an editorial layer, not part of the standard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setup-comp.sh bootstraps a local stack; start-comp.sh starts Postgres, MinIO
and the three dev servers, and is idempotent.

start-comp.sh uses the dev:no-trigger scripts rather than `bun run dev`,
because the default dev script runs `trigger dev` under
`concurrently --kill-others` -- without a Trigger.dev account that process
exits immediately and takes Next and Nest down with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

CLAassistant commented Sep 10, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

39 issues found across 29 files

Confidence score: 1/5

  • check-user-filter.ts and sync.controller.ts can deactivate non-privileged users outside a selected Google Workspace group, causing unintended offboarding; constrain deactivation to the selected scope before merging.
  • directory-client.ts and role-assignments.ts can omit group members or group-admin access when API calls partially fail or return the GROUP enum, weakening 2FA/access reviews; propagate fetch failures and normalize the enum.
  • packages/db/scripts/import-cmmc.ts may connect to remote databases without the repository’s shared TLS policy, allowing plaintext or unverified TLS; use the shared database client/configuration.
  • setup-comp.sh can produce an unusable fresh setup because API secrets are missing, placeholder matching fails, and macOS rejects sort -V; fix secret generation and replace the non-portable version check.

Not reviewed (too large): frameworks/cmmc-level-2/cmmc-level-2.import.json (~5,269 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/integration-platform/src/manifests/google-workspace/index.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/index.ts:84">
P2: Disabling Admin Audit in the connection's Services UI does not stop these checks: the runner still executes every `manifest.checks` entry. Gate the `admin-audit` checks through the enabled-service state, or remove this toggle so disabled audit checks do not keep querying and creating findings.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts:65">
P1: When a selected group contains only part of a Workspace domain, sync deactivates existing non-privileged members outside that group as if they were deleted. Constrain deactivation to users in the selected scope, or skip deactivation for out-of-scope members.</violation>
</file>

<file name="frameworks/cmmc-level-2/README.md">

<violation number="1" location="frameworks/cmmc-level-2/README.md:46">
P2: The documented "Regenerating" workflow cannot regenerate the committed framework. `generator/build.py` hardcodes the output path `/private/tmp/claude-501/-Users-chris-Code-comp/a0fe0e29-c24d-4821-b750-359c9a908dc2/scratchpad/cmmc/payload.json`, and `generator/add_content.py` reads and writes that same machine/user-specific scratchpad path (including a `sys.path.insert` on it). Neither writes to the committed `frameworks/cmmc-level-2/cmmc-level-2.import.json`, and the absolute path will not exist on a fresh checkout or any other machine. So running the documented `python3 generator/build.py && python3 generator/add_content.py` produces no usable/committed output; the regeneration flow is effectively broken. Point both scripts at a repo-relative path to `cmmc-level-2.import.json`.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/directory-client.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/directory-client.ts:91">
P1: Whole-domain group memberships are silently discarded as non-users. A role assigned through such a group can disappear from the access review; expand the customer membership against the fetched users or report it as unresolved.</violation>

<violation number="2" location="packages/integration-platform/src/manifests/google-workspace/directory-client.ts:123">
P1: When reading one selected group fails, this function returns a partial member set and the callers treat it as a successful filter. That can omit real group members from 2FA/access reviews and cause the employee sync to treat them as out of scope. Propagate an unresolved-group failure, or return explicit filter state that makes the sync/check abort instead of operating on partial membership data.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts:33">
P2: When Google reports a super-admin role under a name outside `SUPER_ADMIN_ROLE_NAMES`, this check lowers the finding from high to medium. Resolve the role through Google’s Directory role metadata and `isSuperAdminRole` (or a provider role ID) instead of hardcoding role-name gates.</violation>

<violation number="2" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts:81">
P1: When the audit window contains more than 20,000 activities, `fetchAdminActivities` can return a truncated list and this check still reports no privilege changes as compliant. Propagate the truncation state and fail or mark the review inconclusive instead of emitting the clean pass for incomplete data.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/variables.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/variables.ts:147">
P1: When this dynamic option loader runs through the variables API, `listGroups` sends its required customer selector and pagination token through a second argument that `VariableFetchContext.fetch` silently ignores. The request can fail without a customer selector, and any paginated response makes `listGroups` repeatedly request the first page; adapt the client by serializing `options.params` into the URL before calling `ctx.fetch`.</violation>

<violation number="2" location="packages/integration-platform/src/manifests/google-workspace/variables.ts:153">
P2: When the Directory API fails while loading group or domain options, the picker silently returns an empty list and the operation remains unscoped. Surface the option-loading error so administrators do not mistake an unavailable filter for an empty selection.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/types.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/types.ts:145">
P3: The `GoogleWorkspaceGroupMember.type` union omits `'EXTERNAL'`, which the Google Directory members API actually returns for external users/groups added to a group. The accompanying comment enumerates the possible values ('USER for people, GROUP for a nested group, CUSTOMER for whole-domain') and is therefore incomplete, so the declared type cannot represent real API data for external members. Consider adding `'EXTERNAL'` to the union (and the comment) so the type matches the API shape the interface claims to model.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts:18">
P2: When a malformed or excessively large lookback value is persisted, `lookbackStartTime` throws `RangeError` and the check ends with status `error`. Bound the parsed value to the supported maximum, such as 180 days, before calculating the start time.</violation>

<violation number="2" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts:47">
P2: When both audit checks run, this call downloads the same paginated history that the privilege check already fetched. Cache the activity query per check run or combine the analyses to avoid duplicate Reports API calls and quota pressure.</violation>

<violation number="3" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts:72">
P2: The security check fails every monitored setting change, including changes that strengthen security. Compare `OLD_VALUE` and `NEW_VALUE` before reporting a weakening, or label this as a review-of-any-change check instead.</violation>

<violation number="4" location="packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts:85">
P2: This resource ID can collide for multiple audit events with the same timestamp and name, collapsing findings. Include the activity `uniqueQualifier` plus a stable event-specific discriminator in the resource ID.</violation>
</file>

<file name="packages/db/scripts/import-cmmc.ts">

<violation number="1" location="packages/db/scripts/import-cmmc.ts:19">
P1: When this command targets a remote database, it bypasses the repository’s shared TLS policy and can fall back to plaintext or unverified TLS. Use the shared database client/configuration instead of constructing a raw adapter here.</violation>

<violation number="2" location="packages/db/scripts/import-cmmc.ts:50">
P2: A future or malformed export is imported using the version-1 layout instead of being rejected. Reject unsupported versions before `validateIndices()` as the API importer does.</violation>

<violation number="3" location="packages/db/scripts/import-cmmc.ts:125">
P1: When a payload contains `controlTemplates[].documentTypes`, this importer silently drops those evidence-form mappings. Persist the document link rows as the API importer does.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/role-assignments.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/role-assignments.ts:29">
P1: When the Directory API returns its `GROUP` assignee enum, this comparison is false, so group-admin access is omitted from every member’s review. Normalize the enum before comparing it.</violation>

<violation number="2" location="packages/integration-platform/src/manifests/google-workspace/role-assignments.ts:50">
P1: When the role-assignment API fails, this catch converts the failure into an empty RBAC result, so the access review emits user rows that falsely omit custom delegated roles. Propagate the error or return an explicit incomplete result that the caller records as failed.</violation>

<violation number="3" location="packages/integration-platform/src/manifests/google-workspace/role-assignments.ts:127">
P1: Group-assigned admin roles still omit users in nested groups. `fetchGroupMemberUserIds` does not request indirect membership, so the access review under-reports inherited admin access. Resolve derived memberships for role expansion, or record the group as unresolved instead of treating the direct-member list as complete.</violation>
</file>

<file name="frameworks/cmmc-level-2/import-cmmc.ts">

<violation number="1" location="frameworks/cmmc-level-2/import-cmmc.ts:1">
P3: This new 178-line file duplicates `packages/db/scripts/import-cmmc.ts`, while the README instructs operators to run the existing copy. Keep one canonical importer so fixes cannot diverge between the two paths.</violation>

<violation number="2" location="frameworks/cmmc-level-2/import-cmmc.ts:20">
P1: Running this file writes framework data directly through Prisma, bypassing the `PlatformAdminGuard` on the supported import endpoint and all API RBAC checks. Route imports through the guarded API or add a separately authenticated, audited admin mechanism instead of a database fallback.</violation>
</file>

<file name="apps/api/src/integration-platform/controllers/sync.controller.ts">

<violation number="1" location="apps/api/src/integration-platform/controllers/sync.controller.ts:319">
P3: When `sync_user_filter_mode` contains an unsupported value, this condition logs that an empty list caused the fallback. Emit this warning only for `include` mode with an empty include list.</violation>

<violation number="2" location="apps/api/src/integration-platform/controllers/sync.controller.ts:492">
P1: When `target_groups` is set, this converts a user-level group scope into a domain-wide deactivation boundary. The loop can therefore offboard every non-privileged same-domain member outside the selected group; use exact scoped member identities or disable deletion outside a known authoritative scope.</violation>
</file>

<file name="packages/integration-platform/src/manifests/google-workspace/admin-audit-events.ts">

<violation number="1" location="packages/integration-platform/src/manifests/google-workspace/admin-audit-events.ts:73">
P2: When an admin revokes a privilege, `adminSecurityEventsCheck` reports an unconditional medium security-weakening finding even though revocation improves least privilege and is already covered by the privilege-review check. Remove this event from `HIGH_RISK_SECURITY_EVENTS` or classify it only in the privilege-review check.</violation>
</file>

<file name="setup-comp.sh">

<violation number="1" location="setup-comp.sh:14">
P2: The no-argument setup command only works on the author's `/Users/chris/Code/comp` checkout. Default to the script's repository directory or require and document an explicit path.</violation>

<violation number="2" location="setup-comp.sh:30">
P2: The bootstrap rejects Node 18 even though the repository declares Node `>=18`. Use the repository's supported floor or update the project engine requirement to match this script.</violation>

<violation number="3" location="setup-comp.sh:35">
P1: On macOS, BSD `sort` does not support `-V`, so this version check aborts before setup starts. Use a Node or `awk` version comparison instead of `sort -V`.</violation>

<violation number="4" location="setup-comp.sh:45">
P2: When the caller passes a relative repository path, changing into `$REPO` makes subsequent `$REPO/...` paths point one directory too deep. Normalize `$REPO` to an absolute path before continuing.</violation>

<violation number="5" location="setup-comp.sh:104">
P1: Because the templates put comments after empty assignments, this regex never matches the required secret placeholders. Allow trailing whitespace/comments so a fresh setup generates the secrets before the apps start.</violation>

<violation number="6" location="setup-comp.sh:113">
P1: A fresh run creates `apps/api/.env` without the API's required `SECRET_KEY`, so `auth.server.ts` throws during API startup. Add this secret to the API template and generate it for the API env file.</violation>

<violation number="7" location="setup-comp.sh:137">
P2: The setup instructions name variables that the application never reads, so Google OAuth remains unconfigured. Tell users to set `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET`.</violation>

<violation number="8" location="setup-comp.sh:154">
P2: `bun run db:migrate` is not a root script, so this post-setup command fails from the repository root. Invoke the workspace script, for example `bun run -F @trycompai/db db:migrate`.</violation>
</file>

<file name="start-comp.sh">

<violation number="1" location="start-comp.sh:13">
P2: On Intel macOS or Linux, this launcher cannot reliably locate or start its dependencies because it assumes Apple Silicon Homebrew. Resolve Homebrew’s actual prefix or use the repository’s Docker setup before invoking platform-specific commands.</violation>

<violation number="2" location="start-comp.sh:24">
P2: Running `./start-comp.sh stop` can kill unrelated Nest or Next development servers because these `pkill -f` patterns match every checkout. Track the launched process IDs or scope shutdown to this script’s process group.</violation>

<violation number="3" location="start-comp.sh:34">
P2: The two bootstrap scripts use different local-service managers: setup starts Docker Postgres, while start requires Homebrew PostgreSQL and MinIO. Use the same Docker/Homebrew path or validate and install the services required by `start-comp.sh`.</violation>

<violation number="4" location="start-comp.sh:47">
P2: When port 9000 returns an HTTP error such as 503, `curl` still exits 0 without `--fail`, so the script can declare MinIO ready and skip restarting it. Add `--fail` to each health probe.</violation>

<violation number="5" location="start-comp.sh:75">
P1: When the API fails to bind after the wait, this branch only warns and then starts both frontends. Exit nonzero here so the launcher does not report a usable stack with no auth source.</violation>
</file>

<file name="integrations-catalog/integrations/google-workspace-admin.json">

<violation number="1" location="integrations-catalog/integrations/google-workspace-admin.json:42">
P2: This marks a stale, differently-slugged catalog record active. The implementation is `google-workspace` with four checks and sync support, while this record advertises `google-workspace-admin` with two checks and no sync. Regenerate this catalog entry from the implemented provider, or keep it inactive until the metadata and slug match.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

includedTerms: parseSyncFilterTerms(variables.sync_included_emails),
userFilterMode: variables.sync_user_filter_mode as GoogleWorkspaceUserSyncFilterMode | undefined,
includeSuspended: variables.include_suspended === 'true',
targetGroups: toStringList(variables.target_groups),

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a selected group contains only part of a Workspace domain, sync deactivates existing non-privileged members outside that group as if they were deleted. Constrain deactivation to users in the selected scope, or skip deactivation for out-of-scope members.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts, line 65:

<comment>When a selected group contains only part of a Workspace domain, sync deactivates existing non-privileged members outside that group as if they were deleted. Constrain deactivation to users in the selected scope, or skip deactivation for out-of-scope members.</comment>

<file context>
@@ -32,25 +62,44 @@ export function parseGoogleWorkspaceCheckUserFilter(
     includedTerms: parseSyncFilterTerms(variables.sync_included_emails),
     userFilterMode: variables.sync_user_filter_mode as GoogleWorkspaceUserSyncFilterMode | undefined,
     includeSuspended: variables.include_suspended === 'true',
+    targetGroups: toStringList(variables.target_groups),
+    targetDomains: toStringList(variables.target_domains),
+    // Populated by resolveGoogleWorkspaceUserFilter; parsing stays synchronous
</file context>
Fix with cubic

for (const id of await fetchGroupMemberUserIds({ client, groupId })) {
memberIds.add(id);
}
} catch {

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When reading one selected group fails, this function returns a partial member set and the callers treat it as a successful filter. That can omit real group members from 2FA/access reviews and cause the employee sync to treat them as out of scope. Propagate an unresolved-group failure, or return explicit filter state that makes the sync/check abort instead of operating on partial membership data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/directory-client.ts, line 123:

<comment>When reading one selected group fails, this function returns a partial member set and the callers treat it as a successful filter. That can omit real group members from 2FA/access reviews and cause the employee sync to treat them as out of scope. Propagate an unresolved-group failure, or return explicit filter state that makes the sync/check abort instead of operating on partial membership data.</comment>

<file context>
@@ -0,0 +1,166 @@
+      for (const id of await fetchGroupMemberUserIds({ client, groupId })) {
+        memberIds.add(id);
+      }
+    } catch {
+      // Surface loudly: a group that cannot be read would otherwise silently
+      // shrink the synced population.
</file context>
Fix with cubic


let activities: GoogleWorkspaceActivity[];
try {
activities = await fetchAdminActivities({ ctx, startTime });

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the audit window contains more than 20,000 activities, fetchAdminActivities can return a truncated list and this check still reports no privilege changes as compliant. Propagate the truncation state and fail or mark the review inconclusive instead of emitting the clean pass for incomplete data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts, line 81:

<comment>When the audit window contains more than 20,000 activities, `fetchAdminActivities` can return a truncated list and this check still reports no privilege changes as compliant. Propagate the truncation state and fail or mark the review inconclusive instead of emitting the clean pass for incomplete data.</comment>

<file context>
@@ -0,0 +1,159 @@
+
+    let activities: GoogleWorkspaceActivity[];
+    try {
+      activities = await fetchAdminActivities({ ctx, startTime });
+    } catch (error) {
+      if (isInsufficientScopeError(error)) {
</file context>
Fix with cubic

required: false,
fetchOptions: async (ctx) => {
try {
const groups = await listGroups(ctx);

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When this dynamic option loader runs through the variables API, listGroups sends its required customer selector and pagination token through a second argument that VariableFetchContext.fetch silently ignores. The request can fail without a customer selector, and any paginated response makes listGroups repeatedly request the first page; adapt the client by serializing options.params into the URL before calling ctx.fetch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/variables.ts, line 147:

<comment>When this dynamic option loader runs through the variables API, `listGroups` sends its required customer selector and pagination token through a second argument that `VariableFetchContext.fetch` silently ignores. The request can fail without a customer selector, and any paginated response makes `listGroups` repeatedly request the first page; adapt the client by serializing `options.params` into the URL before calling `ctx.fetch`.</comment>

<file context>
@@ -96,3 +97,80 @@ export const syncIncludedEmailsVariable: CheckVariable = {
+  required: false,
+  fetchOptions: async (ctx) => {
+    try {
+      const groups = await listGroups(ctx);
+      return groups.map((g) => ({
+        value: g.id,
</file context>
Suggested change
const groups = await listGroups(ctx);
const groups = await listGroups({
fetch: async <T>(
path: string,
options?: { params?: Record<string, string> },
): Promise<T> => {
const url = new URL(path, 'https://admin.googleapis.com');
for (const [key, value] of Object.entries(options?.params ?? {})) {
url.searchParams.set(key, value);
}
return ctx.fetch<T>(url.toString());
},
});
Fix with cubic

data: {
name: ct.name,
description: ct.description,
controlFamily: ct.controlFamily ?? null,

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a payload contains controlTemplates[].documentTypes, this importer silently drops those evidence-form mappings. Persist the document link rows as the API importer does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/scripts/import-cmmc.ts, line 125:

<comment>When a payload contains `controlTemplates[].documentTypes`, this importer silently drops those evidence-form mappings. Persist the document link rows as the API importer does.</comment>

<file context>
@@ -0,0 +1,178 @@
+            data: {
+              name: ct.name,
+              description: ct.description,
+              controlFamily: ct.controlFamily ?? null,
+              requirements: {
+                connect: (ct.requirementIndices ?? []).map((i: number) => ({ id: reqs[i].id })),
</file context>
Fix with cubic

Comment thread setup-comp.sh

set -euo pipefail

REPO="${1:-/Users/chris/Code/comp}"

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The no-argument setup command only works on the author's /Users/chris/Code/comp checkout. Default to the script's repository directory or require and document an explicit path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At setup-comp.sh, line 14:

<comment>The no-argument setup command only works on the author's `/Users/chris/Code/comp` checkout. Default to the script's repository directory or require and document an explicit path.</comment>

<file context>
@@ -0,0 +1,165 @@
+
+set -euo pipefail
+
+REPO="${1:-/Users/chris/Code/comp}"
+
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
</file context>
Suggested change
REPO="${1:-/Users/chris/Code/comp}"
REPO="${1:-$(cd -- "$(dirname -- "$0")" && pwd)}"
Fix with cubic

label: g.name ? `${g.name} (${g.email})` : g.email,
}));
} catch {
return [];

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the Directory API fails while loading group or domain options, the picker silently returns an empty list and the operation remains unscoped. Surface the option-loading error so administrators do not mistake an unavailable filter for an empty selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/variables.ts, line 153:

<comment>When the Directory API fails while loading group or domain options, the picker silently returns an empty list and the operation remains unscoped. Surface the option-loading error so administrators do not mistake an unavailable filter for an empty selection.</comment>

<file context>
@@ -96,3 +97,80 @@ export const syncIncludedEmailsVariable: CheckVariable = {
+        label: g.name ? `${g.name} (${g.email})` : g.email,
+      }));
+    } catch {
+      return [];
+    }
+  },
</file context>
Suggested change
return [];
throw new Error('Unable to load Google Workspace group options');
Fix with cubic

id?: string;
email?: string;
/** USER for people, GROUP for a nested group, CUSTOMER for whole-domain. */
type?: 'USER' | 'GROUP' | 'CUSTOMER';

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The GoogleWorkspaceGroupMember.type union omits 'EXTERNAL', which the Google Directory members API actually returns for external users/groups added to a group. The accompanying comment enumerates the possible values ('USER for people, GROUP for a nested group, CUSTOMER for whole-domain') and is therefore incomplete, so the declared type cannot represent real API data for external members. Consider adding 'EXTERNAL' to the union (and the comment) so the type matches the API shape the interface claims to model.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/google-workspace/types.ts, line 145:

<comment>The `GoogleWorkspaceGroupMember.type` union omits `'EXTERNAL'`, which the Google Directory members API actually returns for external users/groups added to a group. The accompanying comment enumerates the possible values ('USER for people, GROUP for a nested group, CUSTOMER for whole-domain') and is therefore incomplete, so the declared type cannot represent real API data for external members. Consider adding `'EXTERNAL'` to the union (and the comment) so the type matches the API shape the interface claims to model.</comment>

<file context>
@@ -79,3 +90,72 @@ export interface GoogleWorkspaceRoleAssignmentsResponse {
+  id?: string;
+  email?: string;
+  /** USER for people, GROUP for a nested group, CUSTOMER for whole-domain. */
+  type?: 'USER' | 'GROUP' | 'CUSTOMER';
+  role?: 'OWNER' | 'MANAGER' | 'MEMBER';
+  status?: string;
</file context>
Suggested change
type?: 'USER' | 'GROUP' | 'CUSTOMER';
/** USER for people, GROUP for a nested group, CUSTOMER for whole-domain, EXTERNAL for out-of-domain members. */
type?: 'USER' | 'GROUP' | 'CUSTOMER' | 'EXTERNAL';
Fix with cubic

@@ -0,0 +1,178 @@
/**

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This new 178-line file duplicates packages/db/scripts/import-cmmc.ts, while the README instructs operators to run the existing copy. Keep one canonical importer so fixes cannot diverge between the two paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frameworks/cmmc-level-2/import-cmmc.ts, line 1:

<comment>This new 178-line file duplicates `packages/db/scripts/import-cmmc.ts`, while the README instructs operators to run the existing copy. Keep one canonical importer so fixes cannot diverge between the two paths.</comment>

<file context>
@@ -0,0 +1,178 @@
+/**
+ * One-off: import the CMMC Level 2 framework definition.
+ *
</file context>
Fix with cubic


let effectiveSyncFilterMode = syncFilterMode;
if (syncFilterMode === 'include' && includedTerms.length === 0) {
if (effectiveSyncFilterMode !== (filterConfig.userFilterMode ?? 'all')) {

@cubic-dev-ai cubic-dev-ai Bot Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When sync_user_filter_mode contains an unsupported value, this condition logs that an empty list caused the fallback. Emit this warning only for include mode with an empty include list.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/integration-platform/controllers/sync.controller.ts, line 319:

<comment>When `sync_user_filter_mode` contains an unsupported value, this condition logs that an empty list caused the fallback. Emit this warning only for `include` mode with an empty include list.</comment>

<file context>
@@ -285,57 +292,46 @@ export class SyncController {
 
-    let effectiveSyncFilterMode = syncFilterMode;
-    if (syncFilterMode === 'include' && includedTerms.length === 0) {
+    if (effectiveSyncFilterMode !== (filterConfig.userFilterMode ?? 'all')) {
       this.logger.warn(
-        `Google Workspace sync for org ${organizationId} is set to include mode, but include list is empty. Falling back to all users.`,
</file context>
Suggested change
if (effectiveSyncFilterMode !== (filterConfig.userFilterMode ?? 'all')) {
if (
filterConfig.userFilterMode === 'include' &&
filterConfig.includedTerms.length === 0
) {
Fix with cubic

filterUsersByOrgUnits became dead production code when Google Workspace
employee sync moved onto the shared scope filter -- isGoogleWorkspaceUserInScope
carries identical OU logic, and the only remaining caller was its own test.

Its coverage moves to the shared filter's suite rather than being lost: exact
and nested path matching, child OU inclusion, partial-segment rejection, root
OU behaviour, and multiple target OUs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants