@striae-org/striae 5.4.2 → 5.4.3
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/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-export/metadata-helpers.ts +2 -4
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/mobile-warning/mobile-warning.module.css +80 -0
- package/app/components/mobile-warning/mobile-warning.tsx +108 -0
- package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
- package/app/config-example/config.json +2 -2
- package/app/root.tsx +2 -0
- package/app/services/audit/audit-file-type.ts +0 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
- package/app/services/audit/index.ts +0 -1
- package/app/types/audit.ts +1 -1
- package/app/utils/data/permissions.ts +17 -15
- package/app/utils/forensics/audit-export-signature.ts +4 -4
- package/app/utils/forensics/export-verification.ts +3 -11
- package/package.json +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/src/audit-worker.example.ts +1 -1
- package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +17 -17
- package/workers/data-worker/src/encryption-utils.ts +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +4 -4
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
- package/app/services/audit/audit-export-csv.ts +0 -130
- package/app/services/audit/audit-export-report.ts +0 -205
- package/app/services/audit/audit-export.service.ts +0 -333
package/wrangler.toml.example
CHANGED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
-
import type { User } from 'firebase/auth';
|
|
4
|
-
import { auditExportService } from '~/services/audit';
|
|
5
|
-
import type { AuditTrail, ValidationAuditEntry } from '~/types';
|
|
6
|
-
|
|
7
|
-
interface UseAuditViewerExportParams {
|
|
8
|
-
user: User | null;
|
|
9
|
-
effectiveCaseNumber?: string;
|
|
10
|
-
filteredEntries: ValidationAuditEntry[];
|
|
11
|
-
auditTrail: AuditTrail | null;
|
|
12
|
-
setError: Dispatch<SetStateAction<string>>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const useAuditViewerExport = ({
|
|
16
|
-
user,
|
|
17
|
-
effectiveCaseNumber,
|
|
18
|
-
filteredEntries,
|
|
19
|
-
auditTrail,
|
|
20
|
-
setError
|
|
21
|
-
}: UseAuditViewerExportParams) => {
|
|
22
|
-
const resolveExportContext = useCallback(() => {
|
|
23
|
-
if (!user) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const identifier = effectiveCaseNumber || user.uid;
|
|
28
|
-
const scopeType: 'case' | 'user' = effectiveCaseNumber ? 'case' : 'user';
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
identifier,
|
|
32
|
-
scopeType,
|
|
33
|
-
context: {
|
|
34
|
-
user,
|
|
35
|
-
scopeType,
|
|
36
|
-
scopeIdentifier: identifier,
|
|
37
|
-
caseNumber: effectiveCaseNumber || undefined
|
|
38
|
-
} as const
|
|
39
|
-
};
|
|
40
|
-
}, [user, effectiveCaseNumber]);
|
|
41
|
-
|
|
42
|
-
const handleExportCSV = useCallback(async () => {
|
|
43
|
-
const exportContextData = resolveExportContext();
|
|
44
|
-
if (!exportContextData) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const filename = auditExportService.generateFilename(
|
|
49
|
-
exportContextData.scopeType,
|
|
50
|
-
exportContextData.identifier,
|
|
51
|
-
'csv'
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
56
|
-
await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContextData.context);
|
|
57
|
-
} else {
|
|
58
|
-
await auditExportService.exportToCSV(filteredEntries, filename, exportContextData.context);
|
|
59
|
-
}
|
|
60
|
-
} catch (exportError) {
|
|
61
|
-
console.error('Export failed:', exportError);
|
|
62
|
-
setError('Failed to export audit trail to CSV');
|
|
63
|
-
}
|
|
64
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
65
|
-
|
|
66
|
-
const handleExportJSON = useCallback(async () => {
|
|
67
|
-
const exportContextData = resolveExportContext();
|
|
68
|
-
if (!exportContextData) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const filename = auditExportService.generateFilename(
|
|
73
|
-
exportContextData.scopeType,
|
|
74
|
-
exportContextData.identifier,
|
|
75
|
-
'json'
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
80
|
-
await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContextData.context);
|
|
81
|
-
} else {
|
|
82
|
-
await auditExportService.exportToJSON(filteredEntries, filename, exportContextData.context);
|
|
83
|
-
}
|
|
84
|
-
} catch (exportError) {
|
|
85
|
-
console.error('Export failed:', exportError);
|
|
86
|
-
setError('Failed to export audit trail to JSON');
|
|
87
|
-
}
|
|
88
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
89
|
-
|
|
90
|
-
const handleGenerateReport = useCallback(async () => {
|
|
91
|
-
const exportContextData = resolveExportContext();
|
|
92
|
-
if (!exportContextData) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const resolvedUser = exportContextData.context.user;
|
|
97
|
-
|
|
98
|
-
const filename = `${exportContextData.scopeType}-audit-report-${exportContextData.identifier}-${new Date().toISOString().split('T')[0]}.txt`;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
let reportContent: string;
|
|
102
|
-
|
|
103
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
104
|
-
reportContent = await auditExportService.generateReportSummary(auditTrail, exportContextData.context);
|
|
105
|
-
} else {
|
|
106
|
-
const totalEntries = filteredEntries.length;
|
|
107
|
-
const successfulActions = filteredEntries.filter(entry => entry.result === 'success').length;
|
|
108
|
-
const failedActions = filteredEntries.filter(entry => entry.result === 'failure').length;
|
|
109
|
-
|
|
110
|
-
const actionCounts = filteredEntries.reduce((accumulator, entry) => {
|
|
111
|
-
accumulator[entry.action] = (accumulator[entry.action] || 0) + 1;
|
|
112
|
-
return accumulator;
|
|
113
|
-
}, {} as Record<string, number>);
|
|
114
|
-
|
|
115
|
-
const detectedDateRange = filteredEntries.length > 0
|
|
116
|
-
? {
|
|
117
|
-
earliest: new Date(Math.min(...filteredEntries.map(entry => new Date(entry.timestamp).getTime()))),
|
|
118
|
-
latest: new Date(Math.max(...filteredEntries.map(entry => new Date(entry.timestamp).getTime())))
|
|
119
|
-
}
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
reportContent = `${effectiveCaseNumber ? 'CASE' : 'USER'} AUDIT REPORT
|
|
123
|
-
Generated: ${new Date().toISOString()}
|
|
124
|
-
${effectiveCaseNumber ? `Case: ${effectiveCaseNumber}` : `User: ${resolvedUser.email}`}
|
|
125
|
-
${effectiveCaseNumber ? '' : `User ID: ${resolvedUser.uid}`}
|
|
126
|
-
|
|
127
|
-
=== SUMMARY ===
|
|
128
|
-
Total Actions: ${totalEntries}
|
|
129
|
-
Successful: ${successfulActions}
|
|
130
|
-
Failed: ${failedActions}
|
|
131
|
-
Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
|
|
132
|
-
|
|
133
|
-
${detectedDateRange ? `Date Range: ${detectedDateRange.earliest.toLocaleDateString()} - ${detectedDateRange.latest.toLocaleDateString()}` : 'No entries found'}
|
|
134
|
-
|
|
135
|
-
=== ACTION BREAKDOWN ===
|
|
136
|
-
${Object.entries(actionCounts)
|
|
137
|
-
.sort(([, actionCountA], [, actionCountB]) => actionCountB - actionCountA)
|
|
138
|
-
.map(([action, count]) => `${action}: ${count}`)
|
|
139
|
-
.join('\n')}
|
|
140
|
-
|
|
141
|
-
=== RECENT ACTIVITIES ===
|
|
142
|
-
${filteredEntries.slice(0, 10).map(entry =>
|
|
143
|
-
`${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
|
|
144
|
-
).join('\n')}
|
|
145
|
-
|
|
146
|
-
Generated by Striae
|
|
147
|
-
`;
|
|
148
|
-
|
|
149
|
-
reportContent = await auditExportService.appendSignedReportIntegrity(
|
|
150
|
-
reportContent,
|
|
151
|
-
exportContextData.context,
|
|
152
|
-
totalEntries
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const blob = new Blob([reportContent], { type: 'text/plain' });
|
|
157
|
-
const url = URL.createObjectURL(blob);
|
|
158
|
-
const anchor = document.createElement('a');
|
|
159
|
-
anchor.href = url;
|
|
160
|
-
anchor.download = filename;
|
|
161
|
-
document.body.appendChild(anchor);
|
|
162
|
-
anchor.click();
|
|
163
|
-
document.body.removeChild(anchor);
|
|
164
|
-
URL.revokeObjectURL(url);
|
|
165
|
-
} catch (reportError) {
|
|
166
|
-
console.error('Report generation failed:', reportError);
|
|
167
|
-
setError('Failed to generate audit report');
|
|
168
|
-
}
|
|
169
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
handleExportCSV,
|
|
173
|
-
handleExportJSON,
|
|
174
|
-
handleGenerateReport
|
|
175
|
-
};
|
|
176
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
'Badge/ID',
|
|
34
|
-
'Total Confirmations In File',
|
|
35
|
-
'Confirmations Successfully Imported',
|
|
36
|
-
'Validation Steps Failed',
|
|
37
|
-
'Case Name',
|
|
38
|
-
'Total Files',
|
|
39
|
-
'MFA Method',
|
|
40
|
-
'Security Incident Type',
|
|
41
|
-
'Security Severity',
|
|
42
|
-
'Confirmed Files'
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const formatForCSV = (value?: string | number | null): string => {
|
|
46
|
-
if (value === undefined || value === null) return '';
|
|
47
|
-
const str = String(value);
|
|
48
|
-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
49
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
50
|
-
}
|
|
51
|
-
return str;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const getSecurityIssues = (entry: ValidationAuditEntry): string => {
|
|
55
|
-
const securityChecks = entry.details.securityChecks;
|
|
56
|
-
if (!securityChecks) {
|
|
57
|
-
return '';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const issues = [];
|
|
61
|
-
|
|
62
|
-
if (securityChecks.selfConfirmationPrevented === true) {
|
|
63
|
-
issues.push('selfConfirmationPrevented');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (securityChecks.fileIntegrityValid === false) {
|
|
67
|
-
issues.push('fileIntegrityValid');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (securityChecks.exporterUidValidated === false) {
|
|
71
|
-
issues.push('exporterUidValidated');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return issues.join('; ');
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
|
|
78
|
-
const fileDetails = entry.details.fileDetails;
|
|
79
|
-
const annotationDetails = entry.details.annotationDetails;
|
|
80
|
-
const sessionDetails = entry.details.sessionDetails;
|
|
81
|
-
const userProfileDetails = entry.details.userProfileDetails;
|
|
82
|
-
const caseDetails = entry.details.caseDetails;
|
|
83
|
-
const performanceMetrics = entry.details.performanceMetrics;
|
|
84
|
-
const securityDetails = entry.details.securityDetails;
|
|
85
|
-
const securityIssues = getSecurityIssues(entry);
|
|
86
|
-
|
|
87
|
-
const values = [
|
|
88
|
-
formatForCSV(entry.timestamp),
|
|
89
|
-
formatForCSV(entry.userEmail),
|
|
90
|
-
formatForCSV(entry.action),
|
|
91
|
-
formatForCSV(entry.result),
|
|
92
|
-
formatForCSV(entry.details.fileName),
|
|
93
|
-
formatForCSV(entry.details.fileType),
|
|
94
|
-
formatForCSV(entry.details.caseNumber),
|
|
95
|
-
formatForCSV(entry.details.confirmationId),
|
|
96
|
-
formatForCSV(entry.details.originalExaminerUid),
|
|
97
|
-
formatForCSV(entry.details.reviewingExaminerUid),
|
|
98
|
-
formatForCSV(fileDetails?.fileId),
|
|
99
|
-
formatForCSV(fileDetails?.originalFileName),
|
|
100
|
-
fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
|
|
101
|
-
formatForCSV(fileDetails?.mimeType),
|
|
102
|
-
formatForCSV(fileDetails?.uploadMethod),
|
|
103
|
-
formatForCSV(fileDetails?.deleteReason),
|
|
104
|
-
formatForCSV(annotationDetails?.annotationId),
|
|
105
|
-
formatForCSV(annotationDetails?.annotationType),
|
|
106
|
-
formatForCSV(annotationDetails?.tool),
|
|
107
|
-
formatForCSV(sessionDetails?.sessionId),
|
|
108
|
-
formatForCSV(sessionDetails?.userAgent),
|
|
109
|
-
performanceMetrics?.processingTimeMs || '',
|
|
110
|
-
entry.details.hashValid !== undefined ? (entry.details.hashValid ? 'Yes' : 'No') : '',
|
|
111
|
-
formatForCSV(entry.details.validationErrors?.join('; ')),
|
|
112
|
-
formatForCSV(securityIssues),
|
|
113
|
-
formatForCSV(entry.details.workflowPhase),
|
|
114
|
-
formatForCSV(userProfileDetails?.profileField),
|
|
115
|
-
formatForCSV(userProfileDetails?.oldValue),
|
|
116
|
-
formatForCSV(userProfileDetails?.newValue),
|
|
117
|
-
formatForCSV(userProfileDetails?.badgeId),
|
|
118
|
-
caseDetails?.totalAnnotations?.toString() || '',
|
|
119
|
-
performanceMetrics?.validationStepsCompleted?.toString() || '',
|
|
120
|
-
performanceMetrics?.validationStepsFailed?.toString() || '',
|
|
121
|
-
formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName),
|
|
122
|
-
caseDetails?.totalFiles?.toString() || '',
|
|
123
|
-
formatForCSV(securityDetails?.mfaMethod),
|
|
124
|
-
formatForCSV(securityDetails?.incidentType),
|
|
125
|
-
formatForCSV(securityDetails?.severity),
|
|
126
|
-
formatForCSV(caseDetails?.confirmedFileNames?.join('; '))
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
return values.join(',');
|
|
130
|
-
};
|
|
@@ -1,205 +0,0 @@
|
|
|
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 Map<string, { uid: string; badgeId?: string; confirmedFiles: Set<string> }>();
|
|
88
|
-
const allConfirmedFiles = new Set<string>();
|
|
89
|
-
|
|
90
|
-
imports.forEach(entry => {
|
|
91
|
-
const metrics = entry.details.performanceMetrics;
|
|
92
|
-
const caseDetails = entry.details.caseDetails;
|
|
93
|
-
|
|
94
|
-
if (metrics?.validationStepsCompleted) {
|
|
95
|
-
totalConfirmationsImported += metrics.validationStepsCompleted;
|
|
96
|
-
}
|
|
97
|
-
if (caseDetails?.totalAnnotations) {
|
|
98
|
-
totalConfirmationsInFiles += caseDetails.totalAnnotations;
|
|
99
|
-
}
|
|
100
|
-
if (entry.details.reviewingExaminerUid) {
|
|
101
|
-
const uid = entry.details.reviewingExaminerUid;
|
|
102
|
-
const badgeId = entry.details.reviewerBadgeId;
|
|
103
|
-
const confirmedFileNames = caseDetails?.confirmedFileNames || [];
|
|
104
|
-
|
|
105
|
-
if (!reviewingExaminers.has(uid)) {
|
|
106
|
-
reviewingExaminers.set(uid, {
|
|
107
|
-
uid,
|
|
108
|
-
badgeId,
|
|
109
|
-
confirmedFiles: new Set()
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const examiner = reviewingExaminers.get(uid)!;
|
|
114
|
-
confirmedFileNames.forEach(file => {
|
|
115
|
-
examiner.confirmedFiles.add(file);
|
|
116
|
-
allConfirmedFiles.add(file);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const examinersDetail = Array.from(reviewingExaminers.values())
|
|
122
|
-
.map(examiner => {
|
|
123
|
-
const badgeInfo = examiner.badgeId ? ` (Badge: ${examiner.badgeId})` : '';
|
|
124
|
-
const filesInfo = examiner.confirmedFiles.size > 0
|
|
125
|
-
? `\n Confirmed Files: ${Array.from(examiner.confirmedFiles).sort().join(', ')}`
|
|
126
|
-
: '';
|
|
127
|
-
return `- UID: ${examiner.uid}${badgeInfo}${filesInfo}`;
|
|
128
|
-
})
|
|
129
|
-
.join('\n');
|
|
130
|
-
|
|
131
|
-
return [
|
|
132
|
-
`Confirmation Operations: ${confirmationEntries.length}`,
|
|
133
|
-
`- Imports: ${imports.length}`,
|
|
134
|
-
`- Exports: ${exports.length}`,
|
|
135
|
-
`- Creations: ${creations.length}`,
|
|
136
|
-
'',
|
|
137
|
-
`Total Confirmations Imported: ${totalConfirmationsImported}`,
|
|
138
|
-
`Total Confirmations in Import Files: ${totalConfirmationsInFiles}`,
|
|
139
|
-
`Reviewing Examiners Involved: ${reviewingExaminers.size}`,
|
|
140
|
-
'',
|
|
141
|
-
reviewingExaminers.size > 0
|
|
142
|
-
? `External Reviewers:\n${examinersDetail}`
|
|
143
|
-
: 'No external reviewers detected',
|
|
144
|
-
'',
|
|
145
|
-
allConfirmedFiles.size > 0
|
|
146
|
-
? `Successfully Confirmed Files (Total: ${allConfirmedFiles.size}):\n${Array.from(allConfirmedFiles).sort().map(file => ` - ${file}`).join('\n')}`
|
|
147
|
-
: 'No files confirmed'
|
|
148
|
-
].join('\n');
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const buildAuditReportContent = (auditTrail: AuditTrail, generatedAt: string): string => {
|
|
152
|
-
const summary = auditTrail.summary;
|
|
153
|
-
const successRate = ((summary.successfulEvents / summary.totalEvents) * 100).toFixed(1);
|
|
154
|
-
|
|
155
|
-
return `
|
|
156
|
-
STRIAE AUDIT TRAIL REPORT
|
|
157
|
-
============================
|
|
158
|
-
|
|
159
|
-
Case Number: ${auditTrail.caseNumber}
|
|
160
|
-
Workflow ID: ${auditTrail.workflowId}
|
|
161
|
-
Report Generated: ${new Date(generatedAt).toLocaleString()}
|
|
162
|
-
|
|
163
|
-
SUMMARY STATISTICS
|
|
164
|
-
------------------
|
|
165
|
-
Total Events: ${summary.totalEvents}
|
|
166
|
-
Successful Events: ${summary.successfulEvents} (${successRate}%)
|
|
167
|
-
Failed Events: ${summary.failedEvents}
|
|
168
|
-
Warning Events: ${summary.warningEvents}
|
|
169
|
-
Security Incidents: ${summary.securityIncidents}
|
|
170
|
-
|
|
171
|
-
COMPLIANCE STATUS
|
|
172
|
-
-----------------
|
|
173
|
-
Status: ${summary.complianceStatus.toUpperCase()}
|
|
174
|
-
${summary.complianceStatus === 'compliant'
|
|
175
|
-
? '✅ All audit events completed successfully'
|
|
176
|
-
: '⚠️ Some audit events failed - requires investigation'}
|
|
177
|
-
|
|
178
|
-
TIMELINE
|
|
179
|
-
--------
|
|
180
|
-
Start Time: ${new Date(summary.startTimestamp).toLocaleString()}
|
|
181
|
-
End Time: ${new Date(summary.endTimestamp).toLocaleString()}
|
|
182
|
-
Duration: ${calculateDuration(summary.startTimestamp, summary.endTimestamp)}
|
|
183
|
-
|
|
184
|
-
PARTICIPANTS
|
|
185
|
-
------------
|
|
186
|
-
Users Involved: ${summary.participatingUsers.length}
|
|
187
|
-
${summary.participatingUsers.map(uid => `- User ID: ${uid}`).join('\n')}
|
|
188
|
-
|
|
189
|
-
WORKFLOW PHASES
|
|
190
|
-
---------------
|
|
191
|
-
${summary.workflowPhases.map(phase => `- ${phase}`).join('\n')}
|
|
192
|
-
|
|
193
|
-
SECURITY ANALYSIS
|
|
194
|
-
-----------------
|
|
195
|
-
${generateSecurityAnalysis(auditTrail.entries)}
|
|
196
|
-
|
|
197
|
-
CONFIRMATION WORKFLOW DETAILS
|
|
198
|
-
------------------------------
|
|
199
|
-
${generateConfirmationSummary(auditTrail.entries)}
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
This report contains ${summary.totalEvents} audit entries providing complete forensic accountability.
|
|
203
|
-
Generated by Striae
|
|
204
|
-
`.trim();
|
|
205
|
-
};
|