@striae-org/striae 4.3.3 → 5.0.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.
- package/.env.example +4 -0
- package/app/components/actions/case-export/download-handlers.ts +60 -4
- package/app/components/actions/case-import/confirmation-import.ts +50 -7
- package/app/components/actions/case-import/confirmation-package.ts +99 -22
- package/app/components/actions/case-import/orchestrator.ts +116 -13
- package/app/components/actions/case-import/validation.ts +171 -7
- package/app/components/actions/case-import/zip-processing.ts +224 -127
- package/app/components/actions/case-manage.ts +110 -10
- package/app/components/actions/confirm-export.ts +32 -3
- package/app/components/audit/user-audit.module.css +49 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
- package/app/components/navbar/navbar.module.css +0 -10
- package/app/components/navbar/navbar.tsx +0 -22
- package/app/components/sidebar/case-import/case-import.module.css +7 -131
- package/app/components/sidebar/case-import/case-import.tsx +7 -14
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
- package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
- package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
- package/app/config-example/config.json +5 -0
- package/app/routes/auth/login.tsx +1 -1
- package/app/services/audit/audit-console-logger.ts +1 -1
- package/app/services/audit/audit-export-csv.ts +1 -1
- package/app/services/audit/audit-export-signing.ts +2 -2
- package/app/services/audit/audit-export.service.ts +1 -1
- package/app/services/audit/audit-worker-client.ts +1 -1
- package/app/services/audit/audit.service.ts +5 -75
- package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
- package/app/services/audit/index.ts +2 -2
- package/app/types/audit.ts +8 -7
- package/app/utils/data/operations/signing-operations.ts +93 -0
- package/app/utils/data/operations/types.ts +6 -0
- package/app/utils/forensics/export-encryption.ts +316 -0
- package/app/utils/forensics/export-verification.ts +1 -409
- package/app/utils/forensics/index.ts +1 -0
- package/app/utils/ui/case-messages.ts +5 -2
- package/package.json +1 -1
- package/scripts/deploy-config.sh +97 -3
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/data-worker.example.ts +130 -0
- package/workers/data-worker/src/encryption-utils.ts +125 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
|
@@ -42,7 +42,7 @@ export const AUDIT_CSV_ENTRY_HEADERS = [
|
|
|
42
42
|
'Confirmed Files'
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const formatForCSV = (value?: string | number | null): string => {
|
|
46
46
|
if (value === undefined || value === null) return '';
|
|
47
47
|
const str = String(value);
|
|
48
48
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
@@ -22,14 +22,14 @@ interface SignAuditExportInput {
|
|
|
22
22
|
hash: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
interface AuditExportSignature {
|
|
26
26
|
algorithm: string;
|
|
27
27
|
keyId: string;
|
|
28
28
|
signedAt: string;
|
|
29
29
|
value: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface SignedAuditExportPayload {
|
|
33
33
|
signatureMetadata: AuditExportSigningPayload;
|
|
34
34
|
signature: AuditExportSignature;
|
|
35
35
|
}
|
|
@@ -8,7 +8,7 @@ import { type AuditExportContext, signAuditExport } from './audit-export-signing
|
|
|
8
8
|
* Audit Export Service
|
|
9
9
|
* Handles exporting audit trails to various formats for compliance and forensic analysis
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
class AuditExportService {
|
|
12
12
|
private static instance: AuditExportService;
|
|
13
13
|
|
|
14
14
|
private constructor() {}
|
|
@@ -2,7 +2,6 @@ import type { User } from 'firebase/auth';
|
|
|
2
2
|
import type {
|
|
3
3
|
ValidationAuditEntry,
|
|
4
4
|
CreateAuditEntryParams,
|
|
5
|
-
AuditTrail,
|
|
6
5
|
AuditQueryParams,
|
|
7
6
|
WorkflowPhase,
|
|
8
7
|
AuditAction,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
import {
|
|
19
18
|
applyAuditEntryFilters,
|
|
20
19
|
applyAuditPagination,
|
|
21
|
-
generateAuditSummary,
|
|
22
20
|
sortAuditEntriesNewestFirst
|
|
23
21
|
} from './audit-query-helpers';
|
|
24
22
|
import { logAuditEntryToConsole } from './audit-console-logger';
|
|
@@ -58,7 +56,7 @@ import {
|
|
|
58
56
|
* Audit Service for ValidationAuditEntry system
|
|
59
57
|
* Provides comprehensive audit logging throughout the confirmation workflow
|
|
60
58
|
*/
|
|
61
|
-
|
|
59
|
+
class AuditService {
|
|
62
60
|
private static instance: AuditService;
|
|
63
61
|
private auditBuffer: ValidationAuditEntry[] = [];
|
|
64
62
|
private workflowId: string | null = null;
|
|
@@ -383,13 +381,15 @@ export class AuditService {
|
|
|
383
381
|
public async logCaseCreation(
|
|
384
382
|
user: User,
|
|
385
383
|
caseNumber: string,
|
|
386
|
-
caseName: string
|
|
384
|
+
caseName: string,
|
|
385
|
+
renamedFromCaseNumber?: string
|
|
387
386
|
): Promise<void> {
|
|
388
387
|
await this.logEventForUser(user,
|
|
389
388
|
buildCaseCreationAuditParams({
|
|
390
389
|
user,
|
|
391
390
|
caseNumber,
|
|
392
|
-
caseName
|
|
391
|
+
caseName,
|
|
392
|
+
renamedFromCaseNumber
|
|
393
393
|
})
|
|
394
394
|
);
|
|
395
395
|
}
|
|
@@ -721,37 +721,6 @@ export class AuditService {
|
|
|
721
721
|
);
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
-
/**
|
|
725
|
-
* Log user account deletion event
|
|
726
|
-
*/
|
|
727
|
-
public async logAccountDeletion(
|
|
728
|
-
user: User,
|
|
729
|
-
result: AuditResult,
|
|
730
|
-
deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
|
|
731
|
-
confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
|
|
732
|
-
casesCount?: number,
|
|
733
|
-
filesCount?: number,
|
|
734
|
-
dataRetentionPeriod?: number,
|
|
735
|
-
emailNotificationSent?: boolean,
|
|
736
|
-
sessionId?: string,
|
|
737
|
-
errors: string[] = []
|
|
738
|
-
): Promise<void> {
|
|
739
|
-
// Wrapper that extracts user data and calls the simplified version
|
|
740
|
-
return this.logAccountDeletionSimple(
|
|
741
|
-
user.uid,
|
|
742
|
-
user.email || '',
|
|
743
|
-
result,
|
|
744
|
-
deletionReason,
|
|
745
|
-
confirmationMethod,
|
|
746
|
-
casesCount,
|
|
747
|
-
filesCount,
|
|
748
|
-
dataRetentionPeriod,
|
|
749
|
-
emailNotificationSent,
|
|
750
|
-
sessionId,
|
|
751
|
-
errors
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
724
|
/**
|
|
756
725
|
* Log user account deletion event with simplified user data
|
|
757
726
|
*/
|
|
@@ -1011,32 +980,6 @@ export class AuditService {
|
|
|
1011
980
|
return await this.getAuditEntries(queryParams, params?.requestingUser);
|
|
1012
981
|
}
|
|
1013
982
|
|
|
1014
|
-
/**
|
|
1015
|
-
* Get audit trail for a case
|
|
1016
|
-
*/
|
|
1017
|
-
public async getAuditTrail(caseNumber: string): Promise<AuditTrail | null> {
|
|
1018
|
-
try {
|
|
1019
|
-
// Implement retrieval from storage
|
|
1020
|
-
const entries = await this.getAuditEntries({ caseNumber });
|
|
1021
|
-
if (!entries || entries.length === 0) {
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const summary = generateAuditSummary(entries);
|
|
1026
|
-
const workflowId = this.workflowId || `${caseNumber}-archived`;
|
|
1027
|
-
|
|
1028
|
-
return {
|
|
1029
|
-
caseNumber,
|
|
1030
|
-
workflowId,
|
|
1031
|
-
entries,
|
|
1032
|
-
summary
|
|
1033
|
-
};
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
console.error('🚨 Audit: Failed to get audit trail:', error);
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
983
|
/**
|
|
1041
984
|
* Get audit entries based on query parameters
|
|
1042
985
|
*/
|
|
@@ -1143,19 +1086,6 @@ export class AuditService {
|
|
|
1143
1086
|
}
|
|
1144
1087
|
}
|
|
1145
1088
|
|
|
1146
|
-
/**
|
|
1147
|
-
* Clear audit buffer (for testing)
|
|
1148
|
-
*/
|
|
1149
|
-
public clearBuffer(): void {
|
|
1150
|
-
this.auditBuffer = [];
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
/**
|
|
1154
|
-
* Get current buffer size (for monitoring)
|
|
1155
|
-
*/
|
|
1156
|
-
public getBufferSize(): number {
|
|
1157
|
-
return this.auditBuffer.length;
|
|
1158
|
-
}
|
|
1159
1089
|
}
|
|
1160
1090
|
|
|
1161
1091
|
// Export singleton instance
|
|
@@ -6,6 +6,7 @@ interface BuildCaseCreationAuditParamsInput {
|
|
|
6
6
|
user: User;
|
|
7
7
|
caseNumber: string;
|
|
8
8
|
caseName: string;
|
|
9
|
+
renamedFromCaseNumber?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const buildCaseCreationAuditParams = (
|
|
@@ -22,7 +23,9 @@ export const buildCaseCreationAuditParams = (
|
|
|
22
23
|
caseNumber: input.caseNumber,
|
|
23
24
|
workflowPhase: 'casework',
|
|
24
25
|
caseDetails: {
|
|
26
|
+
oldCaseName: input.renamedFromCaseNumber,
|
|
25
27
|
newCaseName: input.caseName,
|
|
28
|
+
createdByRename: Boolean(input.renamedFromCaseNumber),
|
|
26
29
|
createdDate: new Date().toISOString(),
|
|
27
30
|
totalFiles: 0,
|
|
28
31
|
totalAnnotations: 0
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { auditService } from './audit.service';
|
|
2
|
+
export { auditExportService } from './audit-export.service';
|
package/app/types/audit.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ValidationAuditEntry {
|
|
|
44
44
|
* Detailed information for each audit entry
|
|
45
45
|
* Contains action-specific data and metadata
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
interface AuditDetails {
|
|
48
48
|
// Core identification
|
|
49
49
|
fileName?: string;
|
|
50
50
|
fileType?: AuditFileType;
|
|
@@ -194,9 +194,10 @@ export interface AuditQueryParams {
|
|
|
194
194
|
/**
|
|
195
195
|
* Case management specific audit details
|
|
196
196
|
*/
|
|
197
|
-
|
|
197
|
+
interface CaseAuditDetails {
|
|
198
198
|
oldCaseName?: string;
|
|
199
199
|
newCaseName?: string;
|
|
200
|
+
createdByRename?: boolean;
|
|
200
201
|
totalFiles?: number;
|
|
201
202
|
totalAnnotations?: number;
|
|
202
203
|
confirmedFileNames?: string[];
|
|
@@ -210,7 +211,7 @@ export interface CaseAuditDetails {
|
|
|
210
211
|
/**
|
|
211
212
|
* File operation specific audit details
|
|
212
213
|
*/
|
|
213
|
-
|
|
214
|
+
interface FileAuditDetails {
|
|
214
215
|
fileId?: string;
|
|
215
216
|
originalFileName?: string;
|
|
216
217
|
fileSize: number;
|
|
@@ -225,7 +226,7 @@ export interface FileAuditDetails {
|
|
|
225
226
|
/**
|
|
226
227
|
* Annotation operation specific audit details
|
|
227
228
|
*/
|
|
228
|
-
|
|
229
|
+
interface AnnotationAuditDetails {
|
|
229
230
|
annotationId?: string;
|
|
230
231
|
annotationType?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
|
|
231
232
|
annotationData?: unknown; // The actual annotation data structure
|
|
@@ -238,7 +239,7 @@ export interface AnnotationAuditDetails {
|
|
|
238
239
|
/**
|
|
239
240
|
* User session specific audit details
|
|
240
241
|
*/
|
|
241
|
-
|
|
242
|
+
interface SessionAuditDetails {
|
|
242
243
|
sessionId?: string;
|
|
243
244
|
userAgent?: string;
|
|
244
245
|
sessionDuration?: number;
|
|
@@ -249,7 +250,7 @@ export interface SessionAuditDetails {
|
|
|
249
250
|
/**
|
|
250
251
|
* Security incident specific audit details
|
|
251
252
|
*/
|
|
252
|
-
|
|
253
|
+
interface SecurityAuditDetails {
|
|
253
254
|
incidentType?: 'unauthorized-access' | 'data-breach' | 'malware' | 'injection' | 'brute-force' | 'privilege-escalation';
|
|
254
255
|
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
255
256
|
targetResource?: string;
|
|
@@ -272,7 +273,7 @@ export interface SecurityAuditDetails {
|
|
|
272
273
|
/**
|
|
273
274
|
* User profile and authentication specific audit details
|
|
274
275
|
*/
|
|
275
|
-
|
|
276
|
+
interface UserProfileAuditDetails {
|
|
276
277
|
profileField?: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId';
|
|
277
278
|
oldValue?: string;
|
|
278
279
|
newValue?: string;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ForensicManifestSignature,
|
|
14
14
|
FORENSIC_MANIFEST_VERSION
|
|
15
15
|
} from '../../forensics/SHA256';
|
|
16
|
+
import type { EncryptionManifest } from '../../forensics/export-encryption';
|
|
16
17
|
import { canAccessCase, validateUserSession } from '../permissions';
|
|
17
18
|
import type {
|
|
18
19
|
AuditExportSigningResponse,
|
|
@@ -223,3 +224,95 @@ export const signAuditExportData = async (
|
|
|
223
224
|
throw error;
|
|
224
225
|
}
|
|
225
226
|
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Request batch decryption of export data file and images from the data worker
|
|
230
|
+
*/
|
|
231
|
+
export const decryptExportBatch = async (
|
|
232
|
+
user: User,
|
|
233
|
+
encryptionManifest: EncryptionManifest,
|
|
234
|
+
encryptedDataBase64: string,
|
|
235
|
+
encryptedImageMap: Record<string, string>
|
|
236
|
+
): Promise<{ plaintext: string; decryptedImages: Record<string, Blob> }> => {
|
|
237
|
+
try {
|
|
238
|
+
const sessionValidation = await validateUserSession(user);
|
|
239
|
+
if (!sessionValidation.valid) {
|
|
240
|
+
throw new Error(`Session validation failed: ${sessionValidation.reason}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Convert encryptedImageMap to array format expected by worker, including per-image IV from manifest
|
|
244
|
+
const encryptedImages = Object.entries(encryptedImageMap).map(([filename, encryptedData]) => {
|
|
245
|
+
const manifestEntry = encryptionManifest.encryptedImages.find(e => e.filename === filename);
|
|
246
|
+
return {
|
|
247
|
+
filename,
|
|
248
|
+
encryptedData,
|
|
249
|
+
iv: manifestEntry?.iv
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const response = await fetchDataApi(user, '/api/forensic/decrypt-export', {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/json'
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
userId: user.uid,
|
|
260
|
+
wrappedKey: encryptionManifest.wrappedKey,
|
|
261
|
+
dataIv: encryptionManifest.dataIv,
|
|
262
|
+
encryptedData: encryptedDataBase64,
|
|
263
|
+
encryptedImages,
|
|
264
|
+
keyId: encryptionManifest.keyId
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const responseData = await response.json().catch(() => null) as {
|
|
269
|
+
success?: boolean;
|
|
270
|
+
error?: string;
|
|
271
|
+
plaintext?: string;
|
|
272
|
+
decryptedImages?: Array<{ filename: string; data: string }>;
|
|
273
|
+
} | null;
|
|
274
|
+
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const errorMessage = responseData?.error || `Failed to decrypt export: ${response.status} ${response.statusText}`;
|
|
277
|
+
|
|
278
|
+
// Special handling for encrypted exports without configured key
|
|
279
|
+
if (response.status === 400 && errorMessage.includes('not configured')) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
'This export is encrypted. To import it, your Striae instance must have EXPORT_ENCRYPTION_PRIVATE_KEY configured.'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw new Error(errorMessage);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!responseData?.success || !responseData.plaintext) {
|
|
289
|
+
throw new Error('Invalid decrypt response from data worker');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Convert decrypted image base64 data back to Blobs
|
|
293
|
+
const decryptedImages: Record<string, Blob> = {};
|
|
294
|
+
if (Array.isArray(responseData.decryptedImages)) {
|
|
295
|
+
for (const imageEntry of responseData.decryptedImages) {
|
|
296
|
+
try {
|
|
297
|
+
const binaryString = atob(imageEntry.data);
|
|
298
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
299
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
300
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
301
|
+
}
|
|
302
|
+
decryptedImages[imageEntry.filename] = new Blob([bytes]);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(`Failed to convert decrypted image ${imageEntry.filename}:`, error);
|
|
305
|
+
throw new Error(`Failed to convert decrypted image: ${imageEntry.filename}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
plaintext: responseData.plaintext,
|
|
312
|
+
decryptedImages
|
|
313
|
+
};
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('Error decrypting export batch:', error);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
@@ -39,4 +39,10 @@ export interface AuditExportSigningResponse {
|
|
|
39
39
|
signature: ForensicManifestSignature;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface DecryptExportBatchResponse {
|
|
43
|
+
success: boolean;
|
|
44
|
+
plaintext: string;
|
|
45
|
+
decryptedImages: Array<{ filename: string; data: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
export type DataOperation<T> = (user: User, ...args: unknown[]) => Promise<T>;
|