-
Notifications
You must be signed in to change notification settings - Fork 414
Feat/google workspace admin audit #3569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
6eaf0be
d8db6b0
f167d46
b164c43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,14 @@ import { GenericDeviceSyncService } from '../services/generic-device-sync.servic | |
| import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository'; | ||
| import { CheckRunRepository } from '../repositories/check-run.repository'; | ||
| import { createCheckContext } from '@trycompai/integration-platform'; | ||
| import { filterUsersByOrgUnits } from './sync-ou-filter'; | ||
| import { | ||
| createBearerTokenClient, | ||
| isGoogleWorkspaceUserInScope, | ||
| isGoogleWorkspaceUserSelectedBySyncTerms, | ||
| parseGoogleWorkspaceCheckUserFilter, | ||
| resolveEffectiveSyncFilterMode, | ||
| resolveGoogleWorkspaceUserFilter, | ||
| } from '@trycompai/integration-platform'; | ||
|
|
||
| interface GoogleWorkspaceUser { | ||
| id: string; | ||
|
|
@@ -285,57 +292,46 @@ export class SyncController { | |
| unknown | ||
| >; | ||
|
|
||
| // Filter by organizational unit if configured | ||
| const targetOrgUnits = Array.isArray(syncVariables.target_org_units) | ||
| ? (syncVariables.target_org_units as string[]) | ||
| : undefined; | ||
| const ouFilteredUsers = filterUsersByOrgUnits(users, targetOrgUnits); | ||
|
|
||
| if (targetOrgUnits && targetOrgUnits.length > 0) { | ||
| this.logger.log( | ||
| `Google Workspace OU filter kept ${ouFilteredUsers.length}/${users.length} users (OUs: ${targetOrgUnits.join(', ')})`, | ||
| ); | ||
| } | ||
| // One implementation of "who is in scope" shared with the Google Workspace | ||
| // checks (packages/integration-platform). This controller used to | ||
| // re-implement the OU + email rules, so sync and the access review could | ||
| // silently disagree about the population. | ||
| // | ||
| // Two stages, matching the previous shape exactly: | ||
| // scopedUsers — org unit / group / domain, suspended users RETAINED | ||
| // because offboarding below needs to see them. | ||
| // filteredUsers — the include/exclude selection actually imported. | ||
| const filterConfig = await resolveGoogleWorkspaceUserFilter({ | ||
| client: createBearerTokenClient(accessToken, (message) => | ||
| this.logger.warn(message), | ||
| ), | ||
| config: parseGoogleWorkspaceCheckUserFilter( | ||
| syncVariables as Record<string, string | string[]>, | ||
| ), | ||
| }); | ||
|
|
||
| const rawSyncFilterMode = syncVariables.sync_user_filter_mode; | ||
| const syncFilterMode: GoogleWorkspaceSyncFilterMode = | ||
| typeof rawSyncFilterMode === 'string' && | ||
| GOOGLE_WORKSPACE_SYNC_FILTER_MODES.has( | ||
| rawSyncFilterMode as GoogleWorkspaceSyncFilterMode, | ||
| ) | ||
| ? (rawSyncFilterMode as GoogleWorkspaceSyncFilterMode) | ||
| : 'all'; | ||
| const excludedTerms = parseSyncFilterTerms( | ||
| syncVariables.sync_excluded_emails, | ||
| ); | ||
| const includedTerms = parseSyncFilterTerms( | ||
| syncVariables.sync_included_emails, | ||
| const scopedUsers = users.filter((user) => | ||
| isGoogleWorkspaceUserInScope(user, filterConfig), | ||
| ); | ||
| const effectiveSyncFilterMode = resolveEffectiveSyncFilterMode(filterConfig); | ||
| const excludedTerms = filterConfig.excludedTerms; | ||
|
|
||
| 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.`, | ||
| `Google Workspace sync for org ${organizationId} requested "${filterConfig.userFilterMode}" mode with an empty list. Falling back to all users.`, | ||
| ); | ||
| effectiveSyncFilterMode = 'all'; | ||
| } | ||
|
|
||
| const filteredUsers = ouFilteredUsers.filter((user) => { | ||
| const email = user.primaryEmail.toLowerCase(); | ||
|
|
||
| if (effectiveSyncFilterMode === 'exclude' && excludedTerms.length > 0) { | ||
| return !matchesSyncFilterTerms(email, excludedTerms); | ||
| } | ||
|
|
||
| if (effectiveSyncFilterMode === 'include') { | ||
| return matchesSyncFilterTerms(email, includedTerms); | ||
| } | ||
|
|
||
| return true; | ||
| }); | ||
| const filteredUsers = scopedUsers.filter((user) => | ||
| isGoogleWorkspaceUserSelectedBySyncTerms(user, filterConfig), | ||
| ); | ||
|
|
||
| this.logger.log( | ||
| `Google Workspace sync filter mode "${effectiveSyncFilterMode}" kept ${filteredUsers.length}/${ouFilteredUsers.length} users`, | ||
| `Google Workspace scope kept ${scopedUsers.length}/${users.length} users ` + | ||
| `(OUs: ${filterConfig.targetOrgUnits?.join(', ') || 'all'}, ` + | ||
| `groups: ${filterConfig.targetGroups?.join(', ') || 'all'}, ` + | ||
| `domains: ${filterConfig.targetDomains?.join(', ') || 'all'}); ` + | ||
| `filter mode "${effectiveSyncFilterMode}" kept ${filteredUsers.length}/${scopedUsers.length}`, | ||
| ); | ||
|
|
||
| // Active users to import/reactivate are based on the selected filter mode | ||
|
|
@@ -349,12 +345,12 @@ export class SyncController { | |
| activeUsers.map((u) => u.primaryEmail.toLowerCase()), | ||
| ); | ||
| const allSuspendedEmails = new Set( | ||
| ouFilteredUsers | ||
| scopedUsers | ||
| .filter((u) => u.suspended) | ||
| .map((u) => u.primaryEmail.toLowerCase()), | ||
| ); | ||
| const allActiveEmails = new Set( | ||
| ouFilteredUsers | ||
| scopedUsers | ||
| .filter((u) => !u.suspended) | ||
| .map((u) => u.primaryEmail.toLowerCase()), | ||
| ); | ||
|
|
@@ -493,7 +489,7 @@ export class SyncController { | |
| }); | ||
|
|
||
| const deactivationGwDomains = new Set( | ||
| ouFilteredUsers.map((u) => u.primaryEmail.split('@')[1]?.toLowerCase()), | ||
| scopedUsers.map((u) => u.primaryEmail.split('@')[1]?.toLowerCase()), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When Prompt for AI agents |
||
| ); | ||
| const deactivationSuspendedEmails = | ||
| effectiveSyncFilterMode === 'include' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # CMMC Level 2 — framework definition | ||
|
|
||
| CMMC 2.0 Level 2: 110 practices for protecting Controlled Unclassified | ||
| Information (CUI), aligned to NIST SP 800-171 Rev 2. | ||
|
|
||
| ## Contents | ||
|
|
||
| | File | What it is | | ||
| |---|---| | ||
| | `cmmc-level-2.import.json` | The import payload. Matches `ImportFrameworkDto`. | | ||
| | `import-cmmc.ts` | Inserts the payload directly via Prisma. | | ||
| | `generator/practices.py` | The 110 practices: NIST id, title, requirement statement. | | ||
| | `generator/build.py` | Builds the payload (requirements, controls, policies, tasks + index links). | | ||
| | `generator/add_content.py` | Adds TipTap policy body content. | | ||
|
|
||
| ## What it contains | ||
|
|
||
| - 110 requirements across 14 families (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) | ||
| - 36 control templates — every requirement covered by at least one | ||
| - 14 policy templates (one per family) with body content | ||
| - 25 task templates | ||
| - Identifiers use CMMC practice format, e.g. `AC.L2-3.1.1` | ||
|
|
||
| ## Importing | ||
|
|
||
| Preferred — the API, which is the supported path: | ||
|
|
||
| POST /v1/framework-editor/framework/import | ||
|
|
||
| It sits behind `PlatformAdminGuard`, so it needs a browser session from a | ||
| user whose `User.role = 'admin'`. | ||
|
|
||
| Fallback used here, when no session is available (e.g. headless): | ||
|
|
||
| cd packages/db && bun scripts/import-cmmc.ts <path-to>/cmmc-level-2.import.json | ||
|
|
||
| `import-cmmc.ts` mirrors `FrameworkExportService.import()` in | ||
| `apps/api/src/framework-editor/framework/framework-export.service.ts` — | ||
| same entities, same link rows, one transaction. If that service changes, | ||
| re-check this script. It refuses to run if a framework with the same name | ||
| already exists. | ||
|
|
||
| ## Regenerating | ||
|
|
||
| python3 generator/build.py && python3 generator/add_content.py | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The documented "Regenerating" workflow cannot regenerate the committed framework. Prompt for AI agents |
||
|
|
||
| (The generator writes to the path hardcoded at the bottom of `build.py`.) | ||
|
|
||
| ## Accuracy | ||
|
|
||
| Requirement statements paraphrase NIST SP 800-171 Rev 2. Family counts and | ||
| practice identifiers were checked against the published structure and total | ||
| 110. The control groupings, policies, and tasks are an editorial layer, not | ||
| part of the standard. **Verify the requirement text against the official | ||
| NIST publication before relying on this for an actual assessment.** | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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_modecontains an unsupported value, this condition logs that an empty list caused the fallback. Emit this warning only forincludemode with an empty include list.Prompt for AI agents