@striae-org/striae 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/app/components/actions/case-export/core-export.ts +2 -2
  2. package/app/components/actions/case-export/data-processing.ts +19 -4
  3. package/app/components/actions/case-export/download-handlers.ts +57 -8
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/annotation-import.ts +2 -2
  6. package/app/components/actions/case-import/confirmation-import.ts +44 -20
  7. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  8. package/app/components/actions/case-import/image-operations.ts +1 -1
  9. package/app/components/actions/case-import/index.ts +1 -0
  10. package/app/components/actions/case-import/orchestrator.ts +16 -6
  11. package/app/components/actions/case-import/storage-operations.ts +7 -7
  12. package/app/components/actions/case-import/validation.ts +7 -100
  13. package/app/components/actions/case-import/zip-processing.ts +47 -5
  14. package/app/components/actions/case-manage.ts +3 -3
  15. package/app/components/actions/confirm-export.ts +47 -16
  16. package/app/components/actions/generate-pdf.ts +3 -3
  17. package/app/components/actions/image-manage.ts +3 -3
  18. package/app/components/actions/notes-manage.ts +3 -3
  19. package/app/components/actions/signout.tsx +1 -1
  20. package/app/components/audit/user-audit-viewer.tsx +2 -3
  21. package/app/components/auth/auth-provider.tsx +2 -2
  22. package/app/components/auth/mfa-enrollment.tsx +3 -3
  23. package/app/components/auth/mfa-verification.tsx +4 -4
  24. package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
  25. package/app/components/canvas/canvas.tsx +1 -1
  26. package/app/components/canvas/confirmation/confirmation.tsx +1 -1
  27. package/app/components/form/form-button.tsx +1 -1
  28. package/app/components/form/form.module.css +9 -0
  29. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  30. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  31. package/app/components/sidebar/case-export/case-export.tsx +2 -54
  32. package/app/components/sidebar/case-import/case-import.tsx +20 -8
  33. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
  34. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  35. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +9 -7
  36. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
  37. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  38. package/app/components/sidebar/cases/case-sidebar.tsx +106 -50
  39. package/app/components/sidebar/cases/cases-modal.tsx +1 -1
  40. package/app/components/sidebar/cases/cases.module.css +101 -18
  41. package/app/components/sidebar/files/files-modal.tsx +3 -2
  42. package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
  43. package/app/components/sidebar/notes/notes.module.css +33 -13
  44. package/app/components/sidebar/sidebar-container.tsx +4 -3
  45. package/app/components/sidebar/sidebar.tsx +2 -2
  46. package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
  47. package/app/components/theme-provider/theme-provider.tsx +1 -1
  48. package/app/components/user/delete-account.tsx +1 -1
  49. package/app/components/user/manage-profile.tsx +3 -3
  50. package/app/components/user/mfa-phone-update.tsx +17 -14
  51. package/app/contexts/auth.context.ts +1 -1
  52. package/app/root.tsx +2 -2
  53. package/app/routes/auth/emailActionHandler.tsx +2 -2
  54. package/app/routes/auth/emailVerification.tsx +2 -2
  55. package/app/routes/auth/login.tsx +134 -11
  56. package/app/routes/auth/passwordReset.tsx +2 -2
  57. package/app/routes/striae/striae.tsx +2 -2
  58. package/app/services/audit/audit-console-logger.ts +46 -0
  59. package/app/services/audit/audit-export-csv.ts +126 -0
  60. package/app/services/audit/audit-export-report.ts +174 -0
  61. package/app/services/audit/audit-export-signing.ts +85 -0
  62. package/app/services/audit/audit-export.service.ts +334 -0
  63. package/app/services/audit/audit-file-type.ts +13 -0
  64. package/app/services/audit/audit-query-helpers.ts +88 -0
  65. package/app/services/audit/audit-worker-client.ts +95 -0
  66. package/app/services/audit/audit.service.ts +990 -0
  67. package/app/services/audit/builders/audit-entry-builder.ts +32 -0
  68. package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
  70. package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
  71. package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
  72. package/app/services/audit/builders/index.ts +40 -0
  73. package/app/services/audit/index.ts +2 -0
  74. package/app/types/case.ts +2 -2
  75. package/app/types/exceljs-bare.d.ts +3 -1
  76. package/app/types/user.ts +1 -1
  77. package/app/utils/SHA256.ts +5 -1
  78. package/app/utils/audit-export-signature.ts +2 -2
  79. package/app/utils/confirmation-signature.ts +8 -4
  80. package/app/utils/data-operations.ts +5 -5
  81. package/app/utils/export-verification.ts +353 -0
  82. package/app/utils/mfa-phone.ts +1 -1
  83. package/app/utils/mfa.ts +1 -1
  84. package/app/utils/permissions.ts +2 -2
  85. package/app/utils/signature-utils.ts +74 -4
  86. package/package.json +11 -9
  87. package/public/favicon.ico +0 -0
  88. package/public/icon-256.png +0 -0
  89. package/public/icon-512.png +0 -0
  90. package/public/manifest.json +39 -0
  91. package/public/shortcut.png +0 -0
  92. package/public/social-image.png +0 -0
  93. package/react-router.config.ts +5 -0
  94. package/worker-configuration.d.ts +4435 -562
  95. package/workers/data-worker/src/data-worker.example.ts +3 -3
  96. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  97. package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
  98. package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
  99. package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
  100. package/app/services/audit-export.service.ts +0 -755
  101. package/app/services/audit.service.ts +0 -1474
  102. package/public/favicon.svg +0 -9
  103. /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
  104. /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
