diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx index ed86c2db7c..1080a918a7 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx @@ -244,8 +244,7 @@ export const MyInfoFieldPanel = ({ searchValue }: { searchValue: string }) => { )} - {user?.betaFlags?.children && - form?.responseMode === FormResponseMode.Email ? ( + {user?.betaFlags?.children ? ( {(provided) => ( diff --git a/src/app/modules/myinfo/myinfo.util.ts b/src/app/modules/myinfo/myinfo.util.ts index e8c15fddd8..ce847be7c2 100644 --- a/src/app/modules/myinfo/myinfo.util.ts +++ b/src/app/modules/myinfo/myinfo.util.ts @@ -521,7 +521,7 @@ export const handleMyInfoChildHashResponse = ( // Validate each answer (child) childAnswer.forEach((attrAnswer, subFieldIndex) => { const key = getMyInfoChildHashKey( - field._id as string, + field._id, subFields[subFieldIndex], childIndex, childName, diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts index 4f61409015..584b588851 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts @@ -166,6 +166,7 @@ export const validateStorageSubmissionParams = celebrate({ */ const asyncVirusScanning = ( responses: ParsedClearFormFieldResponse[], + formId: string, ): ResultAsync< ParsedClearFormFieldResponse, | VirusScanFailedError @@ -176,6 +177,7 @@ const asyncVirusScanning = ( if (isQuarantinedAttachmentResponse(response)) { return SubmissionService.triggerVirusScanThenDownloadCleanFileChain( response, + formId, ) } @@ -191,6 +193,7 @@ const asyncVirusScanning = ( */ const devModeSyncVirusScanning = async ( responses: ParsedClearFormFieldResponse[], + formId: string, ): Promise< Result< ParsedClearFormFieldResponse, @@ -209,6 +212,7 @@ const devModeSyncVirusScanning = async ( const attachmentResponse = await SubmissionService.triggerVirusScanThenDownloadCleanFileChain( response, + formId, ) results.push(attachmentResponse) if (attachmentResponse.isErr()) break @@ -246,8 +250,18 @@ export const scanAndRetrieveAttachments = async ( // run the virus scanning asynchronously for better performance (lower latency). // Note on .combine: if any scans or downloads error out, it will short circuit and return the first error. isDev - ? Result.combine(await devModeSyncVirusScanning(req.body.responses)) - : await ResultAsync.combine(asyncVirusScanning(req.body.responses)) + ? Result.combine( + await devModeSyncVirusScanning( + req.body.responses, + req.formsg.formDef._id.toString(), + ), + ) + : await ResultAsync.combine( + asyncVirusScanning( + req.body.responses, + req.formsg.formDef._id.toString(), + ), + ) if (scanAndRetrieveFilesResult.isErr()) { logger.error({ diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts index bc1f243a13..590b3f44d0 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts @@ -184,6 +184,7 @@ type IdTaggedParsedClearAttachmentResponseV3 = */ const asyncVirusScanning = ( responses: IdTaggedParsedClearAttachmentResponseV3[], + formId: string, ): ResultAsync< IdTaggedParsedClearAttachmentResponseV3, | VirusScanFailedError @@ -191,7 +192,7 @@ const asyncVirusScanning = ( | MaliciousFileDetectedError >[] => responses.map((response) => - triggerVirusScanThenDownloadCleanFileChain(response.answer).map( + triggerVirusScanThenDownloadCleanFileChain(response.answer, formId).map( (attachmentResponse) => ({ ...response, answer: attachmentResponse }), ), ) @@ -203,6 +204,7 @@ const asyncVirusScanning = ( */ const devModeSyncVirusScanning = async ( responses: IdTaggedParsedClearAttachmentResponseV3[], + formId: string, ): Promise< Result< IdTaggedParsedClearAttachmentResponseV3, @@ -216,6 +218,7 @@ const devModeSyncVirusScanning = async ( // await to pause for...of loop until the virus scanning and downloading of clean file is completed. const attachmentResponse = await triggerVirusScanThenDownloadCleanFileChain( response.answer, + formId, ) if (attachmentResponse.isErr()) { results.push(err(attachmentResponse.error)) @@ -267,10 +270,16 @@ export const scanAndRetrieveAttachments = async ( // Note on .combine: if any scans or downloads error out, it will short circuit and return the first error. isDev ? Result.combine( - await devModeSyncVirusScanning(attachmentResponsesToRetrieve), + await devModeSyncVirusScanning( + attachmentResponsesToRetrieve, + req.formsg.formDef._id.toString(), + ), ) : await ResultAsync.combine( - asyncVirusScanning(attachmentResponsesToRetrieve), + asyncVirusScanning( + attachmentResponsesToRetrieve, + req.formsg.formDef._id.toString(), + ), ) if (scanAndRetrieveFilesResult.isErr()) { diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index f573d1bb53..3f4a7e722f 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -432,33 +432,55 @@ export const triggerVirusScanThenDownloadCleanFileChain = < | ParsedClearAttachmentFieldResponseV3, >( response: T, + formId: string, ): ResultAsync< T, | VirusScanFailedError | DownloadCleanFileFailedError | MaliciousFileDetectedError -> => +> => { + const logMeta = { + action: 'triggerVirusScanThenDownloadCleanFileChain', + formId, + quarantineFileKey: response.answer, + } // Step 3: Trigger lambda to scan attachments. - triggerVirusScanning(response.answer) - .mapErr((error) => { - if (error instanceof MaliciousFileDetectedError) - return new MaliciousFileDetectedError(response.filename) - return error - }) - .map((lambdaOutput) => lambdaOutput.body) - // Step 4: Retrieve attachments from the clean bucket. - .andThen((cleanAttachment) => - // Retrieve attachment from clean bucket. - downloadCleanFile( - cleanAttachment.cleanFileKey, - cleanAttachment.destinationVersionId, - ).map((attachmentBuffer) => ({ - ...response, - // Replace content with attachmentBuffer and answer with filename. - content: attachmentBuffer, - answer: response.filename, - })), - ) + return ( + triggerVirusScanning(response.answer) + .mapErr((error) => { + if (error instanceof MaliciousFileDetectedError) { + logger.error({ + message: 'Malicious file detected during lambda virus scan', + meta: logMeta, + error, + }) + return new MaliciousFileDetectedError(response.filename) + } + return error + }) + .map((lambdaOutput) => { + logger.info({ + message: + 'Successfully retrieved clean file from virus scanning lambda', + meta: { ...logMeta, cleanFileKey: lambdaOutput.body.cleanFileKey }, + }) + return lambdaOutput.body + }) + // Step 4: Retrieve attachments from the clean bucket. + .andThen((cleanAttachment) => + // Retrieve attachment from clean bucket. + downloadCleanFile( + cleanAttachment.cleanFileKey, + cleanAttachment.destinationVersionId, + ).map((attachmentBuffer) => ({ + ...response, + // Replace content with attachmentBuffer and answer with filename. + content: attachmentBuffer, + answer: response.filename, + })), + ) + ) +} type AttachmentReducerData = { attachmentMetadata: AttachmentMetadata // type alias for Map diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 495fc5f91d..d02d2308c5 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -91,7 +91,6 @@ import { MyInfoMissingLoginCookieError, } from '../myinfo/myinfo.errors' import { MyInfoKey } from '../myinfo/myinfo.types' -import { getMyInfoChildHashKey } from '../myinfo/myinfo.util' import { InvalidPaymentProductsError, PaymentNotFoundError, @@ -752,17 +751,12 @@ export const getAnswersForChild = ( return [] } return response.answerArray.flatMap((arr, childIdx) => { - // First array element is always child name - const childName = arr[0] return arr.map((answer, idx) => { const subfield = subFields[idx] return { - _id: getMyInfoChildHashKey( - response._id, - subFields[idx], - childIdx, - childName, - ), + // Recreates the individual _id of the child field based on the parent field's _id and the subfield + // e.g., childrenbirthrecords.67585515e1ced6d790a91e14.childname.0 + _id: `${MyInfoAttribute.ChildrenBirthRecords}.${response._id}.${subFields[idx]}.${childIdx}`, fieldType: response.fieldType, // qnChildIdx represents the index of the MyInfo field // childIdx represents the index of the child in this MyInfo field