From 82c7b6fda26da71a90c8b04fc52d940a7bd62a20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:42:09 +0000 Subject: [PATCH 1/3] Initial plan From 3809fbd6f82dfb21ea46415817567132e25a0b6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:50:51 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(system):=20add=20ISO=2027001:2022=20co?= =?UTF-8?q?mpliance=20schemas=20=E2=80=94=20incident=20response,=20supplie?= =?UTF-8?q?r=20security,=20training,=20audit=20scheduling,=20security=20im?= =?UTF-8?q?pact=20assessment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 25 ++ .../spec/src/system/change-management.test.ts | 116 +++++ .../spec/src/system/change-management.zod.ts | 47 ++ packages/spec/src/system/compliance.test.ts | 178 ++++++++ packages/spec/src/system/compliance.zod.ts | 202 ++++++++- .../spec/src/system/incident-response.test.ts | 416 ++++++++++++++++++ .../spec/src/system/incident-response.zod.ts | 368 ++++++++++++++++ packages/spec/src/system/index.ts | 3 + .../spec/src/system/supplier-security.test.ts | 210 +++++++++ .../spec/src/system/supplier-security.zod.ts | 244 ++++++++++ packages/spec/src/system/training.test.ts | 265 +++++++++++ packages/spec/src/system/training.zod.ts | 216 +++++++++ 12 files changed, 2289 insertions(+), 1 deletion(-) create mode 100644 packages/spec/src/system/incident-response.test.ts create mode 100644 packages/spec/src/system/incident-response.zod.ts create mode 100644 packages/spec/src/system/supplier-security.test.ts create mode 100644 packages/spec/src/system/supplier-security.zod.ts create mode 100644 packages/spec/src/system/training.test.ts create mode 100644 packages/spec/src/system/training.zod.ts diff --git a/ROADMAP.md b/ROADMAP.md index 9abeff97a..6d68557c9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -543,6 +543,31 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow - [ ] GDPR/CCPA compliance utilities (right to erasure, data export) - [ ] Change management and approval workflows for schema changes +### 6.5 ISO 27001:2022 Compliance + +> **Goal:** Full schema coverage for ISO 27001:2022 Annex A controls to support certification readiness. + +#### 6.5.1 High Priority (Certification Blockers) — ✅ Schema Complete + +- [x] **Incident Response Protocol** (A.5.24–A.5.28) — `system/incident-response.zod.ts`: Incident classification, severity grading, response phases, notification matrix, escalation policies +- [x] **Audit Scheduling & Finding Tracking** (A.5.35) — `system/compliance.zod.ts`: AuditScheduleSchema, AuditFindingSchema for independent review and remediation tracking +- [x] **Change Management Security Approval** (A.8.32) — `system/change-management.zod.ts`: SecurityImpactAssessment with risk level, data classification, security reviewer workflow + +#### 6.5.2 Medium Priority (Compliance Completeness) — ✅ Schema Complete + +- [x] **Supplier Security Assessment** (A.5.19–A.5.22) — `system/supplier-security.zod.ts`: Supplier risk levels, security requirements, assessment lifecycle, remediation tracking +- [x] **Information Security Training** (A.6.3) — `system/training.zod.ts`: Training courses, completion records, organizational training plans with recertification + +#### 6.5.3 Medium Priority (Pending) + +- [ ] **OAuth Scope Binding** (A.8.1) — API endpoint schema with required OAuth scopes +- [ ] **Permission Registry** (A.8.2) — Transform `manifest.permissions` from `string[]` to structured registry enum + +#### 6.5.4 Low Priority (Enhancements) + +- [ ] Permission delegation and temporary privilege elevation protocol (AWS STS-style) +- [ ] Device trust policy extensions + --- ## Phase 7: AI & Intelligence (🔴 Planned) diff --git a/packages/spec/src/system/change-management.test.ts b/packages/spec/src/system/change-management.test.ts index 0882e2a51..fde346c7e 100644 --- a/packages/spec/src/system/change-management.test.ts +++ b/packages/spec/src/system/change-management.test.ts @@ -653,4 +653,120 @@ describe('ChangeRequestSchema', () => { expect(() => ChangeRequestSchema.parse(rolledBackChange)).not.toThrow(); }); + + it('should accept change with security impact assessment (A.8.32)', () => { + const changeWithSecurityImpact = { + id: 'CHG-2024-SEC-001', + title: 'API Gateway Configuration Change', + description: 'Update API gateway security headers', + type: 'normal', + priority: 'high', + status: 'approved', + requestedBy: 'security_team', + requestedAt: Date.now(), + impact: { + level: 'high', + affectedSystems: ['api-gateway'], + }, + implementation: { + description: 'Update security headers', + steps: [ + { + order: 1, + description: 'Deploy configuration', + estimatedMinutes: 10, + }, + ], + }, + rollbackPlan: { + description: 'Revert configuration', + steps: [ + { + order: 1, + description: 'Restore previous config', + estimatedMinutes: 5, + }, + ], + }, + securityImpact: { + assessed: true, + riskLevel: 'high', + affectedDataClassifications: ['pii', 'confidential'], + requiresSecurityApproval: true, + reviewedBy: 'ciso', + reviewedAt: Date.now(), + reviewNotes: 'Approved with monitoring requirement', + }, + }; + + const parsed = ChangeRequestSchema.parse(changeWithSecurityImpact); + expect(parsed.securityImpact?.assessed).toBe(true); + expect(parsed.securityImpact?.riskLevel).toBe('high'); + expect(parsed.securityImpact?.requiresSecurityApproval).toBe(true); + }); + + it('should accept change with minimal security impact', () => { + const change = { + id: 'CHG-2024-SEC-002', + title: 'Minor UI Change', + description: 'Update button color', + type: 'standard', + priority: 'low', + status: 'draft', + requestedBy: 'user_123', + requestedAt: Date.now(), + impact: { + level: 'low', + affectedSystems: ['ui'], + }, + implementation: { + description: 'Update CSS', + steps: [{ order: 1, description: 'Deploy', estimatedMinutes: 5 }], + }, + rollbackPlan: { + description: 'Revert CSS', + steps: [{ order: 1, description: 'Revert', estimatedMinutes: 5 }], + }, + securityImpact: { + assessed: true, + riskLevel: 'none', + }, + }; + + const parsed = ChangeRequestSchema.parse(change); + expect(parsed.securityImpact?.riskLevel).toBe('none'); + expect(parsed.securityImpact?.requiresSecurityApproval).toBe(false); + }); + + it('should accept all security risk levels', () => { + const levels = ['none', 'low', 'medium', 'high', 'critical'] as const; + + levels.forEach((riskLevel) => { + const change = { + id: `CHG-${riskLevel}`, + title: 'Test', + description: 'Test', + type: 'standard', + priority: 'low', + status: 'draft', + requestedBy: 'user', + requestedAt: Date.now(), + impact: { level: 'low', affectedSystems: ['test'] }, + implementation: { + description: 'Test', + steps: [{ order: 1, description: 'Test', estimatedMinutes: 5 }], + }, + rollbackPlan: { + description: 'Test', + steps: [{ order: 1, description: 'Test', estimatedMinutes: 5 }], + }, + securityImpact: { + assessed: true, + riskLevel, + }, + }; + + expect(() => ChangeRequestSchema.parse(change)).not.toThrow(); + }); + }); }); diff --git a/packages/spec/src/system/change-management.zod.ts b/packages/spec/src/system/change-management.zod.ts index 16c3111d3..41dac4221 100644 --- a/packages/spec/src/system/change-management.zod.ts +++ b/packages/spec/src/system/change-management.zod.ts @@ -319,6 +319,53 @@ export const ChangeRequestSchema = z.object({ actualEnd: z.number().optional().describe('Actual end time'), }).optional().describe('Schedule'), + /** + * Security impact assessment for the change (A.8.32) + */ + securityImpact: z.object({ + /** + * Whether a security impact assessment has been performed + */ + assessed: z.boolean().describe('Whether security impact has been assessed'), + + /** + * Security risk level of this change + */ + riskLevel: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional() + .describe('Security risk level'), + + /** + * Data classifications affected by this change + */ + affectedDataClassifications: z.array(z.enum([ + 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', + ])).optional().describe('Affected data classifications'), + + /** + * Whether the change requires security team approval + */ + requiresSecurityApproval: z.boolean().default(false) + .describe('Whether security team approval is required'), + + /** + * Security reviewer user ID + */ + reviewedBy: z.string().optional() + .describe('Security reviewer user ID'), + + /** + * Security review completion timestamp (Unix milliseconds) + */ + reviewedAt: z.number().optional() + .describe('Security review timestamp'), + + /** + * Security review notes or conditions + */ + reviewNotes: z.string().optional() + .describe('Security review notes or conditions'), + }).optional().describe('Security impact assessment per ISO 27001:2022 A.8.32'), + /** * Approval workflow configuration */ diff --git a/packages/spec/src/system/compliance.test.ts b/packages/spec/src/system/compliance.test.ts index e3682b504..b027a412a 100644 --- a/packages/spec/src/system/compliance.test.ts +++ b/packages/spec/src/system/compliance.test.ts @@ -5,6 +5,10 @@ import { PCIDSSConfigSchema, AuditLogConfigSchema, ComplianceConfigSchema, + AuditFindingSeveritySchema, + AuditFindingStatusSchema, + AuditFindingSchema, + AuditScheduleSchema, } from './compliance.zod'; describe('GDPRConfigSchema', () => { @@ -226,4 +230,178 @@ describe('ComplianceConfigSchema', () => { it('should reject missing auditLog', () => { expect(() => ComplianceConfigSchema.parse({})).toThrow(); }); + + it('should accept configuration with audit schedules', () => { + const config = ComplianceConfigSchema.parse({ + auditLog: { + events: ['create', 'update'], + }, + auditSchedules: [ + { + id: 'AUDIT-2024-Q1', + title: 'Q1 ISO 27001 Internal Audit', + scope: ['access_control', 'encryption'], + framework: 'iso27001', + scheduledAt: 1711929600000, + assessor: 'internal_audit_team', + }, + ], + }); + + expect(config.auditSchedules).toHaveLength(1); + expect(config.auditSchedules![0].framework).toBe('iso27001'); + }); +}); + +describe('AuditFindingSeveritySchema', () => { + it('should accept all valid severities', () => { + const severities = ['critical', 'major', 'minor', 'observation']; + + severities.forEach((severity) => { + expect(() => AuditFindingSeveritySchema.parse(severity)).not.toThrow(); + }); + }); + + it('should reject invalid severity', () => { + expect(() => AuditFindingSeveritySchema.parse('warning')).toThrow(); + }); +}); + +describe('AuditFindingStatusSchema', () => { + it('should accept all valid statuses', () => { + const statuses = ['open', 'in_remediation', 'remediated', 'verified', 'accepted_risk', 'closed']; + + statuses.forEach((status) => { + expect(() => AuditFindingStatusSchema.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid status', () => { + expect(() => AuditFindingStatusSchema.parse('pending')).toThrow(); + }); +}); + +describe('AuditFindingSchema', () => { + it('should accept valid finding', () => { + const finding = AuditFindingSchema.parse({ + id: 'FIND-2024-001', + title: 'Insufficient access logging', + description: 'PHI access events are not being logged for HIPAA compliance', + severity: 'major', + status: 'in_remediation', + controlReference: 'A.8.15', + framework: 'iso27001', + identifiedAt: 1704067200000, + identifiedBy: 'external_auditor', + remediationPlan: 'Implement audit logging for all PHI access events', + remediationDeadline: 1706745600000, + }); + + expect(finding.severity).toBe('major'); + expect(finding.framework).toBe('iso27001'); + }); + + it('should accept minimal finding', () => { + const finding = AuditFindingSchema.parse({ + id: 'FIND-2024-002', + title: 'Missing encryption', + description: 'Field-level encryption not enabled', + severity: 'minor', + status: 'open', + identifiedAt: Date.now(), + identifiedBy: 'internal_audit', + }); + + expect(finding.controlReference).toBeUndefined(); + expect(finding.remediationPlan).toBeUndefined(); + }); + + it('should accept verified finding', () => { + const finding = AuditFindingSchema.parse({ + id: 'FIND-2024-003', + title: 'Weak password policy', + description: 'Password minimum length below 12 characters', + severity: 'observation', + status: 'verified', + identifiedAt: 1704067200000, + identifiedBy: 'auditor', + verifiedAt: 1706745600000, + verifiedBy: 'senior_auditor', + notes: 'Password policy updated and verified', + }); + + expect(finding.verifiedAt).toBe(1706745600000); + expect(finding.verifiedBy).toBe('senior_auditor'); + }); + + it('should reject missing required fields', () => { + expect(() => AuditFindingSchema.parse({})).toThrow(); + }); +}); + +describe('AuditScheduleSchema', () => { + it('should accept valid schedule with defaults', () => { + const schedule = AuditScheduleSchema.parse({ + id: 'AUDIT-2024-Q1', + title: 'Q1 ISO 27001 Internal Audit', + scope: ['access_control', 'encryption', 'incident_response'], + framework: 'iso27001', + scheduledAt: 1711929600000, + assessor: 'internal_audit_team', + }); + + expect(schedule.isExternal).toBe(false); + expect(schedule.recurrenceMonths).toBe(0); + expect(schedule.findings).toBeUndefined(); + }); + + it('should accept full schedule with findings', () => { + const schedule = AuditScheduleSchema.parse({ + id: 'AUDIT-2024-EXT', + title: 'Annual External ISO 27001 Audit', + scope: ['all_controls'], + framework: 'iso27001', + scheduledAt: 1711929600000, + completedAt: 1712534400000, + assessor: 'External Audit Firm LLC', + isExternal: true, + recurrenceMonths: 12, + findings: [ + { + id: 'FIND-001', + title: 'Missing incident response plan', + description: 'No documented incident response procedure', + severity: 'major', + status: 'open', + controlReference: 'A.5.24', + framework: 'iso27001', + identifiedAt: 1712534400000, + identifiedBy: 'External Audit Firm LLC', + }, + ], + }); + + expect(schedule.isExternal).toBe(true); + expect(schedule.recurrenceMonths).toBe(12); + expect(schedule.findings).toHaveLength(1); + }); + + it('should accept all framework values', () => { + const frameworks = ['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']; + + frameworks.forEach((framework) => { + expect(() => AuditScheduleSchema.parse({ + id: `AUDIT-${framework}`, + title: `${framework} audit`, + scope: ['general'], + framework, + scheduledAt: Date.now(), + assessor: 'auditor', + })).not.toThrow(); + }); + }); + + it('should reject missing required fields', () => { + expect(() => AuditScheduleSchema.parse({})).toThrow(); + }); }); diff --git a/packages/spec/src/system/compliance.zod.ts b/packages/spec/src/system/compliance.zod.ts index 64ae9fba1..f74b2fe8d 100644 --- a/packages/spec/src/system/compliance.zod.ts +++ b/packages/spec/src/system/compliance.zod.ts @@ -78,12 +78,212 @@ export const AuditLogConfigSchema = z.object({ export type AuditLogConfig = z.infer; export type AuditLogConfigInput = z.input; +/** + * Audit Finding Severity Schema + * + * Severity classification for audit findings. + */ +export const AuditFindingSeveritySchema = z.enum([ + 'critical', // Immediate remediation required + 'major', // Significant non-conformity + 'minor', // Minor non-conformity + 'observation', // Improvement opportunity +]); + +/** + * Audit Finding Status Schema + * + * Lifecycle status of an audit finding. + */ +export const AuditFindingStatusSchema = z.enum([ + 'open', // Finding identified, not yet addressed + 'in_remediation', // Remediation in progress + 'remediated', // Remediation completed, pending verification + 'verified', // Remediation verified and accepted + 'accepted_risk', // Risk accepted by management + 'closed', // Finding closed +]); + +/** + * Audit Finding Schema (A.5.35) + * + * Individual finding from a compliance or security audit. + * Supports tracking from discovery through remediation and verification. + * + * @example + * ```json + * { + * "id": "FIND-2024-001", + * "title": "Insufficient access logging", + * "description": "PHI access events are not being logged for HIPAA compliance", + * "severity": "major", + * "status": "in_remediation", + * "controlReference": "A.8.15", + * "framework": "iso27001", + * "identifiedAt": 1704067200000, + * "identifiedBy": "external_auditor", + * "remediationPlan": "Implement audit logging for all PHI access events", + * "remediationDeadline": 1706745600000 + * } + * ``` + */ +export const AuditFindingSchema = z.object({ + /** + * Unique finding identifier + */ + id: z.string().describe('Unique finding identifier'), + + /** + * Short descriptive title + */ + title: z.string().describe('Finding title'), + + /** + * Detailed description of the finding + */ + description: z.string().describe('Finding description'), + + /** + * Finding severity + */ + severity: AuditFindingSeveritySchema.describe('Finding severity'), + + /** + * Current status + */ + status: AuditFindingStatusSchema.describe('Finding status'), + + /** + * ISO 27001 control reference (e.g., "A.5.35", "A.8.15") + */ + controlReference: z.string().optional().describe('ISO 27001 control reference'), + + /** + * Compliance framework + */ + framework: z.enum(['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']).optional() + .describe('Related compliance framework'), + + /** + * Timestamp when finding was identified (Unix milliseconds) + */ + identifiedAt: z.number().describe('Identification timestamp'), + + /** + * User or entity who identified the finding + */ + identifiedBy: z.string().describe('Identifier (auditor name or system)'), + + /** + * Planned remediation actions + */ + remediationPlan: z.string().optional().describe('Remediation plan'), + + /** + * Remediation deadline (Unix milliseconds) + */ + remediationDeadline: z.number().optional().describe('Remediation deadline timestamp'), + + /** + * Timestamp when remediation was verified (Unix milliseconds) + */ + verifiedAt: z.number().optional().describe('Verification timestamp'), + + /** + * Verifier name or role + */ + verifiedBy: z.string().optional().describe('Verifier name or role'), + + /** + * Notes or comments + */ + notes: z.string().optional().describe('Additional notes'), +}).describe('Audit finding with remediation tracking per ISO 27001:2022 A.5.35'); + +export type AuditFinding = z.infer; + +/** + * Audit Schedule Schema (A.5.35) + * + * Defines audit scheduling for independent information security reviews. + * Supports recurring audits, scope definition, and assessor assignment. + * + * @example + * ```json + * { + * "id": "AUDIT-2024-Q1", + * "title": "Q1 ISO 27001 Internal Audit", + * "scope": ["access_control", "encryption", "incident_response"], + * "framework": "iso27001", + * "scheduledAt": 1711929600000, + * "assessor": "internal_audit_team", + * "recurrenceMonths": 3 + * } + * ``` + */ +export const AuditScheduleSchema = z.object({ + /** + * Unique audit schedule identifier + */ + id: z.string().describe('Unique audit schedule identifier'), + + /** + * Audit title or name + */ + title: z.string().describe('Audit title'), + + /** + * Scope of areas to audit + */ + scope: z.array(z.string()).describe('Audit scope areas'), + + /** + * Target compliance framework + */ + framework: z.enum(['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']) + .describe('Target compliance framework'), + + /** + * Scheduled audit date (Unix milliseconds) + */ + scheduledAt: z.number().describe('Scheduled audit timestamp'), + + /** + * Actual completion date (Unix milliseconds) + */ + completedAt: z.number().optional().describe('Completion timestamp'), + + /** + * Assessor name, team, or external firm + */ + assessor: z.string().describe('Assessor or audit team'), + + /** + * Whether this is an external (independent) audit + */ + isExternal: z.boolean().default(false).describe('Whether this is an external audit'), + + /** + * Recurrence interval in months (0 = one-time) + */ + recurrenceMonths: z.number().default(0).describe('Recurrence interval in months (0 = one-time)'), + + /** + * Findings from this audit + */ + findings: z.array(AuditFindingSchema).optional().describe('Audit findings'), +}).describe('Audit schedule for independent security reviews per ISO 27001:2022 A.5.35'); + +export type AuditSchedule = z.infer; + export const ComplianceConfigSchema = z.object({ gdpr: GDPRConfigSchema.optional().describe('GDPR compliance settings'), hipaa: HIPAAConfigSchema.optional().describe('HIPAA compliance settings'), pciDss: PCIDSSConfigSchema.optional().describe('PCI-DSS compliance settings'), auditLog: AuditLogConfigSchema.describe('Audit log configuration'), -}).describe('Unified compliance configuration spanning GDPR, HIPAA, and PCI-DSS'); + auditSchedules: z.array(AuditScheduleSchema).optional() + .describe('Scheduled compliance audits (A.5.35)'), +}).describe('Unified compliance configuration spanning GDPR, HIPAA, PCI-DSS, and audit governance'); export type ComplianceConfig = z.infer; export type ComplianceConfigInput = z.input; diff --git a/packages/spec/src/system/incident-response.test.ts b/packages/spec/src/system/incident-response.test.ts new file mode 100644 index 000000000..93367737c --- /dev/null +++ b/packages/spec/src/system/incident-response.test.ts @@ -0,0 +1,416 @@ +import { describe, it, expect } from 'vitest'; +import { + IncidentSeveritySchema, + IncidentCategorySchema, + IncidentStatusSchema, + IncidentResponsePhaseSchema, + IncidentNotificationRuleSchema, + IncidentNotificationMatrixSchema, + IncidentSchema, + IncidentResponsePolicySchema, + type Incident, + type IncidentResponsePhase, + type IncidentNotificationRule, +} from './incident-response.zod'; + +describe('IncidentSeveritySchema', () => { + it('should accept all valid severity levels', () => { + const validLevels = ['critical', 'high', 'medium', 'low']; + + validLevels.forEach((level) => { + expect(() => IncidentSeveritySchema.parse(level)).not.toThrow(); + }); + }); + + it('should reject invalid severity level', () => { + expect(() => IncidentSeveritySchema.parse('extreme')).toThrow(); + }); +}); + +describe('IncidentCategorySchema', () => { + it('should accept all valid categories', () => { + const validCategories = [ + 'data_breach', 'malware', 'unauthorized_access', 'denial_of_service', + 'social_engineering', 'insider_threat', 'physical_security', + 'configuration_error', 'vulnerability_exploit', 'policy_violation', 'other', + ]; + + validCategories.forEach((category) => { + expect(() => IncidentCategorySchema.parse(category)).not.toThrow(); + }); + }); + + it('should reject invalid category', () => { + expect(() => IncidentCategorySchema.parse('unknown_type')).toThrow(); + }); +}); + +describe('IncidentStatusSchema', () => { + it('should accept all valid statuses', () => { + const validStatuses = [ + 'reported', 'triaged', 'investigating', 'containing', + 'eradicating', 'recovering', 'resolved', 'closed', + ]; + + validStatuses.forEach((status) => { + expect(() => IncidentStatusSchema.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid status', () => { + expect(() => IncidentStatusSchema.parse('pending')).toThrow(); + }); +}); + +describe('IncidentResponsePhaseSchema', () => { + it('should accept valid response phase', () => { + const phase: IncidentResponsePhase = { + phase: 'containment', + description: 'Isolate affected systems', + assignedTo: 'security_team', + targetHours: 4, + }; + + expect(() => IncidentResponsePhaseSchema.parse(phase)).not.toThrow(); + }); + + it('should accept all phase types', () => { + const phases = ['identification', 'containment', 'eradication', 'recovery', 'lessons_learned']; + + phases.forEach((phase) => { + expect(() => IncidentResponsePhaseSchema.parse({ + phase, + description: `${phase} phase`, + assignedTo: 'team', + targetHours: 2, + })).not.toThrow(); + }); + }); + + it('should accept optional fields', () => { + const phase = IncidentResponsePhaseSchema.parse({ + phase: 'recovery', + description: 'Restore services', + assignedTo: 'ops_team', + targetHours: 8, + completedAt: 1704067200000, + notes: 'All systems restored successfully', + }); + + expect(phase.completedAt).toBe(1704067200000); + expect(phase.notes).toBe('All systems restored successfully'); + }); + + it('should reject negative target hours', () => { + expect(() => IncidentResponsePhaseSchema.parse({ + phase: 'identification', + description: 'Identify', + assignedTo: 'team', + targetHours: -1, + })).toThrow(); + }); +}); + +describe('IncidentNotificationRuleSchema', () => { + it('should accept valid notification rule', () => { + const rule: IncidentNotificationRule = { + severity: 'critical', + channels: ['email', 'pagerduty'], + recipients: ['ciso', 'security_team'], + withinMinutes: 15, + notifyRegulators: true, + regulatorDeadlineHours: 72, + }; + + expect(() => IncidentNotificationRuleSchema.parse(rule)).not.toThrow(); + }); + + it('should apply defaults', () => { + const rule = IncidentNotificationRuleSchema.parse({ + severity: 'low', + channels: ['email'], + recipients: ['security_team'], + withinMinutes: 60, + }); + + expect(rule.notifyRegulators).toBe(false); + }); + + it('should accept all channel types', () => { + const channels = ['email', 'sms', 'slack', 'pagerduty', 'webhook']; + + expect(() => IncidentNotificationRuleSchema.parse({ + severity: 'high', + channels, + recipients: ['all'], + withinMinutes: 30, + })).not.toThrow(); + }); + + it('should reject invalid channel', () => { + expect(() => IncidentNotificationRuleSchema.parse({ + severity: 'high', + channels: ['carrier_pigeon'], + recipients: ['team'], + withinMinutes: 30, + })).toThrow(); + }); +}); + +describe('IncidentNotificationMatrixSchema', () => { + it('should accept valid notification matrix with defaults', () => { + const matrix = IncidentNotificationMatrixSchema.parse({ + rules: [ + { + severity: 'critical', + channels: ['pagerduty', 'sms'], + recipients: ['ciso', 'security_team'], + withinMinutes: 15, + }, + ], + }); + + expect(matrix.escalationTimeoutMinutes).toBe(30); + expect(matrix.escalationChain).toEqual([]); + expect(matrix.rules).toHaveLength(1); + }); + + it('should accept full matrix configuration', () => { + const matrix = IncidentNotificationMatrixSchema.parse({ + rules: [ + { + severity: 'critical', + channels: ['pagerduty', 'sms', 'email'], + recipients: ['ciso', 'executive_team'], + withinMinutes: 15, + notifyRegulators: true, + regulatorDeadlineHours: 72, + }, + { + severity: 'high', + channels: ['slack', 'email'], + recipients: ['security_team'], + withinMinutes: 30, + }, + { + severity: 'low', + channels: ['email'], + recipients: ['security_team'], + withinMinutes: 120, + }, + ], + escalationTimeoutMinutes: 60, + escalationChain: ['security_lead', 'ciso', 'ceo'], + }); + + expect(matrix.rules).toHaveLength(3); + expect(matrix.escalationTimeoutMinutes).toBe(60); + expect(matrix.escalationChain).toHaveLength(3); + }); +}); + +describe('IncidentSchema', () => { + it('should accept complete incident', () => { + const incident: Incident = { + id: 'INC-2024-001', + title: 'Unauthorized API Access Detected', + description: 'Multiple failed authentication attempts from unknown IP range', + severity: 'high', + category: 'unauthorized_access', + status: 'investigating', + reportedBy: 'monitoring_system', + reportedAt: 1704067200000, + detectedAt: 1704067100000, + affectedSystems: ['api-gateway', 'auth-service'], + affectedDataClassifications: ['pii', 'confidential'], + responsePhases: [ + { + phase: 'identification', + description: 'Identify scope of unauthorized access', + assignedTo: 'security_team', + targetHours: 2, + }, + { + phase: 'containment', + description: 'Block suspicious IP range', + assignedTo: 'network_team', + targetHours: 1, + }, + ], + rootCause: 'Compromised API key', + correctiveActions: ['Rotate all API keys', 'Implement IP allowlisting'], + lessonsLearned: 'Need to implement API key rotation policy', + relatedChangeRequestIds: ['CHG-2024-001'], + metadata: { sourceIp: '10.0.0.1' }, + }; + + expect(() => IncidentSchema.parse(incident)).not.toThrow(); + }); + + it('should accept minimal incident', () => { + const minimal = { + id: 'INC-2024-002', + title: 'Policy Violation', + description: 'Employee accessed restricted data', + severity: 'low', + category: 'policy_violation', + status: 'reported', + reportedBy: 'user_123', + reportedAt: Date.now(), + affectedSystems: ['hr-system'], + }; + + expect(() => IncidentSchema.parse(minimal)).not.toThrow(); + }); + + it('should accept resolved incident with full lifecycle', () => { + const resolved = { + id: 'INC-2024-003', + title: 'Malware Detection', + description: 'Ransomware detected on workstation', + severity: 'critical', + category: 'malware', + status: 'closed', + reportedBy: 'endpoint_detection', + reportedAt: 1704067200000, + detectedAt: 1704067100000, + resolvedAt: 1704153600000, + affectedSystems: ['workstation-42'], + responsePhases: [ + { + phase: 'identification', + description: 'Identify malware type', + assignedTo: 'security_team', + targetHours: 1, + completedAt: 1704070800000, + notes: 'Identified as known ransomware variant', + }, + { + phase: 'containment', + description: 'Isolate affected workstation', + assignedTo: 'it_support', + targetHours: 0.5, + completedAt: 1704072600000, + }, + { + phase: 'eradication', + description: 'Remove malware and reimage', + assignedTo: 'it_support', + targetHours: 4, + completedAt: 1704086400000, + }, + { + phase: 'recovery', + description: 'Restore from backup', + assignedTo: 'it_support', + targetHours: 8, + completedAt: 1704115200000, + }, + { + phase: 'lessons_learned', + description: 'Post-incident review', + assignedTo: 'security_team', + targetHours: 24, + completedAt: 1704153600000, + }, + ], + rootCause: 'Phishing email with malicious attachment', + correctiveActions: [ + 'Block malicious email domain', + 'Update email filtering rules', + 'Deploy additional endpoint protection', + ], + lessonsLearned: 'Need enhanced phishing detection and user training', + }; + + expect(() => IncidentSchema.parse(resolved)).not.toThrow(); + }); + + it('should accept all data classification values', () => { + const classifications = ['pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public']; + + const incident = { + id: 'INC-2024-004', + title: 'Data Breach', + description: 'Comprehensive data breach', + severity: 'critical', + category: 'data_breach', + status: 'reported', + reportedBy: 'system', + reportedAt: Date.now(), + affectedSystems: ['database'], + affectedDataClassifications: classifications, + }; + + expect(() => IncidentSchema.parse(incident)).not.toThrow(); + }); + + it('should reject missing required fields', () => { + expect(() => IncidentSchema.parse({})).toThrow(); + expect(() => IncidentSchema.parse({ id: 'INC-001' })).toThrow(); + }); +}); + +describe('IncidentResponsePolicySchema', () => { + it('should accept valid policy with defaults', () => { + const policy = IncidentResponsePolicySchema.parse({ + notificationMatrix: { + rules: [ + { + severity: 'critical', + channels: ['pagerduty'], + recipients: ['security_team'], + withinMinutes: 15, + }, + ], + }, + defaultResponseTeam: 'security_team', + }); + + expect(policy.enabled).toBe(true); + expect(policy.triageDeadlineHours).toBe(1); + expect(policy.requirePostIncidentReview).toBe(true); + expect(policy.regulatoryNotificationThreshold).toBe('high'); + expect(policy.retentionDays).toBe(2555); + }); + + it('should accept full policy configuration', () => { + const policy = IncidentResponsePolicySchema.parse({ + enabled: true, + notificationMatrix: { + rules: [ + { + severity: 'critical', + channels: ['pagerduty', 'sms', 'email'], + recipients: ['ciso', 'executive_team'], + withinMinutes: 15, + notifyRegulators: true, + regulatorDeadlineHours: 72, + }, + { + severity: 'high', + channels: ['slack', 'email'], + recipients: ['security_team'], + withinMinutes: 30, + }, + ], + escalationTimeoutMinutes: 45, + escalationChain: ['security_lead', 'ciso'], + }, + defaultResponseTeam: 'incident_response_team', + triageDeadlineHours: 2, + requirePostIncidentReview: true, + regulatoryNotificationThreshold: 'critical', + retentionDays: 3650, + }); + + expect(policy.triageDeadlineHours).toBe(2); + expect(policy.regulatoryNotificationThreshold).toBe('critical'); + expect(policy.retentionDays).toBe(3650); + }); + + it('should reject missing required fields', () => { + expect(() => IncidentResponsePolicySchema.parse({})).toThrow(); + expect(() => IncidentResponsePolicySchema.parse({ enabled: true })).toThrow(); + }); +}); diff --git a/packages/spec/src/system/incident-response.zod.ts b/packages/spec/src/system/incident-response.zod.ts new file mode 100644 index 000000000..fcd4730de --- /dev/null +++ b/packages/spec/src/system/incident-response.zod.ts @@ -0,0 +1,368 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { z } from 'zod'; + +/** + * Incident Response Protocol — ISO 27001:2022 (A.5.24–A.5.28) + * + * Defines schemas for information security event management including + * incident classification, severity grading, response procedures, + * and notification matrices. + * + * @see https://www.iso.org/standard/27001 + * @category Security + */ + +/** + * Incident Severity Schema + * + * Severity grading for security incidents following ISO 27001 guidelines. + * Determines response urgency and escalation requirements. + */ +export const IncidentSeveritySchema = z.enum([ + 'critical', // Immediate threat to business operations or data integrity + 'high', // Significant impact requiring urgent response + 'medium', // Moderate impact with controlled response timeline + 'low', // Minor impact with standard response procedures +]); + +/** + * Incident Category Schema + * + * Classification of security incidents by type (A.5.25). + * Used for routing, reporting, and trend analysis. + */ +export const IncidentCategorySchema = z.enum([ + 'data_breach', // Unauthorized access or disclosure of data + 'malware', // Malicious software detection + 'unauthorized_access', // Unauthorized system or data access + 'denial_of_service', // Service availability attack + 'social_engineering', // Phishing, pretexting, or manipulation + 'insider_threat', // Threat originating from internal actors + 'physical_security', // Physical security breach + 'configuration_error', // Security misconfiguration + 'vulnerability_exploit', // Exploitation of known vulnerability + 'policy_violation', // Violation of security policies + 'other', // Other security incidents +]); + +/** + * Incident Status Schema + * + * Current status of a security incident in its lifecycle. + */ +export const IncidentStatusSchema = z.enum([ + 'reported', // Initial report received + 'triaged', // Severity and category assessed + 'investigating', // Active investigation in progress + 'containing', // Containment measures being applied + 'eradicating', // Root cause being removed + 'recovering', // Systems being restored to normal + 'resolved', // Incident resolved + 'closed', // Post-incident review complete +]); + +/** + * Incident Response Phase Schema + * + * Defines structured response phases per NIST SP 800-61 / ISO 27001 (A.5.26). + */ +export const IncidentResponsePhaseSchema = z.object({ + /** + * Phase name identifier + */ + phase: z.enum([ + 'identification', + 'containment', + 'eradication', + 'recovery', + 'lessons_learned', + ]).describe('Response phase name'), + + /** + * Phase description and objectives + */ + description: z.string().describe('Phase description and objectives'), + + /** + * Responsible team or role for this phase + */ + assignedTo: z.string().describe('Responsible team or role'), + + /** + * Target completion time in hours from incident start + */ + targetHours: z.number().min(0).describe('Target completion time in hours'), + + /** + * Actual completion timestamp (Unix milliseconds) + */ + completedAt: z.number().optional().describe('Actual completion timestamp'), + + /** + * Notes and findings during this phase + */ + notes: z.string().optional().describe('Phase notes and findings'), +}).describe('Incident response phase with timing and assignment'); + +export type IncidentResponsePhase = z.infer; + +/** + * Notification Rule Schema + * + * Defines who must be notified and when, based on severity (A.5.27). + */ +export const IncidentNotificationRuleSchema = z.object({ + /** + * Minimum severity level that triggers this notification + */ + severity: IncidentSeveritySchema.describe('Minimum severity to trigger notification'), + + /** + * Notification channels to use + */ + channels: z.array(z.enum([ + 'email', + 'sms', + 'slack', + 'pagerduty', + 'webhook', + ])).describe('Notification channels'), + + /** + * Roles or teams to notify + */ + recipients: z.array(z.string()).describe('Roles or teams to notify'), + + /** + * Maximum time in minutes to send notification after incident detection + */ + withinMinutes: z.number().min(1).describe('Notification deadline in minutes from detection'), + + /** + * Whether to notify external regulators (for data breaches) + */ + notifyRegulators: z.boolean().default(false) + .describe('Whether to notify regulatory authorities'), + + /** + * Regulatory notification deadline in hours (e.g., GDPR 72h) + */ + regulatorDeadlineHours: z.number().optional() + .describe('Regulatory notification deadline in hours'), +}).describe('Incident notification rule per severity level'); + +export type IncidentNotificationRule = z.infer; + +/** + * Notification Matrix Schema + * + * Complete notification matrix mapping severity levels to stakeholder groups (A.5.27). + */ +export const IncidentNotificationMatrixSchema = z.object({ + /** + * Notification rules ordered by severity + */ + rules: z.array(IncidentNotificationRuleSchema) + .describe('Notification rules by severity level'), + + /** + * Default escalation timeout in minutes before auto-escalation + */ + escalationTimeoutMinutes: z.number().default(30) + .describe('Auto-escalation timeout in minutes'), + + /** + * Escalation chain: ordered list of roles to escalate to + */ + escalationChain: z.array(z.string()).default([]) + .describe('Ordered escalation chain of roles'), +}).describe('Incident notification matrix with escalation policies'); + +export type IncidentNotificationMatrix = z.infer; + +/** + * Incident Schema + * + * Comprehensive security incident record following ISO 27001:2022 (A.5.24–A.5.28). + * Tracks the full incident lifecycle from detection through post-incident review. + * + * @example + * ```json + * { + * "id": "INC-2024-001", + * "title": "Unauthorized API Access Detected", + * "description": "Multiple failed authentication attempts from unknown IP range", + * "severity": "high", + * "category": "unauthorized_access", + * "status": "investigating", + * "reportedBy": "monitoring_system", + * "reportedAt": 1704067200000, + * "affectedSystems": ["api-gateway", "auth-service"], + * "affectedDataClassifications": ["pii", "confidential"], + * "responsePhases": [ + * { + * "phase": "identification", + * "description": "Identify scope of unauthorized access", + * "assignedTo": "security_team", + * "targetHours": 2 + * } + * ] + * } + * ``` + */ +export const IncidentSchema = z.object({ + /** + * Unique incident identifier + */ + id: z.string().describe('Unique incident identifier'), + + /** + * Short descriptive title of the incident + */ + title: z.string().describe('Incident title'), + + /** + * Detailed description of the security event + */ + description: z.string().describe('Detailed incident description'), + + /** + * Severity classification + */ + severity: IncidentSeveritySchema.describe('Incident severity level'), + + /** + * Incident category / type + */ + category: IncidentCategorySchema.describe('Incident category'), + + /** + * Current status in the incident lifecycle + */ + status: IncidentStatusSchema.describe('Current incident status'), + + /** + * User or system that reported the incident + */ + reportedBy: z.string().describe('Reporter user ID or system name'), + + /** + * Timestamp when the incident was reported (Unix milliseconds) + */ + reportedAt: z.number().describe('Report timestamp'), + + /** + * Timestamp when the incident was detected (may differ from reported) + */ + detectedAt: z.number().optional().describe('Detection timestamp'), + + /** + * Timestamp when the incident was resolved + */ + resolvedAt: z.number().optional().describe('Resolution timestamp'), + + /** + * Systems affected by the incident + */ + affectedSystems: z.array(z.string()).describe('Affected systems'), + + /** + * Data classifications affected (for data breach assessment) + */ + affectedDataClassifications: z.array(z.enum([ + 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', + ])).optional().describe('Affected data classifications'), + + /** + * Structured response phases tracking + */ + responsePhases: z.array(IncidentResponsePhaseSchema).optional() + .describe('Incident response phases'), + + /** + * Root cause analysis (completed post-incident) + */ + rootCause: z.string().optional().describe('Root cause analysis'), + + /** + * Corrective actions taken or planned + */ + correctiveActions: z.array(z.string()).optional() + .describe('Corrective actions taken or planned'), + + /** + * Lessons learned from the incident (A.5.28) + */ + lessonsLearned: z.string().optional() + .describe('Lessons learned from the incident'), + + /** + * Related change request IDs (if changes resulted from incident) + */ + relatedChangeRequestIds: z.array(z.string()).optional() + .describe('Related change request IDs'), + + /** + * Custom metadata for extensibility + */ + metadata: z.record(z.string(), z.unknown()).optional() + .describe('Custom metadata key-value pairs'), +}).describe('Security incident record per ISO 27001:2022 A.5.24–A.5.28'); + +/** + * Incident Response Policy Schema + * + * Organization-level incident response policy configuration (A.5.24). + */ +export const IncidentResponsePolicySchema = z.object({ + /** + * Whether incident response is enabled + */ + enabled: z.boolean().default(true) + .describe('Enable incident response management'), + + /** + * Notification matrix configuration + */ + notificationMatrix: IncidentNotificationMatrixSchema + .describe('Notification and escalation matrix'), + + /** + * Default response team or role + */ + defaultResponseTeam: z.string() + .describe('Default incident response team or role'), + + /** + * Maximum time in hours to begin initial triage + */ + triageDeadlineHours: z.number().default(1) + .describe('Maximum hours to begin triage after detection'), + + /** + * Whether to require post-incident review for all incidents + */ + requirePostIncidentReview: z.boolean().default(true) + .describe('Require post-incident review for all incidents'), + + /** + * Minimum severity level that requires regulatory notification + */ + regulatoryNotificationThreshold: IncidentSeveritySchema.default('high') + .describe('Minimum severity requiring regulatory notification'), + + /** + * Retention period for incident records in days + */ + retentionDays: z.number().default(2555) + .describe('Incident record retention period in days (default ~7 years)'), +}).describe('Organization-level incident response policy per ISO 27001:2022'); + +// Type exports +export type IncidentSeverity = z.infer; +export type IncidentCategory = z.infer; +export type IncidentStatus = z.infer; +export type Incident = z.infer; +export type IncidentResponsePolicy = z.infer; +export type IncidentNotificationMatrix = z.infer; diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 3d32f32dc..ed83cc552 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -32,6 +32,9 @@ export * from './compliance.zod'; export * from './encryption.zod'; export * from './masking.zod'; export * from './security-context.zod'; +export * from './incident-response.zod'; +export * from './supplier-security.zod'; +export * from './training.zod'; // Runtime Services export * from './job.zod'; diff --git a/packages/spec/src/system/supplier-security.test.ts b/packages/spec/src/system/supplier-security.test.ts new file mode 100644 index 000000000..255ddae07 --- /dev/null +++ b/packages/spec/src/system/supplier-security.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest'; +import { + SupplierRiskLevelSchema, + SupplierAssessmentStatusSchema, + SupplierSecurityRequirementSchema, + SupplierSecurityAssessmentSchema, + SupplierSecurityPolicySchema, + type SupplierSecurityAssessment, + type SupplierSecurityRequirement, +} from './supplier-security.zod'; + +describe('SupplierRiskLevelSchema', () => { + it('should accept all valid risk levels', () => { + const levels = ['critical', 'high', 'medium', 'low']; + + levels.forEach((level) => { + expect(() => SupplierRiskLevelSchema.parse(level)).not.toThrow(); + }); + }); + + it('should reject invalid risk level', () => { + expect(() => SupplierRiskLevelSchema.parse('extreme')).toThrow(); + }); +}); + +describe('SupplierAssessmentStatusSchema', () => { + it('should accept all valid statuses', () => { + const statuses = ['pending', 'in_progress', 'completed', 'expired', 'failed']; + + statuses.forEach((status) => { + expect(() => SupplierAssessmentStatusSchema.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid status', () => { + expect(() => SupplierAssessmentStatusSchema.parse('unknown')).toThrow(); + }); +}); + +describe('SupplierSecurityRequirementSchema', () => { + it('should accept valid requirement with defaults', () => { + const req = SupplierSecurityRequirementSchema.parse({ + id: 'REQ-001', + description: 'Data encryption at rest using AES-256', + }); + + expect(req.mandatory).toBe(true); + expect(req.compliant).toBeUndefined(); + }); + + it('should accept full requirement', () => { + const req: SupplierSecurityRequirement = { + id: 'REQ-002', + description: 'Access control with MFA', + controlReference: 'A.8.5', + mandatory: true, + compliant: true, + evidence: 'SOC 2 Type II report confirms MFA for all users', + }; + + expect(() => SupplierSecurityRequirementSchema.parse(req)).not.toThrow(); + }); + + it('should reject missing required fields', () => { + expect(() => SupplierSecurityRequirementSchema.parse({})).toThrow(); + expect(() => SupplierSecurityRequirementSchema.parse({ id: 'REQ-001' })).toThrow(); + }); +}); + +describe('SupplierSecurityAssessmentSchema', () => { + it('should accept complete assessment', () => { + const assessment: SupplierSecurityAssessment = { + supplierId: 'SUP-001', + supplierName: 'Cloud Provider Inc.', + riskLevel: 'critical', + status: 'completed', + assessedBy: 'security_team', + assessedAt: 1704067200000, + validUntil: 1735689600000, + requirements: [ + { + id: 'REQ-001', + description: 'Data encryption at rest using AES-256', + controlReference: 'A.8.24', + mandatory: true, + compliant: true, + evidence: 'Verified via SOC 2 report', + }, + { + id: 'REQ-002', + description: 'Annual penetration testing', + controlReference: 'A.8.8', + mandatory: true, + compliant: true, + evidence: 'Pen test report provided', + }, + ], + overallCompliant: true, + dataClassificationsShared: ['pii', 'confidential'], + servicesProvided: ['cloud-hosting', 'database-management'], + certifications: ['ISO 27001', 'SOC 2 Type II'], + }; + + expect(() => SupplierSecurityAssessmentSchema.parse(assessment)).not.toThrow(); + }); + + it('should accept minimal assessment', () => { + const minimal = { + supplierId: 'SUP-002', + supplierName: 'SaaS Vendor', + riskLevel: 'low', + status: 'completed', + assessedBy: 'it_team', + assessedAt: Date.now(), + validUntil: Date.now() + 365 * 24 * 60 * 60 * 1000, + requirements: [], + overallCompliant: true, + }; + + expect(() => SupplierSecurityAssessmentSchema.parse(minimal)).not.toThrow(); + }); + + it('should accept assessment with remediation items', () => { + const assessment = { + supplierId: 'SUP-003', + supplierName: 'Data Processor Co.', + riskLevel: 'high', + status: 'completed', + assessedBy: 'security_team', + assessedAt: Date.now(), + validUntil: Date.now() + 365 * 24 * 60 * 60 * 1000, + requirements: [ + { + id: 'REQ-001', + description: 'Encryption at rest', + mandatory: true, + compliant: false, + evidence: 'Not currently implemented', + }, + ], + overallCompliant: false, + remediationItems: [ + { + requirementId: 'REQ-001', + action: 'Implement AES-256 encryption at rest', + deadline: Date.now() + 90 * 24 * 60 * 60 * 1000, + status: 'pending', + }, + ], + }; + + expect(() => SupplierSecurityAssessmentSchema.parse(assessment)).not.toThrow(); + }); + + it('should accept failed assessment', () => { + const failed = { + supplierId: 'SUP-004', + supplierName: 'Failing Vendor', + riskLevel: 'critical', + status: 'failed', + assessedBy: 'external_auditor', + assessedAt: Date.now(), + validUntil: Date.now(), + requirements: [ + { + id: 'REQ-001', + description: 'Basic access control', + mandatory: true, + compliant: false, + }, + ], + overallCompliant: false, + }; + + expect(() => SupplierSecurityAssessmentSchema.parse(failed)).not.toThrow(); + }); + + it('should reject missing required fields', () => { + expect(() => SupplierSecurityAssessmentSchema.parse({})).toThrow(); + expect(() => SupplierSecurityAssessmentSchema.parse({ supplierId: 'SUP-001' })).toThrow(); + }); +}); + +describe('SupplierSecurityPolicySchema', () => { + it('should accept policy with defaults', () => { + const policy = SupplierSecurityPolicySchema.parse({}); + + expect(policy.enabled).toBe(true); + expect(policy.reassessmentIntervalDays).toBe(365); + expect(policy.requirePreOnboardingAssessment).toBe(true); + expect(policy.formalAssessmentThreshold).toBe('medium'); + expect(policy.monitorChanges).toBe(true); + expect(policy.requiredCertifications).toEqual([]); + }); + + it('should accept full policy configuration', () => { + const policy = SupplierSecurityPolicySchema.parse({ + enabled: true, + reassessmentIntervalDays: 180, + requirePreOnboardingAssessment: true, + formalAssessmentThreshold: 'low', + monitorChanges: true, + requiredCertifications: ['ISO 27001', 'SOC 2 Type II'], + }); + + expect(policy.reassessmentIntervalDays).toBe(180); + expect(policy.formalAssessmentThreshold).toBe('low'); + expect(policy.requiredCertifications).toHaveLength(2); + }); +}); diff --git a/packages/spec/src/system/supplier-security.zod.ts b/packages/spec/src/system/supplier-security.zod.ts new file mode 100644 index 000000000..762456a59 --- /dev/null +++ b/packages/spec/src/system/supplier-security.zod.ts @@ -0,0 +1,244 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { z } from 'zod'; + +/** + * Supplier Security Protocol — ISO 27001:2022 (A.5.19–A.5.22) + * + * Defines schemas for supplier information security management including + * risk assessment, security requirements, monitoring, and change control. + * + * @see https://www.iso.org/standard/27001 + * @category Security + */ + +/** + * Supplier Risk Level Schema + * + * Risk classification for supplier relationships based on data access + * and service criticality. + */ +export const SupplierRiskLevelSchema = z.enum([ + 'critical', // Direct access to sensitive data or core infrastructure + 'high', // Significant data processing or service dependency + 'medium', // Limited data access with moderate dependency + 'low', // Minimal data access and low service dependency +]); + +/** + * Supplier Assessment Status Schema + * + * Current status of a supplier security assessment. + */ +export const SupplierAssessmentStatusSchema = z.enum([ + 'pending', // Assessment not yet started + 'in_progress', // Assessment currently underway + 'completed', // Assessment completed + 'expired', // Assessment past its validity period + 'failed', // Supplier did not meet security requirements +]); + +/** + * Supplier Security Requirement Schema + * + * Individual security requirement to assess against a supplier (A.5.20). + */ +export const SupplierSecurityRequirementSchema = z.object({ + /** + * Requirement identifier + */ + id: z.string().describe('Requirement identifier'), + + /** + * Requirement description + */ + description: z.string().describe('Requirement description'), + + /** + * ISO 27001 control reference (e.g., "A.5.19") + */ + controlReference: z.string().optional() + .describe('ISO 27001 control reference'), + + /** + * Whether this requirement is mandatory + */ + mandatory: z.boolean().default(true) + .describe('Whether this requirement is mandatory'), + + /** + * Compliance status + */ + compliant: z.boolean().optional() + .describe('Whether the supplier meets this requirement'), + + /** + * Evidence or notes for compliance assessment + */ + evidence: z.string().optional() + .describe('Compliance evidence or assessment notes'), +}).describe('Individual supplier security requirement'); + +export type SupplierSecurityRequirement = z.infer; + +/** + * Supplier Security Assessment Schema + * + * Comprehensive supplier security assessment record (A.5.19–A.5.21). + * + * @example + * ```json + * { + * "supplierId": "SUP-001", + * "supplierName": "Cloud Provider Inc.", + * "riskLevel": "critical", + * "status": "completed", + * "assessedBy": "security_team", + * "assessedAt": 1704067200000, + * "validUntil": 1735689600000, + * "requirements": [ + * { + * "id": "REQ-001", + * "description": "Data encryption at rest using AES-256", + * "controlReference": "A.8.24", + * "mandatory": true, + * "compliant": true + * } + * ], + * "overallCompliant": true, + * "dataClassificationsShared": ["pii", "confidential"] + * } + * ``` + */ +export const SupplierSecurityAssessmentSchema = z.object({ + /** + * Unique supplier identifier + */ + supplierId: z.string().describe('Unique supplier identifier'), + + /** + * Supplier name + */ + supplierName: z.string().describe('Supplier display name'), + + /** + * Risk classification + */ + riskLevel: SupplierRiskLevelSchema.describe('Supplier risk classification'), + + /** + * Assessment status + */ + status: SupplierAssessmentStatusSchema.describe('Assessment status'), + + /** + * User or team who performed the assessment + */ + assessedBy: z.string().describe('Assessor user ID or team'), + + /** + * Assessment completion timestamp (Unix milliseconds) + */ + assessedAt: z.number().describe('Assessment timestamp'), + + /** + * Assessment validity expiry (Unix milliseconds) + */ + validUntil: z.number().describe('Assessment validity expiry timestamp'), + + /** + * Security requirements assessed + */ + requirements: z.array(SupplierSecurityRequirementSchema) + .describe('Security requirements and their compliance status'), + + /** + * Overall compliance result + */ + overallCompliant: z.boolean().describe('Whether supplier meets all mandatory requirements'), + + /** + * Data classifications shared with this supplier + */ + dataClassificationsShared: z.array(z.enum([ + 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', + ])).optional().describe('Data classifications shared with supplier'), + + /** + * Services provided by the supplier + */ + servicesProvided: z.array(z.string()).optional() + .describe('Services provided by this supplier'), + + /** + * Certifications held by the supplier + */ + certifications: z.array(z.string()).optional() + .describe('Supplier certifications (e.g., ISO 27001, SOC 2)'), + + /** + * Remediation items for non-compliant requirements + */ + remediationItems: z.array(z.object({ + requirementId: z.string().describe('Non-compliant requirement ID'), + action: z.string().describe('Required remediation action'), + deadline: z.number().describe('Remediation deadline timestamp'), + status: z.enum(['pending', 'in_progress', 'completed']).default('pending') + .describe('Remediation status'), + })).optional().describe('Remediation items for non-compliant requirements'), + + /** + * Custom metadata + */ + metadata: z.record(z.string(), z.unknown()).optional() + .describe('Custom metadata key-value pairs'), +}).describe('Supplier security assessment record per ISO 27001:2022 A.5.19–A.5.21'); + +/** + * Supplier Security Policy Schema + * + * Organization-level supplier security management policy (A.5.22). + */ +export const SupplierSecurityPolicySchema = z.object({ + /** + * Whether supplier security management is enabled + */ + enabled: z.boolean().default(true) + .describe('Enable supplier security management'), + + /** + * Reassessment interval in days + */ + reassessmentIntervalDays: z.number().default(365) + .describe('Supplier reassessment interval in days'), + + /** + * Whether to require supplier security assessment before onboarding + */ + requirePreOnboardingAssessment: z.boolean().default(true) + .describe('Require security assessment before supplier onboarding'), + + /** + * Minimum risk level that requires formal assessment + */ + formalAssessmentThreshold: SupplierRiskLevelSchema.default('medium') + .describe('Minimum risk level requiring formal assessment'), + + /** + * Whether to monitor supplier security changes (A.5.22) + */ + monitorChanges: z.boolean().default(true) + .describe('Monitor supplier security posture changes'), + + /** + * Required certifications for critical suppliers + */ + requiredCertifications: z.array(z.string()).default([]) + .describe('Required certifications for critical-risk suppliers'), +}).describe('Organization-level supplier security management policy per ISO 27001:2022'); + +// Type exports +export type SupplierRiskLevel = z.infer; +export type SupplierAssessmentStatus = z.infer; +export type SupplierSecurityAssessment = z.infer; +export type SupplierSecurityPolicy = z.infer; diff --git a/packages/spec/src/system/training.test.ts b/packages/spec/src/system/training.test.ts new file mode 100644 index 000000000..e55e0f122 --- /dev/null +++ b/packages/spec/src/system/training.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect } from 'vitest'; +import { + TrainingCategorySchema, + TrainingCompletionStatusSchema, + TrainingCourseSchema, + TrainingRecordSchema, + TrainingPlanSchema, + type TrainingCourse, + type TrainingRecord, +} from './training.zod'; + +describe('TrainingCategorySchema', () => { + it('should accept all valid categories', () => { + const validCategories = [ + 'security_awareness', 'data_protection', 'incident_response', + 'access_control', 'phishing_awareness', 'compliance', + 'secure_development', 'physical_security', 'business_continuity', 'other', + ]; + + validCategories.forEach((category) => { + expect(() => TrainingCategorySchema.parse(category)).not.toThrow(); + }); + }); + + it('should reject invalid category', () => { + expect(() => TrainingCategorySchema.parse('yoga')).toThrow(); + }); +}); + +describe('TrainingCompletionStatusSchema', () => { + it('should accept all valid statuses', () => { + const statuses = ['not_started', 'in_progress', 'completed', 'failed', 'expired']; + + statuses.forEach((status) => { + expect(() => TrainingCompletionStatusSchema.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid status', () => { + expect(() => TrainingCompletionStatusSchema.parse('skipped')).toThrow(); + }); +}); + +describe('TrainingCourseSchema', () => { + it('should accept valid course with defaults', () => { + const course = TrainingCourseSchema.parse({ + id: 'COURSE-SEC-001', + title: 'Information Security Fundamentals', + description: 'Annual security awareness training for all employees', + category: 'security_awareness', + durationMinutes: 60, + targetRoles: ['all_employees'], + }); + + expect(course.mandatory).toBe(false); + expect(course.passingScore).toBeUndefined(); + expect(course.validityDays).toBeUndefined(); + }); + + it('should accept full course configuration', () => { + const course: TrainingCourse = { + id: 'COURSE-SEC-002', + title: 'Phishing Awareness Training', + description: 'Recognize and report phishing attempts', + category: 'phishing_awareness', + durationMinutes: 30, + mandatory: true, + targetRoles: ['all_employees', 'contractors'], + validityDays: 365, + passingScore: 80, + version: '2.0', + }; + + expect(() => TrainingCourseSchema.parse(course)).not.toThrow(); + }); + + it('should accept all category types', () => { + const categories = [ + 'security_awareness', 'data_protection', 'incident_response', + 'access_control', 'phishing_awareness', 'compliance', + 'secure_development', 'physical_security', 'business_continuity', 'other', + ]; + + categories.forEach((category) => { + expect(() => TrainingCourseSchema.parse({ + id: `COURSE-${category}`, + title: `${category} Training`, + description: `Training for ${category}`, + category, + durationMinutes: 30, + targetRoles: ['all'], + })).not.toThrow(); + }); + }); + + it('should reject invalid duration', () => { + expect(() => TrainingCourseSchema.parse({ + id: 'COURSE-001', + title: 'Test', + description: 'Test', + category: 'other', + durationMinutes: 0, + targetRoles: ['all'], + })).toThrow(); + }); + + it('should reject passing score out of range', () => { + expect(() => TrainingCourseSchema.parse({ + id: 'COURSE-001', + title: 'Test', + description: 'Test', + category: 'other', + durationMinutes: 30, + targetRoles: ['all'], + passingScore: 101, + })).toThrow(); + + expect(() => TrainingCourseSchema.parse({ + id: 'COURSE-001', + title: 'Test', + description: 'Test', + category: 'other', + durationMinutes: 30, + targetRoles: ['all'], + passingScore: -1, + })).toThrow(); + }); + + it('should reject missing required fields', () => { + expect(() => TrainingCourseSchema.parse({})).toThrow(); + expect(() => TrainingCourseSchema.parse({ id: 'COURSE-001' })).toThrow(); + }); +}); + +describe('TrainingRecordSchema', () => { + it('should accept valid completed training record', () => { + const record: TrainingRecord = { + courseId: 'COURSE-SEC-001', + userId: 'user_123', + status: 'completed', + assignedAt: 1704067200000, + completedAt: 1704153600000, + score: 95, + expiresAt: 1735689600000, + }; + + expect(() => TrainingRecordSchema.parse(record)).not.toThrow(); + }); + + it('should accept minimal not-started record', () => { + const record = { + courseId: 'COURSE-SEC-002', + userId: 'user_456', + status: 'not_started', + assignedAt: Date.now(), + }; + + expect(() => TrainingRecordSchema.parse(record)).not.toThrow(); + }); + + it('should accept failed record', () => { + const record = { + courseId: 'COURSE-SEC-003', + userId: 'user_789', + status: 'failed', + assignedAt: 1704067200000, + completedAt: 1704153600000, + score: 45, + notes: 'Did not meet passing score of 80%', + }; + + expect(() => TrainingRecordSchema.parse(record)).not.toThrow(); + }); + + it('should reject score out of range', () => { + expect(() => TrainingRecordSchema.parse({ + courseId: 'COURSE-001', + userId: 'user_123', + status: 'completed', + assignedAt: Date.now(), + score: 150, + })).toThrow(); + }); + + it('should reject missing required fields', () => { + expect(() => TrainingRecordSchema.parse({})).toThrow(); + expect(() => TrainingRecordSchema.parse({ courseId: 'COURSE-001' })).toThrow(); + }); +}); + +describe('TrainingPlanSchema', () => { + it('should accept plan with defaults', () => { + const plan = TrainingPlanSchema.parse({ + courses: [ + { + id: 'COURSE-SEC-001', + title: 'Security Awareness', + description: 'Annual security training', + category: 'security_awareness', + durationMinutes: 60, + targetRoles: ['all_employees'], + }, + ], + }); + + expect(plan.enabled).toBe(true); + expect(plan.recertificationIntervalDays).toBe(365); + expect(plan.trackCompletion).toBe(true); + expect(plan.gracePeriodDays).toBe(30); + expect(plan.sendReminders).toBe(true); + expect(plan.reminderDaysBefore).toBe(14); + }); + + it('should accept full plan configuration', () => { + const plan = TrainingPlanSchema.parse({ + enabled: true, + courses: [ + { + id: 'COURSE-SEC-001', + title: 'Security Fundamentals', + description: 'Core security training', + category: 'security_awareness', + durationMinutes: 60, + mandatory: true, + targetRoles: ['all_employees'], + validityDays: 365, + passingScore: 80, + }, + { + id: 'COURSE-SEC-002', + title: 'Secure Development', + description: 'Secure coding practices', + category: 'secure_development', + durationMinutes: 120, + mandatory: true, + targetRoles: ['developers', 'devops'], + validityDays: 365, + passingScore: 85, + }, + ], + recertificationIntervalDays: 180, + trackCompletion: true, + gracePeriodDays: 14, + sendReminders: true, + reminderDaysBefore: 30, + }); + + expect(plan.courses).toHaveLength(2); + expect(plan.recertificationIntervalDays).toBe(180); + expect(plan.gracePeriodDays).toBe(14); + expect(plan.reminderDaysBefore).toBe(30); + }); + + it('should accept plan with empty courses', () => { + const plan = TrainingPlanSchema.parse({ + courses: [], + }); + + expect(plan.courses).toHaveLength(0); + }); + + it('should reject missing courses', () => { + expect(() => TrainingPlanSchema.parse({})).toThrow(); + }); +}); diff --git a/packages/spec/src/system/training.zod.ts b/packages/spec/src/system/training.zod.ts new file mode 100644 index 000000000..397d6a5a0 --- /dev/null +++ b/packages/spec/src/system/training.zod.ts @@ -0,0 +1,216 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { z } from 'zod'; + +/** + * Information Security Training Protocol — ISO 27001:2022 (A.6.3) + * + * Defines schemas for security awareness and training management including + * course definitions, completion tracking, and organizational training plans. + * + * @see https://www.iso.org/standard/27001 + * @category Security + */ + +/** + * Training Category Schema + * + * Classification of training content by domain. + */ +export const TrainingCategorySchema = z.enum([ + 'security_awareness', // General security awareness + 'data_protection', // Data handling and privacy + 'incident_response', // Incident reporting and response + 'access_control', // Access management best practices + 'phishing_awareness', // Phishing and social engineering + 'compliance', // Regulatory compliance (GDPR, HIPAA, etc.) + 'secure_development', // Secure coding and development practices + 'physical_security', // Physical security awareness + 'business_continuity', // Business continuity and disaster recovery + 'other', // Other training categories +]); + +/** + * Training Completion Status Schema + */ +export const TrainingCompletionStatusSchema = z.enum([ + 'not_started', // Training not yet begun + 'in_progress', // Training currently underway + 'completed', // Training completed successfully + 'failed', // Training assessment not passed + 'expired', // Training certification has expired +]); + +/** + * Training Course Schema + * + * Definition of a security training course or module. + * + * @example + * ```json + * { + * "id": "COURSE-SEC-001", + * "title": "Information Security Fundamentals", + * "description": "Annual security awareness training for all employees", + * "category": "security_awareness", + * "durationMinutes": 60, + * "mandatory": true, + * "targetRoles": ["all_employees"], + * "validityDays": 365, + * "passingScore": 80 + * } + * ``` + */ +export const TrainingCourseSchema = z.object({ + /** + * Unique course identifier + */ + id: z.string().describe('Unique course identifier'), + + /** + * Course title + */ + title: z.string().describe('Course title'), + + /** + * Course description and objectives + */ + description: z.string().describe('Course description and learning objectives'), + + /** + * Training category + */ + category: TrainingCategorySchema.describe('Training category'), + + /** + * Estimated duration in minutes + */ + durationMinutes: z.number().min(1).describe('Estimated course duration in minutes'), + + /** + * Whether this training is mandatory + */ + mandatory: z.boolean().default(false).describe('Whether training is mandatory'), + + /** + * Target roles or groups for this training + */ + targetRoles: z.array(z.string()).describe('Target roles or groups'), + + /** + * Validity period in days before recertification is needed + */ + validityDays: z.number().optional().describe('Certification validity period in days'), + + /** + * Minimum passing score (percentage) for assessment + */ + passingScore: z.number().min(0).max(100).optional() + .describe('Minimum passing score percentage'), + + /** + * Course version for tracking content updates + */ + version: z.string().optional().describe('Course content version'), +}).describe('Security training course definition'); + +/** + * Training Record Schema + * + * Individual employee training completion record. + */ +export const TrainingRecordSchema = z.object({ + /** + * Reference to the course ID + */ + courseId: z.string().describe('Training course identifier'), + + /** + * User who completed (or is assigned) the training + */ + userId: z.string().describe('User identifier'), + + /** + * Completion status + */ + status: TrainingCompletionStatusSchema.describe('Training completion status'), + + /** + * Training assignment date (Unix milliseconds) + */ + assignedAt: z.number().describe('Assignment timestamp'), + + /** + * Training completion date (Unix milliseconds) + */ + completedAt: z.number().optional().describe('Completion timestamp'), + + /** + * Assessment score (percentage) + */ + score: z.number().min(0).max(100).optional().describe('Assessment score percentage'), + + /** + * Certification expiry date (Unix milliseconds) + */ + expiresAt: z.number().optional().describe('Certification expiry timestamp'), + + /** + * Notes or comments from instructor or system + */ + notes: z.string().optional().describe('Training notes or comments'), +}).describe('Individual training completion record'); + +/** + * Training Plan Schema + * + * Organizational training plan defining schedule and requirements (A.6.3). + */ +export const TrainingPlanSchema = z.object({ + /** + * Whether training management is enabled + */ + enabled: z.boolean().default(true).describe('Enable training management'), + + /** + * Training courses in the plan + */ + courses: z.array(TrainingCourseSchema).describe('Training courses'), + + /** + * Default recertification interval in days + */ + recertificationIntervalDays: z.number().default(365) + .describe('Default recertification interval in days'), + + /** + * Whether to track training completion for compliance reporting + */ + trackCompletion: z.boolean().default(true) + .describe('Track training completion for compliance'), + + /** + * Grace period in days after expiry before non-compliance escalation + */ + gracePeriodDays: z.number().default(30) + .describe('Grace period in days after certification expiry'), + + /** + * Whether to send reminders for upcoming training deadlines + */ + sendReminders: z.boolean().default(true) + .describe('Send reminders for upcoming training deadlines'), + + /** + * Days before deadline to send first reminder + */ + reminderDaysBefore: z.number().default(14) + .describe('Days before deadline to send first reminder'), +}).describe('Organizational training plan per ISO 27001:2022 A.6.3'); + +// Type exports +export type TrainingCategory = z.infer; +export type TrainingCompletionStatus = z.infer; +export type TrainingCourse = z.infer; +export type TrainingRecord = z.infer; +export type TrainingPlan = z.infer; From 9bc437cb62995a653262c12fdfcc8c73c3697d15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:23:16 +0000 Subject: [PATCH 3/3] refactor: deduplicate data classification and compliance framework enums via shared schemas from security-context.zod.ts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../spec/src/system/change-management.zod.ts | 6 +-- packages/spec/src/system/compliance.zod.ts | 5 ++- .../spec/src/system/incident-response.zod.ts | 6 +-- .../spec/src/system/security-context.zod.ts | 39 +++++++++++++------ .../spec/src/system/supplier-security.zod.ts | 6 +-- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/spec/src/system/change-management.zod.ts b/packages/spec/src/system/change-management.zod.ts index 41dac4221..bb927b3c3 100644 --- a/packages/spec/src/system/change-management.zod.ts +++ b/packages/spec/src/system/change-management.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { DataClassificationSchema } from './security-context.zod'; /** * Change Type Enum @@ -337,9 +338,8 @@ export const ChangeRequestSchema = z.object({ /** * Data classifications affected by this change */ - affectedDataClassifications: z.array(z.enum([ - 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', - ])).optional().describe('Affected data classifications'), + affectedDataClassifications: z.array(DataClassificationSchema) + .optional().describe('Affected data classifications'), /** * Whether the change requires security team approval diff --git a/packages/spec/src/system/compliance.zod.ts b/packages/spec/src/system/compliance.zod.ts index f74b2fe8d..71a1a0a4d 100644 --- a/packages/spec/src/system/compliance.zod.ts +++ b/packages/spec/src/system/compliance.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { ComplianceFrameworkSchema } from './security-context.zod'; /** * Compliance protocol for GDPR, CCPA, HIPAA, SOX, PCI-DSS @@ -161,7 +162,7 @@ export const AuditFindingSchema = z.object({ /** * Compliance framework */ - framework: z.enum(['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']).optional() + framework: ComplianceFrameworkSchema.optional() .describe('Related compliance framework'), /** @@ -240,7 +241,7 @@ export const AuditScheduleSchema = z.object({ /** * Target compliance framework */ - framework: z.enum(['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']) + framework: ComplianceFrameworkSchema .describe('Target compliance framework'), /** diff --git a/packages/spec/src/system/incident-response.zod.ts b/packages/spec/src/system/incident-response.zod.ts index fcd4730de..2cee6dbe6 100644 --- a/packages/spec/src/system/incident-response.zod.ts +++ b/packages/spec/src/system/incident-response.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { DataClassificationSchema } from './security-context.zod'; /** * Incident Response Protocol — ISO 27001:2022 (A.5.24–A.5.28) @@ -270,9 +271,8 @@ export const IncidentSchema = z.object({ /** * Data classifications affected (for data breach assessment) */ - affectedDataClassifications: z.array(z.enum([ - 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', - ])).optional().describe('Affected data classifications'), + affectedDataClassifications: z.array(DataClassificationSchema) + .optional().describe('Affected data classifications'), /** * Structured response phases tracking diff --git a/packages/spec/src/system/security-context.zod.ts b/packages/spec/src/system/security-context.zod.ts index 458ccf6b9..fc4e4484b 100644 --- a/packages/spec/src/system/security-context.zod.ts +++ b/packages/spec/src/system/security-context.zod.ts @@ -21,12 +21,32 @@ import { z } from 'zod'; * @category Security */ +/** + * Shared data classification enum used across security subsystems. + * Defines the canonical set of data sensitivity labels. + */ +export const DataClassificationSchema = z.enum([ + 'pii', 'phi', 'pci', 'financial', 'confidential', 'internal', 'public', +]).describe('Data classification level'); + +export type DataClassification = z.infer; + +/** + * Shared compliance framework enum used across compliance and security schemas. + * Defines the canonical set of regulatory frameworks. + */ +export const ComplianceFrameworkSchema = z.enum([ + 'gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001', +]).describe('Compliance framework identifier'); + +export type ComplianceFramework = z.infer; + /** * Compliance-driven audit requirement. * Maps specific compliance frameworks to the audit event types that MUST be captured. */ export const ComplianceAuditRequirementSchema = z.object({ - framework: z.enum(['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']) + framework: ComplianceFrameworkSchema .describe('Compliance framework identifier'), requiredEvents: z.array(z.string()) .describe('Audit event types required by this framework (e.g., "data.delete", "auth.login")'), @@ -43,11 +63,10 @@ export type ComplianceAuditRequirement = z.infer