diff --git a/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts b/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts index fa250967fa..5faa1be4f5 100644 --- a/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts +++ b/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts @@ -6,6 +6,7 @@ import { ConnectionRepository } from '../repositories/connection.repository'; import { CredentialVaultService } from '../services/credential-vault.service'; import { OAuthCredentialsService } from '../services/oauth-credentials.service'; import { IntegrationSyncLoggerService } from '../services/integration-sync-logger.service'; +import { GenericDeviceSyncService } from '../services/generic-device-sync.service'; import { GenericEmployeeSyncService } from '../services/generic-employee-sync.service'; import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository'; import { CheckRunRepository } from '../repositories/check-run.repository'; @@ -104,6 +105,9 @@ describe('SyncController - Google Workspace employees', () => { useValue: { logSync: jest.fn() }, }, { provide: GenericEmployeeSyncService, useValue: {} }, + // Required by SyncController's constructor; the suite could not + // instantiate the controller without it. + { provide: GenericDeviceSyncService, useValue: {} }, { provide: DynamicIntegrationRepository, useValue: {} }, { provide: CheckRunRepository, useValue: {} }, ], @@ -306,7 +310,8 @@ describe('SyncController - Google Workspace employees', () => { expect(result.skipped).toBe(0); expect(mockedDb.member.update).toHaveBeenCalledWith({ where: { id: 'mem_back' }, - data: { deactivated: false, isActive: true }, + // offboardDate is cleared on reactivation (97636c4ea). + data: { deactivated: false, isActive: true, offboardDate: null }, }); }); diff --git a/apps/api/src/integration-platform/controllers/sync-ou-filter.spec.ts b/apps/api/src/integration-platform/controllers/sync-ou-filter.spec.ts deleted file mode 100644 index 83622c05d4..0000000000 --- a/apps/api/src/integration-platform/controllers/sync-ou-filter.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { filterUsersByOrgUnits } from './sync-ou-filter'; - -interface TestUser { - primaryEmail: string; - orgUnitPath: string; - suspended?: boolean; -} - -describe('filterUsersByOrgUnits', () => { - const users: TestUser[] = [ - { primaryEmail: 'alice@example.com', orgUnitPath: '/' }, - { primaryEmail: 'bob@example.com', orgUnitPath: '/Engineering' }, - { primaryEmail: 'carol@example.com', orgUnitPath: '/Engineering/Frontend' }, - { primaryEmail: 'dave@example.com', orgUnitPath: '/Marketing' }, - { primaryEmail: 'eve@example.com', orgUnitPath: '/HR' }, - { - primaryEmail: 'frank@example.com', - orgUnitPath: '/Unlisted', - suspended: true, - }, - ]; - - it('returns all users when no target OUs specified', () => { - const result = filterUsersByOrgUnits(users, undefined); - expect(result).toEqual(users); - }); - - it('returns all users when target OUs is an empty array', () => { - const result = filterUsersByOrgUnits(users, []); - expect(result).toEqual(users); - }); - - it('filters users to only those in selected OUs', () => { - const result = filterUsersByOrgUnits(users, ['/Engineering']); - expect(result.map((u) => u.primaryEmail)).toEqual([ - 'bob@example.com', - 'carol@example.com', - ]); - }); - - it('includes users in child OUs of selected OUs', () => { - const result = filterUsersByOrgUnits(users, ['/Engineering']); - expect(result.map((u) => u.primaryEmail)).toContain('carol@example.com'); - }); - - it('exact match on OU path works', () => { - const result = filterUsersByOrgUnits(users, ['/Engineering/Frontend']); - expect(result.map((u) => u.primaryEmail)).toEqual(['carol@example.com']); - }); - - it('supports multiple target OUs', () => { - const result = filterUsersByOrgUnits(users, ['/Engineering', '/Marketing']); - expect(result.map((u) => u.primaryEmail)).toEqual([ - 'bob@example.com', - 'carol@example.com', - 'dave@example.com', - ]); - }); - - it('root OU includes all users', () => { - const result = filterUsersByOrgUnits(users, ['/']); - expect(result).toEqual(users); - }); - - it('excludes users not in any selected OU', () => { - const result = filterUsersByOrgUnits(users, ['/Engineering']); - const emails = result.map((u) => u.primaryEmail); - expect(emails).not.toContain('alice@example.com'); - expect(emails).not.toContain('dave@example.com'); - expect(emails).not.toContain('eve@example.com'); - expect(emails).not.toContain('frank@example.com'); - }); - - it('does not match partial OU path names', () => { - // /Eng should NOT match /Engineering - const result = filterUsersByOrgUnits(users, ['/Eng']); - expect(result).toEqual([]); - }); - - it('preserves suspended user status through filtering', () => { - const result = filterUsersByOrgUnits(users, ['/Unlisted']); - expect(result).toEqual([ - { - primaryEmail: 'frank@example.com', - orgUnitPath: '/Unlisted', - suspended: true, - }, - ]); - }); -}); diff --git a/apps/api/src/integration-platform/controllers/sync-ou-filter.ts b/apps/api/src/integration-platform/controllers/sync-ou-filter.ts deleted file mode 100644 index e798f21d46..0000000000 --- a/apps/api/src/integration-platform/controllers/sync-ou-filter.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Filters users by organizational unit paths. - * Matches users whose orgUnitPath equals or is a child of any target OU. - * - * @param users - Array of objects with an orgUnitPath property - * @param targetOrgUnits - Array of OU paths to include (undefined/empty = all users) - * @returns Filtered array of users - */ -export function filterUsersByOrgUnits( - users: T[], - targetOrgUnits: string[] | undefined, -): T[] { - if (!targetOrgUnits || targetOrgUnits.length === 0) { - return users; - } - - return users.filter((user) => { - const userOu = user.orgUnitPath ?? '/'; - return targetOrgUnits.some( - (ou) => ou === '/' || userOu === ou || userOu.startsWith(`${ou}/`), - ); - }); -} diff --git a/apps/api/src/integration-platform/controllers/sync.controller.ts b/apps/api/src/integration-platform/controllers/sync.controller.ts index e510db91ff..888fe02aba 100644 --- a/apps/api/src/integration-platform/controllers/sync.controller.ts +++ b/apps/api/src/integration-platform/controllers/sync.controller.ts @@ -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, + ), + }); - 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()), ); const deactivationSuspendedEmails = effectiveSyncFilterMode === 'include' diff --git a/frameworks/cmmc-level-2/README.md b/frameworks/cmmc-level-2/README.md new file mode 100644 index 0000000000..108f120d1c --- /dev/null +++ b/frameworks/cmmc-level-2/README.md @@ -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 /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 + +(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.** diff --git a/frameworks/cmmc-level-2/cmmc-level-2.import.json b/frameworks/cmmc-level-2/cmmc-level-2.import.json new file mode 100644 index 0000000000..32092d3bc0 --- /dev/null +++ b/frameworks/cmmc-level-2/cmmc-level-2.import.json @@ -0,0 +1,5269 @@ +{ + "version": "1", + "framework": { + "name": "CMMC Level 2", + "version": "2.0", + "description": "Cybersecurity Maturity Model Certification (CMMC) 2.0 Level 2 \u2014 110 practices for the protection of Controlled Unclassified Information (CUI), aligned to NIST SP 800-171 Rev 2.", + "visible": true + }, + "requirements": [ + { + "name": "Authorized Access Control", + "identifier": "AC.L2-3.1.1", + "description": "Limit system access to authorized users, processes acting on behalf of authorized users, and devices (including other systems).", + "requirementFamily": "AC - Access Control", + "sortOrder": 0 + }, + { + "name": "Transaction & Function Control", + "identifier": "AC.L2-3.1.2", + "description": "Limit system access to the types of transactions and functions that authorized users are permitted to execute.", + "requirementFamily": "AC - Access Control", + "sortOrder": 1 + }, + { + "name": "Control CUI Flow", + "identifier": "AC.L2-3.1.3", + "description": "Control the flow of CUI in accordance with approved authorizations.", + "requirementFamily": "AC - Access Control", + "sortOrder": 2 + }, + { + "name": "Separation of Duties", + "identifier": "AC.L2-3.1.4", + "description": "Separate the duties of individuals to reduce the risk of malevolent activity without collusion.", + "requirementFamily": "AC - Access Control", + "sortOrder": 3 + }, + { + "name": "Least Privilege", + "identifier": "AC.L2-3.1.5", + "description": "Employ the principle of least privilege, including for specific security functions and privileged accounts.", + "requirementFamily": "AC - Access Control", + "sortOrder": 4 + }, + { + "name": "Non-Privileged Account Use", + "identifier": "AC.L2-3.1.6", + "description": "Use non-privileged accounts or roles when accessing nonsecurity functions.", + "requirementFamily": "AC - Access Control", + "sortOrder": 5 + }, + { + "name": "Privileged Functions", + "identifier": "AC.L2-3.1.7", + "description": "Prevent non-privileged users from executing privileged functions and capture the execution of such functions in audit logs.", + "requirementFamily": "AC - Access Control", + "sortOrder": 6 + }, + { + "name": "Unsuccessful Logon Attempts", + "identifier": "AC.L2-3.1.8", + "description": "Limit unsuccessful logon attempts.", + "requirementFamily": "AC - Access Control", + "sortOrder": 7 + }, + { + "name": "Privacy & Security Notices", + "identifier": "AC.L2-3.1.9", + "description": "Provide privacy and security notices consistent with applicable CUI rules.", + "requirementFamily": "AC - Access Control", + "sortOrder": 8 + }, + { + "name": "Session Lock", + "identifier": "AC.L2-3.1.10", + "description": "Use session lock with pattern-hiding displays to prevent access and viewing of data after a period of inactivity.", + "requirementFamily": "AC - Access Control", + "sortOrder": 9 + }, + { + "name": "Session Termination", + "identifier": "AC.L2-3.1.11", + "description": "Terminate (automatically) a user session after a defined condition.", + "requirementFamily": "AC - Access Control", + "sortOrder": 10 + }, + { + "name": "Control Remote Access", + "identifier": "AC.L2-3.1.12", + "description": "Monitor and control remote access sessions.", + "requirementFamily": "AC - Access Control", + "sortOrder": 11 + }, + { + "name": "Remote Access Confidentiality", + "identifier": "AC.L2-3.1.13", + "description": "Employ cryptographic mechanisms to protect the confidentiality of remote access sessions.", + "requirementFamily": "AC - Access Control", + "sortOrder": 12 + }, + { + "name": "Remote Access Routing", + "identifier": "AC.L2-3.1.14", + "description": "Route remote access via managed access control points.", + "requirementFamily": "AC - Access Control", + "sortOrder": 13 + }, + { + "name": "Privileged Remote Access", + "identifier": "AC.L2-3.1.15", + "description": "Authorize remote execution of privileged commands and remote access to security-relevant information.", + "requirementFamily": "AC - Access Control", + "sortOrder": 14 + }, + { + "name": "Wireless Access Authorization", + "identifier": "AC.L2-3.1.16", + "description": "Authorize wireless access prior to allowing such connections.", + "requirementFamily": "AC - Access Control", + "sortOrder": 15 + }, + { + "name": "Wireless Access Protection", + "identifier": "AC.L2-3.1.17", + "description": "Protect wireless access using authentication and encryption.", + "requirementFamily": "AC - Access Control", + "sortOrder": 16 + }, + { + "name": "Mobile Device Connection", + "identifier": "AC.L2-3.1.18", + "description": "Control connection of mobile devices.", + "requirementFamily": "AC - Access Control", + "sortOrder": 17 + }, + { + "name": "Encrypt CUI on Mobile", + "identifier": "AC.L2-3.1.19", + "description": "Encrypt CUI on mobile devices and mobile computing platforms.", + "requirementFamily": "AC - Access Control", + "sortOrder": 18 + }, + { + "name": "External Connections", + "identifier": "AC.L2-3.1.20", + "description": "Verify and control/limit connections to and use of external systems.", + "requirementFamily": "AC - Access Control", + "sortOrder": 19 + }, + { + "name": "Portable Storage Use", + "identifier": "AC.L2-3.1.21", + "description": "Limit use of portable storage devices on external systems.", + "requirementFamily": "AC - Access Control", + "sortOrder": 20 + }, + { + "name": "Control Public Information", + "identifier": "AC.L2-3.1.22", + "description": "Control CUI posted or processed on publicly accessible systems.", + "requirementFamily": "AC - Access Control", + "sortOrder": 21 + }, + { + "name": "Role-Based Risk Awareness", + "identifier": "AT.L2-3.2.1", + "description": "Ensure that managers, systems administrators, and users of organizational systems are made aware of the security risks associated with their activities and of the applicable policies, standards, and procedures related to the security of those systems.", + "requirementFamily": "AT - Awareness and Training", + "sortOrder": 22 + }, + { + "name": "Role-Based Training", + "identifier": "AT.L2-3.2.2", + "description": "Ensure that personnel are trained to carry out their assigned information security-related duties and responsibilities.", + "requirementFamily": "AT - Awareness and Training", + "sortOrder": 23 + }, + { + "name": "Insider Threat Awareness", + "identifier": "AT.L2-3.2.3", + "description": "Provide security awareness training on recognizing and reporting potential indicators of insider threat.", + "requirementFamily": "AT - Awareness and Training", + "sortOrder": 24 + }, + { + "name": "System Auditing", + "identifier": "AU.L2-3.3.1", + "description": "Create and retain system audit logs and records to the extent needed to enable the monitoring, analysis, investigation, and reporting of unlawful or unauthorized system activity.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 25 + }, + { + "name": "User Accountability", + "identifier": "AU.L2-3.3.2", + "description": "Ensure that the actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 26 + }, + { + "name": "Event Review", + "identifier": "AU.L2-3.3.3", + "description": "Review and update logged events.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 27 + }, + { + "name": "Audit Failure Alerting", + "identifier": "AU.L2-3.3.4", + "description": "Alert in the event of an audit logging process failure.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 28 + }, + { + "name": "Audit Correlation", + "identifier": "AU.L2-3.3.5", + "description": "Correlate audit record review, analysis, and reporting processes for investigation and response to indications of unlawful, unauthorized, suspicious, or unusual activity.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 29 + }, + { + "name": "Reduction & Reporting", + "identifier": "AU.L2-3.3.6", + "description": "Provide audit record reduction and report generation to support on-demand analysis and reporting.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 30 + }, + { + "name": "Authoritative Time Source", + "identifier": "AU.L2-3.3.7", + "description": "Provide a system capability that compares and synchronizes internal system clocks with an authoritative source to generate time stamps for audit records.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 31 + }, + { + "name": "Audit Protection", + "identifier": "AU.L2-3.3.8", + "description": "Protect audit information and audit logging tools from unauthorized access, modification, and deletion.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 32 + }, + { + "name": "Audit Management", + "identifier": "AU.L2-3.3.9", + "description": "Limit management of audit logging functionality to a subset of privileged users.", + "requirementFamily": "AU - Audit and Accountability", + "sortOrder": 33 + }, + { + "name": "System Baselining", + "identifier": "CM.L2-3.4.1", + "description": "Establish and maintain baseline configurations and inventories of organizational systems (including hardware, software, firmware, and documentation) throughout the respective system development life cycles.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 34 + }, + { + "name": "Security Configuration Enforcement", + "identifier": "CM.L2-3.4.2", + "description": "Establish and enforce security configuration settings for information technology products employed in organizational systems.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 35 + }, + { + "name": "System Change Management", + "identifier": "CM.L2-3.4.3", + "description": "Track, review, approve or disapprove, and log changes to organizational systems.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 36 + }, + { + "name": "Security Impact Analysis", + "identifier": "CM.L2-3.4.4", + "description": "Analyze the security impact of changes prior to implementation.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 37 + }, + { + "name": "Access Restrictions for Change", + "identifier": "CM.L2-3.4.5", + "description": "Define, document, approve, and enforce physical and logical access restrictions associated with changes to organizational systems.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 38 + }, + { + "name": "Least Functionality", + "identifier": "CM.L2-3.4.6", + "description": "Employ the principle of least functionality by configuring organizational systems to provide only essential capabilities.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 39 + }, + { + "name": "Nonessential Functionality", + "identifier": "CM.L2-3.4.7", + "description": "Restrict, disable, or prevent the use of nonessential programs, functions, ports, protocols, and services.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 40 + }, + { + "name": "Application Execution Policy", + "identifier": "CM.L2-3.4.8", + "description": "Apply deny-by-exception (blacklisting) policy to prevent the use of unauthorized software or deny-all, permit-by-exception (whitelisting) policy to allow the execution of authorized software.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 41 + }, + { + "name": "User-Installed Software", + "identifier": "CM.L2-3.4.9", + "description": "Control and monitor user-installed software.", + "requirementFamily": "CM - Configuration Management", + "sortOrder": 42 + }, + { + "name": "Identification", + "identifier": "IA.L2-3.5.1", + "description": "Identify system users, processes acting on behalf of users, and devices.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 43 + }, + { + "name": "Authentication", + "identifier": "IA.L2-3.5.2", + "description": "Authenticate (or verify) the identities of users, processes, or devices, as a prerequisite to allowing access to organizational systems.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 44 + }, + { + "name": "Multifactor Authentication", + "identifier": "IA.L2-3.5.3", + "description": "Use multifactor authentication for local and network access to privileged accounts and for network access to non-privileged accounts.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 45 + }, + { + "name": "Replay-Resistant Authentication", + "identifier": "IA.L2-3.5.4", + "description": "Employ replay-resistant authentication mechanisms for network access to privileged and non-privileged accounts.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 46 + }, + { + "name": "Identifier Reuse", + "identifier": "IA.L2-3.5.5", + "description": "Prevent reuse of identifiers for a defined period.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 47 + }, + { + "name": "Identifier Handling", + "identifier": "IA.L2-3.5.6", + "description": "Disable identifiers after a defined period of inactivity.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 48 + }, + { + "name": "Password Complexity", + "identifier": "IA.L2-3.5.7", + "description": "Enforce a minimum password complexity and change of characters when new passwords are created.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 49 + }, + { + "name": "Password Reuse", + "identifier": "IA.L2-3.5.8", + "description": "Prohibit password reuse for a specified number of generations.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 50 + }, + { + "name": "Temporary Passwords", + "identifier": "IA.L2-3.5.9", + "description": "Allow temporary password use for system logons with an immediate change to a permanent password.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 51 + }, + { + "name": "Cryptographically-Protected Passwords", + "identifier": "IA.L2-3.5.10", + "description": "Store and transmit only cryptographically-protected passwords.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 52 + }, + { + "name": "Obscure Feedback", + "identifier": "IA.L2-3.5.11", + "description": "Obscure feedback of authentication information.", + "requirementFamily": "IA - Identification and Authentication", + "sortOrder": 53 + }, + { + "name": "Incident Handling", + "identifier": "IR.L2-3.6.1", + "description": "Establish an operational incident-handling capability for organizational systems that includes preparation, detection, analysis, containment, recovery, and user response activities.", + "requirementFamily": "IR - Incident Response", + "sortOrder": 54 + }, + { + "name": "Incident Reporting", + "identifier": "IR.L2-3.6.2", + "description": "Track, document, and report incidents to designated officials and/or authorities both internal and external to the organization.", + "requirementFamily": "IR - Incident Response", + "sortOrder": 55 + }, + { + "name": "Incident Response Testing", + "identifier": "IR.L2-3.6.3", + "description": "Test the organizational incident response capability.", + "requirementFamily": "IR - Incident Response", + "sortOrder": 56 + }, + { + "name": "Perform Maintenance", + "identifier": "MA.L2-3.7.1", + "description": "Perform maintenance on organizational systems.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 57 + }, + { + "name": "System Maintenance Control", + "identifier": "MA.L2-3.7.2", + "description": "Provide controls on the tools, techniques, mechanisms, and personnel used to conduct system maintenance.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 58 + }, + { + "name": "Equipment Sanitization", + "identifier": "MA.L2-3.7.3", + "description": "Ensure equipment removed for off-site maintenance is sanitized of any CUI.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 59 + }, + { + "name": "Media Inspection", + "identifier": "MA.L2-3.7.4", + "description": "Check media containing diagnostic and test programs for malicious code before the media are used in organizational systems.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 60 + }, + { + "name": "Nonlocal Maintenance MFA", + "identifier": "MA.L2-3.7.5", + "description": "Require multifactor authentication to establish nonlocal maintenance sessions via external network connections and terminate such connections when nonlocal maintenance is complete.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 61 + }, + { + "name": "Maintenance Personnel", + "identifier": "MA.L2-3.7.6", + "description": "Supervise the maintenance activities of maintenance personnel without required access authorization.", + "requirementFamily": "MA - Maintenance", + "sortOrder": 62 + }, + { + "name": "Media Protection", + "identifier": "MP.L2-3.8.1", + "description": "Protect (i.e., physically control and securely store) system media containing CUI, both paper and digital.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 63 + }, + { + "name": "Media Access", + "identifier": "MP.L2-3.8.2", + "description": "Limit access to CUI on system media to authorized users.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 64 + }, + { + "name": "Media Disposal", + "identifier": "MP.L2-3.8.3", + "description": "Sanitize or destroy system media containing CUI before disposal or release for reuse.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 65 + }, + { + "name": "Media Markings", + "identifier": "MP.L2-3.8.4", + "description": "Mark media with necessary CUI markings and distribution limitations.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 66 + }, + { + "name": "Media Accountability", + "identifier": "MP.L2-3.8.5", + "description": "Control access to media containing CUI and maintain accountability for media during transport outside of controlled areas.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 67 + }, + { + "name": "Portable Storage Encryption", + "identifier": "MP.L2-3.8.6", + "description": "Implement cryptographic mechanisms to protect the confidentiality of CUI stored on digital media during transport unless otherwise protected by alternative physical safeguards.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 68 + }, + { + "name": "Removable Media", + "identifier": "MP.L2-3.8.7", + "description": "Control the use of removable media on system components.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 69 + }, + { + "name": "Shared Media", + "identifier": "MP.L2-3.8.8", + "description": "Prohibit the use of portable storage devices when such devices have no identifiable owner.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 70 + }, + { + "name": "Protect Backups", + "identifier": "MP.L2-3.8.9", + "description": "Protect the confidentiality of backup CUI at storage locations.", + "requirementFamily": "MP - Media Protection", + "sortOrder": 71 + }, + { + "name": "Screen Individuals", + "identifier": "PS.L2-3.9.1", + "description": "Screen individuals prior to authorizing access to organizational systems containing CUI.", + "requirementFamily": "PS - Personnel Security", + "sortOrder": 72 + }, + { + "name": "Personnel Actions", + "identifier": "PS.L2-3.9.2", + "description": "Ensure that organizational systems containing CUI are protected during and after personnel actions such as terminations and transfers.", + "requirementFamily": "PS - Personnel Security", + "sortOrder": 73 + }, + { + "name": "Limit Physical Access", + "identifier": "PE.L2-3.10.1", + "description": "Limit physical access to organizational systems, equipment, and the respective operating environments to authorized individuals.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 74 + }, + { + "name": "Monitor Facility", + "identifier": "PE.L2-3.10.2", + "description": "Protect and monitor the physical facility and support infrastructure for organizational systems.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 75 + }, + { + "name": "Escort Visitors", + "identifier": "PE.L2-3.10.3", + "description": "Escort visitors and monitor visitor activity.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 76 + }, + { + "name": "Physical Access Logs", + "identifier": "PE.L2-3.10.4", + "description": "Maintain audit logs of physical access.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 77 + }, + { + "name": "Manage Physical Access", + "identifier": "PE.L2-3.10.5", + "description": "Control and manage physical access devices.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 78 + }, + { + "name": "Alternative Work Sites", + "identifier": "PE.L2-3.10.6", + "description": "Enforce safeguarding measures for CUI at alternate work sites.", + "requirementFamily": "PE - Physical Protection", + "sortOrder": 79 + }, + { + "name": "Risk Assessments", + "identifier": "RA.L2-3.11.1", + "description": "Periodically assess the risk to organizational operations (including mission, functions, image, or reputation), organizational assets, and individuals, resulting from the operation of organizational systems and the associated processing, storage, or transmission of CUI.", + "requirementFamily": "RA - Risk Assessment", + "sortOrder": 80 + }, + { + "name": "Vulnerability Scan", + "identifier": "RA.L2-3.11.2", + "description": "Scan for vulnerabilities in organizational systems and applications periodically and when new vulnerabilities affecting those systems and applications are identified.", + "requirementFamily": "RA - Risk Assessment", + "sortOrder": 81 + }, + { + "name": "Vulnerability Remediation", + "identifier": "RA.L2-3.11.3", + "description": "Remediate vulnerabilities in accordance with risk assessments.", + "requirementFamily": "RA - Risk Assessment", + "sortOrder": 82 + }, + { + "name": "Security Control Assessment", + "identifier": "CA.L2-3.12.1", + "description": "Periodically assess the security controls in organizational systems to determine if the controls are effective in their application.", + "requirementFamily": "CA - Security Assessment", + "sortOrder": 83 + }, + { + "name": "Plan of Action", + "identifier": "CA.L2-3.12.2", + "description": "Develop and implement plans of action designed to correct deficiencies and reduce or eliminate vulnerabilities in organizational systems.", + "requirementFamily": "CA - Security Assessment", + "sortOrder": 84 + }, + { + "name": "Continuous Monitoring", + "identifier": "CA.L2-3.12.3", + "description": "Monitor security controls on an ongoing basis to ensure the continued effectiveness of the controls.", + "requirementFamily": "CA - Security Assessment", + "sortOrder": 85 + }, + { + "name": "System Security Plan", + "identifier": "CA.L2-3.12.4", + "description": "Develop, document, and periodically update system security plans that describe system boundaries, system environments of operation, how security requirements are implemented, and the relationships with or connections to other systems.", + "requirementFamily": "CA - Security Assessment", + "sortOrder": 86 + }, + { + "name": "Boundary Protection", + "identifier": "SC.L2-3.13.1", + "description": "Monitor, control, and protect communications (i.e., information transmitted or received by organizational systems) at the external boundaries and key internal boundaries of organizational systems.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 87 + }, + { + "name": "Security Engineering", + "identifier": "SC.L2-3.13.2", + "description": "Employ architectural designs, software development techniques, and systems engineering principles that promote effective information security within organizational systems.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 88 + }, + { + "name": "Role Separation", + "identifier": "SC.L2-3.13.3", + "description": "Separate user functionality from system management functionality.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 89 + }, + { + "name": "Shared Resource Control", + "identifier": "SC.L2-3.13.4", + "description": "Prevent unauthorized and unintended information transfer via shared system resources.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 90 + }, + { + "name": "Public-Access System Separation", + "identifier": "SC.L2-3.13.5", + "description": "Implement subnetworks for publicly accessible system components that are physically or logically separated from internal networks.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 91 + }, + { + "name": "Network Communication by Exception", + "identifier": "SC.L2-3.13.6", + "description": "Deny network communications traffic by default and allow network communications traffic by exception (i.e., deny all, permit by exception).", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 92 + }, + { + "name": "Split Tunneling", + "identifier": "SC.L2-3.13.7", + "description": "Prevent remote devices from simultaneously establishing non-remote connections with organizational systems and communicating via some other connection to resources in external networks (i.e., split tunneling).", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 93 + }, + { + "name": "Data in Transit", + "identifier": "SC.L2-3.13.8", + "description": "Implement cryptographic mechanisms to prevent unauthorized disclosure of CUI during transmission unless otherwise protected by alternative physical safeguards.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 94 + }, + { + "name": "Connections Termination", + "identifier": "SC.L2-3.13.9", + "description": "Terminate network connections associated with communications sessions at the end of the sessions or after a defined period of inactivity.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 95 + }, + { + "name": "Key Management", + "identifier": "SC.L2-3.13.10", + "description": "Establish and manage cryptographic keys for cryptography employed in organizational systems.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 96 + }, + { + "name": "CUI Encryption", + "identifier": "SC.L2-3.13.11", + "description": "Employ FIPS-validated cryptography when used to protect the confidentiality of CUI.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 97 + }, + { + "name": "Collaborative Device Control", + "identifier": "SC.L2-3.13.12", + "description": "Prohibit remote activation of collaborative computing devices and provide indication of devices in use to users present at the device.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 98 + }, + { + "name": "Mobile Code", + "identifier": "SC.L2-3.13.13", + "description": "Control and monitor the use of mobile code.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 99 + }, + { + "name": "Voice over Internet Protocol", + "identifier": "SC.L2-3.13.14", + "description": "Control and monitor the use of Voice over Internet Protocol (VoIP) technologies.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 100 + }, + { + "name": "Communications Authenticity", + "identifier": "SC.L2-3.13.15", + "description": "Protect the authenticity of communications sessions.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 101 + }, + { + "name": "Data at Rest", + "identifier": "SC.L2-3.13.16", + "description": "Protect the confidentiality of CUI at rest.", + "requirementFamily": "SC - System and Communications Protection", + "sortOrder": 102 + }, + { + "name": "Flaw Remediation", + "identifier": "SI.L2-3.14.1", + "description": "Identify, report, and correct system flaws in a timely manner.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 103 + }, + { + "name": "Malicious Code Protection", + "identifier": "SI.L2-3.14.2", + "description": "Provide protection from malicious code at designated locations within organizational systems.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 104 + }, + { + "name": "Security Alerts & Advisories", + "identifier": "SI.L2-3.14.3", + "description": "Monitor system security alerts and advisories and take action in response.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 105 + }, + { + "name": "Update Malicious Code Protection", + "identifier": "SI.L2-3.14.4", + "description": "Update malicious code protection mechanisms when new releases are available.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 106 + }, + { + "name": "System & File Scanning", + "identifier": "SI.L2-3.14.5", + "description": "Perform periodic scans of organizational systems and real-time scans of files from external sources as files are downloaded, opened, or executed.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 107 + }, + { + "name": "Monitor Communications for Attacks", + "identifier": "SI.L2-3.14.6", + "description": "Monitor organizational systems, including inbound and outbound communications traffic, to detect attacks and indicators of potential attacks.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 108 + }, + { + "name": "Identify Unauthorized Use", + "identifier": "SI.L2-3.14.7", + "description": "Identify unauthorized use of organizational systems.", + "requirementFamily": "SI - System and Information Integrity", + "sortOrder": 109 + } + ], + "policyTemplates": [ + { + "name": "Access Control Policy", + "description": "Governs how access to systems and CUI is authorized, provisioned, reviewed, and revoked, including least privilege, separation of duties, remote access, wireless, and external systems.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Access Control Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs how access to systems and CUI is authorized, provisioned, reviewed, and revoked, including least privilege, separation of duties, remote access, wireless, and external systems." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Access Control domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.1 \u2014 Limit system access to authorized users, processes acting on behalf of authorized users, and devices (including other systems)." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.2 \u2014 Limit system access to the types of transactions and functions that authorized users are permitted to execute." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.3 \u2014 Control the flow of CUI in accordance with approved authorizations." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.4 \u2014 Separate the duties of individuals to reduce the risk of malevolent activity without collusion." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.5 \u2014 Employ the principle of least privilege, including for specific security functions and privileged accounts." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.6 \u2014 Use non-privileged accounts or roles when accessing nonsecurity functions." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.7 \u2014 Prevent non-privileged users from executing privileged functions and capture the execution of such functions in audit logs." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.8 \u2014 Limit unsuccessful logon attempts." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.9 \u2014 Provide privacy and security notices consistent with applicable CUI rules." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.10 \u2014 Use session lock with pattern-hiding displays to prevent access and viewing of data after a period of inactivity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.11 \u2014 Terminate (automatically) a user session after a defined condition." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.12 \u2014 Monitor and control remote access sessions." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.13 \u2014 Employ cryptographic mechanisms to protect the confidentiality of remote access sessions." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.14 \u2014 Route remote access via managed access control points." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.15 \u2014 Authorize remote execution of privileged commands and remote access to security-relevant information." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.16 \u2014 Authorize wireless access prior to allowing such connections." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.17 \u2014 Protect wireless access using authentication and encryption." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.18 \u2014 Control connection of mobile devices." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.19 \u2014 Encrypt CUI on mobile devices and mobile computing platforms." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.20 \u2014 Verify and control/limit connections to and use of external systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.21 \u2014 Limit use of portable storage devices on external systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AC.L2-3.1.22 \u2014 Control CUI posted or processed on publicly accessible systems." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Security Awareness and Training Policy", + "description": "Establishes security awareness and role-based training requirements, including insider threat recognition and reporting.", + "frequency": "yearly", + "department": "hr", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Security Awareness and Training Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Establishes security awareness and role-based training requirements, including insider threat recognition and reporting." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Awareness and Training domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AT.L2-3.2.1 \u2014 Ensure that managers, systems administrators, and users of organizational systems are made aware of the security risks associated with their activities and of the applicable policies, standards, and procedures related to the security of those systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AT.L2-3.2.2 \u2014 Ensure that personnel are trained to carry out their assigned information security-related duties and responsibilities." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AT.L2-3.2.3 \u2014 Provide security awareness training on recognizing and reporting potential indicators of insider threat." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Audit Logging and Accountability Policy", + "description": "Defines what events are logged, how logs are protected and retained, and how audit records are reviewed and correlated.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Audit Logging and Accountability Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines what events are logged, how logs are protected and retained, and how audit records are reviewed and correlated." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Audit and Accountability domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.1 \u2014 Create and retain system audit logs and records to the extent needed to enable the monitoring, analysis, investigation, and reporting of unlawful or unauthorized system activity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.2 \u2014 Ensure that the actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.3 \u2014 Review and update logged events." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.4 \u2014 Alert in the event of an audit logging process failure." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.5 \u2014 Correlate audit record review, analysis, and reporting processes for investigation and response to indications of unlawful, unauthorized, suspicious, or unusual activity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.6 \u2014 Provide audit record reduction and report generation to support on-demand analysis and reporting." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.7 \u2014 Provide a system capability that compares and synchronizes internal system clocks with an authoritative source to generate time stamps for audit records." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.8 \u2014 Protect audit information and audit logging tools from unauthorized access, modification, and deletion." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "AU.L2-3.3.9 \u2014 Limit management of audit logging functionality to a subset of privileged users." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Configuration Management Policy", + "description": "Defines baseline configurations, change control, security impact analysis, least functionality, and software restrictions.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Configuration Management Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines baseline configurations, change control, security impact analysis, least functionality, and software restrictions." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Configuration Management domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.1 \u2014 Establish and maintain baseline configurations and inventories of organizational systems (including hardware, software, firmware, and documentation) throughout the respective system development life cycles." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.2 \u2014 Establish and enforce security configuration settings for information technology products employed in organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.3 \u2014 Track, review, approve or disapprove, and log changes to organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.4 \u2014 Analyze the security impact of changes prior to implementation." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.5 \u2014 Define, document, approve, and enforce physical and logical access restrictions associated with changes to organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.6 \u2014 Employ the principle of least functionality by configuring organizational systems to provide only essential capabilities." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.7 \u2014 Restrict, disable, or prevent the use of nonessential programs, functions, ports, protocols, and services." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.8 \u2014 Apply deny-by-exception (blacklisting) policy to prevent the use of unauthorized software or deny-all, permit-by-exception (whitelisting) policy to allow the execution of authorized software." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CM.L2-3.4.9 \u2014 Control and monitor user-installed software." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Identification and Authentication Policy", + "description": "Defines identity management, multifactor authentication, and password and identifier lifecycle requirements.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Identification and Authentication Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines identity management, multifactor authentication, and password and identifier lifecycle requirements." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Identification and Authentication domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.1 \u2014 Identify system users, processes acting on behalf of users, and devices." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.2 \u2014 Authenticate (or verify) the identities of users, processes, or devices, as a prerequisite to allowing access to organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.3 \u2014 Use multifactor authentication for local and network access to privileged accounts and for network access to non-privileged accounts." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.4 \u2014 Employ replay-resistant authentication mechanisms for network access to privileged and non-privileged accounts." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.5 \u2014 Prevent reuse of identifiers for a defined period." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.6 \u2014 Disable identifiers after a defined period of inactivity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.7 \u2014 Enforce a minimum password complexity and change of characters when new passwords are created." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.8 \u2014 Prohibit password reuse for a specified number of generations." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.9 \u2014 Allow temporary password use for system logons with an immediate change to a permanent password." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.10 \u2014 Store and transmit only cryptographically-protected passwords." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IA.L2-3.5.11 \u2014 Obscure feedback of authentication information." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Incident Response Policy", + "description": "Establishes the incident handling capability, reporting obligations, and testing of response procedures.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Incident Response Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Establishes the incident handling capability, reporting obligations, and testing of response procedures." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Incident Response domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IR.L2-3.6.1 \u2014 Establish an operational incident-handling capability for organizational systems that includes preparation, detection, analysis, containment, recovery, and user response activities." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IR.L2-3.6.2 \u2014 Track, document, and report incidents to designated officials and/or authorities both internal and external to the organization." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "IR.L2-3.6.3 \u2014 Test the organizational incident response capability." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "System Maintenance Policy", + "description": "Governs local and nonlocal maintenance, maintenance tooling, personnel supervision, and sanitization of equipment removed for service.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "System Maintenance Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs local and nonlocal maintenance, maintenance tooling, personnel supervision, and sanitization of equipment removed for service." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Maintenance domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.1 \u2014 Perform maintenance on organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.2 \u2014 Provide controls on the tools, techniques, mechanisms, and personnel used to conduct system maintenance." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.3 \u2014 Ensure equipment removed for off-site maintenance is sanitized of any CUI." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.4 \u2014 Check media containing diagnostic and test programs for malicious code before the media are used in organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.5 \u2014 Require multifactor authentication to establish nonlocal maintenance sessions via external network connections and terminate such connections when nonlocal maintenance is complete." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MA.L2-3.7.6 \u2014 Supervise the maintenance activities of maintenance personnel without required access authorization." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Media Protection Policy", + "description": "Governs marking, storage, access, transport, sanitization, and disposal of media containing CUI, including removable media and backups.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Media Protection Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs marking, storage, access, transport, sanitization, and disposal of media containing CUI, including removable media and backups." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Media Protection domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.1 \u2014 Protect (i.e., physically control and securely store) system media containing CUI, both paper and digital." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.2 \u2014 Limit access to CUI on system media to authorized users." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.3 \u2014 Sanitize or destroy system media containing CUI before disposal or release for reuse." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.4 \u2014 Mark media with necessary CUI markings and distribution limitations." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.5 \u2014 Control access to media containing CUI and maintain accountability for media during transport outside of controlled areas." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.6 \u2014 Implement cryptographic mechanisms to protect the confidentiality of CUI stored on digital media during transport unless otherwise protected by alternative physical safeguards." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.7 \u2014 Control the use of removable media on system components." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.8 \u2014 Prohibit the use of portable storage devices when such devices have no identifiable owner." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "MP.L2-3.8.9 \u2014 Protect the confidentiality of backup CUI at storage locations." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Personnel Security Policy", + "description": "Defines pre-authorization screening and protection of CUI during and after terminations and transfers.", + "frequency": "yearly", + "department": "hr", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Personnel Security Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines pre-authorization screening and protection of CUI during and after terminations and transfers." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Personnel Security domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PS.L2-3.9.1 \u2014 Screen individuals prior to authorizing access to organizational systems containing CUI." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PS.L2-3.9.2 \u2014 Ensure that organizational systems containing CUI are protected during and after personnel actions such as terminations and transfers." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Physical Protection Policy", + "description": "Governs physical access authorization, visitor escort, access device management, facility monitoring, and alternate work sites.", + "frequency": "yearly", + "department": "admin", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Physical Protection Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs physical access authorization, visitor escort, access device management, facility monitoring, and alternate work sites." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Physical Protection domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.1 \u2014 Limit physical access to organizational systems, equipment, and the respective operating environments to authorized individuals." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.2 \u2014 Protect and monitor the physical facility and support infrastructure for organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.3 \u2014 Escort visitors and monitor visitor activity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.4 \u2014 Maintain audit logs of physical access." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.5 \u2014 Control and manage physical access devices." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "PE.L2-3.10.6 \u2014 Enforce safeguarding measures for CUI at alternate work sites." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Risk Assessment Policy", + "description": "Defines periodic risk assessment, vulnerability scanning cadence, and risk-based remediation of findings.", + "frequency": "yearly", + "department": "gov", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Risk Assessment Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines periodic risk assessment, vulnerability scanning cadence, and risk-based remediation of findings." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Risk Assessment domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "RA.L2-3.11.1 \u2014 Periodically assess the risk to organizational operations (including mission, functions, image, or reputation), organizational assets, and individuals, resulting from the operation of organizational systems and the associated processing, storage, or transmission of CUI." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "RA.L2-3.11.2 \u2014 Scan for vulnerabilities in organizational systems and applications periodically and when new vulnerabilities affecting those systems and applications are identified." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "RA.L2-3.11.3 \u2014 Remediate vulnerabilities in accordance with risk assessments." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "Security Assessment Policy", + "description": "Defines periodic control assessment, continuous monitoring, plans of action, and maintenance of the system security plan.", + "frequency": "yearly", + "department": "gov", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Security Assessment Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Defines periodic control assessment, continuous monitoring, plans of action, and maintenance of the system security plan." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the Security Assessment domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CA.L2-3.12.1 \u2014 Periodically assess the security controls in organizational systems to determine if the controls are effective in their application." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CA.L2-3.12.2 \u2014 Develop and implement plans of action designed to correct deficiencies and reduce or eliminate vulnerabilities in organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CA.L2-3.12.3 \u2014 Monitor security controls on an ongoing basis to ensure the continued effectiveness of the controls." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "CA.L2-3.12.4 \u2014 Develop, document, and periodically update system security plans that describe system boundaries, system environments of operation, how security requirements are implemented, and the relationships with or connections to other systems." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "System and Communications Protection Policy", + "description": "Governs boundary protection, network segmentation, cryptographic protection of CUI in transit and at rest, key management, and session integrity.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "System and Communications Protection Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs boundary protection, network segmentation, cryptographic protection of CUI in transit and at rest, key management, and session integrity." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the System and Communications Protection domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.1 \u2014 Monitor, control, and protect communications (i.e., information transmitted or received by organizational systems) at the external boundaries and key internal boundaries of organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.2 \u2014 Employ architectural designs, software development techniques, and systems engineering principles that promote effective information security within organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.3 \u2014 Separate user functionality from system management functionality." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.4 \u2014 Prevent unauthorized and unintended information transfer via shared system resources." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.5 \u2014 Implement subnetworks for publicly accessible system components that are physically or logically separated from internal networks." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.6 \u2014 Deny network communications traffic by default and allow network communications traffic by exception (i.e., deny all, permit by exception)." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.7 \u2014 Prevent remote devices from simultaneously establishing non-remote connections with organizational systems and communicating via some other connection to resources in external networks (i.e., split tunneling)." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.8 \u2014 Implement cryptographic mechanisms to prevent unauthorized disclosure of CUI during transmission unless otherwise protected by alternative physical safeguards." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.9 \u2014 Terminate network connections associated with communications sessions at the end of the sessions or after a defined period of inactivity." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.10 \u2014 Establish and manage cryptographic keys for cryptography employed in organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.11 \u2014 Employ FIPS-validated cryptography when used to protect the confidentiality of CUI." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.12 \u2014 Prohibit remote activation of collaborative computing devices and provide indication of devices in use to users present at the device." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.13 \u2014 Control and monitor the use of mobile code." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.14 \u2014 Control and monitor the use of Voice over Internet Protocol (VoIP) technologies." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.15 \u2014 Protect the authenticity of communications sessions." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SC.L2-3.13.16 \u2014 Protect the confidentiality of CUI at rest." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + }, + { + "name": "System and Information Integrity Policy", + "description": "Governs flaw remediation, malicious code protection, security alert handling, and system monitoring for attacks and unauthorized use.", + "frequency": "yearly", + "department": "it", + "content": { + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "System and Information Integrity Policy" + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Purpose" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Governs flaw remediation, malicious code protection, security alert handling, and system monitoring for attacks and unauthorized use." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Scope" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Policy Requirements" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The organization implements the following CMMC Level 2 practices in the System and Information Integrity domain:" + } + ] + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.1 \u2014 Identify, report, and correct system flaws in a timely manner." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.2 \u2014 Provide protection from malicious code at designated locations within organizational systems." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.3 \u2014 Monitor system security alerts and advisories and take action in response." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.4 \u2014 Update malicious code protection mechanisms when new releases are available." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.5 \u2014 Perform periodic scans of organizational systems and real-time scans of files from external sources as files are downloaded, opened, or executed." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.6 \u2014 Monitor organizational systems, including inbound and outbound communications traffic, to detect attacks and indicators of potential attacks." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "SI.L2-3.14.7 \u2014 Identify unauthorized use of organizational systems." + } + ] + } + ] + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Roles and Responsibilities" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Exceptions" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable." + } + ] + }, + { + "type": "heading", + "attrs": { + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Review" + } + ] + }, + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements." + } + ] + } + ] + } + } + ], + "taskTemplates": [ + { + "name": "Review user access and privileges", + "description": "Recertify user accounts, roles, and privileged access; remove access no longer required.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review remote access configurations", + "description": "Verify remote access routes through managed access control points and that encryption is enforced.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Deliver security awareness training", + "description": "Deliver and record annual awareness training, including insider threat indicators.", + "frequency": "yearly", + "department": "hr", + "automationStatus": "MANUAL" + }, + { + "name": "Review and update logged events", + "description": "Review the set of logged event types and adjust to current threat and audit needs.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review audit records for anomalies", + "description": "Review correlated audit records for unlawful, unauthorized, suspicious, or unusual activity.", + "frequency": "monthly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review baseline configurations", + "description": "Verify system baselines and inventory remain accurate and hardening settings are enforced.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review user-installed software", + "description": "Audit installed software against the approved list and remove unauthorized applications.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Validate MFA enrollment coverage", + "description": "Confirm multifactor authentication is enforced for privileged and network accounts.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Test the incident response plan", + "description": "Exercise the incident response capability and record results and corrective actions.", + "frequency": "yearly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review maintenance activity records", + "description": "Verify maintenance was authorized, supervised where required, and equipment sanitized before off-site service.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Sanitize and dispose of media", + "description": "Sanitize or destroy media containing CUI before disposal or reuse and record the disposition.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Verify backup protection", + "description": "Confirm backups containing CUI are encrypted and access to backup locations is restricted.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review screening and offboarding records", + "description": "Confirm screening was completed before access was granted and access was revoked on termination or transfer.", + "frequency": "quarterly", + "department": "hr", + "automationStatus": "MANUAL" + }, + { + "name": "Review physical access logs", + "description": "Review facility access logs for unauthorized or anomalous entry.", + "frequency": "monthly", + "department": "admin", + "automationStatus": "MANUAL" + }, + { + "name": "Review physical access devices", + "description": "Inventory and reconcile keys, badges, and other physical access devices.", + "frequency": "quarterly", + "department": "admin", + "automationStatus": "MANUAL" + }, + { + "name": "Conduct vulnerability scans", + "description": "Scan systems and applications for vulnerabilities and record results.", + "frequency": "monthly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Remediate identified vulnerabilities", + "description": "Remediate or formally accept vulnerabilities according to assessed risk.", + "frequency": "monthly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Perform organizational risk assessment", + "description": "Assess risk to operations, assets, and individuals from systems processing CUI.", + "frequency": "yearly", + "department": "gov", + "automationStatus": "MANUAL" + }, + { + "name": "Assess security controls", + "description": "Assess control effectiveness and document findings.", + "frequency": "yearly", + "department": "gov", + "automationStatus": "MANUAL" + }, + { + "name": "Update the system security plan", + "description": "Review and update the SSP covering boundaries, environment, and control implementation.", + "frequency": "yearly", + "department": "gov", + "automationStatus": "MANUAL" + }, + { + "name": "Review plan of action and milestones", + "description": "Review POA&M items for progress and closure evidence.", + "frequency": "quarterly", + "department": "gov", + "automationStatus": "MANUAL" + }, + { + "name": "Review boundary and firewall rules", + "description": "Verify deny-by-default rules and review exceptions at network boundaries.", + "frequency": "quarterly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review cryptographic key management", + "description": "Review key generation, rotation, storage, and retirement practices.", + "frequency": "yearly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Update malicious code protection", + "description": "Confirm anti-malware definitions and engines are current across managed endpoints.", + "frequency": "monthly", + "department": "it", + "automationStatus": "MANUAL" + }, + { + "name": "Review system monitoring alerts", + "description": "Review monitoring alerts for attacks, indicators of attack, and unauthorized use.", + "frequency": "monthly", + "department": "it", + "automationStatus": "MANUAL" + } + ], + "controlTemplates": [ + { + "name": "Account Authorization and Least Privilege", + "description": "Authorize accounts and devices, restrict permitted transactions and functions, separate duties, and enforce least privilege including use of non-privileged accounts for nonsecurity functions.", + "controlFamily": "AC - Access Control", + "requirementIndices": [ + 0, + 1, + 3, + 4, + 5, + 6 + ], + "policyTemplateIndices": [ + 0 + ], + "taskTemplateIndices": [ + 0, + 1 + ] + }, + { + "name": "Session and Logon Controls", + "description": "Limit unsuccessful logon attempts, present required notices, and lock or terminate sessions after inactivity or defined conditions.", + "controlFamily": "AC - Access Control", + "requirementIndices": [ + 7, + 8, + 9, + 10 + ], + "policyTemplateIndices": [ + 0 + ], + "taskTemplateIndices": [ + 0, + 1 + ] + }, + { + "name": "Remote Access Management", + "description": "Monitor and control remote access, encrypt remote sessions, route through managed access control points, and authorize privileged remote actions.", + "controlFamily": "AC - Access Control", + "requirementIndices": [ + 11, + 12, + 13, + 14 + ], + "policyTemplateIndices": [ + 0 + ], + "taskTemplateIndices": [ + 0, + 1 + ] + }, + { + "name": "Wireless and Mobile Device Access", + "description": "Authorize and protect wireless access, control mobile device connections, and encrypt CUI on mobile platforms.", + "controlFamily": "AC - Access Control", + "requirementIndices": [ + 15, + 16, + 17, + 18 + ], + "policyTemplateIndices": [ + 0 + ], + "taskTemplateIndices": [ + 0, + 1 + ] + }, + { + "name": "External Systems and CUI Flow Control", + "description": "Control the flow of CUI, verify and limit use of external systems and portable storage, and control CUI on publicly accessible systems.", + "controlFamily": "AC - Access Control", + "requirementIndices": [ + 2, + 19, + 20, + 21 + ], + "policyTemplateIndices": [ + 0 + ], + "taskTemplateIndices": [ + 0, + 1 + ] + }, + { + "name": "Security Awareness and Role-Based Training", + "description": "Make personnel aware of security risks and applicable policy, train them for their assigned security duties, and cover insider threat indicators.", + "controlFamily": "AT - Awareness and Training", + "requirementIndices": [ + 22, + 23, + 24 + ], + "policyTemplateIndices": [ + 1 + ], + "taskTemplateIndices": [ + 2 + ] + }, + { + "name": "Audit Logging and User Accountability", + "description": "Create and retain audit logs sufficient for investigation, uniquely attribute actions to users, and review the set of logged events.", + "controlFamily": "AU - Audit and Accountability", + "requirementIndices": [ + 25, + 26, + 27 + ], + "policyTemplateIndices": [ + 2 + ], + "taskTemplateIndices": [ + 3, + 4 + ] + }, + { + "name": "Audit Monitoring, Correlation, and Reporting", + "description": "Alert on logging failures, correlate records for investigation, support on-demand reporting, and synchronize clocks to an authoritative time source.", + "controlFamily": "AU - Audit and Accountability", + "requirementIndices": [ + 28, + 29, + 30, + 31 + ], + "policyTemplateIndices": [ + 2 + ], + "taskTemplateIndices": [ + 3, + 4 + ] + }, + { + "name": "Audit Information Protection", + "description": "Protect audit records and tooling from unauthorized access, modification, and deletion, and restrict who can manage logging.", + "controlFamily": "AU - Audit and Accountability", + "requirementIndices": [ + 32, + 33 + ], + "policyTemplateIndices": [ + 2 + ], + "taskTemplateIndices": [ + 3, + 4 + ] + }, + { + "name": "Baseline Configuration and Inventory", + "description": "Establish and maintain baseline configurations and inventories and enforce security configuration settings across IT products.", + "controlFamily": "CM - Configuration Management", + "requirementIndices": [ + 34, + 35 + ], + "policyTemplateIndices": [ + 3 + ], + "taskTemplateIndices": [ + 5, + 6 + ] + }, + { + "name": "Change Control and Security Impact Analysis", + "description": "Track, review, and approve changes, analyze security impact before implementation, and enforce access restrictions for change.", + "controlFamily": "CM - Configuration Management", + "requirementIndices": [ + 36, + 37, + 38 + ], + "policyTemplateIndices": [ + 3 + ], + "taskTemplateIndices": [ + 5, + 6 + ] + }, + { + "name": "Least Functionality and Software Restriction", + "description": "Provide only essential capabilities, disable nonessential services and ports, enforce allow/deny listing, and control user-installed software.", + "controlFamily": "CM - Configuration Management", + "requirementIndices": [ + 39, + 40, + 41, + 42 + ], + "policyTemplateIndices": [ + 3 + ], + "taskTemplateIndices": [ + 5, + 6 + ] + }, + { + "name": "Identification and Authentication of Users and Devices", + "description": "Identify users, processes, and devices, authenticate them before granting access, and obscure authentication feedback.", + "controlFamily": "IA - Identification and Authentication", + "requirementIndices": [ + 43, + 44, + 53 + ], + "policyTemplateIndices": [ + 4 + ], + "taskTemplateIndices": [ + 7 + ] + }, + { + "name": "Multifactor and Replay-Resistant Authentication", + "description": "Require MFA for privileged and network access and employ replay-resistant authentication mechanisms.", + "controlFamily": "IA - Identification and Authentication", + "requirementIndices": [ + 45, + 46 + ], + "policyTemplateIndices": [ + 4 + ], + "taskTemplateIndices": [ + 7 + ] + }, + { + "name": "Identifier and Password Lifecycle Management", + "description": "Manage identifier reuse and inactivity disabling, and enforce password complexity, reuse limits, temporary password handling, and cryptographic protection.", + "controlFamily": "IA - Identification and Authentication", + "requirementIndices": [ + 47, + 48, + 49, + 50, + 51, + 52 + ], + "policyTemplateIndices": [ + 4 + ], + "taskTemplateIndices": [ + 7 + ] + }, + { + "name": "Incident Response Capability", + "description": "Maintain an operational incident handling capability covering preparation through recovery, report incidents to designated authorities, and test the capability.", + "controlFamily": "IR - Incident Response", + "requirementIndices": [ + 54, + 55, + 56 + ], + "policyTemplateIndices": [ + 5 + ], + "taskTemplateIndices": [ + 8 + ] + }, + { + "name": "System Maintenance Controls", + "description": "Perform and control system maintenance, sanitize equipment removed off-site, inspect diagnostic media, require MFA for nonlocal maintenance, and supervise unescorted maintenance personnel.", + "controlFamily": "MA - Maintenance", + "requirementIndices": [ + 57, + 58, + 59, + 60, + 61, + 62 + ], + "policyTemplateIndices": [ + 6 + ], + "taskTemplateIndices": [ + 9 + ] + }, + { + "name": "Media Handling, Marking, and Accountability", + "description": "Physically control and securely store media containing CUI, limit access to authorized users, apply required markings, and maintain accountability during transport.", + "controlFamily": "MP - Media Protection", + "requirementIndices": [ + 63, + 64, + 66, + 67 + ], + "policyTemplateIndices": [ + 7 + ], + "taskTemplateIndices": [ + 10, + 11 + ] + }, + { + "name": "Media Sanitization and Disposal", + "description": "Sanitize or destroy media containing CUI before disposal or release for reuse.", + "controlFamily": "MP - Media Protection", + "requirementIndices": [ + 65 + ], + "policyTemplateIndices": [ + 7 + ], + "taskTemplateIndices": [ + 10, + 11 + ] + }, + { + "name": "Removable Media and Backup Protection", + "description": "Encrypt CUI on digital media in transport, control removable media use, prohibit unowned portable devices, and protect backup confidentiality.", + "controlFamily": "MP - Media Protection", + "requirementIndices": [ + 68, + 69, + 70, + 71 + ], + "policyTemplateIndices": [ + 7 + ], + "taskTemplateIndices": [ + 10, + 11 + ] + }, + { + "name": "Personnel Screening and Access Changes", + "description": "Screen individuals before authorizing access to CUI and protect systems during and after terminations and transfers.", + "controlFamily": "PS - Personnel Security", + "requirementIndices": [ + 72, + 73 + ], + "policyTemplateIndices": [ + 8 + ], + "taskTemplateIndices": [ + 12 + ] + }, + { + "name": "Physical Access Authorization and Devices", + "description": "Limit physical access to authorized individuals, escort and monitor visitors, and control and manage physical access devices.", + "controlFamily": "PE - Physical Protection", + "requirementIndices": [ + 74, + 76, + 78 + ], + "policyTemplateIndices": [ + 9 + ], + "taskTemplateIndices": [ + 13, + 14 + ] + }, + { + "name": "Facility Monitoring and Access Logging", + "description": "Protect and monitor the physical facility and supporting infrastructure and maintain physical access audit logs.", + "controlFamily": "PE - Physical Protection", + "requirementIndices": [ + 75, + 77 + ], + "policyTemplateIndices": [ + 9 + ], + "taskTemplateIndices": [ + 13, + 14 + ] + }, + { + "name": "Alternate Work Site Safeguards", + "description": "Enforce safeguarding measures for CUI at alternate work sites.", + "controlFamily": "PE - Physical Protection", + "requirementIndices": [ + 79 + ], + "policyTemplateIndices": [ + 9 + ], + "taskTemplateIndices": [ + 13, + 14 + ] + }, + { + "name": "Organizational Risk Assessment", + "description": "Periodically assess risk to operations, assets, and individuals arising from systems that process, store, or transmit CUI.", + "controlFamily": "RA - Risk Assessment", + "requirementIndices": [ + 80 + ], + "policyTemplateIndices": [ + 10 + ], + "taskTemplateIndices": [ + 15, + 16, + 17 + ] + }, + { + "name": "Vulnerability Scanning and Remediation", + "description": "Scan for vulnerabilities periodically and on new disclosures, and remediate in accordance with assessed risk.", + "controlFamily": "RA - Risk Assessment", + "requirementIndices": [ + 81, + 82 + ], + "policyTemplateIndices": [ + 10 + ], + "taskTemplateIndices": [ + 15, + 16, + 17 + ] + }, + { + "name": "Control Assessment and Continuous Monitoring", + "description": "Periodically assess control effectiveness and monitor controls on an ongoing basis.", + "controlFamily": "CA - Security Assessment", + "requirementIndices": [ + 83, + 85 + ], + "policyTemplateIndices": [ + 11 + ], + "taskTemplateIndices": [ + 18, + 19, + 20 + ] + }, + { + "name": "Plans of Action and System Security Planning", + "description": "Maintain plans of action to correct deficiencies and develop and update system security plans describing boundaries and control implementation.", + "controlFamily": "CA - Security Assessment", + "requirementIndices": [ + 84, + 86 + ], + "policyTemplateIndices": [ + 11 + ], + "taskTemplateIndices": [ + 18, + 19, + 20 + ] + }, + { + "name": "Boundary Protection and Network Segmentation", + "description": "Monitor and protect communications at external and key internal boundaries, isolate publicly accessible components, deny traffic by default, and prevent split tunneling.", + "controlFamily": "SC - System and Communications Protection", + "requirementIndices": [ + 87, + 91, + 92, + 93 + ], + "policyTemplateIndices": [ + 12 + ], + "taskTemplateIndices": [ + 21, + 22 + ] + }, + { + "name": "Secure Architecture and Functional Separation", + "description": "Apply security engineering principles, separate user from management functionality, and prevent unauthorized transfer via shared resources.", + "controlFamily": "SC - System and Communications Protection", + "requirementIndices": [ + 88, + 89, + 90 + ], + "policyTemplateIndices": [ + 12 + ], + "taskTemplateIndices": [ + 21, + 22 + ] + }, + { + "name": "Cryptographic Protection of CUI", + "description": "Encrypt CUI in transit and at rest, employ FIPS-validated cryptography, and manage cryptographic keys.", + "controlFamily": "SC - System and Communications Protection", + "requirementIndices": [ + 94, + 96, + 97, + 102 + ], + "policyTemplateIndices": [ + 12 + ], + "taskTemplateIndices": [ + 21, + 22 + ] + }, + { + "name": "Session and Communications Integrity", + "description": "Terminate sessions at end or after inactivity and protect the authenticity of communications sessions.", + "controlFamily": "SC - System and Communications Protection", + "requirementIndices": [ + 95, + 101 + ], + "policyTemplateIndices": [ + 12 + ], + "taskTemplateIndices": [ + 21, + 22 + ] + }, + { + "name": "Collaborative Computing, Mobile Code, and VoIP", + "description": "Prohibit remote activation of collaborative computing devices, indicate device use, and control and monitor mobile code and VoIP.", + "controlFamily": "SC - System and Communications Protection", + "requirementIndices": [ + 98, + 99, + 100 + ], + "policyTemplateIndices": [ + 12 + ], + "taskTemplateIndices": [ + 21, + 22 + ] + }, + { + "name": "Flaw Remediation and Security Alerting", + "description": "Identify, report, and correct system flaws in a timely manner and act on security alerts and advisories.", + "controlFamily": "SI - System and Information Integrity", + "requirementIndices": [ + 103, + 105 + ], + "policyTemplateIndices": [ + 13 + ], + "taskTemplateIndices": [ + 23, + 24 + ] + }, + { + "name": "Malicious Code Protection", + "description": "Provide malicious code protection at designated locations, keep mechanisms updated, and perform periodic and real-time scanning.", + "controlFamily": "SI - System and Information Integrity", + "requirementIndices": [ + 104, + 106, + 107 + ], + "policyTemplateIndices": [ + 13 + ], + "taskTemplateIndices": [ + 23, + 24 + ] + }, + { + "name": "System Monitoring and Unauthorized Use Detection", + "description": "Monitor systems and inbound/outbound traffic for attacks and identify unauthorized use.", + "controlFamily": "SI - System and Information Integrity", + "requirementIndices": [ + 108, + 109 + ], + "policyTemplateIndices": [ + 13 + ], + "taskTemplateIndices": [ + 23, + 24 + ] + } + ] +} \ No newline at end of file diff --git a/frameworks/cmmc-level-2/generator/add_content.py b/frameworks/cmmc-level-2/generator/add_content.py new file mode 100644 index 0000000000..5cae28419d --- /dev/null +++ b/frameworks/cmmc-level-2/generator/add_content.py @@ -0,0 +1,52 @@ +import json, sys +sys.path.insert(0,'/private/tmp/claude-501/-Users-chris-Code-comp/a0fe0e29-c24d-4821-b750-359c9a908dc2/scratchpad/cmmc') +from practices import PRACTICES, FAMILIES + +p = '/private/tmp/claude-501/-Users-chris-Code-comp/a0fe0e29-c24d-4821-b750-359c9a908dc2/scratchpad/cmmc/payload.json' +payload = json.load(open(p)) + +fam_by_prefix = {pre:(abbr,name) for abbr,name,pre in FAMILIES} +def prefix(nid): return '.'.join(nid.split('.')[:2]) + +# practices grouped by family abbreviation +by_abbr = {} +for nid, title, desc in PRACTICES: + abbr, _ = fam_by_prefix[prefix(nid)] + by_abbr.setdefault(abbr, []).append((nid, title, desc)) + +def h(level, text): + return {"type":"heading","attrs":{"level":level},"content":[{"type":"text","text":text}]} +def para(text): + return {"type":"paragraph","content":[{"type":"text","text":text}]} +def bullets(items): + return {"type":"bulletList","content":[ + {"type":"listItem","content":[para(t)]} for t in items]} + +# policy order matches FAMILIES order in build.py +ORDER = [a for a,_,_ in FAMILIES] +for i, pol in enumerate(payload["policyTemplates"]): + abbr = ORDER[i] + _, fname = [(a,n) for a,n,_ in FAMILIES if a==abbr][0][0], [n for a,n,_ in FAMILIES if a==abbr][0] + prac = by_abbr[abbr] + pol["content"] = {"type":"doc","content":[ + h(1, pol["name"]), + h(2, "Purpose"), + para(pol["description"]), + h(2, "Scope"), + para("This policy applies to all personnel, contractors, systems, and facilities that store, process, or transmit Controlled Unclassified Information (CUI), and to the people and third parties acting on the organization's behalf."), + h(2, "Policy Requirements"), + para(f"The organization implements the following CMMC Level 2 practices in the {fname} domain:"), + bullets([f"{abbr}.L2-{nid} — {desc}" for nid, _t, desc in prac]), + h(2, "Roles and Responsibilities"), + para("Control owners are accountable for implementing and evidencing the practices above. The security function reviews implementation at least annually and reports exceptions through the risk management process."), + h(2, "Exceptions"), + para("Exceptions require documented risk acceptance by the system owner and the security function, with a defined expiry and compensating controls where applicable."), + h(2, "Review"), + para("This policy is reviewed at least annually and after significant changes to the environment or to CMMC requirements."), + ]} + +json.dump(payload, open(p,'w'), indent=2) +tot = sum(len(x["content"]["content"]) for x in payload["policyTemplates"]) +print(f"added content to {len(payload['policyTemplates'])} policies ({tot} total nodes)") +print("sample:", payload["policyTemplates"][0]["name"], "->", + len(payload["policyTemplates"][0]["content"]["content"]), "nodes") diff --git a/frameworks/cmmc-level-2/generator/build.py b/frameworks/cmmc-level-2/generator/build.py new file mode 100644 index 0000000000..39168c06af --- /dev/null +++ b/frameworks/cmmc-level-2/generator/build.py @@ -0,0 +1,149 @@ +import json, sys +sys.path.insert(0, '/private/tmp/claude-501/-Users-chris-Code-comp/a0fe0e29-c24d-4821-b750-359c9a908dc2/scratchpad/cmmc') +from practices import PRACTICES, FAMILIES + +fam_by_prefix = {pre: (abbr, name) for abbr, name, pre in FAMILIES} +def prefix(nid): return '.'.join(nid.split('.')[:2]) + +# ---- requirements (index order == PRACTICES order) ---- +requirements, idx_by_nist = [], {} +for i, (nid, title, desc) in enumerate(PRACTICES): + abbr, fname = fam_by_prefix[prefix(nid)] + idx_by_nist[nid] = i + requirements.append({ + "name": title, + "identifier": f"{abbr}.L2-{nid}", + "description": desc, + "requirementFamily": f"{abbr} - {fname}", + "sortOrder": i, + }) + +# ---- policy templates: one per family ---- +POLICIES = [ + ("AC","Access Control Policy","Governs how access to systems and CUI is authorized, provisioned, reviewed, and revoked, including least privilege, separation of duties, remote access, wireless, and external systems.","yearly","it"), + ("AT","Security Awareness and Training Policy","Establishes security awareness and role-based training requirements, including insider threat recognition and reporting.","yearly","hr"), + ("AU","Audit Logging and Accountability Policy","Defines what events are logged, how logs are protected and retained, and how audit records are reviewed and correlated.","yearly","it"), + ("CM","Configuration Management Policy","Defines baseline configurations, change control, security impact analysis, least functionality, and software restrictions.","yearly","it"), + ("IA","Identification and Authentication Policy","Defines identity management, multifactor authentication, and password and identifier lifecycle requirements.","yearly","it"), + ("IR","Incident Response Policy","Establishes the incident handling capability, reporting obligations, and testing of response procedures.","yearly","it"), + ("MA","System Maintenance Policy","Governs local and nonlocal maintenance, maintenance tooling, personnel supervision, and sanitization of equipment removed for service.","yearly","it"), + ("MP","Media Protection Policy","Governs marking, storage, access, transport, sanitization, and disposal of media containing CUI, including removable media and backups.","yearly","it"), + ("PS","Personnel Security Policy","Defines pre-authorization screening and protection of CUI during and after terminations and transfers.","yearly","hr"), + ("PE","Physical Protection Policy","Governs physical access authorization, visitor escort, access device management, facility monitoring, and alternate work sites.","yearly","admin"), + ("RA","Risk Assessment Policy","Defines periodic risk assessment, vulnerability scanning cadence, and risk-based remediation of findings.","yearly","gov"), + ("CA","Security Assessment Policy","Defines periodic control assessment, continuous monitoring, plans of action, and maintenance of the system security plan.","yearly","gov"), + ("SC","System and Communications Protection Policy","Governs boundary protection, network segmentation, cryptographic protection of CUI in transit and at rest, key management, and session integrity.","yearly","it"), + ("SI","System and Information Integrity Policy","Governs flaw remediation, malicious code protection, security alert handling, and system monitoring for attacks and unauthorized use.","yearly","it"), +] +policy_templates = [{"name":n,"description":d,"frequency":f,"department":dept} for _,n,d,f,dept in POLICIES] +pol_idx = {abbr:i for i,(abbr,_,_,_,_) in enumerate(POLICIES)} + +# ---- task templates ---- +TASKS = [ + ("AC","Review user access and privileges","Recertify user accounts, roles, and privileged access; remove access no longer required.","quarterly","it"), + ("AC","Review remote access configurations","Verify remote access routes through managed access control points and that encryption is enforced.","quarterly","it"), + ("AT","Deliver security awareness training","Deliver and record annual awareness training, including insider threat indicators.","yearly","hr"), + ("AU","Review and update logged events","Review the set of logged event types and adjust to current threat and audit needs.","quarterly","it"), + ("AU","Review audit records for anomalies","Review correlated audit records for unlawful, unauthorized, suspicious, or unusual activity.","monthly","it"), + ("CM","Review baseline configurations","Verify system baselines and inventory remain accurate and hardening settings are enforced.","quarterly","it"), + ("CM","Review user-installed software","Audit installed software against the approved list and remove unauthorized applications.","quarterly","it"), + ("IA","Validate MFA enrollment coverage","Confirm multifactor authentication is enforced for privileged and network accounts.","quarterly","it"), + ("IR","Test the incident response plan","Exercise the incident response capability and record results and corrective actions.","yearly","it"), + ("MA","Review maintenance activity records","Verify maintenance was authorized, supervised where required, and equipment sanitized before off-site service.","quarterly","it"), + ("MP","Sanitize and dispose of media","Sanitize or destroy media containing CUI before disposal or reuse and record the disposition.","quarterly","it"), + ("MP","Verify backup protection","Confirm backups containing CUI are encrypted and access to backup locations is restricted.","quarterly","it"), + ("PS","Review screening and offboarding records","Confirm screening was completed before access was granted and access was revoked on termination or transfer.","quarterly","hr"), + ("PE","Review physical access logs","Review facility access logs for unauthorized or anomalous entry.","monthly","admin"), + ("PE","Review physical access devices","Inventory and reconcile keys, badges, and other physical access devices.","quarterly","admin"), + ("RA","Conduct vulnerability scans","Scan systems and applications for vulnerabilities and record results.","monthly","it"), + ("RA","Remediate identified vulnerabilities","Remediate or formally accept vulnerabilities according to assessed risk.","monthly","it"), + ("RA","Perform organizational risk assessment","Assess risk to operations, assets, and individuals from systems processing CUI.","yearly","gov"), + ("CA","Assess security controls","Assess control effectiveness and document findings.","yearly","gov"), + ("CA","Update the system security plan","Review and update the SSP covering boundaries, environment, and control implementation.","yearly","gov"), + ("CA","Review plan of action and milestones","Review POA&M items for progress and closure evidence.","quarterly","gov"), + ("SC","Review boundary and firewall rules","Verify deny-by-default rules and review exceptions at network boundaries.","quarterly","it"), + ("SC","Review cryptographic key management","Review key generation, rotation, storage, and retirement practices.","yearly","it"), + ("SI","Update malicious code protection","Confirm anti-malware definitions and engines are current across managed endpoints.","monthly","it"), + ("SI","Review system monitoring alerts","Review monitoring alerts for attacks, indicators of attack, and unauthorized use.","monthly","it"), +] +task_templates = [{"name":n,"description":d,"frequency":f,"department":dept,"automationStatus":"MANUAL"} for _,n,d,f,dept in TASKS] +def tasks_for(abbr): return [i for i,(a,_,_,_,_) in enumerate(TASKS) if a == abbr] + +# ---- control templates ---- +CONTROLS = [ + ("AC","Account Authorization and Least Privilege","Authorize accounts and devices, restrict permitted transactions and functions, separate duties, and enforce least privilege including use of non-privileged accounts for nonsecurity functions.",["3.1.1","3.1.2","3.1.4","3.1.5","3.1.6","3.1.7"]), + ("AC","Session and Logon Controls","Limit unsuccessful logon attempts, present required notices, and lock or terminate sessions after inactivity or defined conditions.",["3.1.8","3.1.9","3.1.10","3.1.11"]), + ("AC","Remote Access Management","Monitor and control remote access, encrypt remote sessions, route through managed access control points, and authorize privileged remote actions.",["3.1.12","3.1.13","3.1.14","3.1.15"]), + ("AC","Wireless and Mobile Device Access","Authorize and protect wireless access, control mobile device connections, and encrypt CUI on mobile platforms.",["3.1.16","3.1.17","3.1.18","3.1.19"]), + ("AC","External Systems and CUI Flow Control","Control the flow of CUI, verify and limit use of external systems and portable storage, and control CUI on publicly accessible systems.",["3.1.3","3.1.20","3.1.21","3.1.22"]), + ("AT","Security Awareness and Role-Based Training","Make personnel aware of security risks and applicable policy, train them for their assigned security duties, and cover insider threat indicators.",["3.2.1","3.2.2","3.2.3"]), + ("AU","Audit Logging and User Accountability","Create and retain audit logs sufficient for investigation, uniquely attribute actions to users, and review the set of logged events.",["3.3.1","3.3.2","3.3.3"]), + ("AU","Audit Monitoring, Correlation, and Reporting","Alert on logging failures, correlate records for investigation, support on-demand reporting, and synchronize clocks to an authoritative time source.",["3.3.4","3.3.5","3.3.6","3.3.7"]), + ("AU","Audit Information Protection","Protect audit records and tooling from unauthorized access, modification, and deletion, and restrict who can manage logging.",["3.3.8","3.3.9"]), + ("CM","Baseline Configuration and Inventory","Establish and maintain baseline configurations and inventories and enforce security configuration settings across IT products.",["3.4.1","3.4.2"]), + ("CM","Change Control and Security Impact Analysis","Track, review, and approve changes, analyze security impact before implementation, and enforce access restrictions for change.",["3.4.3","3.4.4","3.4.5"]), + ("CM","Least Functionality and Software Restriction","Provide only essential capabilities, disable nonessential services and ports, enforce allow/deny listing, and control user-installed software.",["3.4.6","3.4.7","3.4.8","3.4.9"]), + ("IA","Identification and Authentication of Users and Devices","Identify users, processes, and devices, authenticate them before granting access, and obscure authentication feedback.",["3.5.1","3.5.2","3.5.11"]), + ("IA","Multifactor and Replay-Resistant Authentication","Require MFA for privileged and network access and employ replay-resistant authentication mechanisms.",["3.5.3","3.5.4"]), + ("IA","Identifier and Password Lifecycle Management","Manage identifier reuse and inactivity disabling, and enforce password complexity, reuse limits, temporary password handling, and cryptographic protection.",["3.5.5","3.5.6","3.5.7","3.5.8","3.5.9","3.5.10"]), + ("IR","Incident Response Capability","Maintain an operational incident handling capability covering preparation through recovery, report incidents to designated authorities, and test the capability.",["3.6.1","3.6.2","3.6.3"]), + ("MA","System Maintenance Controls","Perform and control system maintenance, sanitize equipment removed off-site, inspect diagnostic media, require MFA for nonlocal maintenance, and supervise unescorted maintenance personnel.",["3.7.1","3.7.2","3.7.3","3.7.4","3.7.5","3.7.6"]), + ("MP","Media Handling, Marking, and Accountability","Physically control and securely store media containing CUI, limit access to authorized users, apply required markings, and maintain accountability during transport.",["3.8.1","3.8.2","3.8.4","3.8.5"]), + ("MP","Media Sanitization and Disposal","Sanitize or destroy media containing CUI before disposal or release for reuse.",["3.8.3"]), + ("MP","Removable Media and Backup Protection","Encrypt CUI on digital media in transport, control removable media use, prohibit unowned portable devices, and protect backup confidentiality.",["3.8.6","3.8.7","3.8.8","3.8.9"]), + ("PS","Personnel Screening and Access Changes","Screen individuals before authorizing access to CUI and protect systems during and after terminations and transfers.",["3.9.1","3.9.2"]), + ("PE","Physical Access Authorization and Devices","Limit physical access to authorized individuals, escort and monitor visitors, and control and manage physical access devices.",["3.10.1","3.10.3","3.10.5"]), + ("PE","Facility Monitoring and Access Logging","Protect and monitor the physical facility and supporting infrastructure and maintain physical access audit logs.",["3.10.2","3.10.4"]), + ("PE","Alternate Work Site Safeguards","Enforce safeguarding measures for CUI at alternate work sites.",["3.10.6"]), + ("RA","Organizational Risk Assessment","Periodically assess risk to operations, assets, and individuals arising from systems that process, store, or transmit CUI.",["3.11.1"]), + ("RA","Vulnerability Scanning and Remediation","Scan for vulnerabilities periodically and on new disclosures, and remediate in accordance with assessed risk.",["3.11.2","3.11.3"]), + ("CA","Control Assessment and Continuous Monitoring","Periodically assess control effectiveness and monitor controls on an ongoing basis.",["3.12.1","3.12.3"]), + ("CA","Plans of Action and System Security Planning","Maintain plans of action to correct deficiencies and develop and update system security plans describing boundaries and control implementation.",["3.12.2","3.12.4"]), + ("SC","Boundary Protection and Network Segmentation","Monitor and protect communications at external and key internal boundaries, isolate publicly accessible components, deny traffic by default, and prevent split tunneling.",["3.13.1","3.13.5","3.13.6","3.13.7"]), + ("SC","Secure Architecture and Functional Separation","Apply security engineering principles, separate user from management functionality, and prevent unauthorized transfer via shared resources.",["3.13.2","3.13.3","3.13.4"]), + ("SC","Cryptographic Protection of CUI","Encrypt CUI in transit and at rest, employ FIPS-validated cryptography, and manage cryptographic keys.",["3.13.8","3.13.10","3.13.11","3.13.16"]), + ("SC","Session and Communications Integrity","Terminate sessions at end or after inactivity and protect the authenticity of communications sessions.",["3.13.9","3.13.15"]), + ("SC","Collaborative Computing, Mobile Code, and VoIP","Prohibit remote activation of collaborative computing devices, indicate device use, and control and monitor mobile code and VoIP.",["3.13.12","3.13.13","3.13.14"]), + ("SI","Flaw Remediation and Security Alerting","Identify, report, and correct system flaws in a timely manner and act on security alerts and advisories.",["3.14.1","3.14.3"]), + ("SI","Malicious Code Protection","Provide malicious code protection at designated locations, keep mechanisms updated, and perform periodic and real-time scanning.",["3.14.2","3.14.4","3.14.5"]), + ("SI","System Monitoring and Unauthorized Use Detection","Monitor systems and inbound/outbound traffic for attacks and identify unauthorized use.",["3.14.6","3.14.7"]), +] +control_templates = [] +for abbr, name, desc, nids in CONTROLS: + fname = fam_by_prefix[[p for p,(a,_) in fam_by_prefix.items() if a==abbr][0]][1] + control_templates.append({ + "name": name, + "description": desc, + "controlFamily": f"{abbr} - {fname}", + "requirementIndices": [idx_by_nist[n] for n in nids], + "policyTemplateIndices": [pol_idx[abbr]], + "taskTemplateIndices": tasks_for(abbr), + }) + +payload = { + "version": "1", + "framework": { + "name": "CMMC Level 2", + "version": "2.0", + "description": "Cybersecurity Maturity Model Certification (CMMC) 2.0 Level 2 — 110 practices for the protection of Controlled Unclassified Information (CUI), aligned to NIST SP 800-171 Rev 2.", + "visible": True, + }, + "requirements": requirements, + "policyTemplates": policy_templates, + "taskTemplates": task_templates, + "controlTemplates": control_templates, +} + +# ---- coverage check: every requirement must be covered by >=1 control ---- +covered = set() +for ct in control_templates: covered.update(ct["requirementIndices"]) +missing = [requirements[i]["identifier"] for i in range(len(requirements)) if i not in covered] + +out = '/private/tmp/claude-501/-Users-chris-Code-comp/a0fe0e29-c24d-4821-b750-359c9a908dc2/scratchpad/cmmc/payload.json' +json.dump(payload, open(out,'w'), indent=2) +print(f"requirements : {len(requirements)}") +print(f"control templates : {len(control_templates)}") +print(f"policy templates : {len(policy_templates)}") +print(f"task templates : {len(task_templates)}") +print(f"uncovered reqs : {len(missing)} {missing if missing else ''}") +print(f"written : {out}") diff --git a/frameworks/cmmc-level-2/generator/practices.py b/frameworks/cmmc-level-2/generator/practices.py new file mode 100644 index 0000000000..84a45e2386 --- /dev/null +++ b/frameworks/cmmc-level-2/generator/practices.py @@ -0,0 +1,133 @@ +# CMMC 2.0 Level 2 practices. Requirement statements paraphrase +# NIST SP 800-171 Rev 2 (3.1.1 - 3.14.7). 110 practices / 14 families. + +FAMILIES = [ + ("AC", "Access Control", "3.1"), + ("AT", "Awareness and Training", "3.2"), + ("AU", "Audit and Accountability", "3.3"), + ("CM", "Configuration Management", "3.4"), + ("IA", "Identification and Authentication", "3.5"), + ("IR", "Incident Response", "3.6"), + ("MA", "Maintenance", "3.7"), + ("MP", "Media Protection", "3.8"), + ("PS", "Personnel Security", "3.9"), + ("PE", "Physical Protection", "3.10"), + ("RA", "Risk Assessment", "3.11"), + ("CA", "Security Assessment", "3.12"), + ("SC", "System and Communications Protection", "3.13"), + ("SI", "System and Information Integrity", "3.14"), +] + +# (nist_id, short title, requirement statement) +PRACTICES = [ +("3.1.1","Authorized Access Control","Limit system access to authorized users, processes acting on behalf of authorized users, and devices (including other systems)."), +("3.1.2","Transaction & Function Control","Limit system access to the types of transactions and functions that authorized users are permitted to execute."), +("3.1.3","Control CUI Flow","Control the flow of CUI in accordance with approved authorizations."), +("3.1.4","Separation of Duties","Separate the duties of individuals to reduce the risk of malevolent activity without collusion."), +("3.1.5","Least Privilege","Employ the principle of least privilege, including for specific security functions and privileged accounts."), +("3.1.6","Non-Privileged Account Use","Use non-privileged accounts or roles when accessing nonsecurity functions."), +("3.1.7","Privileged Functions","Prevent non-privileged users from executing privileged functions and capture the execution of such functions in audit logs."), +("3.1.8","Unsuccessful Logon Attempts","Limit unsuccessful logon attempts."), +("3.1.9","Privacy & Security Notices","Provide privacy and security notices consistent with applicable CUI rules."), +("3.1.10","Session Lock","Use session lock with pattern-hiding displays to prevent access and viewing of data after a period of inactivity."), +("3.1.11","Session Termination","Terminate (automatically) a user session after a defined condition."), +("3.1.12","Control Remote Access","Monitor and control remote access sessions."), +("3.1.13","Remote Access Confidentiality","Employ cryptographic mechanisms to protect the confidentiality of remote access sessions."), +("3.1.14","Remote Access Routing","Route remote access via managed access control points."), +("3.1.15","Privileged Remote Access","Authorize remote execution of privileged commands and remote access to security-relevant information."), +("3.1.16","Wireless Access Authorization","Authorize wireless access prior to allowing such connections."), +("3.1.17","Wireless Access Protection","Protect wireless access using authentication and encryption."), +("3.1.18","Mobile Device Connection","Control connection of mobile devices."), +("3.1.19","Encrypt CUI on Mobile","Encrypt CUI on mobile devices and mobile computing platforms."), +("3.1.20","External Connections","Verify and control/limit connections to and use of external systems."), +("3.1.21","Portable Storage Use","Limit use of portable storage devices on external systems."), +("3.1.22","Control Public Information","Control CUI posted or processed on publicly accessible systems."), +("3.2.1","Role-Based Risk Awareness","Ensure that managers, systems administrators, and users of organizational systems are made aware of the security risks associated with their activities and of the applicable policies, standards, and procedures related to the security of those systems."), +("3.2.2","Role-Based Training","Ensure that personnel are trained to carry out their assigned information security-related duties and responsibilities."), +("3.2.3","Insider Threat Awareness","Provide security awareness training on recognizing and reporting potential indicators of insider threat."), +("3.3.1","System Auditing","Create and retain system audit logs and records to the extent needed to enable the monitoring, analysis, investigation, and reporting of unlawful or unauthorized system activity."), +("3.3.2","User Accountability","Ensure that the actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions."), +("3.3.3","Event Review","Review and update logged events."), +("3.3.4","Audit Failure Alerting","Alert in the event of an audit logging process failure."), +("3.3.5","Audit Correlation","Correlate audit record review, analysis, and reporting processes for investigation and response to indications of unlawful, unauthorized, suspicious, or unusual activity."), +("3.3.6","Reduction & Reporting","Provide audit record reduction and report generation to support on-demand analysis and reporting."), +("3.3.7","Authoritative Time Source","Provide a system capability that compares and synchronizes internal system clocks with an authoritative source to generate time stamps for audit records."), +("3.3.8","Audit Protection","Protect audit information and audit logging tools from unauthorized access, modification, and deletion."), +("3.3.9","Audit Management","Limit management of audit logging functionality to a subset of privileged users."), +("3.4.1","System Baselining","Establish and maintain baseline configurations and inventories of organizational systems (including hardware, software, firmware, and documentation) throughout the respective system development life cycles."), +("3.4.2","Security Configuration Enforcement","Establish and enforce security configuration settings for information technology products employed in organizational systems."), +("3.4.3","System Change Management","Track, review, approve or disapprove, and log changes to organizational systems."), +("3.4.4","Security Impact Analysis","Analyze the security impact of changes prior to implementation."), +("3.4.5","Access Restrictions for Change","Define, document, approve, and enforce physical and logical access restrictions associated with changes to organizational systems."), +("3.4.6","Least Functionality","Employ the principle of least functionality by configuring organizational systems to provide only essential capabilities."), +("3.4.7","Nonessential Functionality","Restrict, disable, or prevent the use of nonessential programs, functions, ports, protocols, and services."), +("3.4.8","Application Execution Policy","Apply deny-by-exception (blacklisting) policy to prevent the use of unauthorized software or deny-all, permit-by-exception (whitelisting) policy to allow the execution of authorized software."), +("3.4.9","User-Installed Software","Control and monitor user-installed software."), +("3.5.1","Identification","Identify system users, processes acting on behalf of users, and devices."), +("3.5.2","Authentication","Authenticate (or verify) the identities of users, processes, or devices, as a prerequisite to allowing access to organizational systems."), +("3.5.3","Multifactor Authentication","Use multifactor authentication for local and network access to privileged accounts and for network access to non-privileged accounts."), +("3.5.4","Replay-Resistant Authentication","Employ replay-resistant authentication mechanisms for network access to privileged and non-privileged accounts."), +("3.5.5","Identifier Reuse","Prevent reuse of identifiers for a defined period."), +("3.5.6","Identifier Handling","Disable identifiers after a defined period of inactivity."), +("3.5.7","Password Complexity","Enforce a minimum password complexity and change of characters when new passwords are created."), +("3.5.8","Password Reuse","Prohibit password reuse for a specified number of generations."), +("3.5.9","Temporary Passwords","Allow temporary password use for system logons with an immediate change to a permanent password."), +("3.5.10","Cryptographically-Protected Passwords","Store and transmit only cryptographically-protected passwords."), +("3.5.11","Obscure Feedback","Obscure feedback of authentication information."), +("3.6.1","Incident Handling","Establish an operational incident-handling capability for organizational systems that includes preparation, detection, analysis, containment, recovery, and user response activities."), +("3.6.2","Incident Reporting","Track, document, and report incidents to designated officials and/or authorities both internal and external to the organization."), +("3.6.3","Incident Response Testing","Test the organizational incident response capability."), +("3.7.1","Perform Maintenance","Perform maintenance on organizational systems."), +("3.7.2","System Maintenance Control","Provide controls on the tools, techniques, mechanisms, and personnel used to conduct system maintenance."), +("3.7.3","Equipment Sanitization","Ensure equipment removed for off-site maintenance is sanitized of any CUI."), +("3.7.4","Media Inspection","Check media containing diagnostic and test programs for malicious code before the media are used in organizational systems."), +("3.7.5","Nonlocal Maintenance MFA","Require multifactor authentication to establish nonlocal maintenance sessions via external network connections and terminate such connections when nonlocal maintenance is complete."), +("3.7.6","Maintenance Personnel","Supervise the maintenance activities of maintenance personnel without required access authorization."), +("3.8.1","Media Protection","Protect (i.e., physically control and securely store) system media containing CUI, both paper and digital."), +("3.8.2","Media Access","Limit access to CUI on system media to authorized users."), +("3.8.3","Media Disposal","Sanitize or destroy system media containing CUI before disposal or release for reuse."), +("3.8.4","Media Markings","Mark media with necessary CUI markings and distribution limitations."), +("3.8.5","Media Accountability","Control access to media containing CUI and maintain accountability for media during transport outside of controlled areas."), +("3.8.6","Portable Storage Encryption","Implement cryptographic mechanisms to protect the confidentiality of CUI stored on digital media during transport unless otherwise protected by alternative physical safeguards."), +("3.8.7","Removable Media","Control the use of removable media on system components."), +("3.8.8","Shared Media","Prohibit the use of portable storage devices when such devices have no identifiable owner."), +("3.8.9","Protect Backups","Protect the confidentiality of backup CUI at storage locations."), +("3.9.1","Screen Individuals","Screen individuals prior to authorizing access to organizational systems containing CUI."), +("3.9.2","Personnel Actions","Ensure that organizational systems containing CUI are protected during and after personnel actions such as terminations and transfers."), +("3.10.1","Limit Physical Access","Limit physical access to organizational systems, equipment, and the respective operating environments to authorized individuals."), +("3.10.2","Monitor Facility","Protect and monitor the physical facility and support infrastructure for organizational systems."), +("3.10.3","Escort Visitors","Escort visitors and monitor visitor activity."), +("3.10.4","Physical Access Logs","Maintain audit logs of physical access."), +("3.10.5","Manage Physical Access","Control and manage physical access devices."), +("3.10.6","Alternative Work Sites","Enforce safeguarding measures for CUI at alternate work sites."), +("3.11.1","Risk Assessments","Periodically assess the risk to organizational operations (including mission, functions, image, or reputation), organizational assets, and individuals, resulting from the operation of organizational systems and the associated processing, storage, or transmission of CUI."), +("3.11.2","Vulnerability Scan","Scan for vulnerabilities in organizational systems and applications periodically and when new vulnerabilities affecting those systems and applications are identified."), +("3.11.3","Vulnerability Remediation","Remediate vulnerabilities in accordance with risk assessments."), +("3.12.1","Security Control Assessment","Periodically assess the security controls in organizational systems to determine if the controls are effective in their application."), +("3.12.2","Plan of Action","Develop and implement plans of action designed to correct deficiencies and reduce or eliminate vulnerabilities in organizational systems."), +("3.12.3","Continuous Monitoring","Monitor security controls on an ongoing basis to ensure the continued effectiveness of the controls."), +("3.12.4","System Security Plan","Develop, document, and periodically update system security plans that describe system boundaries, system environments of operation, how security requirements are implemented, and the relationships with or connections to other systems."), +("3.13.1","Boundary Protection","Monitor, control, and protect communications (i.e., information transmitted or received by organizational systems) at the external boundaries and key internal boundaries of organizational systems."), +("3.13.2","Security Engineering","Employ architectural designs, software development techniques, and systems engineering principles that promote effective information security within organizational systems."), +("3.13.3","Role Separation","Separate user functionality from system management functionality."), +("3.13.4","Shared Resource Control","Prevent unauthorized and unintended information transfer via shared system resources."), +("3.13.5","Public-Access System Separation","Implement subnetworks for publicly accessible system components that are physically or logically separated from internal networks."), +("3.13.6","Network Communication by Exception","Deny network communications traffic by default and allow network communications traffic by exception (i.e., deny all, permit by exception)."), +("3.13.7","Split Tunneling","Prevent remote devices from simultaneously establishing non-remote connections with organizational systems and communicating via some other connection to resources in external networks (i.e., split tunneling)."), +("3.13.8","Data in Transit","Implement cryptographic mechanisms to prevent unauthorized disclosure of CUI during transmission unless otherwise protected by alternative physical safeguards."), +("3.13.9","Connections Termination","Terminate network connections associated with communications sessions at the end of the sessions or after a defined period of inactivity."), +("3.13.10","Key Management","Establish and manage cryptographic keys for cryptography employed in organizational systems."), +("3.13.11","CUI Encryption","Employ FIPS-validated cryptography when used to protect the confidentiality of CUI."), +("3.13.12","Collaborative Device Control","Prohibit remote activation of collaborative computing devices and provide indication of devices in use to users present at the device."), +("3.13.13","Mobile Code","Control and monitor the use of mobile code."), +("3.13.14","Voice over Internet Protocol","Control and monitor the use of Voice over Internet Protocol (VoIP) technologies."), +("3.13.15","Communications Authenticity","Protect the authenticity of communications sessions."), +("3.13.16","Data at Rest","Protect the confidentiality of CUI at rest."), +("3.14.1","Flaw Remediation","Identify, report, and correct system flaws in a timely manner."), +("3.14.2","Malicious Code Protection","Provide protection from malicious code at designated locations within organizational systems."), +("3.14.3","Security Alerts & Advisories","Monitor system security alerts and advisories and take action in response."), +("3.14.4","Update Malicious Code Protection","Update malicious code protection mechanisms when new releases are available."), +("3.14.5","System & File Scanning","Perform periodic scans of organizational systems and real-time scans of files from external sources as files are downloaded, opened, or executed."), +("3.14.6","Monitor Communications for Attacks","Monitor organizational systems, including inbound and outbound communications traffic, to detect attacks and indicators of potential attacks."), +("3.14.7","Identify Unauthorized Use","Identify unauthorized use of organizational systems."), +] diff --git a/frameworks/cmmc-level-2/import-cmmc.ts b/frameworks/cmmc-level-2/import-cmmc.ts new file mode 100644 index 0000000000..535ec4c323 --- /dev/null +++ b/frameworks/cmmc-level-2/import-cmmc.ts @@ -0,0 +1,178 @@ +/** + * One-off: import the CMMC Level 2 framework definition. + * + * Mirrors FrameworkExportService.import() in + * apps/api/src/framework-editor/framework/framework-export.service.ts — + * same entities, same link rows, same single transaction. Used instead of + * POST /v1/framework-editor/framework/import because that route is behind + * PlatformAdminGuard, which requires a browser session cookie. + */ +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import fs from 'node:fs'; + +const payloadPath = process.argv[2]; +if (!payloadPath) throw new Error('usage: bun import-cmmc.ts '); + +const dto = JSON.parse(fs.readFileSync(payloadPath, 'utf-8')); + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); + +function normalizeTipTapDoc(content: unknown): Record { + if (Array.isArray(content)) return { type: 'doc', content }; + if (content !== null && typeof content === 'object') { + const node = content as { type?: unknown; content?: unknown }; + if (node.type === 'doc') { + return { type: 'doc', content: Array.isArray(node.content) ? node.content : [] }; + } + if (typeof node.type === 'string') return { type: 'doc', content: [content] }; + } + return { type: 'doc', content: [] }; +} + +// Same index validation the API performs before writing anything. +function validateIndices() { + const reqCount = dto.requirements?.length ?? 0; + const polCount = dto.policyTemplates?.length ?? 0; + const taskCount = dto.taskTemplates?.length ?? 0; + for (const ct of dto.controlTemplates ?? []) { + for (const i of ct.requirementIndices ?? []) + if (i < 0 || i >= reqCount) throw new Error(`"${ct.name}" bad requirement index ${i}`); + for (const i of ct.policyTemplateIndices ?? []) + if (i < 0 || i >= polCount) throw new Error(`"${ct.name}" bad policy index ${i}`); + for (const i of ct.taskTemplateIndices ?? []) + if (i < 0 || i >= taskCount) throw new Error(`"${ct.name}" bad task index ${i}`); + } +} + +async function main() { + validateIndices(); + + const existing = await prisma.frameworkEditorFramework.findFirst({ + where: { name: dto.framework.name }, + }); + if (existing) { + console.error(`Framework "${dto.framework.name}" already exists (${existing.id}). Aborting.`); + process.exit(1); + } + + const framework = await prisma.$transaction( + async (tx) => { + const fw = await tx.frameworkEditorFramework.create({ + data: { + name: dto.framework.name, + version: dto.framework.version, + description: dto.framework.description, + visible: dto.framework.visible ?? false, + }, + }); + + // Sequential so array index order is guaranteed to match the payload. + const reqs = []; + for (const r of dto.requirements ?? []) { + reqs.push( + await tx.frameworkEditorRequirement.create({ + data: { + frameworkId: fw.id, + name: r.name, + identifier: r.identifier ?? '', + description: r.description, + requirementFamily: r.requirementFamily || null, + sortOrder: r.sortOrder ?? null, + }, + }), + ); + } + + const pols = []; + for (const p of dto.policyTemplates ?? []) { + pols.push( + await tx.frameworkEditorPolicyTemplate.create({ + data: { + name: p.name, + description: p.description, + frequency: p.frequency, + department: p.department, + content: normalizeTipTapDoc(p.content), + }, + }), + ); + } + + const tsks = []; + for (const t of dto.taskTemplates ?? []) { + tsks.push( + await tx.frameworkEditorTaskTemplate.create({ + data: { + name: t.name, + description: t.description, + frequency: t.frequency, + department: t.department, + automationStatus: t.automationStatus, + }, + }), + ); + } + + const ctrls = []; + for (const ct of dto.controlTemplates ?? []) { + ctrls.push( + await tx.frameworkEditorControlTemplate.create({ + data: { + name: ct.name, + description: ct.description, + controlFamily: ct.controlFamily ?? null, + requirements: { + connect: (ct.requirementIndices ?? []).map((i: number) => ({ id: reqs[i].id })), + }, + }, + }), + ); + } + + const policyLinks = (dto.controlTemplates ?? []).flatMap((ct: any, ci: number) => + (ct.policyTemplateIndices ?? []).map((pi: number) => ({ + frameworkId: fw.id, + controlTemplateId: ctrls[ci].id, + policyTemplateId: pols[pi].id, + })), + ); + const taskLinks = (dto.controlTemplates ?? []).flatMap((ct: any, ci: number) => + (ct.taskTemplateIndices ?? []).map((ti: number) => ({ + frameworkId: fw.id, + controlTemplateId: ctrls[ci].id, + taskTemplateId: tsks[ti].id, + })), + ); + + if (policyLinks.length) + await tx.frameworkEditorControlPolicyTemplateLink.createMany({ + data: policyLinks, + skipDuplicates: true, + }); + if (taskLinks.length) + await tx.frameworkEditorControlTaskTemplateLink.createMany({ + data: taskLinks, + skipDuplicates: true, + }); + + console.log( + `Imported "${fw.name}" (${fw.id}): ${reqs.length} requirements, ` + + `${ctrls.length} controls, ${pols.length} policies, ${tsks.length} tasks, ` + + `${policyLinks.length} policy links, ${taskLinks.length} task links`, + ); + return fw; + }, + { maxWait: 30_000, timeout: 300_000 }, + ); + + console.log('framework id:', framework.id); +} + +main() + .catch((e) => { + console.error('IMPORT FAILED:', e.message); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/integrations-catalog/integrations/google-workspace-admin.json b/integrations-catalog/integrations/google-workspace-admin.json index 027ae0f94c..7422dd58ad 100644 --- a/integrations-catalog/integrations/google-workspace-admin.json +++ b/integrations-catalog/integrations/google-workspace-admin.json @@ -39,5 +39,5 @@ } ], "checkCount": 2, - "isActive": false + "isActive": true } diff --git a/packages/db/scripts/import-cmmc.ts b/packages/db/scripts/import-cmmc.ts new file mode 100644 index 0000000000..535ec4c323 --- /dev/null +++ b/packages/db/scripts/import-cmmc.ts @@ -0,0 +1,178 @@ +/** + * One-off: import the CMMC Level 2 framework definition. + * + * Mirrors FrameworkExportService.import() in + * apps/api/src/framework-editor/framework/framework-export.service.ts — + * same entities, same link rows, same single transaction. Used instead of + * POST /v1/framework-editor/framework/import because that route is behind + * PlatformAdminGuard, which requires a browser session cookie. + */ +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import fs from 'node:fs'; + +const payloadPath = process.argv[2]; +if (!payloadPath) throw new Error('usage: bun import-cmmc.ts '); + +const dto = JSON.parse(fs.readFileSync(payloadPath, 'utf-8')); + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); + +function normalizeTipTapDoc(content: unknown): Record { + if (Array.isArray(content)) return { type: 'doc', content }; + if (content !== null && typeof content === 'object') { + const node = content as { type?: unknown; content?: unknown }; + if (node.type === 'doc') { + return { type: 'doc', content: Array.isArray(node.content) ? node.content : [] }; + } + if (typeof node.type === 'string') return { type: 'doc', content: [content] }; + } + return { type: 'doc', content: [] }; +} + +// Same index validation the API performs before writing anything. +function validateIndices() { + const reqCount = dto.requirements?.length ?? 0; + const polCount = dto.policyTemplates?.length ?? 0; + const taskCount = dto.taskTemplates?.length ?? 0; + for (const ct of dto.controlTemplates ?? []) { + for (const i of ct.requirementIndices ?? []) + if (i < 0 || i >= reqCount) throw new Error(`"${ct.name}" bad requirement index ${i}`); + for (const i of ct.policyTemplateIndices ?? []) + if (i < 0 || i >= polCount) throw new Error(`"${ct.name}" bad policy index ${i}`); + for (const i of ct.taskTemplateIndices ?? []) + if (i < 0 || i >= taskCount) throw new Error(`"${ct.name}" bad task index ${i}`); + } +} + +async function main() { + validateIndices(); + + const existing = await prisma.frameworkEditorFramework.findFirst({ + where: { name: dto.framework.name }, + }); + if (existing) { + console.error(`Framework "${dto.framework.name}" already exists (${existing.id}). Aborting.`); + process.exit(1); + } + + const framework = await prisma.$transaction( + async (tx) => { + const fw = await tx.frameworkEditorFramework.create({ + data: { + name: dto.framework.name, + version: dto.framework.version, + description: dto.framework.description, + visible: dto.framework.visible ?? false, + }, + }); + + // Sequential so array index order is guaranteed to match the payload. + const reqs = []; + for (const r of dto.requirements ?? []) { + reqs.push( + await tx.frameworkEditorRequirement.create({ + data: { + frameworkId: fw.id, + name: r.name, + identifier: r.identifier ?? '', + description: r.description, + requirementFamily: r.requirementFamily || null, + sortOrder: r.sortOrder ?? null, + }, + }), + ); + } + + const pols = []; + for (const p of dto.policyTemplates ?? []) { + pols.push( + await tx.frameworkEditorPolicyTemplate.create({ + data: { + name: p.name, + description: p.description, + frequency: p.frequency, + department: p.department, + content: normalizeTipTapDoc(p.content), + }, + }), + ); + } + + const tsks = []; + for (const t of dto.taskTemplates ?? []) { + tsks.push( + await tx.frameworkEditorTaskTemplate.create({ + data: { + name: t.name, + description: t.description, + frequency: t.frequency, + department: t.department, + automationStatus: t.automationStatus, + }, + }), + ); + } + + const ctrls = []; + for (const ct of dto.controlTemplates ?? []) { + ctrls.push( + await tx.frameworkEditorControlTemplate.create({ + data: { + name: ct.name, + description: ct.description, + controlFamily: ct.controlFamily ?? null, + requirements: { + connect: (ct.requirementIndices ?? []).map((i: number) => ({ id: reqs[i].id })), + }, + }, + }), + ); + } + + const policyLinks = (dto.controlTemplates ?? []).flatMap((ct: any, ci: number) => + (ct.policyTemplateIndices ?? []).map((pi: number) => ({ + frameworkId: fw.id, + controlTemplateId: ctrls[ci].id, + policyTemplateId: pols[pi].id, + })), + ); + const taskLinks = (dto.controlTemplates ?? []).flatMap((ct: any, ci: number) => + (ct.taskTemplateIndices ?? []).map((ti: number) => ({ + frameworkId: fw.id, + controlTemplateId: ctrls[ci].id, + taskTemplateId: tsks[ti].id, + })), + ); + + if (policyLinks.length) + await tx.frameworkEditorControlPolicyTemplateLink.createMany({ + data: policyLinks, + skipDuplicates: true, + }); + if (taskLinks.length) + await tx.frameworkEditorControlTaskTemplateLink.createMany({ + data: taskLinks, + skipDuplicates: true, + }); + + console.log( + `Imported "${fw.name}" (${fw.id}): ${reqs.length} requirements, ` + + `${ctrls.length} controls, ${pols.length} policies, ${tsks.length} tasks, ` + + `${policyLinks.length} policy links, ${taskLinks.length} task links`, + ); + return fw; + }, + { maxWait: 30_000, timeout: 300_000 }, + ); + + console.log('framework id:', framework.id); +} + +main() + .catch((e) => { + console.error('IMPORT FAILED:', e.message); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/packages/integration-platform/src/index.ts b/packages/integration-platform/src/index.ts index a3d92f852f..ebd1ec90a6 100644 --- a/packages/integration-platform/src/index.ts +++ b/packages/integration-platform/src/index.ts @@ -136,6 +136,25 @@ export { manifest as githubManifest } from './manifests/github'; // Directory sync email include/exclude terms (Google Workspace, JumpCloud, checks) export { matchesSyncFilterTerms, parseSyncFilterTerms } from './sync-filter/email-exclusion-terms'; +// Google Workspace user scoping. Exported so the API's employee sync applies +// the exact same rules as the checks instead of keeping a parallel copy — +// divergence here means the access review and the personnel list disagree. +export { + filterGoogleWorkspaceUsersForChecks, + isGoogleWorkspaceUserInScope, + isGoogleWorkspaceUserSelectedBySyncTerms, + parseGoogleWorkspaceCheckUserFilter, + resolveEffectiveSyncFilterMode, + resolveGoogleWorkspaceUserFilter, + shouldIncludeGoogleWorkspaceUserForCheck, + type GoogleWorkspaceCheckUserFilterConfig, + type GoogleWorkspaceUserSyncFilterMode, +} from './manifests/google-workspace/check-user-filter'; +export { + createBearerTokenClient, + type GoogleWorkspaceDirectoryClient, +} from './manifests/google-workspace/directory-client'; + // AWS credential helpers (used by frontend setup dialogs) export { awsRemediationScript, diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/admin-audit.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/admin-audit.test.ts new file mode 100644 index 0000000000..a5cc5b0a27 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/admin-audit.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, it } from 'bun:test'; +import { adminPrivilegeChangesCheck } from '../checks/admin-privilege-changes'; +import { adminSecurityEventsCheck } from '../checks/admin-security-events'; +import { getEventParameter, isInsufficientScopeError, lookbackStartTime } from '../admin-audit-events'; +import type { CheckContext, CheckResult, CheckVariableValues } from '../../../types'; +import type { GoogleWorkspaceActivity } from '../types'; + +const activity = ( + time: string, + actorEmail: string | undefined, + events: GoogleWorkspaceActivity['events'], +): GoogleWorkspaceActivity => ({ + id: { time, applicationName: 'admin' }, + actor: actorEmail ? { email: actorEmail, callerType: 'USER' } : undefined, + ipAddress: '198.51.100.7', + events, +}); + +type RunResult = { passed: CheckResult[]; failed: CheckResult[]; logs: string[] }; + +/** Drive a check against a canned activity list (or a fetch that throws). */ +async function runCheck( + check: typeof adminPrivilegeChangesCheck, + { + activities = [], + variables = {}, + fetchError, + }: { + activities?: GoogleWorkspaceActivity[]; + variables?: CheckVariableValues; + fetchError?: Error; + }, +): Promise { + const passed: CheckResult[] = []; + const failed: CheckResult[] = []; + const logs: string[] = []; + + const ctx: CheckContext = { + accessToken: 'tok', + credentials: {}, + variables, + connectionId: 'conn_1', + organizationId: 'org_1', + metadata: {}, + log: (m: string) => { + logs.push(m); + }, + warn: () => {}, + error: () => {}, + pass: (r) => { + passed.push(r as CheckResult); + }, + fail: (r) => { + failed.push(r as CheckResult); + }, + fetch: (async (): Promise => { + if (fetchError) throw fetchError; + return { items: activities } as unknown as T; + }) as CheckContext['fetch'], + } as CheckContext; + + await check.run(ctx); + return { passed, failed, logs }; +} + +function scopeError(status: number): Error { + const err = new Error(`HTTP ${status}: Forbidden`) as Error & { status: number }; + err.status = status; + return err; +} + +describe('admin-audit helpers', () => { + it('reads string, bool, int, and multi-value parameters', () => { + const event = { + name: 'ASSIGN_ROLE', + parameters: [ + { name: 'ROLE_NAME', value: '_SEED_ADMIN_ROLE' }, + { name: 'FLAG', boolValue: false }, + { name: 'COUNT', intValue: '3' }, + { name: 'LIST', multiValue: ['a', 'b'] }, + ], + }; + expect(getEventParameter(event, 'ROLE_NAME')).toBe('_SEED_ADMIN_ROLE'); + expect(getEventParameter(event, 'FLAG')).toBe('false'); + expect(getEventParameter(event, 'COUNT')).toBe('3'); + expect(getEventParameter(event, 'LIST')).toBe('a, b'); + expect(getEventParameter(event, 'MISSING')).toBeUndefined(); + }); + + it('treats 401/403 as missing scope but not 500 or non-errors', () => { + expect(isInsufficientScopeError(scopeError(403))).toBe(true); + expect(isInsufficientScopeError(scopeError(401))).toBe(true); + expect(isInsufficientScopeError(scopeError(500))).toBe(false); + expect(isInsufficientScopeError('nope')).toBe(false); + }); + + it('computes the lookback start time from a fixed now', () => { + const now = new Date('2026-03-31T00:00:00.000Z'); + expect(lookbackStartTime(30, now)).toBe('2026-03-01T00:00:00.000Z'); + }); +}); + +describe('adminPrivilegeChangesCheck', () => { + it('passes with evidence when no privilege changes occurred', async () => { + const { passed, failed } = await runCheck(adminPrivilegeChangesCheck, { activities: [] }); + expect(failed).toHaveLength(0); + expect(passed).toHaveLength(1); + expect(passed[0].title).toBe('No admin privilege changes in review window'); + expect(passed[0].evidence?.lookbackDays).toBe(30); + }); + + it('flags an unapproved privilege change for review', async () => { + const { failed } = await runCheck(adminPrivilegeChangesCheck, { + activities: [ + activity('2026-03-01T10:00:00.000Z', 'rogue@corp.com', [ + { + name: 'ASSIGN_ROLE', + parameters: [ + { name: 'ROLE_NAME', value: 'Groups Admin' }, + { name: 'USER_EMAIL', value: 'newadmin@corp.com' }, + ], + }, + ]), + ], + }); + expect(failed).toHaveLength(1); + expect(failed[0].severity).toBe('medium'); + expect(failed[0].description).toContain('rogue@corp.com'); + expect(failed[0].description).toContain('newadmin@corp.com'); + }); + + it('escalates super admin grants to high severity', async () => { + const { failed } = await runCheck(adminPrivilegeChangesCheck, { + activities: [ + activity('2026-03-02T10:00:00.000Z', 'rogue@corp.com', [ + { + name: 'ASSIGN_ROLE', + parameters: [ + { name: 'ROLE_NAME', value: '_SEED_ADMIN_ROLE' }, + { name: 'USER_EMAIL', value: 'newadmin@corp.com' }, + ], + }, + ]), + ], + }); + expect(failed).toHaveLength(1); + expect(failed[0].severity).toBe('high'); + expect(failed[0].title).toContain('Super admin'); + }); + + it('passes changes made by an approved actor, case-insensitively', async () => { + const { passed, failed } = await runCheck(adminPrivilegeChangesCheck, { + variables: { admin_audit_approved_actors: ['IT-Automation@corp.com'] }, + activities: [ + activity('2026-03-03T10:00:00.000Z', 'it-automation@corp.com', [ + { name: 'ASSIGN_ROLE', parameters: [{ name: 'USER_EMAIL', value: 'x@corp.com' }] }, + ]), + ], + }); + expect(failed).toHaveLength(0); + expect(passed).toHaveLength(1); + expect(passed[0].title).toBe('Privilege change by approved admin'); + }); + + it('ignores events that are not privilege changes', async () => { + const { passed, failed } = await runCheck(adminPrivilegeChangesCheck, { + activities: [activity('2026-03-04T10:00:00.000Z', 'a@corp.com', [{ name: 'LOGIN' }])], + }); + expect(failed).toHaveLength(0); + expect(passed[0].title).toBe('No admin privilege changes in review window'); + }); + + it('reports a missing audit scope as actionable instead of throwing', async () => { + const { failed } = await runCheck(adminPrivilegeChangesCheck, { + fetchError: scopeError(403), + }); + expect(failed).toHaveLength(1); + expect(failed[0].title).toBe('Admin audit log not accessible'); + expect(failed[0].remediation).toContain('Reconnect'); + }); + + it('rethrows errors that are not scope problems', async () => { + await expect( + runCheck(adminPrivilegeChangesCheck, { fetchError: scopeError(500) }), + ).rejects.toThrow('HTTP 500'); + }); +}); + +describe('adminSecurityEventsCheck', () => { + it('passes when no monitored security settings changed', async () => { + const { passed, failed } = await runCheck(adminSecurityEventsCheck, { activities: [] }); + expect(failed).toHaveLength(0); + expect(passed[0].title).toBe('No security-weakening admin changes detected'); + expect(Array.isArray(passed[0].evidence?.monitoredEvents)).toBe(true); + }); + + it('flags a 2SV enforcement change with its mapped severity', async () => { + const { failed } = await runCheck(adminSecurityEventsCheck, { + activities: [ + activity('2026-03-05T10:00:00.000Z', 'admin@corp.com', [ + { + name: 'ENFORCE_STRONG_AUTHENTICATION', + parameters: [{ name: 'NEW_VALUE', value: 'false' }], + }, + ]), + ], + }); + expect(failed).toHaveLength(1); + expect(failed[0].severity).toBe('high'); + expect(failed[0].evidence?.newValue).toBe('false'); + }); + + it('handles a system activity with no actor email', async () => { + const { failed } = await runCheck(adminSecurityEventsCheck, { + activities: [ + activity('2026-03-06T10:00:00.000Z', undefined, [ + { name: 'TOGGLE_ENABLE_OAUTH2_ACCESS' }, + ]), + ], + }); + expect(failed).toHaveLength(1); + expect(failed[0].description).toContain('unknown actor'); + }); + + it('honours a configured lookback window', async () => { + const { passed } = await runCheck(adminSecurityEventsCheck, { + variables: { admin_audit_lookback_days: '90' }, + }); + expect(passed[0].evidence?.lookbackDays).toBe(90); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts index 772454e89a..2b4dc8b1ef 100644 --- a/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'bun:test'; import { filterGoogleWorkspaceUsersForChecks, + isGoogleWorkspaceUserInScope, parseGoogleWorkspaceCheckUserFilter, shouldIncludeGoogleWorkspaceUserForCheck, } from '../check-user-filter'; @@ -97,3 +98,61 @@ describe('filterGoogleWorkspaceUsersForChecks', () => { expect(filterGoogleWorkspaceUsersForChecks(users, config)).toHaveLength(1); }); }); + +describe('isGoogleWorkspaceUserInScope — org unit matching', () => { + /** Build a scope matcher for an OU selection; no args means no OU filter. */ + const scopedTo = + (...targetOrgUnits: string[]) => + (orgUnitPath: string) => + isGoogleWorkspaceUserInScope( + baseUser({ orgUnitPath }), + parseGoogleWorkspaceCheckUserFilter( + targetOrgUnits.length > 0 ? { target_org_units: targetOrgUnits } : {}, + ), + ); + + it('treats a missing or empty OU selection as "no OU filter"', () => { + expect(scopedTo()('/Anything')).toBe(true); + // An explicitly empty list must disable the filter, not select nobody. + expect( + isGoogleWorkspaceUserInScope( + baseUser({ orgUnitPath: '/Anything' }), + parseGoogleWorkspaceCheckUserFilter({ target_org_units: [] }), + ), + ).toBe(true); + }); + + it('matches an OU exactly, including a nested path', () => { + expect(scopedTo('/Engineering')('/Engineering')).toBe(true); + expect(scopedTo('/Engineering/Frontend')('/Engineering/Frontend')).toBe(true); + }); + + it('includes child OUs of a selected OU', () => { + expect(scopedTo('/Engineering')('/Engineering/Frontend')).toBe(true); + }); + + it('does not match a partial OU path segment', () => { + // '/Eng' must not sweep in '/Engineering' — the filter compares whole path + // segments, so a prefix that stops mid-segment selects nobody. Getting this + // wrong silently widens sync and check scope. + expect(scopedTo('/Eng')('/Engineering')).toBe(false); + expect(scopedTo('/Engineer')('/Engineering/Frontend')).toBe(false); + }); + + it('treats the root OU as matching every user', () => { + const inRoot = scopedTo('/'); + expect(inRoot('/')).toBe(true); + expect(inRoot('/Engineering/Frontend')).toBe(true); + }); + + it('supports multiple target OUs', () => { + const inEngOrMarketing = scopedTo('/Engineering', '/Marketing'); + expect(inEngOrMarketing('/Engineering')).toBe(true); + expect(inEngOrMarketing('/Marketing')).toBe(true); + expect(inEngOrMarketing('/HR')).toBe(false); + }); + + it('excludes a user at the root when a specific OU is selected', () => { + expect(scopedTo('/Engineering')('/')).toBe(false); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/directory-client.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/directory-client.test.ts new file mode 100644 index 0000000000..1baf195623 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/directory-client.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'bun:test'; +import { + fetchGroupMemberUserIds, + fetchMemberIdsForGroups, + listDomains, + listGroups, + type GoogleWorkspaceDirectoryClient, +} from '../directory-client'; + +function makeClient(handler: (path: string) => unknown): GoogleWorkspaceDirectoryClient & { + warnings: string[]; + paths: string[]; +} { + const warnings: string[] = []; + const paths: string[] = []; + return { + warnings, + paths, + warn: (m: string) => { + warnings.push(m); + }, + fetch: async (path: string): Promise => { + paths.push(path); + return handler(path) as T; + }, + }; +} + +describe('fetchGroupMemberUserIds', () => { + it('returns USER members and skips nested groups', async () => { + const client = makeClient(() => ({ + members: [ + { id: 'u1', email: 'a@x.com', type: 'USER' }, + { id: 'g2', email: 'nested@x.com', type: 'GROUP' }, + { id: 'u2', email: 'b@x.com', type: 'USER' }, + { id: 'c1', type: 'CUSTOMER' }, + ], + })); + expect(await fetchGroupMemberUserIds({ client, groupId: 'g1' })).toEqual(['u1', 'u2']); + expect(client.warnings.join(' ')).toContain('Nested group'); + }); + + it('follows pagination', async () => { + let call = 0; + const client = makeClient(() => { + call += 1; + return call === 1 + ? { members: [{ id: 'u1', type: 'USER' }], nextPageToken: 'p2' } + : { members: [{ id: 'u2', type: 'USER' }] }; + }); + expect(await fetchGroupMemberUserIds({ client, groupId: 'g1' })).toEqual(['u1', 'u2']); + }); + + it('url-encodes the group id', async () => { + const client = makeClient(() => ({ members: [] })); + await fetchGroupMemberUserIds({ client, groupId: 'team+eng@x.com' }); + expect(client.paths[0]).toContain('team%2Beng%40x.com'); + }); +}); + +describe('fetchMemberIdsForGroups', () => { + it('returns undefined when no groups are selected — meaning no filter', async () => { + const client = makeClient(() => ({ members: [] })); + expect(await fetchMemberIdsForGroups({ client, groupIds: undefined })).toBeUndefined(); + expect(await fetchMemberIdsForGroups({ client, groupIds: [] })).toBeUndefined(); + }); + + it('unions members across groups, de-duplicating', async () => { + const client = makeClient((path) => + path.includes('g1') + ? { members: [{ id: 'u1', type: 'USER' }, { id: 'u2', type: 'USER' }] } + : { members: [{ id: 'u2', type: 'USER' }, { id: 'u3', type: 'USER' }] }, + ); + const ids = await fetchMemberIdsForGroups({ client, groupIds: ['g1', 'g2'] }); + expect([...(ids ?? [])].sort()).toEqual(['u1', 'u2', 'u3']); + }); + + it('warns and continues when one group cannot be read', async () => { + const client = makeClient((path) => { + if (path.includes('g1')) throw new Error('403'); + return { members: [{ id: 'u2', type: 'USER' }] }; + }); + const ids = await fetchMemberIdsForGroups({ client, groupIds: ['g1', 'g2'] }); + expect([...(ids ?? [])]).toEqual(['u2']); + expect(client.warnings.join(' ')).toContain('Could not read members of group g1'); + }); +}); + +describe('listGroups / listDomains', () => { + it('pages through groups', async () => { + let call = 0; + const client = makeClient(() => { + call += 1; + return call === 1 + ? { groups: [{ id: 'g1', email: 'a@x.com' }], nextPageToken: 'p2' } + : { groups: [{ id: 'g2', email: 'b@x.com' }] }; + }); + expect((await listGroups(client)).map((g) => g.id)).toEqual(['g1', 'g2']); + }); + + it('maps domains to names', async () => { + const client = makeClient(() => ({ + domains: [ + { domainName: 'x.com', isPrimary: true, verified: true, creationTime: '' }, + { domainName: 'y.com', isPrimary: false, verified: true, creationTime: '' }, + ], + })); + expect(await listDomains(client)).toEqual(['x.com', 'y.com']); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/role-assignments.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/role-assignments.test.ts new file mode 100644 index 0000000000..3192456548 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/role-assignments.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'bun:test'; +import { isGroupAssignment, resolveRoleAssignments } from '../role-assignments'; +import type { CheckContext } from '../../../types'; +import type { GoogleWorkspaceRoleAssignment } from '../types'; + +/** Context whose fetch is driven by a path -> payload function. */ +function makeCtx( + handler: (path: string) => unknown, +): CheckContext & { warnings: string[] } { + const warnings: string[] = []; + const ctx = { + accessToken: 'tok', + credentials: {}, + variables: {}, + connectionId: 'conn_1', + organizationId: 'org_1', + log: () => {}, + warn: (m: string) => { + warnings.push(m); + }, + error: () => {}, + pass: () => {}, + fail: () => {}, + fetch: (async (path: string): Promise => handler(path) as T), + warnings, + } as unknown as CheckContext & { warnings: string[] }; + return ctx; +} + +const assignment = ( + over: Partial & { assignedTo: string }, +): GoogleWorkspaceRoleAssignment => ({ + roleAssignmentId: `ra_${over.assignedTo}`, + roleId: 'role_1', + scopeType: 'CUSTOMER', + ...over, +}); + +const roleMap = new Map([['role_1', 'Groups Admin']]); + +describe('isGroupAssignment', () => { + it('treats an absent assigneeType as a user assignment', () => { + expect(isGroupAssignment(assignment({ assignedTo: 'u1' }))).toBe(false); + expect(isGroupAssignment(assignment({ assignedTo: 'u1', assigneeType: 'user' }))).toBe(false); + expect(isGroupAssignment(assignment({ assignedTo: 'g1', assigneeType: 'group' }))).toBe(true); + }); +}); + +describe('resolveRoleAssignments', () => { + it('maps direct assignments to the user with direct provenance', async () => { + const ctx = makeCtx(() => { + throw new Error('should not fetch groups'); + }); + const { grantsByUserId, unresolvedGroupAssignments } = await resolveRoleAssignments({ + ctx, + assignments: [assignment({ assignedTo: 'u1' })], + roleMap, + }); + expect(grantsByUserId.get('u1')).toEqual([{ roleName: 'Groups Admin', source: 'direct' }]); + expect(unresolvedGroupAssignments).toHaveLength(0); + }); + + it('expands a group assignment to every member — the bug being fixed', async () => { + const ctx = makeCtx(() => ({ + members: [ + { id: 'u1', type: 'USER' }, + { id: 'u2', type: 'USER' }, + ], + })); + const { grantsByUserId } = await resolveRoleAssignments({ + ctx, + assignments: [assignment({ assignedTo: 'g1', assigneeType: 'group' })], + roleMap, + }); + // Previously this produced grantsByUserId.get('g1') and no user rows at all. + expect(grantsByUserId.get('g1')).toBeUndefined(); + expect(grantsByUserId.get('u1')).toEqual([ + { roleName: 'Groups Admin', source: 'group', viaGroup: 'g1' }, + ]); + expect(grantsByUserId.get('u2')).toHaveLength(1); + }); + + it('records unresolved groups instead of silently dropping them', async () => { + const ctx = makeCtx(() => { + const err = new Error('HTTP 403: Forbidden') as Error & { status: number }; + err.status = 403; + throw err; + }); + const { grantsByUserId, unresolvedGroupAssignments } = await resolveRoleAssignments({ + ctx, + assignments: [assignment({ assignedTo: 'g1', assigneeType: 'group' })], + roleMap, + }); + expect(grantsByUserId.size).toBe(0); + expect(unresolvedGroupAssignments).toEqual([{ groupId: 'g1', roleName: 'Groups Admin' }]); + }); + + it('fetches each group once across multiple role assignments', async () => { + let groupFetches = 0; + const ctx = makeCtx((path) => { + if (path.includes('/groups/')) { + groupFetches += 1; + return { members: [{ id: 'u1', type: 'USER' }] }; + } + throw new Error(`unexpected ${path}`); + }); + const { grantsByUserId } = await resolveRoleAssignments({ + ctx, + assignments: [ + assignment({ assignedTo: 'g1', assigneeType: 'group', roleId: 'role_1' }), + assignment({ assignedTo: 'g1', assigneeType: 'group', roleId: 'role_2' }), + ], + roleMap, + }); + expect(groupFetches).toBe(1); + expect(grantsByUserId.get('u1')).toHaveLength(2); + }); + + it('combines direct and group grants for the same user', async () => { + const ctx = makeCtx(() => ({ members: [{ id: 'u1', type: 'USER' }] })); + const { grantsByUserId } = await resolveRoleAssignments({ + ctx, + assignments: [ + assignment({ assignedTo: 'u1' }), + assignment({ assignedTo: 'g1', assigneeType: 'group', roleId: 'role_2' }), + ], + roleMap, + }); + const grants = grantsByUserId.get('u1') ?? []; + expect(grants.map((g) => g.source).sort()).toEqual(['direct', 'group']); + }); + + it('falls back to the role id when the role name is unknown', async () => { + const ctx = makeCtx(() => ({ members: [] })); + const { grantsByUserId } = await resolveRoleAssignments({ + ctx, + assignments: [assignment({ assignedTo: 'u1', roleId: 'role_zzz' })], + roleMap, + }); + expect(grantsByUserId.get('u1')?.[0].roleName).toBe('Role role_zzz'); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/user-filter-group-domain.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/user-filter-group-domain.test.ts new file mode 100644 index 0000000000..90e2c41cba --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/user-filter-group-domain.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'bun:test'; +import { + filterGoogleWorkspaceUsersForChecks, + parseGoogleWorkspaceCheckUserFilter, + resolveGoogleWorkspaceUserFilter, +} from '../check-user-filter'; +import type { GoogleWorkspaceDirectoryClient } from '../directory-client'; +import type { GoogleWorkspaceUser } from '../types'; + +const user = (over: Partial & { primaryEmail: string; id: string }): GoogleWorkspaceUser => ({ + name: { givenName: 'T', familyName: 'U', fullName: 'T U' }, + 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: '/', + ...over, +}); + +const client = (members: Record): GoogleWorkspaceDirectoryClient => ({ + warn: () => {}, + fetch: async (path: string): Promise => { + const groupId = decodeURIComponent(path.split('/groups/')[1]?.split('/')[0] ?? ''); + return { + members: (members[groupId] ?? []).map((id) => ({ id, type: 'USER' })), + } as unknown as T; + }, +}); + +const alice = user({ id: 'u1', primaryEmail: 'alice@corp.com' }); +const bob = user({ id: 'u2', primaryEmail: 'bob@corp.com' }); +const carol = user({ id: 'u3', primaryEmail: 'carol@contractor.io' }); + +describe('domain filtering', () => { + it('keeps only users in the selected domains', () => { + const config = parseGoogleWorkspaceCheckUserFilter({ target_domains: ['corp.com'] }); + const kept = filterGoogleWorkspaceUsersForChecks([alice, bob, carol], config); + expect(kept.map((u) => u.id)).toEqual(['u1', 'u2']); + }); + + it('tolerates a leading @ and mixed case', () => { + const config = parseGoogleWorkspaceCheckUserFilter({ target_domains: ['@CORP.com'] }); + expect(filterGoogleWorkspaceUsersForChecks([alice, carol], config).map((u) => u.id)).toEqual([ + 'u1', + ]); + }); + + it('is inactive when no domains are selected', () => { + const config = parseGoogleWorkspaceCheckUserFilter({}); + expect(filterGoogleWorkspaceUsersForChecks([alice, carol], config)).toHaveLength(2); + }); +}); + +describe('group filtering', () => { + it('keeps only members of the selected groups', async () => { + const base = parseGoogleWorkspaceCheckUserFilter({ target_groups: ['eng@corp.com'] }); + const config = await resolveGoogleWorkspaceUserFilter({ + client: client({ 'eng@corp.com': ['u1'] }), + config: base, + }); + expect(filterGoogleWorkspaceUsersForChecks([alice, bob, carol], config).map((u) => u.id)).toEqual( + ['u1'], + ); + }); + + it('unions membership across multiple groups', async () => { + const base = parseGoogleWorkspaceCheckUserFilter({ + target_groups: ['eng@corp.com', 'ops@corp.com'], + }); + const config = await resolveGoogleWorkspaceUserFilter({ + client: client({ 'eng@corp.com': ['u1'], 'ops@corp.com': ['u3'] }), + config: base, + }); + expect(filterGoogleWorkspaceUsersForChecks([alice, bob, carol], config).map((u) => u.id)).toEqual( + ['u1', 'u3'], + ); + }); + + it('excludes everyone when the selected group is empty, rather than disabling the filter', async () => { + const base = parseGoogleWorkspaceCheckUserFilter({ target_groups: ['empty@corp.com'] }); + const config = await resolveGoogleWorkspaceUserFilter({ + client: client({ 'empty@corp.com': [] }), + config: base, + }); + expect(filterGoogleWorkspaceUsersForChecks([alice, bob], config)).toHaveLength(0); + }); + + it('is inactive when no groups are selected', async () => { + const base = parseGoogleWorkspaceCheckUserFilter({}); + const config = await resolveGoogleWorkspaceUserFilter({ client: client({}), config: base }); + expect(config.targetGroupMemberIds).toBeUndefined(); + expect(filterGoogleWorkspaceUsersForChecks([alice, bob], config)).toHaveLength(2); + }); +}); + +describe('combined with existing filters', () => { + it('applies domain, group, OU and email rules together', async () => { + const base = parseGoogleWorkspaceCheckUserFilter({ + target_domains: ['corp.com'], + target_groups: ['eng@corp.com'], + sync_user_filter_mode: 'exclude', + sync_excluded_emails: ['bob@corp.com'], + }); + const config = await resolveGoogleWorkspaceUserFilter({ + client: client({ 'eng@corp.com': ['u1', 'u2'] }), + config: base, + }); + // carol fails domain, bob is excluded by email, alice survives all four. + expect(filterGoogleWorkspaceUsersForChecks([alice, bob, carol], config).map((u) => u.id)).toEqual( + ['u1'], + ); + }); + + it('still drops suspended and archived users', async () => { + const suspended = user({ id: 'u4', primaryEmail: 'sus@corp.com', suspended: true }); + const config = parseGoogleWorkspaceCheckUserFilter({ target_domains: ['corp.com'] }); + expect(filterGoogleWorkspaceUsersForChecks([suspended], config)).toHaveLength(0); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/admin-audit-events.ts b/packages/integration-platform/src/manifests/google-workspace/admin-audit-events.ts new file mode 100644 index 0000000000..67ed2c4f90 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/admin-audit-events.ts @@ -0,0 +1,167 @@ +import type { CheckContext } from '../../types'; +import type { + GoogleWorkspaceActivitiesResponse, + GoogleWorkspaceActivity, + GoogleWorkspaceActivityEvent, +} from './types'; + +/** Reports API lives under a different path prefix than the Directory API. */ +const ADMIN_ACTIVITY_PATH = '/admin/reports/v1/activity/users/all/applications/admin'; + +/** Google caps Reports API page size at 1000. */ +const MAX_PAGE_SIZE = '1000'; + +/** Stop paging runaway histories rather than hanging a check run. */ +const MAX_PAGES = 20; + +/** + * Admin console events that grant, revoke, or redefine privilege. + * + * Names come from Google's "Admin audit activity events" for the `admin` + * application. Unknown names are ignored rather than guessed at, so a + * rename upstream degrades to "no events found" rather than a wrong verdict. + */ +export const PRIVILEGE_CHANGE_EVENTS: ReadonlySet = new Set([ + 'ASSIGN_ROLE', + 'UNASSIGN_ROLE', + 'CREATE_ROLE', + 'DELETE_ROLE', + 'RENAME_ROLE', + 'UPDATE_ROLE', + 'ADD_PRIVILEGE', + 'REMOVE_PRIVILEGE', + 'GRANT_ADMIN_PRIVILEGE', + 'REVOKE_ADMIN_PRIVILEGE', + 'GRANT_DELEGATED_ADMIN_PRIVILEGES', + 'REVOKE_DELEGATED_ADMIN_PRIVILEGES', +]); + +/** + * Admin actions that weaken the security posture of the tenant. Each carries + * the severity to report and the remediation an admin should follow. + */ +export const HIGH_RISK_SECURITY_EVENTS: ReadonlyMap< + string, + { severity: 'critical' | 'high' | 'medium'; summary: string; remediation: string } +> = new Map([ + ['ENFORCE_STRONG_AUTHENTICATION', { + severity: 'high', + summary: '2-Step Verification enforcement was changed', + remediation: + 'Confirm the change was intentional. Re-enable 2SV enforcement in Admin Console > Security > Authentication > 2-Step Verification.', + }], + ['ALLOW_STRONG_AUTHENTICATION', { + severity: 'medium', + summary: '2-Step Verification availability was changed', + remediation: 'Verify the change was approved and that 2SV remains available to all users.', + }], + ['TOGGLE_ALLOW_ADMIN_PASSWORD_RESET', { + severity: 'medium', + summary: 'Admin password-reset setting was changed', + remediation: 'Review whether admins should be able to reset user passwords in this tenant.', + }], + ['CHANGE_TWO_STEP_VERIFICATION_ENROLLMENT_PERIOD_DURATION', { + severity: 'medium', + summary: '2SV enrollment grace period was changed', + remediation: 'Confirm the grace period still meets policy; shorten it if it was extended without approval.', + }], + ['TOGGLE_AUTOMATIC_CONTACT_SHARING', { + severity: 'medium', + summary: 'Automatic contact sharing was toggled', + remediation: 'Verify the directory sharing change was approved.', + }], + ['REVOKE_ADMIN_PRIVILEGE', { + severity: 'medium', + summary: 'Admin privilege was revoked', + remediation: 'Confirm the revocation was intended and that no required admin coverage was lost.', + }], + ['TOGGLE_ENABLE_OAUTH2_ACCESS', { + severity: 'high', + summary: 'OAuth 2.0 API access setting was changed', + remediation: 'Review third-party API access settings in Admin Console > Security > API controls.', + }], +]); + +/** Role names Google uses for the built-in super administrator role. */ +export const SUPER_ADMIN_ROLE_NAMES: ReadonlySet = new Set([ + '_SEED_ADMIN_ROLE', + 'Super Admin', + '_SUPER_ADMIN_ROLE', +]); + +/** Read a named parameter off an activity event. */ +export function getEventParameter( + event: GoogleWorkspaceActivityEvent, + name: string, +): string | undefined { + const param = event.parameters?.find((p) => p.name === name); + if (!param) return undefined; + if (typeof param.value === 'string') return param.value; + if (typeof param.boolValue === 'boolean') return String(param.boolValue); + if (typeof param.intValue === 'string') return param.intValue; + if (param.multiValue?.length) return param.multiValue.join(', '); + return undefined; +} + +/** + * True when the API rejected the call because the connection lacks the + * audit scope. The runtime attaches `status` to the thrown Error. + */ +export function isInsufficientScopeError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + const status = (error as Error & { status?: unknown }).status; + return status === 403 || status === 401; +} + +/** Human-readable actor for a finding title. */ +export function describeActor(activity: GoogleWorkspaceActivity): string { + return activity.actor?.email ?? activity.actor?.profileId ?? 'unknown actor'; +} + +/** ISO timestamp for `lookbackDays` ago, which is what startTime expects. */ +export function lookbackStartTime(lookbackDays: number, now: Date = new Date()): string { + return new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000).toISOString(); +} + +/** + * Fetch admin console activities since `startTime`, following pagination. + * + * Throws whatever `ctx.fetch` throws — callers use `isInsufficientScopeError` + * to turn a missing-scope rejection into an actionable finding instead of a + * check crash, since existing connections predate the audit scope. + */ +export async function fetchAdminActivities({ + ctx, + startTime, + eventName, +}: { + ctx: CheckContext; + startTime: string; + eventName?: string; +}): Promise { + const activities: GoogleWorkspaceActivity[] = []; + let pageToken: string | undefined; + let pages = 0; + + do { + const params: Record = { startTime, maxResults: MAX_PAGE_SIZE }; + if (eventName) params.eventName = eventName; + if (pageToken) params.pageToken = pageToken; + + const response = await ctx.fetch(ADMIN_ACTIVITY_PATH, { + params, + }); + + if (response.items?.length) activities.push(...response.items); + + pageToken = response.nextPageToken; + pages += 1; + + if (pageToken && pages >= MAX_PAGES) { + ctx.warn(`Stopped paging admin activity after ${MAX_PAGES} pages; results may be truncated`); + break; + } + } while (pageToken); + + return activities; +} diff --git a/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts b/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts index 09837c877a..ac78072619 100644 --- a/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts +++ b/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts @@ -1,7 +1,27 @@ import { matchesSyncFilterTerms, parseSyncFilterTerms } from '../../sync-filter/email-exclusion-terms'; import type { CheckVariableValues } from '../../types'; +import { + fetchMemberIdsForGroups, + type GoogleWorkspaceDirectoryClient, +} from './directory-client'; import type { GoogleWorkspaceUser } from './types'; +/** Read a variable that may arrive as a string or an array of strings. */ +function toStringList(value: unknown): string[] | undefined { + if (Array.isArray(value)) { + const items = value.map((v) => String(v).trim()).filter(Boolean); + return items.length > 0 ? items : undefined; + } + if (typeof value === 'string' && value.trim()) return [value.trim()]; + return undefined; +} + +/** Domain portion of an email, lowercased. */ +function emailDomain(email: string): string { + const at = email.lastIndexOf('@'); + return at === -1 ? '' : email.slice(at + 1).toLowerCase(); +} + /** Sync mode for directory users — aligned with `sync_user_filter_mode` connection variables. */ export type GoogleWorkspaceUserSyncFilterMode = 'all' | 'exclude' | 'include'; @@ -12,6 +32,16 @@ export interface GoogleWorkspaceCheckUserFilterConfig { includedTerms: string[]; userFilterMode: GoogleWorkspaceUserSyncFilterMode | undefined; includeSuspended: boolean; + /** Group ids/emails selected for filtering; undefined means no group filter. */ + targetGroups: string[] | undefined; + /** Verified domains selected for filtering; undefined means no domain filter. */ + targetDomains: string[] | undefined; + /** + * Member ids of `targetGroups`, resolved by `resolveGoogleWorkspaceUserFilter`. + * Undefined means the filter is not active; an empty set means it is active + * and matched nobody. + */ + targetGroupMemberIds: Set | undefined; } /** @@ -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 + // and pure so the filter itself remains trivially testable. + targetGroupMemberIds: undefined, }; } /** - * Whether a directory user should be included in a GWS security check, using the same rules as - * `sync.controller.ts` employee sync (OU first, then email terms). + * Resolve the async part of the filter — expanding selected groups into member + * ids. Call once per sync/check run, before filtering users. */ -export function shouldIncludeGoogleWorkspaceUserForCheck( +export async function resolveGoogleWorkspaceUserFilter({ + client, + config, +}: { + client: GoogleWorkspaceDirectoryClient; + config: GoogleWorkspaceCheckUserFilterConfig; +}): Promise { + const targetGroupMemberIds = await fetchMemberIdsForGroups({ + client, + groupIds: config.targetGroups, + }); + return { ...config, targetGroupMemberIds }; +} + +/** + * Stage 1 — is this user within the configured *scope* at all? + * + * Org unit, group membership, and domain. Deliberately says nothing about + * suspended/archived: employee sync needs suspended users in scope so it can + * drive offboarding, while security checks exclude them. Callers apply their + * own activeness rule on top. + */ +export function isGoogleWorkspaceUserInScope( user: GoogleWorkspaceUser, config: GoogleWorkspaceCheckUserFilterConfig, ): boolean { - if (user.suspended && !config.includeSuspended) { - return false; - } - - if (user.archived) { - return false; - } - const { targetOrgUnits } = config; if (targetOrgUnits && targetOrgUnits.length > 0) { const userOu = user.orgUnitPath ?? '/'; @@ -62,22 +111,82 @@ export function shouldIncludeGoogleWorkspaceUserForCheck( } } + const { targetDomains } = config; + if (targetDomains && targetDomains.length > 0) { + const domain = emailDomain(user.primaryEmail); + const inDomain = targetDomains.some((d) => d.replace(/^@/, '').toLowerCase() === domain); + if (!inDomain) { + return false; + } + } + + // Group membership is resolved up front; an active-but-empty set correctly + // excludes everyone rather than silently disabling the filter. + if (config.targetGroupMemberIds && !config.targetGroupMemberIds.has(user.id)) { + return false; + } + + return true; +} + +/** + * The include/exclude mode actually in force. + * + * 'include' with an empty list falls back to 'all' so a half-configured filter + * can never silently drop everyone. + */ +export function resolveEffectiveSyncFilterMode( + config: GoogleWorkspaceCheckUserFilterConfig, +): GoogleWorkspaceUserSyncFilterMode { + const mode = config.userFilterMode ?? 'all'; + if (mode === 'include' && config.includedTerms.length === 0) return 'all'; + return mode === 'exclude' || mode === 'include' ? mode : 'all'; +} + +/** + * Stage 2 — does the email include/exclude selection pick this user? + */ +export function isGoogleWorkspaceUserSelectedBySyncTerms( + user: GoogleWorkspaceUser, + config: GoogleWorkspaceCheckUserFilterConfig, +): boolean { const email = user.primaryEmail.toLowerCase(); + const mode = resolveEffectiveSyncFilterMode(config); - if (config.userFilterMode === 'exclude' && config.excludedTerms.length > 0) { + if (mode === 'exclude' && config.excludedTerms.length > 0) { return !matchesSyncFilterTerms(email, config.excludedTerms); } - if (config.userFilterMode === 'include') { - if (config.includedTerms.length === 0) { - return true; - } + if (mode === 'include') { return matchesSyncFilterTerms(email, config.includedTerms); } return true; } +/** + * Whether a directory user should be included in a GWS security check — + * scope, then activeness, then sync term selection. + */ +export function shouldIncludeGoogleWorkspaceUserForCheck( + user: GoogleWorkspaceUser, + config: GoogleWorkspaceCheckUserFilterConfig, +): boolean { + if (user.suspended && !config.includeSuspended) { + return false; + } + + if (user.archived) { + return false; + } + + if (!isGoogleWorkspaceUserInScope(user, config)) { + return false; + } + + return isGoogleWorkspaceUserSelectedBySyncTerms(user, config); +} + export function filterGoogleWorkspaceUsersForChecks( users: GoogleWorkspaceUser[], config: GoogleWorkspaceCheckUserFilterConfig, diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts b/packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts new file mode 100644 index 0000000000..0dde49fe77 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/checks/admin-privilege-changes.ts @@ -0,0 +1,159 @@ +import { TASK_TEMPLATES } from '../../../task-mappings'; +import type { CheckContext, IntegrationCheck } from '../../../types'; +import { + PRIVILEGE_CHANGE_EVENTS, + SUPER_ADMIN_ROLE_NAMES, + describeActor, + fetchAdminActivities, + getEventParameter, + isInsufficientScopeError, + lookbackStartTime, +} from '../admin-audit-events'; +import { + adminAuditApprovedActorsVariable, + adminAuditLookbackDaysVariable, +} from '../variables'; +import type { GoogleWorkspaceActivity, GoogleWorkspaceActivityEvent } from '../types'; + +const DEFAULT_LOOKBACK_DAYS = 30; + +function parseLookbackDays(raw: unknown): number { + const parsed = Number.parseInt(String(raw ?? ''), 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LOOKBACK_DAYS; +} + +function parseApprovedActors(raw: unknown): Set { + const values = Array.isArray(raw) ? raw : typeof raw === 'string' && raw ? [raw] : []; + return new Set(values.map((v) => String(v).trim().toLowerCase()).filter(Boolean)); +} + +/** A privilege grant touching the super admin role is materially riskier. */ +function touchesSuperAdmin(event: GoogleWorkspaceActivityEvent): boolean { + const roleName = getEventParameter(event, 'ROLE_NAME'); + return roleName ? SUPER_ADMIN_ROLE_NAMES.has(roleName) : false; +} + +function buildEvidence( + activity: GoogleWorkspaceActivity, + event: GoogleWorkspaceActivityEvent, +): Record { + return { + event: event.name, + eventType: event.type, + actor: describeActor(activity), + occurredAt: activity.id.time, + ipAddress: activity.ipAddress, + roleName: getEventParameter(event, 'ROLE_NAME'), + targetUser: getEventParameter(event, 'USER_EMAIL'), + privilegeName: getEventParameter(event, 'PRIVILEGE_NAME'), + }; +} + +/** + * Surface admin privilege changes for review. + * + * Compliance framings (CMMC AC.L2-3.1.5, SOC 2 CC6.3) require that privileged + * access changes are tracked and reviewed, not that they never happen — so an + * unrecognized actor granting privilege is a finding to review, while a change + * by an approved actor passes with the same evidence recorded. + */ +export const adminPrivilegeChangesCheck: IntegrationCheck = { + id: 'admin-privilege-changes', + name: 'Admin Privilege Changes Reviewed', + description: + 'Reviews Google Workspace admin role and privilege changes so grants of administrative access are tracked and attributable', + service: 'admin-audit', + taskMapping: TASK_TEMPLATES.internalSecurityAudit, + defaultSeverity: 'medium', + variables: [adminAuditLookbackDaysVariable, adminAuditApprovedActorsVariable], + + run: async (ctx: CheckContext) => { + const lookbackDays = parseLookbackDays(ctx.variables[adminAuditLookbackDaysVariable.id]); + const approvedActors = parseApprovedActors( + ctx.variables[adminAuditApprovedActorsVariable.id], + ); + const startTime = lookbackStartTime(lookbackDays); + + ctx.log(`Reviewing admin privilege changes since ${startTime} (${lookbackDays} days)`); + + let activities: GoogleWorkspaceActivity[]; + try { + activities = await fetchAdminActivities({ ctx, startTime }); + } catch (error) { + if (isInsufficientScopeError(error)) { + // Connections created before the audit scope was added cannot read the + // Reports API. Report it as actionable rather than erroring the run. + ctx.fail({ + title: 'Admin audit log not accessible', + description: + 'Comp could not read the Google Workspace admin audit log. The connection is missing the admin.reports.audit.readonly scope, or the authorizing account is not a super admin.', + resourceType: 'connection', + resourceId: ctx.connectionId, + severity: 'medium', + remediation: + 'Reconnect Google Workspace and approve the audit log permission, authorizing with a super admin account.', + evidence: { startTime, lookbackDays }, + }); + return; + } + throw error; + } + + ctx.log(`Fetched ${activities.length} admin activities`); + + let privilegeChangeCount = 0; + + for (const activity of activities) { + for (const event of activity.events ?? []) { + if (!PRIVILEGE_CHANGE_EVENTS.has(event.name)) continue; + + privilegeChangeCount += 1; + + const actor = describeActor(activity); + const target = getEventParameter(event, 'USER_EMAIL') ?? 'unknown target'; + const roleName = getEventParameter(event, 'ROLE_NAME'); + const isSuperAdminChange = touchesSuperAdmin(event); + const resourceId = `${activity.id.time}:${event.name}:${target}`; + const evidence = buildEvidence(activity, event); + + if (approvedActors.has(actor.toLowerCase())) { + ctx.pass({ + title: 'Privilege change by approved admin', + description: `${actor} performed ${event.name} on ${target}${roleName ? ` (role: ${roleName})` : ''}. Actor is on the approved list.`, + resourceType: 'admin_activity', + resourceId, + evidence, + }); + continue; + } + + ctx.fail({ + title: isSuperAdminChange + ? 'Super admin privilege change requires review' + : 'Admin privilege change requires review', + description: `${actor} performed ${event.name} on ${target}${roleName ? ` (role: ${roleName})` : ''} at ${activity.id.time}.`, + resourceType: 'admin_activity', + resourceId, + severity: isSuperAdminChange ? 'high' : 'medium', + remediation: isSuperAdminChange + ? 'Confirm this super admin grant was authorized through your access request process. Revoke it in Admin Console > Account > Admin roles if it was not, and add the actor to Approved Admin Actors once verified.' + : 'Confirm this privilege change was authorized. Revoke it in Admin Console > Account > Admin roles if it was not, and add the actor to Approved Admin Actors once verified.', + evidence, + }); + } + } + + if (privilegeChangeCount === 0) { + // No changes in the window is itself the evidence auditors want. + ctx.pass({ + title: 'No admin privilege changes in review window', + description: `No admin role or privilege changes were recorded in Google Workspace in the last ${lookbackDays} days.`, + resourceType: 'connection', + resourceId: ctx.connectionId, + evidence: { startTime, lookbackDays, activitiesReviewed: activities.length }, + }); + } + + ctx.log(`Admin privilege change review complete (${privilegeChangeCount} changes)`); + }, +}; diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts b/packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts new file mode 100644 index 0000000000..6020afa354 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/checks/admin-security-events.ts @@ -0,0 +1,118 @@ +import { TASK_TEMPLATES } from '../../../task-mappings'; +import type { CheckContext, IntegrationCheck } from '../../../types'; +import { + HIGH_RISK_SECURITY_EVENTS, + describeActor, + fetchAdminActivities, + getEventParameter, + isInsufficientScopeError, + lookbackStartTime, +} from '../admin-audit-events'; +import { adminAuditLookbackDaysVariable } from '../variables'; +import type { GoogleWorkspaceActivity } from '../types'; + +const DEFAULT_LOOKBACK_DAYS = 30; + +function parseLookbackDays(raw: unknown): number { + const parsed = Number.parseInt(String(raw ?? ''), 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LOOKBACK_DAYS; +} + +/** + * Flag admin console changes that weaken the tenant's security posture. + * + * Distinct from `admin-privilege-changes`: that check is about *who holds + * access*, this one is about *settings that protect the tenant* — 2SV + * enforcement, API access, admin password reset. Both read the same audit + * log, so they share the fetch helper and the lookback variable. + */ +export const adminSecurityEventsCheck: IntegrationCheck = { + id: 'admin-security-events', + name: 'Admin Security Setting Changes', + description: + 'Detects Google Workspace admin console changes that weaken security posture, such as 2-Step Verification enforcement or API access settings', + service: 'admin-audit', + taskMapping: TASK_TEMPLATES.internalSecurityAudit, + defaultSeverity: 'high', + variables: [adminAuditLookbackDaysVariable], + + run: async (ctx: CheckContext) => { + const lookbackDays = parseLookbackDays(ctx.variables[adminAuditLookbackDaysVariable.id]); + const startTime = lookbackStartTime(lookbackDays); + + ctx.log(`Reviewing admin security setting changes since ${startTime} (${lookbackDays} days)`); + + let activities: GoogleWorkspaceActivity[]; + try { + activities = await fetchAdminActivities({ ctx, startTime }); + } catch (error) { + if (isInsufficientScopeError(error)) { + ctx.fail({ + title: 'Admin audit log not accessible', + description: + 'Comp could not read the Google Workspace admin audit log. The connection is missing the admin.reports.audit.readonly scope, or the authorizing account is not a super admin.', + resourceType: 'connection', + resourceId: ctx.connectionId, + severity: 'medium', + remediation: + 'Reconnect Google Workspace and approve the audit log permission, authorizing with a super admin account.', + evidence: { startTime, lookbackDays }, + }); + return; + } + throw error; + } + + ctx.log(`Fetched ${activities.length} admin activities`); + + let riskEventCount = 0; + + for (const activity of activities) { + for (const event of activity.events ?? []) { + const risk = HIGH_RISK_SECURITY_EVENTS.get(event.name); + if (!risk) continue; + + riskEventCount += 1; + + const actor = describeActor(activity); + const newValue = + getEventParameter(event, 'NEW_VALUE') ?? getEventParameter(event, 'SETTING_NAME'); + + ctx.fail({ + title: risk.summary, + description: `${actor} triggered ${event.name}${newValue ? ` (new value: ${newValue})` : ''} at ${activity.id.time}.`, + resourceType: 'admin_activity', + resourceId: `${activity.id.time}:${event.name}`, + severity: risk.severity, + remediation: risk.remediation, + evidence: { + event: event.name, + eventType: event.type, + actor, + occurredAt: activity.id.time, + ipAddress: activity.ipAddress, + newValue, + oldValue: getEventParameter(event, 'OLD_VALUE'), + }, + }); + } + } + + if (riskEventCount === 0) { + ctx.pass({ + title: 'No security-weakening admin changes detected', + description: `No monitored admin console security settings were changed in the last ${lookbackDays} days.`, + resourceType: 'connection', + resourceId: ctx.connectionId, + evidence: { + startTime, + lookbackDays, + activitiesReviewed: activities.length, + monitoredEvents: [...HIGH_RISK_SECURITY_EVENTS.keys()], + }, + }); + } + + ctx.log(`Admin security event review complete (${riskEventCount} risk events)`); + }, +}; 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 c80ff73ccf..f763e0608e 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 @@ -3,14 +3,21 @@ import type { CheckContext, IntegrationCheck } from '../../../types'; import { filterGoogleWorkspaceUsersForChecks, parseGoogleWorkspaceCheckUserFilter, + resolveGoogleWorkspaceUserFilter, } from '../check-user-filter'; -import type { - GoogleWorkspaceRoleAssignmentsResponse, - GoogleWorkspaceRolesResponse, - GoogleWorkspaceUser, - GoogleWorkspaceUsersResponse, -} from '../types'; -import { includeSuspendedVariable, targetOrgUnitsVariable } from '../variables'; +import type { GoogleWorkspaceUser, GoogleWorkspaceUsersResponse } from '../types'; +import { + fetchRoleAssignments, + fetchRoleMap, + resolveRoleAssignments, + type ResolvedRoleGrant, +} from '../role-assignments'; +import { + includeSuspendedVariable, + targetDomainsVariable, + targetGroupsVariable, + targetOrgUnitsVariable, +} from '../variables'; /** * Employee Access Review Check @@ -23,80 +30,54 @@ export const employeeAccessCheck: IntegrationCheck = { description: 'Fetch all employees and their roles from Google Workspace for access review', service: 'user-sync', taskMapping: TASK_TEMPLATES.employeeAccess, - variables: [targetOrgUnitsVariable, includeSuspendedVariable], + variables: [ + targetOrgUnitsVariable, + targetGroupsVariable, + targetDomainsVariable, + includeSuspendedVariable, + ], run: async (ctx: CheckContext) => { ctx.log('Starting Google Workspace Employee Access check'); - const userFilterConfig = parseGoogleWorkspaceCheckUserFilter(ctx.variables); - - // Fetch all roles first to build a role ID -> name map - ctx.log('Fetching available roles...'); - const roleMap = new Map(); - - try { - let rolesPageToken: string | undefined; - do { - const params: Record = { customer: 'my_customer' }; - if (rolesPageToken) { - params.pageToken = rolesPageToken; - } - - const rolesResponse = await ctx.fetch( - '/admin/directory/v1/customer/my_customer/roles', - { params }, - ); - - if (rolesResponse.items) { - for (const role of rolesResponse.items) { - roleMap.set(role.roleId, role.roleName); - } - } - - rolesPageToken = rolesResponse.nextPageToken; - } while (rolesPageToken); - - ctx.log(`Fetched ${roleMap.size} roles`); - } catch (error) { - ctx.log( - 'Could not fetch roles (may need additional permissions), continuing with basic info', - ); - } + const userFilterConfig = await resolveGoogleWorkspaceUserFilter({ + client: ctx, + config: parseGoogleWorkspaceCheckUserFilter(ctx.variables), + }); - // Fetch role assignments to map users to their roles - ctx.log('Fetching role assignments...'); - const userRolesMap = new Map(); // userId -> roleNames[] - - try { - let assignmentsPageToken: string | undefined; - do { - const params: Record = { customer: 'my_customer' }; - if (assignmentsPageToken) { - params.pageToken = assignmentsPageToken; - } - - const assignmentsResponse = await ctx.fetch( - '/admin/directory/v1/customer/my_customer/roleassignments', - { params }, - ); - - if (assignmentsResponse.items) { - for (const assignment of assignmentsResponse.items) { - const roleName = roleMap.get(assignment.roleId) || `Role ${assignment.roleId}`; - const existing = userRolesMap.get(assignment.assignedTo) || []; - existing.push(roleName); - userRolesMap.set(assignment.assignedTo, existing); - } - } - - assignmentsPageToken = assignmentsResponse.nextPageToken; - } while (assignmentsPageToken); - - ctx.log(`Fetched ${userRolesMap.size} user role assignments`); - } catch (error) { - ctx.log( - 'Could not fetch role assignments (may need additional permissions), continuing with basic admin status', + // Roles, assignments, then resolution. Group-assigned roles are expanded + // to their members — assignments are not all user-scoped. + ctx.log('Fetching roles and role assignments...'); + const roleMap = await fetchRoleMap(ctx); + const assignments = await fetchRoleAssignments(ctx); + ctx.log(`Fetched ${roleMap.size} roles and ${assignments.length} role assignments`); + + const { grantsByUserId, unresolvedGroupAssignments } = await resolveRoleAssignments({ + ctx, + assignments, + roleMap, + }); + ctx.log(`Resolved admin roles for ${grantsByUserId.size} users`); + + if (unresolvedGroupAssignments.length > 0) { + // Never let this pass silently: unexpanded group roles mean the review + // under-reports who holds admin access, which is worse than a visible + // finding on an otherwise all-pass inventory check. + ctx.warn( + `${unresolvedGroupAssignments.length} group-assigned role(s) could not be expanded`, ); + ctx.fail({ + title: 'Group-assigned admin roles could not be resolved', + description: + `${unresolvedGroupAssignments.length} admin role(s) are assigned to groups that Comp could not read. ` + + 'Users holding admin access through those groups are missing from this access review.', + resourceType: 'connection', + resourceId: ctx.connectionId, + severity: 'medium', + remediation: + 'Reconnect Google Workspace and approve the group directory permission (admin.directory.group.readonly) so group-assigned admin roles can be expanded.', + evidence: { unresolvedGroupAssignments }, + }); } // Fetch all users with pagination @@ -147,8 +128,9 @@ export const employeeAccessCheck: IntegrationCheck = { // Build the employee list with roles const employeeList = activeUsers.map((user) => { - // Get assigned roles from the role assignments - const assignedRoles = userRolesMap.get(user.id) || []; + const grants: ResolvedRoleGrant[] = grantsByUserId.get(user.id) ?? []; + const assignedRoles = grants.map((g) => g.roleName); + const groupGrantedRoles = grants.filter((g) => g.source === 'group'); // Derive a role description let role: string; @@ -167,6 +149,10 @@ export const employeeAccessCheck: IntegrationCheck = { name: user.name.fullName, role, roles: assignedRoles.length > 0 ? assignedRoles : user.isAdmin ? ['Super Admin'] : ['User'], + // Provenance matters for access review: a role held via group + // membership is revoked by changing the group, not the user. + roleGrants: grants, + hasGroupGrantedRoles: groupGrantedRoles.length > 0, isAdmin: user.isAdmin, isDelegatedAdmin: user.isDelegatedAdmin, orgUnit: user.orgUnitPath, diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/index.ts b/packages/integration-platform/src/manifests/google-workspace/checks/index.ts index 71d0bc885d..aaa351e992 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/index.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/index.ts @@ -1,2 +1,4 @@ export { employeeAccessCheck } from './employee-access'; export { twoFactorAuthCheck } from './two-factor-auth'; +export { adminPrivilegeChangesCheck } from './admin-privilege-changes'; +export { adminSecurityEventsCheck } from './admin-security-events'; diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts b/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts index a0cf7bb59a..65528f7145 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts @@ -3,9 +3,15 @@ import type { CheckContext, IntegrationCheck } from '../../../types'; import { filterGoogleWorkspaceUsersForChecks, parseGoogleWorkspaceCheckUserFilter, + resolveGoogleWorkspaceUserFilter, } from '../check-user-filter'; import type { GoogleWorkspaceUser, GoogleWorkspaceUsersResponse } from '../types'; -import { includeSuspendedVariable, targetOrgUnitsVariable } from '../variables'; +import { + includeSuspendedVariable, + targetDomainsVariable, + targetGroupsVariable, + targetOrgUnitsVariable, +} from '../variables'; /** * Check that all users have 2-Step Verification enabled @@ -17,12 +23,20 @@ export const twoFactorAuthCheck: IntegrationCheck = { description: 'Verify all users have 2-Step Verification (2FA) enabled in Google Workspace', service: 'mfa-compliance', taskMapping: TASK_TEMPLATES.twoFactorAuth, - variables: [targetOrgUnitsVariable, includeSuspendedVariable], + variables: [ + targetOrgUnitsVariable, + targetGroupsVariable, + targetDomainsVariable, + includeSuspendedVariable, + ], run: async (ctx: CheckContext) => { ctx.log('Starting Google Workspace 2FA check'); - const userFilterConfig = parseGoogleWorkspaceCheckUserFilter(ctx.variables); + const userFilterConfig = await resolveGoogleWorkspaceUserFilter({ + client: ctx, + config: parseGoogleWorkspaceCheckUserFilter(ctx.variables), + }); // Fetch all users with pagination const allUsers: GoogleWorkspaceUser[] = []; diff --git a/packages/integration-platform/src/manifests/google-workspace/directory-client.ts b/packages/integration-platform/src/manifests/google-workspace/directory-client.ts new file mode 100644 index 0000000000..56e6be4df9 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/directory-client.ts @@ -0,0 +1,166 @@ +import type { + GoogleWorkspaceDomainsResponse, + GoogleWorkspaceGroupMembersResponse, + GoogleWorkspaceGroup, +} from './types'; + +const DIRECTORY_BASE = 'https://admin.googleapis.com'; + +/** + * The slice of a client this module needs. + * + * `CheckContext` satisfies this structurally, and the API's employee sync can + * adapt a raw bearer-token fetch via `createBearerTokenClient` — so checks and + * sync share one implementation of group/domain resolution instead of + * maintaining parallel copies. + */ +export interface GoogleWorkspaceDirectoryClient { + fetch: (path: string, options?: { params?: Record }) => Promise; + warn?: (message: string) => void; +} + +interface GroupListResponse { + groups?: GoogleWorkspaceGroup[]; + nextPageToken?: string; +} + +/** Adapt a raw access token to the client shape (used outside check runs). */ +export function createBearerTokenClient( + accessToken: string, + warn?: (message: string) => void, +): GoogleWorkspaceDirectoryClient { + return { + warn, + fetch: async (path: string, options?: { params?: Record }): Promise => { + const url = new URL(path, DIRECTORY_BASE); + for (const [key, value] of Object.entries(options?.params ?? {})) { + url.searchParams.set(key, value); + } + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const error = new Error( + `Google Directory API ${response.status}: ${response.statusText}`, + ) as Error & { status: number }; + error.status = response.status; + throw error; + } + + return (await response.json()) as T; + }, + }; +} + +/** + * Direct USER members of a group. + * + * Nested groups are NOT expanded — a GROUP-typed member is skipped with a + * warning rather than recursed into, which avoids membership cycles and + * matches how directory-based access reviews scope "members". + */ +export async function fetchGroupMemberUserIds({ + client, + groupId, +}: { + client: GoogleWorkspaceDirectoryClient; + groupId: string; +}): Promise { + const memberIds: string[] = []; + let pageToken: string | undefined; + + do { + const params: Record = { maxResults: '200' }; + if (pageToken) params.pageToken = pageToken; + + const response = await client.fetch( + `/admin/directory/v1/groups/${encodeURIComponent(groupId)}/members`, + { params }, + ); + + for (const member of response.members ?? []) { + if (member.type === 'GROUP') { + client.warn?.(`Nested group ${member.email ?? member.id} in ${groupId} not expanded`); + continue; + } + if (member.type && member.type !== 'USER') continue; + if (member.id) memberIds.push(member.id); + } + + pageToken = response.nextPageToken; + } while (pageToken); + + return memberIds; +} + +/** + * Union of member ids across several groups. + * + * Returns `undefined` when no groups are requested, which callers read as + * "no group filter" — distinct from an empty set, which means "groups were + * requested and matched nobody". + */ +export async function fetchMemberIdsForGroups({ + client, + groupIds, +}: { + client: GoogleWorkspaceDirectoryClient; + groupIds: string[] | undefined; +}): Promise | undefined> { + if (!groupIds?.length) return undefined; + + const memberIds = new Set(); + for (const groupId of groupIds) { + try { + 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. + client.warn?.( + `Could not read members of group ${groupId}; users in it will be excluded. ` + + 'admin.directory.group.readonly may not be granted.', + ); + } + } + + return memberIds; +} + +/** All groups in the tenant, for the group picker. */ +export async function listGroups( + client: GoogleWorkspaceDirectoryClient, +): Promise { + const groups: GoogleWorkspaceGroup[] = []; + let pageToken: string | undefined; + + do { + const params: Record = { customer: 'my_customer', maxResults: '200' }; + if (pageToken) params.pageToken = pageToken; + + const response = await client.fetch('/admin/directory/v1/groups', { + params, + }); + + if (response.groups?.length) groups.push(...response.groups); + pageToken = response.nextPageToken; + } while (pageToken); + + return groups; +} + +/** Verified domains in the tenant, for the domain picker. */ +export async function listDomains( + client: GoogleWorkspaceDirectoryClient, +): Promise { + const response = await client.fetch( + '/admin/directory/v1/customer/my_customer/domains', + ); + return (response.domains ?? []).map((d) => d.domainName); +} diff --git a/packages/integration-platform/src/manifests/google-workspace/index.ts b/packages/integration-platform/src/manifests/google-workspace/index.ts index 88b4d5678a..7c73e4bdf2 100644 --- a/packages/integration-platform/src/manifests/google-workspace/index.ts +++ b/packages/integration-platform/src/manifests/google-workspace/index.ts @@ -1,6 +1,15 @@ import type { IntegrationManifest } from '../../types'; -import { employeeAccessCheck, twoFactorAuthCheck } from './checks'; import { + adminPrivilegeChangesCheck, + adminSecurityEventsCheck, + employeeAccessCheck, + twoFactorAuthCheck, +} from './checks'; +import { + adminAuditApprovedActorsVariable, + adminAuditLookbackDaysVariable, + targetDomainsVariable, + targetGroupsVariable, syncExcludedEmailsVariable, syncIncludedEmailsVariable, syncUserFilterModeVariable, @@ -24,7 +33,15 @@ export const googleWorkspaceManifest: IntegrationManifest = { scopes: [ 'https://www.googleapis.com/auth/admin.directory.user.readonly', 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', + // Expands admin roles assigned to groups; without it, group-granted + // admin access is invisible to the access review. + 'https://www.googleapis.com/auth/admin.directory.group.readonly', + // Lists verified domains for the domain filter. + 'https://www.googleapis.com/auth/admin.directory.domain.readonly', 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly', + // Admin audit log (Reports API) — powers the admin-audit service. + // Connections created before this was added must reconnect to grant it. + 'https://www.googleapis.com/auth/admin.reports.audit.readonly', ], pkce: false, clientAuthMethod: 'body', @@ -39,7 +56,7 @@ export const googleWorkspaceManifest: IntegrationManifest = { setupInstructions: `To enable Google Workspace Admin SDK: 1. Go to Google Cloud Console (console.cloud.google.com) 2. Create or select a project -3. Enable the Admin SDK API +3. Enable the Admin SDK API (Directory and Reports) 4. Create OAuth 2.0 credentials (Web application type) 5. Add the callback URL shown below to "Authorized redirect URIs" 6. Copy the Client ID and Client Secret @@ -64,10 +81,24 @@ Note: The user authorizing must be a Google Workspace admin.`, services: [ { id: 'user-sync', name: 'User Sync', description: 'Sync users from Google Workspace as organization members', enabledByDefault: true, implemented: true }, { id: 'mfa-compliance', name: 'MFA Compliance', description: 'Monitor two-factor authentication enforcement', enabledByDefault: true, implemented: true }, - { id: 'admin-audit', name: 'Admin Audit', description: 'Track admin console activity and permission changes', implemented: false }, + { id: 'admin-audit', name: 'Admin Audit', description: 'Track admin console activity and permission changes', enabledByDefault: true, implemented: true }, ], - variables: [targetOrgUnitsVariable, syncUserFilterModeVariable, syncExcludedEmailsVariable, syncIncludedEmailsVariable], + variables: [ + targetOrgUnitsVariable, + targetGroupsVariable, + targetDomainsVariable, + syncUserFilterModeVariable, + syncExcludedEmailsVariable, + syncIncludedEmailsVariable, + adminAuditLookbackDaysVariable, + adminAuditApprovedActorsVariable, + ], - checks: [twoFactorAuthCheck, employeeAccessCheck], + checks: [ + twoFactorAuthCheck, + employeeAccessCheck, + adminPrivilegeChangesCheck, + adminSecurityEventsCheck, + ], }; diff --git a/packages/integration-platform/src/manifests/google-workspace/role-assignments.ts b/packages/integration-platform/src/manifests/google-workspace/role-assignments.ts new file mode 100644 index 0000000000..014f777d90 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/role-assignments.ts @@ -0,0 +1,145 @@ +import type { CheckContext } from '../../types'; +import { fetchGroupMemberUserIds } from './directory-client'; +import type { + GoogleWorkspaceGroupMembersResponse, + GoogleWorkspaceRoleAssignment, + GoogleWorkspaceRoleAssignmentsResponse, + GoogleWorkspaceRolesResponse, +} from './types'; + +/** How a user came to hold an admin role. */ +export type RoleGrantSource = 'direct' | 'group'; + +export interface ResolvedRoleGrant { + roleName: string; + source: RoleGrantSource; + /** Group email/id the role came through, when source is 'group'. */ + viaGroup?: string; +} + +export interface RoleResolution { + /** userId -> the roles they hold, with provenance. */ + grantsByUserId: Map; + /** Group-assigned roles we could not expand (missing scope, API error). */ + unresolvedGroupAssignments: Array<{ groupId: string; roleName: string }>; +} + +/** Google omits assigneeType on user assignments; absent means 'user'. */ +export function isGroupAssignment(assignment: GoogleWorkspaceRoleAssignment): boolean { + return assignment.assigneeType === 'group'; +} + +/** Fetch roleId -> roleName, following pagination. Empty map on failure. */ +export async function fetchRoleMap(ctx: CheckContext): Promise> { + const roleMap = new Map(); + let pageToken: string | undefined; + + try { + do { + const params: Record = { customer: 'my_customer' }; + if (pageToken) params.pageToken = pageToken; + + const response = await ctx.fetch( + '/admin/directory/v1/customer/my_customer/roles', + { params }, + ); + + for (const role of response.items ?? []) roleMap.set(role.roleId, role.roleName); + pageToken = response.nextPageToken; + } while (pageToken); + } catch { + ctx.warn('Could not fetch roles; role names will fall back to role IDs'); + } + + return roleMap; +} + +/** Fetch every role assignment, following pagination. Empty list on failure. */ +export async function fetchRoleAssignments( + ctx: CheckContext, +): Promise { + const assignments: GoogleWorkspaceRoleAssignment[] = []; + let pageToken: string | undefined; + + try { + do { + const params: Record = { customer: 'my_customer' }; + if (pageToken) params.pageToken = pageToken; + + const response = await ctx.fetch( + '/admin/directory/v1/customer/my_customer/roleassignments', + { params }, + ); + + if (response.items?.length) assignments.push(...response.items); + pageToken = response.nextPageToken; + } while (pageToken); + } catch { + ctx.warn('Could not fetch role assignments; admin roles will fall back to user flags'); + } + + return assignments; +} + +/** + * Resolve role assignments to per-user grants, expanding group assignments + * to their members. + * + * Previously every assignment's `assignedTo` was treated as a user id, so a + * role assigned to a group matched no user and that admin access was invisible + * to the access review. Group expansion needs admin.directory.group.readonly; + * when it is missing, the assignment is reported in + * `unresolvedGroupAssignments` so the caller can say so explicitly rather than + * silently under-reporting who holds admin access. + */ +export async function resolveRoleAssignments({ + ctx, + assignments, + roleMap, +}: { + ctx: CheckContext; + assignments: GoogleWorkspaceRoleAssignment[]; + roleMap: Map; +}): Promise { + const grantsByUserId = new Map(); + const unresolvedGroupAssignments: RoleResolution['unresolvedGroupAssignments'] = []; + const groupMemberCache = new Map(); + + const addGrant = (userId: string, grant: ResolvedRoleGrant): void => { + const existing = grantsByUserId.get(userId) ?? []; + existing.push(grant); + grantsByUserId.set(userId, existing); + }; + + for (const assignment of assignments) { + const roleName = roleMap.get(assignment.roleId) ?? `Role ${assignment.roleId}`; + + if (!isGroupAssignment(assignment)) { + addGrant(assignment.assignedTo, { roleName, source: 'direct' }); + continue; + } + + const groupId = assignment.assignedTo; + + let memberIds = groupMemberCache.get(groupId); + if (!memberIds) { + try { + memberIds = await fetchGroupMemberUserIds({ client: ctx, groupId }); + groupMemberCache.set(groupId, memberIds); + } catch { + ctx.warn( + `Could not expand group ${groupId} for role "${roleName}"; ` + + 'admin.directory.group.readonly may not be granted', + ); + unresolvedGroupAssignments.push({ groupId, roleName }); + continue; + } + } + + for (const memberId of memberIds) { + addGrant(memberId, { roleName, source: 'group', viaGroup: groupId }); + } + } + + return { grantsByUserId, unresolvedGroupAssignments }; +} diff --git a/packages/integration-platform/src/manifests/google-workspace/types.ts b/packages/integration-platform/src/manifests/google-workspace/types.ts index f854f077c2..da4baf5b91 100644 --- a/packages/integration-platform/src/manifests/google-workspace/types.ts +++ b/packages/integration-platform/src/manifests/google-workspace/types.ts @@ -69,7 +69,18 @@ export interface GoogleWorkspaceRolesResponse { export interface GoogleWorkspaceRoleAssignment { roleAssignmentId: string; roleId: string; - assignedTo: string; // User ID + /** + * The assignee's directory ID. This is a USER id when `assigneeType` is + * 'user' (or absent), and a GROUP id when it is 'group' — the two id spaces + * are distinct, so this must be read together with `assigneeType`. + */ + assignedTo: string; + /** + * Who the role is assigned to. Google added this field after the original + * API shape, and omits it on older/user assignments, so treat `undefined` + * as 'user'. + */ + assigneeType?: 'user' | 'group'; scopeType: 'CUSTOMER' | 'ORG_UNIT'; orgUnitId?: string; } @@ -79,3 +90,72 @@ export interface GoogleWorkspaceRoleAssignmentsResponse { items: GoogleWorkspaceRoleAssignment[]; nextPageToken?: string; } + +// ── Reports API (admin audit log) ─────────────────────────────────────── +// GET /admin/reports/v1/activity/users/all/applications/admin +// Requires the admin.reports.audit.readonly scope. + +export interface GoogleWorkspaceActivityParameter { + name: string; + value?: string; + boolValue?: boolean; + intValue?: string; + multiValue?: string[]; +} + +export interface GoogleWorkspaceActivityEvent { + /** Event group, e.g. "DELEGATED_ADMIN_SETTINGS", "USER_SETTINGS" */ + type?: string; + /** Specific event, e.g. "ASSIGN_ROLE", "GRANT_ADMIN_PRIVILEGE" */ + name: string; + parameters?: GoogleWorkspaceActivityParameter[]; +} + +export interface GoogleWorkspaceActivity { + kind?: string; + id: { + time: string; + uniqueQualifier?: string; + applicationName?: string; + customerId?: string; + }; + /** Absent for system-generated activity, hence optional email. */ + actor?: { + callerType?: string; + email?: string; + profileId?: string; + }; + ipAddress?: string; + events?: GoogleWorkspaceActivityEvent[]; +} + +export interface GoogleWorkspaceActivitiesResponse { + kind?: string; + items?: GoogleWorkspaceActivity[]; + nextPageToken?: string; +} + +// ── Groups ────────────────────────────────────────────────────────────── +// Requires the admin.directory.group.readonly scope. + +export interface GoogleWorkspaceGroupMember { + 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; +} + +export interface GoogleWorkspaceGroupMembersResponse { + kind?: string; + members?: GoogleWorkspaceGroupMember[]; + nextPageToken?: string; +} + +export interface GoogleWorkspaceGroup { + id: string; + email: string; + name?: string; + description?: string; +} diff --git a/packages/integration-platform/src/manifests/google-workspace/variables.ts b/packages/integration-platform/src/manifests/google-workspace/variables.ts index 5a89434523..b605608bdf 100644 --- a/packages/integration-platform/src/manifests/google-workspace/variables.ts +++ b/packages/integration-platform/src/manifests/google-workspace/variables.ts @@ -1,4 +1,5 @@ import type { CheckVariable } from '../../types'; +import { listDomains, listGroups } from './directory-client'; import type { GoogleWorkspaceOrgUnitsResponse } from './types'; /** @@ -96,3 +97,80 @@ export const syncIncludedEmailsVariable: CheckVariable = { required: false, placeholder: 'Type a value and press Enter', }; + +/** + * How far back the admin audit checks look. Google retains admin activity + * for six months, so anything beyond that returns nothing. + */ +export const adminAuditLookbackDaysVariable: CheckVariable = { + id: 'admin_audit_lookback_days', + label: 'Audit Lookback Window', + helpText: 'How far back to review admin console activity. Google retains admin audit logs for 6 months.', + type: 'select', + required: false, + default: '30', + options: [ + { value: '7', label: 'Last 7 days' }, + { value: '30', label: 'Last 30 days' }, + { value: '90', label: 'Last 90 days' }, + { value: '180', label: 'Last 180 days' }, + ], +}; + +/** + * Admins whose privilege changes are expected (e.g. the IT automation + * account), so routine activity does not read as a finding every run. + */ +export const adminAuditApprovedActorsVariable: CheckVariable = { + id: 'admin_audit_approved_actors', + label: 'Approved Admin Actors', + helpText: + 'Emails whose privilege changes are expected and should pass rather than be flagged for review. Press Enter after each value.', + type: 'multi-select', + required: false, + placeholder: 'admin@company.com', +}; + +/** + * Restrict sync and checks to members of specific groups. + * Only direct members count; nested groups are not expanded. + */ +export const targetGroupsVariable: CheckVariable = { + id: 'target_groups', + label: 'Groups', + helpText: + 'Limit sync and security checks to members of these groups (leave empty for all). Only direct members are included — nested groups are not expanded.', + type: 'multi-select', + required: false, + fetchOptions: async (ctx) => { + try { + const groups = await listGroups(ctx); + return groups.map((g) => ({ + value: g.id, + label: g.name ? `${g.name} (${g.email})` : g.email, + })); + } catch { + return []; + } + }, +}; + +/** + * Restrict sync and checks to users whose email is in specific verified domains. + */ +export const targetDomainsVariable: CheckVariable = { + id: 'target_domains', + label: 'Domains', + helpText: + 'Limit sync and security checks to users in these verified domains (leave empty for all).', + type: 'multi-select', + required: false, + fetchOptions: async (ctx) => { + try { + const domains = await listDomains(ctx); + return domains.map((d) => ({ value: d, label: d })); + } catch { + return []; + } + }, +}; diff --git a/setup-comp.sh b/setup-comp.sh new file mode 100644 index 0000000000..ba86ecb974 --- /dev/null +++ b/setup-comp.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# +# setup-comp.sh — bootstrap a local Comp AI dev environment on macOS. +# +# Usage: +# chmod +x setup-comp.sh +# ./setup-comp.sh [path-to-repo] +# +# Defaults to /Users/chris/Code/comp. Safe to re-run; it won't clobber +# .env files that already exist. + +set -euo pipefail + +REPO="${1:-/Users/chris/Code/comp}" + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } +warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } +die() { printf ' \033[31m✗\033[0m %s\n' "$*" >&2; exit 1; } + +# --------------------------------------------------------------- +bold "1. Checking prerequisites" + +command -v node >/dev/null || die "node not found. Install with: brew install node" +command -v bun >/dev/null || die "bun not found. Install with: brew install oven-sh/bun/bun" +command -v docker >/dev/null || die "docker not found. Install Docker Desktop." +command -v git >/dev/null || die "git not found." + +NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" +[ "$NODE_MAJOR" -ge 20 ] || die "Node >=20 required, found $(node -v)" +ok "node $(node -v)" + +# Bun >= 1.1.36 +BUN_VER="$(bun --version)" +if [ "$(printf '%s\n1.1.36\n' "$BUN_VER" | sort -V | head -1)" != "1.1.36" ]; then + die "Bun >=1.1.36 required, found $BUN_VER" +fi +ok "bun $BUN_VER" + +docker info >/dev/null 2>&1 || die "Docker daemon isn't running. Start Docker Desktop and re-run." +ok "docker running" + +[ -d "$REPO" ] || die "Repo not found at $REPO" +[ -f "$REPO/package.json" ] || die "$REPO doesn't look like the comp repo (no package.json)" +cd "$REPO" +ok "repo at $REPO" + +bold "" +bold " Current checkout" +echo " branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')" +echo " commit: $(git rev-parse --short HEAD 2>/dev/null || echo '?')" +echo " latest tag: $(git describe --tags --abbrev=0 2>/dev/null || echo 'none fetched')" +warn "Issue #3331 reports apps/api failing to start from a fresh checkout of main." +warn "If you hit a HybridAuthGuard DI error, try a release tag instead:" +warn " git fetch --tags && git checkout \$(git describe --tags --abbrev=0)" + +# --------------------------------------------------------------- +bold "" +bold "2. Installing dependencies" + +bun install +ok "bun install" + +bun add -d concurrently >/dev/null 2>&1 && ok "concurrently" || warn "concurrently install skipped" + +if ! command -v turbo >/dev/null 2>&1; then + bun add -g turbo && ok "turbo (global)" +else + ok "turbo already installed" +fi + +# --------------------------------------------------------------- +bold "" +bold "3. Scaffolding .env files" + +# The repo ships .env.example files; copy each one next to itself as .env +# rather than guessing at variable names. +MADE=0 +while IFS= read -r example; do + target="${example%.example}" + rel="${example#$REPO/}" + if [ -f "$target" ]; then + warn "$(dirname "$rel")/.env already exists — leaving it alone" + else + cp "$example" "$target" + ok "created ${target#$REPO/} from $rel" + MADE=$((MADE+1)) + fi +done < <(find "$REPO" -name '.env.example' -not -path '*/node_modules/*' | sort) + +if [ "$MADE" -eq 0 ] && ! find "$REPO" -name '.env.example' -not -path '*/node_modules/*' | grep -q .; then + warn "No .env.example files found. Check the repo docs for the expected env layout." +fi + +# --------------------------------------------------------------- +bold "" +bold "4. Generating secrets" + +# Fill any empty secret-ish vars with a generated value, in place. +fill_secret() { + local file="$1" key="$2" + [ -f "$file" ] || return 0 + # only fill if the key exists and its value is empty or a placeholder + if grep -qE "^${key}=(\"\")?(''),?$|^${key}=$|^${key}=\"\"$|^${key}=<" "$file" 2>/dev/null; then + local val + val="$(openssl rand -base64 32)" + # macOS sed needs the empty -i argument + sed -i '' "s|^${key}=.*|${key}=\"${val}\"|" "$file" + ok "$key set in ${file#$REPO/}" + fi +} + +for f in "$REPO/apps/app/.env" "$REPO/apps/portal/.env" "$REPO/packages/db/.env"; do + for key in AUTH_SECRET SECRET_KEY BETTER_AUTH_SECRET REVALIDATION_SECRET; do + fill_secret "$f" "$key" + done +done + +# --------------------------------------------------------------- +bold "" +bold "5. Starting Postgres" + +if bun run --silent docker:up 2>/dev/null || bun docker:up; then + ok "postgres container up (db: comp, user/pass: postgres/postgres)" +else + warn "bun docker:up failed — check package.json for the right script name" +fi + +# --------------------------------------------------------------- +bold "" +bold "6. What's left for you" +cat <<'EOF' + + These need real credentials before the app will fully work. Open the + .env files created above and fill in: + + GOOGLE_ID / GOOGLE_SECRET + https://console.cloud.google.com → APIs & Services → Credentials + Create an OAuth 2.0 Client (Web application) with these redirect URIs: + http://localhost:3000/api/auth/callback/google + http://localhost:3002/api/auth/callback/google + and these authorized origins: + http://localhost:3000 + http://localhost:3002 + + UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN + https://console.upstash.com → create a Redis database (free tier is fine) + + RESEND_API_KEY (transactional email — optional locally) + TRIGGER_SECRET_KEY (background workflows — optional locally) + + Then run migrations and start the stack: + + bun run db:migrate # or: bunx prisma migrate deploy --schema packages/db/prisma + bun run dev + + App: http://localhost:3000 + Portal: http://localhost:3002 + + Note: NEXT_PUBLIC_* vars are baked in at build time. If you change one, + rebuild — restarting alone won't pick it up. + +EOF + +bold "Done." diff --git a/start-comp.sh b/start-comp.sh new file mode 100755 index 0000000000..f9825f35bb --- /dev/null +++ b/start-comp.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# +# start-comp.sh — start the local Comp AI demo stack. +# +# ./start-comp.sh start everything +# ./start-comp.sh stop stop everything +# +# Postgres runs as a Homebrew service and restarts at login, so normally +# you only need this for the three app servers. + +set -uo pipefail +cd "$(dirname "$0")" +export PATH="/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH" + +LOGDIR="${TMPDIR:-/tmp}/comp-logs" +mkdir -p "$LOGDIR" + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } +warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } + +stop_all() { + bold "Stopping Comp servers" + pkill -f "nest start --watch" 2>/dev/null && ok "api" || warn "api not running" + pkill -f "next dev --turbo -p 3000" 2>/dev/null && ok "app" || warn "app not running" + pkill -f "next dev --turbopack -p 3002" 2>/dev/null && ok "portal" || warn "portal not running" + echo "Postgres and MinIO left running." + echo " (brew services stop postgresql@17 / brew services stop minio)" +} + +if [ "${1:-start}" = "stop" ]; then stop_all; exit 0; fi + +bold "1. Postgres" +if ! pg_isready -h localhost -p 5432 >/dev/null 2>&1; then + brew services start postgresql@17 >/dev/null 2>&1 + printf ' waiting' + for _ in $(seq 1 30); do + pg_isready -h localhost -p 5432 >/dev/null 2>&1 && break + printf '.'; sleep 1 + done + echo +fi +pg_isready -h localhost -p 5432 >/dev/null 2>&1 && ok "postgres ready" || { warn "postgres did NOT come up"; exit 1; } + +bold "" +bold "2. MinIO (S3)" +if ! curl -s -o /dev/null --max-time 3 http://localhost:9000/minio/health/live; then + brew services start minio >/dev/null 2>&1 + printf ' waiting' + for _ in $(seq 1 30); do + curl -s -o /dev/null --max-time 2 http://localhost:9000/minio/health/live && break + printf '.'; sleep 1 + done + echo +fi +curl -s -o /dev/null --max-time 3 http://localhost:9000/minio/health/live \ + && ok "minio ready on :9000" \ + || warn "minio did NOT come up — file uploads will fail" + +bold "" +bold "3. Starting servers" + +# The API must be up first — it's the auth source for both frontends. +if lsof -nP -iTCP:3333 -sTCP:LISTEN >/dev/null 2>&1; then + ok "api already on :3333" +else + ( cd apps/api && bun run dev:no-trigger > "$LOGDIR/api.log" 2>&1 & ) + printf ' waiting for api' + for _ in $(seq 1 90); do + grep -qa "Nest application successfully started" "$LOGDIR/api.log" 2>/dev/null && break + grep -qa "ExceptionHandler" "$LOGDIR/api.log" 2>/dev/null && { echo; warn "api failed — see $LOGDIR/api.log"; break; } + printf '.'; sleep 1 + done + echo + lsof -nP -iTCP:3333 -sTCP:LISTEN >/dev/null 2>&1 && ok "api on :3333" || warn "api not listening — see $LOGDIR/api.log" +fi + +if lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then + ok "app already on :3000" +else + ( cd apps/app && bun run dev:no-trigger > "$LOGDIR/app.log" 2>&1 & ) + ok "app starting on :3000" +fi + +if lsof -nP -iTCP:3002 -sTCP:LISTEN >/dev/null 2>&1; then + ok "portal already on :3002" +else + ( cd apps/portal && bun run dev > "$LOGDIR/portal.log" 2>&1 & ) + ok "portal starting on :3002" +fi + +bold "" +cat <