@@ -0,0 +1,174 @@
1
+ import { type AuditTrail, type ValidationAuditEntry } from '~/types';
2
+
3
+ const calculateDuration = (start: string, end: string): string => {
4
+ const startTime = new Date(start).getTime();
5
+ const endTime = new Date(end).getTime();
6
+ const durationMs = endTime - startTime;
7
+
8
+ const hours = Math.floor(durationMs / (1000 * 60 * 60));
9
+ const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
10
+ const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
11
+
12
+ if (hours > 0) {
13
+ return `${hours}h ${minutes}m ${seconds}s`;
14
+ }
15
+
16
+ if (minutes > 0) {
17
+ return `${minutes}m ${seconds}s`;
18
+ }
19
+
20
+ return `${seconds}s`;
21
+ };
22
+
23
+ const generateSecurityAnalysis = (entries: ValidationAuditEntry[]): string => {
24
+ const securityEntries = entries.filter(entry => entry.details.securityChecks);
25
+
26
+ if (securityEntries.length === 0) {
27
+ return 'No security-sensitive operations detected.';
28
+ }
29
+
30
+ let selfConfirmationAttempts = 0;
31
+ let fileIntegrityFailures = 0;
32
+ let exporterValidationFailures = 0;
33
+ let legitimateImports = 0;
34
+
35
+ securityEntries.forEach(entry => {
36
+ const checks = entry.details.securityChecks!;
37
+
38
+ if (checks.selfConfirmationPrevented === true) {
39
+ selfConfirmationAttempts++;
40
+ }
41
+ if (checks.fileIntegrityValid === false) {
42
+ fileIntegrityFailures++;
43
+ }
44
+ if (checks.exporterUidValidated === false) {
45
+ exporterValidationFailures++;
46
+ }
47
+
48
+ if (
49
+ entry.action === 'import' &&
50
+ entry.details.workflowPhase === 'confirmation' &&
51
+ entry.result === 'success' &&
52
+ checks.selfConfirmationPrevented === false
53
+ ) {
54
+ legitimateImports++;
55
+ }
56
+ });
57
+
58
+ return [
59
+ `Total Security-Sensitive Operations: ${securityEntries.length}`,
60
+ `Legitimate Confirmation Imports: ${legitimateImports}`,
61
+ `Self-Confirmation Attempts Blocked: ${selfConfirmationAttempts}`,
62
+ `File Integrity Failures: ${fileIntegrityFailures}`,
63
+ `Exporter Validation Failures: ${exporterValidationFailures}`,
64
+ '',
65
+ selfConfirmationAttempts === 0 && fileIntegrityFailures === 0 && exporterValidationFailures === 0
66
+ ? '✅ No security violations detected'
67
+ : '⚠️ Security violations detected - review required'
68
+ ].join('\n');
69
+ };
70
+
71
+ const generateConfirmationSummary = (entries: ValidationAuditEntry[]): string => {
72
+ const confirmationEntries = entries.filter(entry =>
73
+ entry.details.workflowPhase === 'confirmation' ||
74
+ (entry.action === 'import' && entry.details.fileType === 'confirmation-data')
75
+ );
76
+
77
+ if (confirmationEntries.length === 0) {
78
+ return 'No confirmation workflow operations detected.';
79
+ }
80
+
81
+ const imports = confirmationEntries.filter(entry => entry.action === 'import');
82
+ const exports = confirmationEntries.filter(entry => entry.action === 'export');
83
+ const creations = confirmationEntries.filter(entry => entry.action === 'confirm');
84
+
85
+ let totalConfirmationsImported = 0;
86
+ let totalConfirmationsInFiles = 0;
87
+ const reviewingExaminers = new Set<string>();
88
+
89
+ imports.forEach(entry => {
90
+ const metrics = entry.details.performanceMetrics;
91
+ const caseDetails = entry.details.caseDetails;
92
+
93
+ if (metrics?.validationStepsCompleted) {
94
+ totalConfirmationsImported += metrics.validationStepsCompleted;
95
+ }
96
+ if (caseDetails?.totalAnnotations) {
97
+ totalConfirmationsInFiles += caseDetails.totalAnnotations;
98
+ }
99
+ if (entry.details.reviewingExaminerUid) {
100
+ reviewingExaminers.add(entry.details.reviewingExaminerUid);
101
+ }
102
+ });
103
+
104
+ return [
105
+ `Confirmation Operations: ${confirmationEntries.length}`,
106
+ `- Imports: ${imports.length}`,
107
+ `- Exports: ${exports.length}`,
108
+ `- Creations: ${creations.length}`,
109
+ '',
110
+ `Total Confirmations Imported: ${totalConfirmationsImported}`,
111
+ `Total Confirmations in Import Files: ${totalConfirmationsInFiles}`,
112
+ `Reviewing Examiners Involved: ${reviewingExaminers.size}`,
113
+ '',
114
+ reviewingExaminers.size > 0
115
+ ? `External Reviewers: ${Array.from(reviewingExaminers).join(', ')}`
116
+ : 'No external reviewers detected'
117
+ ].join('\n');
118
+ };
119
+
120
+ export const buildAuditReportContent = (auditTrail: AuditTrail, generatedAt: string): string => {
121
+ const summary = auditTrail.summary;
122
+ const successRate = ((summary.successfulEvents / summary.totalEvents) * 100).toFixed(1);
123
+
124
+ return `
125
+ STRIAE AUDIT TRAIL REPORT
126
+ ============================
127
+
128
+ Case Number: ${auditTrail.caseNumber}
129
+ Workflow ID: ${auditTrail.workflowId}
130
+ Report Generated: ${new Date(generatedAt).toLocaleString()}
131
+
132
+ SUMMARY STATISTICS
133
+ ------------------
134
+ Total Events: ${summary.totalEvents}
135
+ Successful Events: ${summary.successfulEvents} (${successRate}%)
136
+ Failed Events: ${summary.failedEvents}
137
+ Warning Events: ${summary.warningEvents}
138
+ Security Incidents: ${summary.securityIncidents}
139
+
140
+ COMPLIANCE STATUS
141
+ -----------------
142
+ Status: ${summary.complianceStatus.toUpperCase()}
143
+ ${summary.complianceStatus === 'compliant'
144
+ ? '✅ All audit events completed successfully'
145
+ : '⚠️ Some audit events failed - requires investigation'}
146
+
147
+ TIMELINE
148
+ --------
149
+ Start Time: ${new Date(summary.startTimestamp).toLocaleString()}
150
+ End Time: ${new Date(summary.endTimestamp).toLocaleString()}
151
+ Duration: ${calculateDuration(summary.startTimestamp, summary.endTimestamp)}
152
+
153
+ PARTICIPANTS
154
+ ------------
155
+ Users Involved: ${summary.participatingUsers.length}
156
+ ${summary.participatingUsers.map(uid => `- User ID: ${uid}`).join('\n')}
157
+
158
+ WORKFLOW PHASES
159
+ ---------------
160
+ ${summary.workflowPhases.map(phase => `- ${phase}`).join('\n')}
161
+
162
+ SECURITY ANALYSIS
163
+ -----------------
164
+ ${generateSecurityAnalysis(auditTrail.entries)}
165
+
166
+ CONFIRMATION WORKFLOW DETAILS
167
+ ------------------------------
168
+ ${generateConfirmationSummary(auditTrail.entries)}
169
+
170
+ ---
171
+ This report contains ${summary.totalEvents} audit entries providing complete forensic accountability.
172
+ Generated by Striae
173
+ `.trim();
174
+ };
@@ -0,0 +1,85 @@
1
+ import type { User } from 'firebase/auth';
2
+ import { signAuditExportData } from '~/utils/data-operations';
3
+ import {
4
+ AUDIT_EXPORT_SIGNATURE_VERSION,
5
+ type AuditExportFormat,
6
+ type AuditExportSigningPayload,
7
+ type AuditExportType
8
+ } from '~/utils/audit-export-signature';
9
+
10
+ export interface AuditExportContext {
11
+ user: User;
12
+ scopeType: 'case' | 'user';
13
+ scopeIdentifier: string;
14
+ caseNumber?: string;
15
+ }
16
+
17
+ interface SignAuditExportInput {
18
+ exportFormat: AuditExportFormat;
19
+ exportType: AuditExportType;
20
+ generatedAt: string;
21
+ totalEntries: number;
22
+ hash: string;
23
+ }
24
+
25
+ export interface AuditExportSignature {
26
+ algorithm: string;
27
+ keyId: string;
28
+ signedAt: string;
29
+ value: string;
30
+ }
31
+
32
+ export interface SignedAuditExportPayload {
33
+ signatureMetadata: AuditExportSigningPayload;
34
+ signature: AuditExportSignature;
35
+ }
36
+
37
+ const buildAuditSignaturePayload = (
38
+ exportFormat: AuditExportFormat,
39
+ exportType: AuditExportType,
40
+ generatedAt: string,
41
+ totalEntries: number,
42
+ hash: string,
43
+ context: AuditExportContext
44
+ ): AuditExportSigningPayload => {
45
+ return {
46
+ signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION,
47
+ exportFormat,
48
+ exportType,
49
+ scopeType: context.scopeType,
50
+ scopeIdentifier: context.scopeIdentifier,
51
+ generatedAt,
52
+ totalEntries,
53
+ hash: hash.toUpperCase()
54
+ };
55
+ };
56
+
57
+ export const signAuditExport = async (
58
+ payload: SignAuditExportInput,
59
+ context: AuditExportContext
60
+ ): Promise<SignedAuditExportPayload> => {
61
+ const signatureMetadata = buildAuditSignaturePayload(
62
+ payload.exportFormat,
63
+ payload.exportType,
64
+ payload.generatedAt,
65
+ payload.totalEntries,
66
+ payload.hash,
67
+ context
68
+ );
69
+
70
+ const caseNumber =
71
+ context.scopeType === 'case'
72
+ ? (context.caseNumber || context.scopeIdentifier)
73
+ : undefined;
74
+
75
+ const signatureResponse = await signAuditExportData(
76
+ context.user,
77
+ signatureMetadata,
78
+ { caseNumber }
79
+ );
80
+
81
+ return {
82
+ signatureMetadata,
83
+ signature: signatureResponse.signature
84
+ };
85
+ };
@@ -0,0 +1,334 @@
1
+ import { type ValidationAuditEntry, type AuditTrail } from '~/types';
2
+ import { calculateSHA256Secure } from '~/utils/SHA256';
3
+ import { type AuditExportType } from '~/utils/audit-export-signature';
4
+ import { AUDIT_CSV_ENTRY_HEADERS, entryToCSVRow } from './audit-export-csv';
5
+ import { buildAuditReportContent } from './audit-export-report';
6
+ import { type AuditExportContext, signAuditExport } from './audit-export-signing';
7
+
8
+ /**
9
+ * Audit Export Service
10
+ * Handles exporting audit trails to various formats for compliance and forensic analysis
11
+ */
12
+ export class AuditExportService {
13
+ private static instance: AuditExportService;
14
+
15
+ private constructor() {}
16
+
17
+ public static getInstance(): AuditExportService {
18
+ if (!AuditExportService.instance) {
19
+ AuditExportService.instance = new AuditExportService();
20
+ }
21
+ return AuditExportService.instance;
22
+ }
23
+
24
+ /**
25
+ * Export audit entries to CSV format
26
+ */
27
+ public async exportToCSV(
28
+ entries: ValidationAuditEntry[],
29
+ filename: string,
30
+ context: AuditExportContext
31
+ ): Promise<void> {
32
+ const csvData = [
33
+ AUDIT_CSV_ENTRY_HEADERS.join(','),
34
+ ...entries.map(entry => entryToCSVRow(entry))
35
+ ].join('\n');
36
+
37
+ const generatedAt = new Date().toISOString();
38
+ const hash = await calculateSHA256Secure(csvData);
39
+ const signaturePayload = await signAuditExport(
40
+ {
41
+ exportFormat: 'csv',
42
+ exportType: 'entries',
43
+ generatedAt,
44
+ totalEntries: entries.length,
45
+ hash: hash.toUpperCase()
46
+ },
47
+ context
48
+ );
49
+
50
+ // Add hash metadata header
51
+ const csvContent = [
52
+ `# Striae Audit Export - Generated: ${generatedAt}`,
53
+ `# Total Entries: ${entries.length}`,
54
+ `# SHA-256 Hash: ${hash.toUpperCase()}`,
55
+ `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
56
+ `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
57
+ `# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)`,
58
+ '',
59
+ csvData
60
+ ].join('\n');
61
+
62
+ this.downloadFile(csvContent, filename, 'text/csv');
63
+ }
64
+
65
+ /**
66
+ * Export audit trail to detailed CSV with summary
67
+ */
68
+ public async exportAuditTrailToCSV(
69
+ auditTrail: AuditTrail,
70
+ filename: string,
71
+ context: AuditExportContext
72
+ ): Promise<void> {
73
+ const summaryHeaders = [
74
+ 'Case Number',
75
+ 'Workflow ID',
76
+ 'Total Events',
77
+ 'Successful Events',
78
+ 'Failed Events',
79
+ 'Warning Events',
80
+ 'Compliance Status',
81
+ 'Security Incidents',
82
+ 'Start Time',
83
+ 'End Time',
84
+ 'Participating Users'
85
+ ];
86
+
87
+ const summaryRow = [
88
+ auditTrail.caseNumber,
89
+ auditTrail.workflowId,
90
+ auditTrail.summary.totalEvents,
91
+ auditTrail.summary.successfulEvents,
92
+ auditTrail.summary.failedEvents,
93
+ auditTrail.summary.warningEvents,
94
+ auditTrail.summary.complianceStatus.toUpperCase(),
95
+ auditTrail.summary.securityIncidents,
96
+ auditTrail.summary.startTimestamp,
97
+ auditTrail.summary.endTimestamp,
98
+ auditTrail.summary.participatingUsers.join('; ')
99
+ ].join(',');
100
+
101
+ const csvData = [
102
+ summaryHeaders.join(','),
103
+ summaryRow,
104
+ '',
105
+ AUDIT_CSV_ENTRY_HEADERS.join(','),
106
+ ...auditTrail.entries.map(entry => entryToCSVRow(entry))
107
+ ].join('\n');
108
+
109
+ const generatedAt = new Date().toISOString();
110
+ const hash = await calculateSHA256Secure(csvData);
111
+ const signaturePayload = await signAuditExport(
112
+ {
113
+ exportFormat: 'csv',
114
+ exportType: 'trail',
115
+ generatedAt,
116
+ totalEntries: auditTrail.summary.totalEvents,
117
+ hash: hash.toUpperCase()
118
+ },
119
+ context
120
+ );
121
+
122
+ const csvContent = [
123
+ '# Striae Audit Trail Export - Generated: ' + generatedAt,
124
+ `# Case: ${auditTrail.caseNumber} | Workflow: ${auditTrail.workflowId}`,
125
+ `# Total Events: ${auditTrail.summary.totalEvents}`,
126
+ `# SHA-256 Hash: ${hash.toUpperCase()}`,
127
+ `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
128
+ `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
129
+ '# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)',
130
+ '',
131
+ '# AUDIT TRAIL SUMMARY',
132
+ csvData
133
+ ].join('\n');
134
+
135
+ this.downloadFile(csvContent, filename, 'text/csv');
136
+ }
137
+
138
+ /**
139
+ * Generate filename with timestamp
140
+ */
141
+ public generateFilename(type: 'case' | 'user', identifier: string, format: 'csv' | 'json'): string {
142
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
143
+ const sanitizedId = identifier.replace(/[^a-zA-Z0-9-_]/g, '_');
144
+ return `striae-audit-${type}-${sanitizedId}-${timestamp}.${format}`;
145
+ }
146
+
147
+ /**
148
+ * Download file helper
149
+ */
150
+ private downloadFile(content: string, filename: string, mimeType: string): void {
151
+ const blob = new Blob([content], { type: mimeType });
152
+ const url = URL.createObjectURL(blob);
153
+
154
+ const link = document.createElement('a');
155
+ link.href = url;
156
+ link.download = filename;
157
+ link.style.display = 'none';
158
+
159
+ document.body.appendChild(link);
160
+ link.click();
161
+ document.body.removeChild(link);
162
+
163
+ // Clean up the URL object
164
+ setTimeout(() => URL.revokeObjectURL(url), 100);
165
+ }
166
+
167
+ /**
168
+ * Export audit entries to JSON format (for technical analysis)
169
+ */
170
+ public async exportToJSON(
171
+ entries: ValidationAuditEntry[],
172
+ filename: string,
173
+ context: AuditExportContext
174
+ ): Promise<void> {
175
+ const generatedAt = new Date().toISOString();
176
+
177
+ const exportData = {
178
+ metadata: {
179
+ exportTimestamp: generatedAt,
180
+ exportVersion: '1.0',
181
+ totalEntries: entries.length,
182
+ application: 'Striae',
183
+ exportType: 'entries' as AuditExportType,
184
+ scopeType: context.scopeType,
185
+ scopeIdentifier: context.scopeIdentifier
186
+ },
187
+ auditEntries: entries
188
+ };
189
+
190
+ const jsonContent = JSON.stringify(exportData, null, 2);
191
+
192
+ // Calculate hash for integrity verification
193
+ const hash = await calculateSHA256Secure(jsonContent);
194
+ const signaturePayload = await signAuditExport(
195
+ {
196
+ exportFormat: 'json',
197
+ exportType: exportData.metadata.exportType,
198
+ generatedAt,
199
+ totalEntries: entries.length,
200
+ hash: hash.toUpperCase()
201
+ },
202
+ context
203
+ );
204
+
205
+ // Create final export with hash included
206
+ const finalExportData = {
207
+ metadata: {
208
+ ...exportData.metadata,
209
+ hash: hash.toUpperCase(),
210
+ integrityNote: 'Verify hash and signature before trusting this export',
211
+ signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
212
+ signature: signaturePayload.signature
213
+ },
214
+ auditEntries: entries
215
+ };
216
+
217
+ const finalJsonContent = JSON.stringify(finalExportData, null, 2);
218
+ this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
219
+ }
220
+
221
+ /**
222
+ * Export full audit trail to JSON
223
+ */
224
+ public async exportAuditTrailToJSON(
225
+ auditTrail: AuditTrail,
226
+ filename: string,
227
+ context: AuditExportContext
228
+ ): Promise<void> {
229
+ const generatedAt = new Date().toISOString();
230
+
231
+ const exportData = {
232
+ metadata: {
233
+ exportTimestamp: generatedAt,
234
+ exportVersion: '1.0',
235
+ totalEntries: auditTrail.summary.totalEvents,
236
+ application: 'Striae',
237
+ exportType: 'trail' as AuditExportType,
238
+ scopeType: context.scopeType,
239
+ scopeIdentifier: context.scopeIdentifier
240
+ },
241
+ auditTrail
242
+ };
243
+
244
+ const jsonContent = JSON.stringify(exportData, null, 2);
245
+
246
+ // Calculate hash for integrity verification
247
+ const hash = await calculateSHA256Secure(jsonContent);
248
+ const signaturePayload = await signAuditExport(
249
+ {
250
+ exportFormat: 'json',
251
+ exportType: exportData.metadata.exportType,
252
+ generatedAt,
253
+ totalEntries: auditTrail.summary.totalEvents,
254
+ hash: hash.toUpperCase()
255
+ },
256
+ context
257
+ );
258
+
259
+ // Create final export with hash included
260
+ const finalExportData = {
261
+ metadata: {
262
+ ...exportData.metadata,
263
+ hash: hash.toUpperCase(),
264
+ integrityNote: 'Verify hash and signature before trusting this export',
265
+ signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
266
+ signature: signaturePayload.signature
267
+ },
268
+ auditTrail
269
+ };
270
+
271
+ const finalJsonContent = JSON.stringify(finalExportData, null, 2);
272
+ this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
273
+ }
274
+
275
+ /**
276
+ * Generate audit report summary text
277
+ */
278
+ public async generateReportSummary(auditTrail: AuditTrail, context: AuditExportContext): Promise<string> {
279
+ const summary = auditTrail.summary;
280
+ const generatedAt = new Date().toISOString();
281
+
282
+ const reportContent = buildAuditReportContent(auditTrail, generatedAt);
283
+
284
+ return this.appendSignedReportIntegrity(
285
+ reportContent,
286
+ context,
287
+ summary.totalEvents,
288
+ generatedAt
289
+ );
290
+ }
291
+
292
+ /**
293
+ * Append signed integrity metadata to a plain-text audit report.
294
+ */
295
+ public async appendSignedReportIntegrity(
296
+ reportContent: string,
297
+ context: AuditExportContext,
298
+ totalEntries: number,
299
+ generatedAt: string = new Date().toISOString()
300
+ ): Promise<string> {
301
+ const hash = await calculateSHA256Secure(reportContent);
302
+ const signaturePayload = await signAuditExport(
303
+ {
304
+ exportFormat: 'txt',
305
+ exportType: 'report',
306
+ generatedAt,
307
+ totalEntries,
308
+ hash: hash.toUpperCase()
309
+ },
310
+ context
311
+ );
312
+
313
+ return reportContent + `
314
+
315
+ ============================
316
+ INTEGRITY VERIFICATION
317
+ ============================
318
+ Report Content SHA-256 Hash: ${hash.toUpperCase()}
319
+ Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}
320
+ Audit Signature: ${JSON.stringify(signaturePayload.signature)}
321
+
322
+ Verification Instructions:
323
+ 1. Copy the entire report content above the "INTEGRITY VERIFICATION" section
324
+ 2. Calculate SHA256 hash of that content (excluding this verification section)
325
+ 3. Validate audit signature metadata and signature with your signature verification workflow (for example OpenSSL or an internal verifier)
326
+ 4. Confirm both hash and signature validation pass before relying on this report
327
+
328
+ This report requires both hash and signature validation for tamper detection.
329
+ Generated by Striae`;
330
+ }
331
+ }
332
+
333
+ // Export singleton instance
334
+ export const auditExportService = AuditExportService.getInstance();
@@ -0,0 +1,13 @@
1
+ import { type AuditFileType } from '~/types';
2
+
3
+ export const getAuditFileTypeFromMime = (mimeType: string): AuditFileType => {
4
+ if (mimeType.startsWith('image/')) return 'image-file';
5
+ if (mimeType === 'application/pdf') return 'pdf-document';
6
+ if (mimeType === 'application/json') return 'json-data';
7
+ if (mimeType === 'text/csv') return 'csv-export';
8
+ return 'unknown';
9
+ };
10
+
11
+ export const isImageMimeType = (mimeType: string): boolean => {
12
+ return mimeType.startsWith('image/');
13
+ };
@@ -0,0 +1,88 @@
1
+ import {
2
+ type AuditQueryParams,
3
+ type AuditSummary,
4
+ type ValidationAuditEntry,
5
+ type WorkflowPhase
6
+ } from '~/types';
7
+
8
+ export const applyAuditEntryFilters = (
9
+ entries: ValidationAuditEntry[],
10
+ params: AuditQueryParams
11
+ ): ValidationAuditEntry[] => {
12
+ let filtered = entries;
13
+
14
+ if (params.caseNumber) {
15
+ filtered = filtered.filter(entry => entry.details.caseNumber === params.caseNumber);
16
+ }
17
+
18
+ if (params.userId) {
19
+ filtered = filtered.filter(entry => entry.userId === params.userId);
20
+ }
21
+
22
+ if (params.action) {
23
+ filtered = filtered.filter(entry => entry.action === params.action);
24
+ }
25
+
26
+ if (params.result) {
27
+ filtered = filtered.filter(entry => entry.result === params.result);
28
+ }
29
+
30
+ if (params.workflowPhase) {
31
+ filtered = filtered.filter(entry => entry.details.workflowPhase === params.workflowPhase);
32
+ }
33
+
34
+ return filtered;
35
+ };
36
+
37
+ export const applyAuditPagination = (
38
+ entries: ValidationAuditEntry[],
39
+ params: Pick<AuditQueryParams, 'offset' | 'limit'>
40
+ ): ValidationAuditEntry[] => {
41
+ if (params.offset || params.limit) {
42
+ const offset = params.offset || 0;
43
+ const limit = params.limit || 100;
44
+ return entries.slice(offset, offset + limit);
45
+ }
46
+
47
+ return entries;
48
+ };
49
+
50
+ export const sortAuditEntriesNewestFirst = (
51
+ entries: ValidationAuditEntry[]
52
+ ): ValidationAuditEntry[] => {
53
+ const sorted = [...entries];
54
+ sorted.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
55
+ return sorted;
56
+ };
57
+
58
+ export const generateAuditSummary = (entries: ValidationAuditEntry[]): AuditSummary => {
59
+ const successCount = entries.filter(entry => entry.result === 'success').length;
60
+ const failureCount = entries.filter(entry => entry.result === 'failure').length;
61
+ const warningCount = entries.filter(entry => entry.result === 'warning').length;
62
+
63
+ const phases = [...new Set(entries
64
+ .map(entry => entry.details.workflowPhase)
65
+ .filter(Boolean))] as WorkflowPhase[];
66
+
67
+ const users = [...new Set(entries.map(entry => entry.userId))];
68
+
69
+ const timestamps = entries.map(entry => entry.timestamp).sort();
70
+ const securityIncidents = entries.filter(entry =>
71
+ entry.result === 'failure' &&
72
+ (entry.details.securityChecks?.selfConfirmationPrevented === true ||
73
+ !entry.details.securityChecks?.fileIntegrityValid)
74
+ ).length;
75
+
76
+ return {
77
+ totalEvents: entries.length,
78
+ successfulEvents: successCount,
79
+ failedEvents: failureCount,
80
+ warningEvents: warningCount,
81
+ workflowPhases: phases,
82
+ participatingUsers: users,
83
+ startTimestamp: timestamps[0] || new Date().toISOString(),
84
+ endTimestamp: timestamps[timestamps.length - 1] || new Date().toISOString(),
85
+ complianceStatus: failureCount === 0 ? 'compliant' : 'non-compliant',
86
+ securityIncidents
87
+ };
88
+ };