Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TechDebt: Centralize Virus Scan #1365

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 @@
'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 @@
*
* @see https://www.npmjs.com/package/express-openapi#argsconsumesmiddleware
*/
multerRequestHandler(req, res, (error?: any) => {
multerRequestHandler(req, res, async (error?: any) => {

Check warning on line 93 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L93

Added line #L93 was not covered by tests
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);

Check warning on line 100 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L99-L100

Added lines #L99 - L100 were not covered by tests

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

Check warning on line 103 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L103

Added line #L103 was not covered by tests
}
});

try {
await Promise.all(virusScanPromises);

Check warning on line 108 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L107-L108

Added lines #L107 - L108 were not covered by tests
} catch (error) {
// If a virus is detected, return error and do not continue
return next(error);

Check warning on line 111 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L111

Added line #L111 was not covered by tests
}

// 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
Loading