Skip to content

Commit

Permalink
TechDebt: Centralize Virus Scan (#1365)
Browse files Browse the repository at this point in the history
* Centralize Virus scan
  • Loading branch information
NickPhura authored Sep 16, 2024
1 parent edb69b0 commit 556ba8d
Show file tree
Hide file tree
Showing 19 changed files with 27 additions and 227 deletions.
23 changes: 20 additions & 3 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
import swaggerUIExperss from 'swagger-ui-express';
import { defaultPoolConfig, initDBPool } from './database/db';
import { ensureHTTPError, HTTP500 } from './errors/http-error';
import { ensureHTTPError, HTTP400, HTTP500 } from './errors/http-error';
import {
authorizeAndAuthenticateMiddleware,
getCritterbaseProxyMiddleware,
replaceAuthorizationHeaderMiddleware
} from './middleware/critterbase-proxy';
import { rootAPIDoc } from './openapi/root-api-doc';
import { authenticateRequest, authenticateRequestOptional } from './request-handlers/security/authentication';
import { scanFileForVirus } from './utils/file-utils';
import { getLogger } from './utils/logger';

const defaultLog = getLogger('app');
Expand Down Expand Up @@ -75,7 +76,7 @@ const openAPIFramework = initialize({
'application/json': express.json({ limit: MAX_REQ_BODY_SIZE }),
'multipart/form-data': function (req, res, next) {
const multerRequestHandler = multer({
storage: multer.memoryStorage(),
storage: multer.memoryStorage(), // TODO change to local/PVC storage and stream file uploads to S3?
limits: { fileSize: MAX_UPLOAD_FILE_SIZE }
}).array('media', MAX_UPLOAD_NUM_FILES);

Expand All @@ -89,11 +90,27 @@ const openAPIFramework = initialize({
*
* @see https://www.npmjs.com/package/express-openapi#argsconsumesmiddleware
*/
multerRequestHandler(req, res, (error?: any) => {
multerRequestHandler(req, res, async (error?: any) => {
if (error) {
return next(error);
}

// Scan files for malicious content, if enabled
const virusScanPromises = (req.files as Express.Multer.File[]).map(async function (file) {
const isSafe = await scanFileForVirus(file);

if (!isSafe) {
throw new HTTP400('Malicious file content detected.', [{ file_name: file.originalname }]);
}
});

try {
await Promise.all(virusScanPromises);
} catch (error) {
// If a virus is detected, return error and do not continue
return next(error);
}

// Ensure `req.files` or `req.body.media` is always set to an array
const multerFiles = req.files ?? [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const expectedError = new Error('cannot process request');
sinon.stub(AttachmentService.prototype, 'upsertProjectReportAttachment').rejects(expectedError);

Expand All @@ -69,7 +67,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);
sinon.stub(file_utils, 'uploadFileToS3').resolves();

const expectedResponse = { attachmentId: 1, revision_count: 1 };
Expand Down
10 changes: 1 addition & 9 deletions api/src/paths/project/{projectId}/attachments/report/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../constants/roles';
import { getDBConnection } from '../../../../../database/db';
import { HTTP400 } from '../../../../../errors/http-error';
import { fileSchema } from '../../../../../openapi/schemas/file';
import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization';
import { AttachmentService } from '../../../../../services/attachment-service';
import { scanFileForVirus, uploadFileToS3 } from '../../../../../utils/file-utils';
import { uploadFileToS3 } from '../../../../../utils/file-utils';
import { getLogger } from '../../../../../utils/logger';
import { getFileFromRequest } from '../../../../../utils/request';

Expand Down Expand Up @@ -158,13 +157,6 @@ export function uploadMedia(): RequestHandler {
try {
await connection.open();

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const attachmentService = new AttachmentService(connection);

//Upsert a report attachment
Expand Down
3 changes: 0 additions & 3 deletions api/src/paths/project/{projectId}/attachments/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const expectedError = new Error('cannot process request');
sinon.stub(AttachmentService.prototype, 'upsertProjectAttachment').rejects(expectedError);

Expand All @@ -67,7 +65,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);
sinon.stub(file_utils, 'uploadFileToS3').resolves();

const expectedResponse = { attachmentId: 1, revision_count: 1 };
Expand Down
10 changes: 1 addition & 9 deletions api/src/paths/project/{projectId}/attachments/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import { Operation } from 'express-openapi';
import { ATTACHMENT_TYPE } from '../../../../constants/attachments';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../constants/roles';
import { getDBConnection } from '../../../../database/db';
import { HTTP400 } from '../../../../errors/http-error';
import { fileSchema } from '../../../../openapi/schemas/file';
import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization';
import { AttachmentService } from '../../../../services/attachment-service';
import { scanFileForVirus, uploadFileToS3 } from '../../../../utils/file-utils';
import { uploadFileToS3 } from '../../../../utils/file-utils';
import { getLogger } from '../../../../utils/logger';
import { getFileFromRequest } from '../../../../utils/request';

Expand Down Expand Up @@ -133,13 +132,6 @@ export function uploadMedia(): RequestHandler {
try {
await connection.open();

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const attachmentService = new AttachmentService(connection);

const upsertResult = await attachmentService.upsertProjectAttachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ describe('uploadMedia', () => {
}
} as any;

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const expectedError = new Error('cannot process request');
sinon.stub(AttachmentService.prototype, 'upsertSurveyReportAttachment').rejects(expectedError);

Expand All @@ -59,7 +57,6 @@ describe('uploadMedia', () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);
sinon.stub(file_utils, 'uploadFileToS3').resolves();

const mockReq = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../errors/http-error';
import { fileSchema } from '../../../../../../../openapi/schemas/file';
import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization';
import { AttachmentService } from '../../../../../../../services/attachment-service';
import { scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils';
import { uploadFileToS3 } from '../../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../../utils/logger';
import { getFileFromRequest } from '../../../../../../../utils/request';

Expand Down Expand Up @@ -175,13 +174,6 @@ export function uploadMedia(): RequestHandler {
try {
await connection.open();

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const attachmentService = new AttachmentService(connection);

const upsertResult = await attachmentService.upsertSurveyReportAttachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,10 @@ describe('postSurveyTelemetryCredentialAttachment', () => {
sinon.restore();
});

it('should throw an error when file has malicious content', async () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(false); // fail virus scan

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.keycloak_token = {} as KeycloakUserInformation;
mockReq.params = {
projectId: '1',
surveyId: '2'
};
mockReq.files = [
{
fieldname: 'media',
originalname: 'test.keyx',
encoding: '7bit',
mimetype: 'text/plain',
size: 340
}
] as Express.Multer.File[];

const requestHandler = postSurveyTelemetryCredentialAttachment();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled');
}
});

it('should throw an error when file type is invalid', async () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.keycloak_token = {} as KeycloakUserInformation;
Expand Down Expand Up @@ -93,8 +57,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const upsertSurveyTelemetryCredentialAttachmentStub = sinon
.stub(AttachmentService.prototype, 'upsertSurveyTelemetryCredentialAttachment')
.resolves({ survey_telemetry_credential_attachment_id: 44, key: 'path/to/file/test.keyx' });
Expand Down Expand Up @@ -134,8 +96,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const upsertSurveyTelemetryCredentialAttachmentStub = sinon
.stub(AttachmentService.prototype, 'upsertSurveyTelemetryCredentialAttachment')
.resolves({ survey_telemetry_credential_attachment_id: 44, key: 'path/to/file/test.keyx' });
Expand Down Expand Up @@ -175,8 +135,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const upsertSurveyTelemetryCredentialAttachmentStub = sinon
.stub(AttachmentService.prototype, 'upsertSurveyTelemetryCredentialAttachment')
.resolves({ survey_telemetry_credential_attachment_id: 44, key: 'path/to/file/test.keyx' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { authorizeRequestHandler } from '../../../../../../../request-handlers/s
import { AttachmentService } from '../../../../../../../services/attachment-service';
import { BctwKeyxService } from '../../../../../../../services/bctw-service/bctw-keyx-service';
import { getBctwUser } from '../../../../../../../services/bctw-service/bctw-service';
import { scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils';
import { uploadFileToS3 } from '../../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../../utils/logger';
import { isValidTelementryCredentialFile } from '../../../../../../../utils/media/media-utils';
import { getFileFromRequest } from '../../../../../../../utils/request';
Expand Down Expand Up @@ -139,13 +139,6 @@ export function postSurveyTelemetryCredentialAttachment(): RequestHandler {
files: { ...rawMediaFile, buffer: 'Too big to print' }
});

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const isTelemetryCredentialFile = isValidTelementryCredentialFile(rawMediaFile);

if (isTelemetryCredentialFile.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);

const expectedError = new Error('cannot process request');
sinon.stub(AttachmentService.prototype, 'upsertSurveyAttachment').rejects(expectedError);

Expand All @@ -67,7 +65,6 @@ describe('uploadMedia', () => {
}
});

sinon.stub(file_utils, 'scanFileForVirus').resolves(true);
sinon.stub(file_utils, 'uploadFileToS3').resolves();

const expectedResponse = { attachmentId: 1, revision_count: 1 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import { Operation } from 'express-openapi';
import { ATTACHMENT_TYPE } from '../../../../../../constants/attachments';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../database/db';
import { HTTP400 } from '../../../../../../errors/http-error';
import { fileSchema } from '../../../../../../openapi/schemas/file';
import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization';
import { AttachmentService } from '../../../../../../services/attachment-service';
import { scanFileForVirus, uploadFileToS3 } from '../../../../../../utils/file-utils';
import { uploadFileToS3 } from '../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../utils/logger';
import { getFileFromRequest } from '../../../../../../utils/request';

Expand Down Expand Up @@ -124,13 +123,6 @@ export function uploadMedia(): RequestHandler {
try {
await connection.open();

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const attachmentService = new AttachmentService(connection);

const upsertResult = await attachmentService.upsertSurveyAttachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../errors/http-error';
import { csvFileSchema } from '../../../../../../../openapi/schemas/file';
import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization';
import { ImportCapturesStrategy } from '../../../../../../../services/import-services/capture/import-captures-strategy';
import { importCSV } from '../../../../../../../services/import-services/import-csv';
import { scanFileForVirus } from '../../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../../utils/logger';
import { parseMulterFile } from '../../../../../../../utils/media/media-utils';
import { getFileFromRequest } from '../../../../../../../utils/request';
Expand Down Expand Up @@ -135,13 +133,6 @@ export function importCsv(): RequestHandler {
try {
await connection.open();

// Check for viruses / malware
const virusScanResult = await scanFileForVirus(rawFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, import cancelled.');
}

const importCsvCaptures = new ImportCapturesStrategy(connection, surveyId);

// Pass CSV file and importer as dependencies
Expand Down
Loading

0 comments on commit 556ba8d

Please sign in to comment.