@striae-org/striae 3.2.0 → 3.2.2
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.
- package/README.md +3 -32
- package/app/components/actions/case-export/core-export.ts +2 -2
- package/app/components/actions/case-export/data-processing.ts +65 -10
- package/app/components/actions/case-export/download-handlers.ts +130 -44
- package/app/components/actions/case-export/metadata-helpers.ts +32 -14
- package/app/components/actions/case-import/annotation-import.ts +2 -2
- package/app/components/actions/case-import/confirmation-import.ts +3 -3
- package/app/components/actions/case-import/image-operations.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +4 -4
- package/app/components/actions/case-import/storage-operations.ts +7 -7
- package/app/components/actions/case-import/validation.ts +3 -3
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/actions/case-manage.ts +3 -3
- package/app/components/actions/confirm-export.ts +3 -3
- package/app/components/actions/generate-pdf.ts +3 -3
- package/app/components/actions/image-manage.ts +3 -3
- package/app/components/actions/notes-manage.ts +3 -3
- package/app/components/actions/signout.tsx +1 -1
- package/app/components/audit/user-audit-viewer.tsx +2 -3
- package/app/components/auth/auth-provider.tsx +2 -2
- package/app/components/auth/mfa-enrollment.tsx +3 -3
- package/app/components/auth/mfa-verification.tsx +4 -4
- package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
- package/app/components/canvas/canvas.tsx +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +1 -1
- package/app/components/form/base-form.tsx +1 -1
- package/app/components/sidebar/case-export/case-export.tsx +15 -15
- package/app/components/sidebar/case-import/case-import.tsx +2 -2
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +3 -3
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -19
- package/app/components/sidebar/cases/cases-modal.tsx +1 -1
- package/app/components/sidebar/files/files-modal.tsx +3 -2
- package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
- package/app/components/sidebar/sidebar-container.tsx +5 -4
- package/app/components/sidebar/sidebar.tsx +2 -2
- package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
- package/app/components/theme-provider/theme-provider.tsx +1 -1
- package/app/components/user/delete-account.tsx +1 -1
- package/app/components/user/manage-profile.tsx +2 -2
- package/app/components/user/mfa-phone-update.tsx +2 -2
- package/app/contexts/auth.context.ts +1 -1
- package/app/entry.client.tsx +12 -12
- package/app/entry.server.tsx +4 -4
- package/app/hooks/useInactivityTimeout.ts +1 -1
- package/app/root.tsx +3 -3
- package/app/routes/auth/emailActionHandler.tsx +3 -3
- package/app/routes/auth/emailVerification.tsx +3 -3
- package/app/routes/auth/login.tsx +6 -6
- package/app/routes/auth/passwordReset.tsx +3 -3
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +2 -2
- package/app/services/audit/audit-console-logger.ts +46 -0
- package/app/services/audit/audit-export-csv.ts +126 -0
- package/app/services/audit/audit-export-report.ts +174 -0
- package/app/services/audit/audit-export-signing.ts +85 -0
- package/app/services/audit/audit-export.service.ts +334 -0
- package/app/services/audit/audit-file-type.ts +13 -0
- package/app/services/audit/audit-query-helpers.ts +88 -0
- package/app/services/audit/audit-worker-client.ts +95 -0
- package/app/services/audit/audit.service.ts +990 -0
- package/app/services/audit/builders/audit-entry-builder.ts +32 -0
- package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
- package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
- package/app/services/audit/builders/index.ts +40 -0
- package/app/services/audit/index.ts +2 -0
- package/app/types/case.ts +2 -2
- package/app/types/exceljs-bare.d.ts +9 -0
- package/app/types/user.ts +1 -1
- package/app/utils/audit-export-signature.ts +2 -2
- package/app/utils/confirmation-signature.ts +3 -3
- package/app/utils/data-operations.ts +5 -5
- package/app/utils/mfa-phone.ts +1 -1
- package/app/utils/mfa.ts +1 -1
- package/app/utils/permissions.ts +2 -2
- package/functions/[[path]].ts +2 -2
- package/package.json +34 -20
- package/public/vendor/exceljs.LICENSE +22 -0
- package/public/vendor/exceljs.bare.min.js +45 -0
- package/scripts/deploy-all.sh +52 -0
- package/scripts/deploy-config.sh +282 -1
- package/tsconfig.json +18 -8
- package/vite.config.ts +6 -22
- package/worker-configuration.d.ts +4435 -562
- package/workers/audit-worker/package.json +8 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +8 -4
- package/workers/data-worker/src/data-worker.example.ts +3 -3
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +8 -4
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +8 -4
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +8 -4
- package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
- package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
- package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +8 -4
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/services/audit-export.service.ts +0 -755
- package/app/services/audit.service.ts +0 -1474
- /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
- /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import paths from '~/config/config.json';
|
|
2
|
+
import { type ValidationAuditEntry } from '~/types';
|
|
3
|
+
import { getDataApiKey } from '~/utils/auth';
|
|
4
|
+
|
|
5
|
+
const AUDIT_WORKER_URL = paths.audit_worker_url;
|
|
6
|
+
|
|
7
|
+
interface FetchAuditEntriesParams {
|
|
8
|
+
userId: string;
|
|
9
|
+
startDate?: string;
|
|
10
|
+
endDate?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface FetchAuditEntriesResponse {
|
|
14
|
+
entries: ValidationAuditEntry[];
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PersistAuditEntryResponse {
|
|
19
|
+
success: boolean;
|
|
20
|
+
entryCount: number;
|
|
21
|
+
filename: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PersistAuditEntryResult =
|
|
25
|
+
| {
|
|
26
|
+
ok: true;
|
|
27
|
+
entryCount: number;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
ok: false;
|
|
31
|
+
status: number;
|
|
32
|
+
errorData: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function fetchAuditEntriesForUser(
|
|
36
|
+
params: FetchAuditEntriesParams
|
|
37
|
+
): Promise<ValidationAuditEntry[] | null> {
|
|
38
|
+
const apiKey = await getDataApiKey();
|
|
39
|
+
const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
|
|
40
|
+
url.searchParams.set('userId', params.userId);
|
|
41
|
+
|
|
42
|
+
if (params.startDate) {
|
|
43
|
+
url.searchParams.set('startDate', params.startDate);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (params.endDate) {
|
|
47
|
+
url.searchParams.set('endDate', params.endDate);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const response = await fetch(url.toString(), {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
headers: {
|
|
53
|
+
'X-Custom-Auth-Key': apiKey
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = (await response.json()) as FetchAuditEntriesResponse;
|
|
62
|
+
return result.entries;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function persistAuditEntryForUser(
|
|
66
|
+
entry: ValidationAuditEntry
|
|
67
|
+
): Promise<PersistAuditEntryResult> {
|
|
68
|
+
const apiKey = await getDataApiKey();
|
|
69
|
+
const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
|
|
70
|
+
url.searchParams.set('userId', entry.userId);
|
|
71
|
+
|
|
72
|
+
const response = await fetch(url.toString(), {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'X-Custom-Auth-Key': apiKey
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(entry)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorData = await response.json().catch(() => ({}));
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
status: response.status,
|
|
86
|
+
errorData
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = (await response.json()) as PersistAuditEntryResponse;
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
entryCount: result.entryCount
|
|
94
|
+
};
|
|
95
|
+
}
|