@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { SidebarContainer } from '~/components/sidebar/sidebar-container';
|
|
4
4
|
import { Toolbar } from '~/components/toolbar/toolbar';
|
|
@@ -9,7 +9,7 @@ import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
|
9
9
|
import { generatePDF } from '~/components/actions/generate-pdf';
|
|
10
10
|
import { getUserApiKey } from '~/utils/auth';
|
|
11
11
|
import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
|
|
12
|
-
import { AnnotationData, FileData } from '~/types';
|
|
12
|
+
import { type AnnotationData, type FileData } from '~/types';
|
|
13
13
|
import { checkCaseIsReadOnly } from '~/components/actions/case-manage';
|
|
14
14
|
import paths from '~/config/config.json';
|
|
15
15
|
import styles from './striae.module.css';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ValidationAuditEntry } from '~/types';
|
|
2
|
+
|
|
3
|
+
export const getAuditSecurityIssuesForConsole = (
|
|
4
|
+
entry: ValidationAuditEntry
|
|
5
|
+
): string[] => {
|
|
6
|
+
const checks = entry.details.securityChecks;
|
|
7
|
+
if (!checks) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const securityIssues = [];
|
|
12
|
+
|
|
13
|
+
// For console diagnostics, self-confirmation is relevant for import actions only.
|
|
14
|
+
if (entry.action === 'import' && checks.selfConfirmationPrevented === true) {
|
|
15
|
+
securityIssues.push('selfConfirmationPrevented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (checks.fileIntegrityValid === false) {
|
|
19
|
+
securityIssues.push('fileIntegrityValid');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (checks.exporterUidValidated === false) {
|
|
23
|
+
securityIssues.push('exporterUidValidated');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return securityIssues;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const logAuditEntryToConsole = (entry: ValidationAuditEntry): void => {
|
|
30
|
+
const icon = entry.result === 'success' ? '✅' :
|
|
31
|
+
entry.result === 'failure' ? '❌' : '⚠️';
|
|
32
|
+
|
|
33
|
+
console.log(
|
|
34
|
+
`${icon} Audit [${entry.action.toUpperCase()}]: ${entry.details.fileName} ` +
|
|
35
|
+
`(Case: ${entry.details.caseNumber || 'N/A'}) - ${entry.result.toUpperCase()}`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (entry.details.validationErrors.length > 0) {
|
|
39
|
+
console.log(' Errors:', entry.details.validationErrors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const securityIssues = getAuditSecurityIssuesForConsole(entry);
|
|
43
|
+
if (securityIssues.length > 0) {
|
|
44
|
+
console.warn(' Security Issues:', securityIssues);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type ValidationAuditEntry } from '~/types';
|
|
2
|
+
|
|
3
|
+
export const AUDIT_CSV_ENTRY_HEADERS = [
|
|
4
|
+
'Timestamp',
|
|
5
|
+
'User Email',
|
|
6
|
+
'Action',
|
|
7
|
+
'Result',
|
|
8
|
+
'File Name',
|
|
9
|
+
'File Type',
|
|
10
|
+
'Case Number',
|
|
11
|
+
'Confirmation ID',
|
|
12
|
+
'Original Examiner UID',
|
|
13
|
+
'Reviewing Examiner UID',
|
|
14
|
+
'File ID',
|
|
15
|
+
'Original Filename',
|
|
16
|
+
'File Size (MB)',
|
|
17
|
+
'MIME Type',
|
|
18
|
+
'Upload Method',
|
|
19
|
+
'Delete Reason',
|
|
20
|
+
'Annotation ID',
|
|
21
|
+
'Annotation Type',
|
|
22
|
+
'Annotation Tool',
|
|
23
|
+
'Session ID',
|
|
24
|
+
'User Agent',
|
|
25
|
+
'Processing Time (ms)',
|
|
26
|
+
'Hash Valid',
|
|
27
|
+
'Validation Errors',
|
|
28
|
+
'Security Issues',
|
|
29
|
+
'Workflow Phase',
|
|
30
|
+
'Profile Field',
|
|
31
|
+
'Old Value',
|
|
32
|
+
'New Value',
|
|
33
|
+
'Total Confirmations In File',
|
|
34
|
+
'Confirmations Successfully Imported',
|
|
35
|
+
'Validation Steps Failed',
|
|
36
|
+
'Case Name',
|
|
37
|
+
'Total Files',
|
|
38
|
+
'MFA Method',
|
|
39
|
+
'Security Incident Type',
|
|
40
|
+
'Security Severity'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const formatForCSV = (value?: string | number | null): string => {
|
|
44
|
+
if (value === undefined || value === null) return '';
|
|
45
|
+
const str = String(value);
|
|
46
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
47
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
48
|
+
}
|
|
49
|
+
return str;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getSecurityIssues = (entry: ValidationAuditEntry): string => {
|
|
53
|
+
const securityChecks = entry.details.securityChecks;
|
|
54
|
+
if (!securityChecks) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const issues = [];
|
|
59
|
+
|
|
60
|
+
if (securityChecks.selfConfirmationPrevented === true) {
|
|
61
|
+
issues.push('selfConfirmationPrevented');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (securityChecks.fileIntegrityValid === false) {
|
|
65
|
+
issues.push('fileIntegrityValid');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (securityChecks.exporterUidValidated === false) {
|
|
69
|
+
issues.push('exporterUidValidated');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return issues.join('; ');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
|
|
76
|
+
const fileDetails = entry.details.fileDetails;
|
|
77
|
+
const annotationDetails = entry.details.annotationDetails;
|
|
78
|
+
const sessionDetails = entry.details.sessionDetails;
|
|
79
|
+
const userProfileDetails = entry.details.userProfileDetails;
|
|
80
|
+
const caseDetails = entry.details.caseDetails;
|
|
81
|
+
const performanceMetrics = entry.details.performanceMetrics;
|
|
82
|
+
const securityDetails = entry.details.securityDetails;
|
|
83
|
+
const securityIssues = getSecurityIssues(entry);
|
|
84
|
+
|
|
85
|
+
const values = [
|
|
86
|
+
formatForCSV(entry.timestamp),
|
|
87
|
+
formatForCSV(entry.userEmail),
|
|
88
|
+
formatForCSV(entry.action),
|
|
89
|
+
formatForCSV(entry.result),
|
|
90
|
+
formatForCSV(entry.details.fileName),
|
|
91
|
+
formatForCSV(entry.details.fileType),
|
|
92
|
+
formatForCSV(entry.details.caseNumber),
|
|
93
|
+
formatForCSV(entry.details.confirmationId),
|
|
94
|
+
formatForCSV(entry.details.originalExaminerUid),
|
|
95
|
+
formatForCSV(entry.details.reviewingExaminerUid),
|
|
96
|
+
formatForCSV(fileDetails?.fileId),
|
|
97
|
+
formatForCSV(fileDetails?.originalFileName),
|
|
98
|
+
fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
|
|
99
|
+
formatForCSV(fileDetails?.mimeType),
|
|
100
|
+
formatForCSV(fileDetails?.uploadMethod),
|
|
101
|
+
formatForCSV(fileDetails?.deleteReason),
|
|
102
|
+
formatForCSV(annotationDetails?.annotationId),
|
|
103
|
+
formatForCSV(annotationDetails?.annotationType),
|
|
104
|
+
formatForCSV(annotationDetails?.tool),
|
|
105
|
+
formatForCSV(sessionDetails?.sessionId),
|
|
106
|
+
formatForCSV(sessionDetails?.userAgent),
|
|
107
|
+
performanceMetrics?.processingTimeMs || '',
|
|
108
|
+
entry.details.hashValid !== undefined ? (entry.details.hashValid ? 'Yes' : 'No') : '',
|
|
109
|
+
formatForCSV(entry.details.validationErrors?.join('; ')),
|
|
110
|
+
formatForCSV(securityIssues),
|
|
111
|
+
formatForCSV(entry.details.workflowPhase),
|
|
112
|
+
formatForCSV(userProfileDetails?.profileField),
|
|
113
|
+
formatForCSV(userProfileDetails?.oldValue),
|
|
114
|
+
formatForCSV(userProfileDetails?.newValue),
|
|
115
|
+
caseDetails?.totalAnnotations?.toString() || '',
|
|
116
|
+
performanceMetrics?.validationStepsCompleted?.toString() || '',
|
|
117
|
+
performanceMetrics?.validationStepsFailed?.toString() || '',
|
|
118
|
+
formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName),
|
|
119
|
+
caseDetails?.totalFiles?.toString() || '',
|
|
120
|
+
formatForCSV(securityDetails?.mfaMethod),
|
|
121
|
+
formatForCSV(securityDetails?.incidentType),
|
|
122
|
+
formatForCSV(securityDetails?.severity)
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return values.join(',');
|
|
126
|
+
};
|
|
@@ -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
|
+
};
|