-
Notifications
You must be signed in to change notification settings - Fork 337
feat(isms): add Roles, Responsibilities & Authorities document (CS-698) #3391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3673f6f
feat(isms): add Roles, Responsibilities & Authorities document (CS-698)
tofikwest fe8a3ea
Merge branch 'main' into tofik/cs-698-feature-area-roles-ismsroles
tofikwest 773a6b8
fix(isms): address CS-698 review — validation, concurrency, UI hardening
tofikwest 24abbeb
fix(isms): harden roles submit gate + strict date parsing (CS-698 rev…
tofikwest 1a4503a
Merge branch 'main' into tofik/cs-698-feature-area-roles-ismsroles
tofikwest dde58a6
Merge branch 'main' into tofik/cs-698-feature-area-roles-ismsroles
tofikwest 9d48b3d
fix(isms): close CS-698 review round 3 — submit race, active-member +…
tofikwest 65fa38e
fix(isms): audit-route validation counts active members + trims requi…
tofikwest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import type { SeedRoleDefinition } from './types'; | ||
|
|
||
| /** | ||
| * The four seeded ISMS governance roles (clause 5.3) and their pre-filled | ||
| * default text. Drafted from the reference document | ||
| * ("05a - ISMS Roles, Responsibilities & Authorities") so a customer with no ISO | ||
| * expertise gets a workable baseline; every field is editable in the app. | ||
| * | ||
| * Order is the render/seed order. `roleKey` is the idempotency key for seeding. | ||
| */ | ||
| export const SEED_ROLE_DEFINITIONS: SeedRoleDefinition[] = [ | ||
| { | ||
| roleKey: 'top_management', | ||
| name: 'Top Management', | ||
| description: | ||
| 'The executive leadership accountable for the ISMS. Top management sets the direction of the information security programme and provides the mandate and resources for it to operate.', | ||
| responsibilities: | ||
| 'Approve the ISMS scope, information security policy, and objectives; provide the resources the ISMS needs; direct and support the people contributing to the ISMS; promote continual improvement; and accept or escalate residual risk.', | ||
| authorities: | ||
| 'Approve the ISMS and accept residual risk on behalf of the organisation.', | ||
| authorityGrantedBy: 'The executive office and the Board of Directors', | ||
| requiredCompetence: | ||
| 'Executive understanding of the organisation, its risk appetite, and its legal and regulatory obligations, sufficient to direct the ISMS and be accountable for it.', | ||
| }, | ||
| { | ||
| roleKey: 'spo', | ||
| name: 'Security & Privacy Owner (SPO)', | ||
| description: | ||
| 'The person who owns and operates the ISMS day to day and is the focal point for information security and privacy across the organisation.', | ||
| responsibilities: | ||
| 'Operate the ISMS day to day — scope, Statement of Applicability, risk, vendor, policy, training, incident, and audit programmes; ensure the ISMS conforms to ISO/IEC 27001; report ISMS performance to top management; and assign control, risk, and policy owners.', | ||
| authorities: | ||
| 'Direct the security and privacy programme; assign control, risk, and policy owners; and approve exceptions within the organisation’s risk appetite.', | ||
| authorityGrantedBy: 'Top Management', | ||
| requiredCompetence: | ||
| 'Working knowledge of ISO/IEC 27001 and the organisation’s control environment, with the experience to operate an ISMS and make risk-based decisions within the agreed risk appetite.', | ||
| }, | ||
| { | ||
| roleKey: 'deputy_spo', | ||
| name: 'Deputy Security & Privacy Owner', | ||
| description: | ||
| 'The documented backup for the Security & Privacy Owner, ensuring the ISMS remains covered during the SPO’s absence.', | ||
| responsibilities: | ||
| 'Provide documented backup for the SPO; act with the SPO’s authority during the SPO’s absence; and participate in incident response and management reviews as required.', | ||
| authorities: 'Act with the SPO’s authority during the SPO’s absence.', | ||
| authorityGrantedBy: 'Top Management', | ||
| requiredCompetence: | ||
| 'Sufficient familiarity with the ISMS and the organisation’s controls to stand in for the SPO and make interim risk-based decisions.', | ||
| }, | ||
| { | ||
| roleKey: 'internal_auditor', | ||
| name: 'Internal Auditor', | ||
| description: | ||
| 'The person or party responsible for independently auditing the ISMS to confirm it conforms to ISO/IEC 27001 and is effectively implemented and maintained.', | ||
| responsibilities: | ||
| 'Plan and conduct internal audits of the ISMS at the intervals defined in the audit programme; report findings and nonconformities; and maintain independence and impartiality from the areas audited.', | ||
| authorities: | ||
| 'Access the information, systems, and personnel needed to conduct the audit, and report findings directly to top management.', | ||
| authorityGrantedBy: 'Top Management', | ||
| requiredCompetence: | ||
| 'Knowledge of ISO/IEC 27001 auditing (e.g. ISO 19011 principles) and the independence to audit the ISMS objectively; where in-house audit competence is not held, it is engaged externally.', | ||
| }, | ||
| ]; | ||
|
|
||
| /** roleKey values of the seeded roles, for validation and lookups. */ | ||
| export const SEED_ROLE_KEYS: string[] = SEED_ROLE_DEFINITIONS.map( | ||
| (role) => role.roleKey, | ||
| ); | ||
|
|
||
| /** | ||
| * The two governance rows that appear only in the generated document (never as | ||
| * editable role cards): the per-artifact operational owners and the whole | ||
| * workforce. Holders/text are fixed. | ||
| */ | ||
| export const AUTO_DOC_ROLE_ROWS = [ | ||
| { | ||
| name: 'Control / asset / risk / policy owners', | ||
| holders: 'Identified in the platform per policy, control, risk, task, and vendor.', | ||
| responsibilities: | ||
| 'Implement, operate, and evidence the specific controls, policies, risks, and evidence tasks assigned to them. Operational ownership is assigned at the artifact level in Comp AI.', | ||
| authority: 'Make operational decisions for their assigned item. Authority granted by the SPO.', | ||
| }, | ||
| { | ||
| name: 'All personnel and contractors', | ||
| holders: 'All workforce members.', | ||
| responsibilities: | ||
| 'Comply with the policy set; complete required training; protect company data on personal devices; and report incidents and near-misses.', | ||
| authority: 'Use approved tools and identity flows. Granted on engagement.', | ||
| }, | ||
| ] as const; | ||
|
|
||
| /** | ||
| * Non-removable note reproduced verbatim in the generated document (§2). Clarifies | ||
| * that Comp AI application-access levels are not ISMS governance roles. | ||
| */ | ||
| export const APPLICATION_ACCESS_NOTE = | ||
| 'Comp AI application-access levels (Owner / Admin / Auditor / Employee / Contractor) are not ISMS governance roles. The ISMS governance roles are defined below.'; | ||
|
|
||
| /** The application-access levels and what they grant (generated document §2). */ | ||
| export const APPLICATION_ACCESS_LEVELS: string[] = [ | ||
| 'Owner — the account creator.', | ||
| 'Admin — administrative access to the GRC platform.', | ||
| 'Auditor — read access provided to external auditors.', | ||
| 'Employee — access to the employee portal.', | ||
| 'Contractor — limited access for engaged contractors.', | ||
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { db } from '@db'; | ||
| import type { Prisma } from '@db'; | ||
| import type { IsmsTeamSizeBand, OperationalOwnershipRow } from './types'; | ||
| import { teamSizeBand } from './roles'; | ||
|
|
||
| /** | ||
| * Extra data the Roles document (5.3) needs at export time but that isn't on the | ||
| * document's own rows: display names for assigned members (assignments store a | ||
| * plain memberId, no FK), the live per-artifact operational owners, and the | ||
| * team-size band. Resolved once and frozen into the version snapshot so a | ||
| * historical export re-renders byte-faithfully. | ||
| */ | ||
| export interface RolesExtras { | ||
| /** memberId → display name (name, else email, else a placeholder). */ | ||
| memberNames: Record<string, string>; | ||
| operationalOwnership: OperationalOwnershipRow[]; | ||
| band: IsmsTeamSizeBand; | ||
| } | ||
|
|
||
| type Client = Prisma.TransactionClient | typeof db; | ||
|
|
||
| const OWNER_DISPLAY_CAP = 12; | ||
|
|
||
| type NamedAssignee = { | ||
| assignee: { user: { name: string | null; email: string | null } | null } | null; | ||
| }; | ||
|
|
||
| function memberDisplayName(user: { name: string | null; email: string | null } | null): string { | ||
| return user?.name?.trim() || user?.email?.trim() || 'Unknown member'; | ||
| } | ||
|
|
||
| function dedupeOwners(rows: NamedAssignee[]): string[] { | ||
| const seen = new Set<string>(); | ||
| const names: string[] = []; | ||
| for (const row of rows) { | ||
| if (!row.assignee?.user) continue; | ||
| const name = memberDisplayName(row.assignee.user); | ||
| if (seen.has(name)) continue; | ||
| seen.add(name); | ||
| names.push(name); | ||
| } | ||
| names.sort((a, b) => a.localeCompare(b)); | ||
| if (names.length <= OWNER_DISPLAY_CAP) return names; | ||
| return [ | ||
| ...names.slice(0, OWNER_DISPLAY_CAP), | ||
| `and ${names.length - OWNER_DISPLAY_CAP} more`, | ||
| ]; | ||
| } | ||
|
|
||
| /** Load the Roles document's export extras for an organization. */ | ||
| export async function loadRolesExtras({ | ||
| organizationId, | ||
| client, | ||
| }: { | ||
| organizationId: string; | ||
| client?: Client; | ||
| }): Promise<RolesExtras> { | ||
| const prisma = client ?? db; | ||
| const assigneeSelect = { | ||
| assignee: { select: { user: { select: { name: true, email: true } } } }, | ||
| } as const; | ||
|
|
||
| const [members, memberCount, policies, risks, tasks, vendors] = | ||
| await Promise.all([ | ||
| prisma.member.findMany({ | ||
| where: { organizationId }, | ||
| select: { id: true, user: { select: { name: true, email: true } } }, | ||
| }), | ||
| prisma.member.count({ where: { organizationId, deactivated: false } }), | ||
| prisma.policy.findMany({ | ||
| where: { organizationId, assigneeId: { not: null } }, | ||
| select: assigneeSelect, | ||
| }), | ||
| prisma.risk.findMany({ | ||
| where: { organizationId, assigneeId: { not: null } }, | ||
| select: assigneeSelect, | ||
| }), | ||
| prisma.task.findMany({ | ||
| where: { organizationId, assigneeId: { not: null } }, | ||
| select: assigneeSelect, | ||
| }), | ||
| prisma.vendor.findMany({ | ||
| where: { organizationId, assigneeId: { not: null } }, | ||
| select: assigneeSelect, | ||
| }), | ||
| ]); | ||
|
|
||
| const memberNames: Record<string, string> = {}; | ||
| for (const member of members) { | ||
| memberNames[member.id] = memberDisplayName(member.user); | ||
| } | ||
|
|
||
| // Controls have no owner field in the platform; their ownership is descriptive | ||
| // (assigned per control via their linked tasks). Kept in the matrix per the | ||
| // reference document, without enumerating names. | ||
| const operationalOwnership: OperationalOwnershipRow[] = [ | ||
| { | ||
| artifact: 'Policies', | ||
| assignedWhere: 'Policy assignee / approver in Comp AI', | ||
| ownerResponsibility: | ||
| 'Keep the policy current and accurate; ensure required acknowledgement.', | ||
| owners: dedupeOwners(policies), | ||
| }, | ||
| { | ||
| artifact: 'Controls', | ||
| assignedWhere: 'Control owner in Comp AI', | ||
| ownerResponsibility: 'Implement, operate, and evidence the control.', | ||
| owners: [], | ||
| }, | ||
| { | ||
| artifact: 'Risks', | ||
| assignedWhere: 'Risk owner in Comp AI', | ||
| ownerResponsibility: | ||
| 'Assess, treat, and monitor the risk within the risk appetite.', | ||
| owners: dedupeOwners(risks), | ||
| }, | ||
| { | ||
| artifact: 'Evidence tasks', | ||
| assignedWhere: 'Task assignee in Comp AI', | ||
| ownerResponsibility: 'Complete and evidence the task by its due date.', | ||
| owners: dedupeOwners(tasks), | ||
| }, | ||
| { | ||
| artifact: 'Vendors / sub-processors', | ||
| assignedWhere: 'Vendor owner in Comp AI', | ||
| ownerResponsibility: | ||
| 'Perform due diligence and ongoing security review; maintain the DPA.', | ||
| owners: dedupeOwners(vendors), | ||
| }, | ||
| ]; | ||
|
|
||
| return { | ||
| memberNames, | ||
| operationalOwnership, | ||
| band: teamSizeBand(memberCount), | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { buildExportSections } from './registry'; | ||
| import { generateIsmsExportFile } from '../utils/export-generator'; | ||
| import { buildExportMetadata } from '../utils/export-metadata'; | ||
| import type { DocumentExportInput, RoleExportRow } from './types'; | ||
|
|
||
| /** | ||
| * End-to-end render check for the Roles document (5.3): the section builder + | ||
| * both real renderers (jsPDF, docx) must produce non-empty files. Guards the | ||
| * whole export pipeline for the new type without needing a live org. | ||
| */ | ||
| function role(overrides: Partial<RoleExportRow>): RoleExportRow { | ||
| return { | ||
| roleKey: 'spo', | ||
| name: 'Security & Privacy Owner (SPO)', | ||
| description: 'Owns the ISMS.', | ||
| responsibilities: 'Operate the ISMS.', | ||
| authorities: 'Direct the programme.', | ||
| authorityGrantedBy: 'Top Management', | ||
| requiredCompetence: 'ISO 27001 knowledge.', | ||
| holders: ['Alex Petrisor'], | ||
| auditRoute: null, | ||
| auditRouteHolderName: null, | ||
| auditFirmName: null, | ||
| auditEvidenceRef: null, | ||
| auditCourse: null, | ||
| auditDueDate: null, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| const INPUT: DocumentExportInput = { | ||
| contextIssues: [], | ||
| interestedParties: [], | ||
| requirements: [], | ||
| objectives: [], | ||
| narrative: null, | ||
| roles: [ | ||
| role({ roleKey: 'top_management', name: 'Top Management', holders: ['Raoul'] }), | ||
| role({}), | ||
| role({ | ||
| roleKey: 'internal_auditor', | ||
| name: 'Internal Auditor', | ||
| auditRoute: 'external', | ||
| auditFirmName: 'Acme Audit LLP', | ||
| holders: ['External auditor'], | ||
| }), | ||
| ], | ||
| operationalOwnership: [ | ||
| { | ||
| artifact: 'Policies', | ||
| assignedWhere: 'Policy assignee in Comp AI', | ||
| ownerResponsibility: 'Keep it current.', | ||
| owners: ['Alice'], | ||
| }, | ||
| ], | ||
| band: 'standard', | ||
| }; | ||
|
|
||
| function metadata() { | ||
| return buildExportMetadata({ | ||
| type: 'roles_and_responsibilities', | ||
| title: 'Roles, Responsibilities and Authorities', | ||
| frameworkName: 'ISO 27001', | ||
| version: 1, | ||
| status: 'approved', | ||
| preparedBy: 'Comp AI', | ||
| owner: null, | ||
| approverName: 'Raoul Plickat', | ||
| approvedAt: new Date('2026-05-26T00:00:00.000Z'), | ||
| declinedAt: null, | ||
| organizationName: 'Pressmaster AI Inc.', | ||
| primaryColor: '#004D3D', | ||
| }); | ||
| } | ||
|
|
||
| describe('Roles document export', () => { | ||
| const sections = buildExportSections({ | ||
| type: 'roles_and_responsibilities', | ||
| input: INPUT, | ||
| }); | ||
|
|
||
| it('renders a non-empty PDF', async () => { | ||
| const result = await generateIsmsExportFile({ | ||
| sections, | ||
| metadata: metadata(), | ||
| format: 'pdf', | ||
| }); | ||
| expect(result.fileBuffer.length).toBeGreaterThan(0); | ||
| expect(result.mimeType).toBe('application/pdf'); | ||
| }); | ||
|
|
||
| it('renders a non-empty DOCX', async () => { | ||
| const result = await generateIsmsExportFile({ | ||
| sections, | ||
| metadata: metadata(), | ||
| format: 'docx', | ||
| }); | ||
| expect(result.fileBuffer.length).toBeGreaterThan(0); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.