├── assets ├── clean-file.txt ├── infected-file.txt └── readme.api-documentation.png ├── nodejs-rest-client ├── src │ ├── core │ │ ├── lib │ │ │ ├── logger │ │ │ │ ├── index.ts │ │ │ │ ├── transport │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ILoggerTransport.ts │ │ │ │ │ └── impl │ │ │ │ │ │ └── WinstonLoggerTransport.ts │ │ │ │ └── logger │ │ │ │ │ └── CoreLogger.ts │ │ │ └── clamav │ │ │ │ ├── client │ │ │ │ ├── types │ │ │ │ │ ├── ClamAVPingDetails.ts │ │ │ │ │ ├── ClamAVScanStatus.ts │ │ │ │ │ ├── ClamAVVersionDetails.ts │ │ │ │ │ ├── ClamAVConnectionOptions.ts │ │ │ │ │ └── ClamAVScanDetails.ts │ │ │ │ ├── errors │ │ │ │ │ └── ClamAVClientError.ts │ │ │ │ ├── parser │ │ │ │ │ └── ClamAVClientResponseParser.ts │ │ │ │ └── ClamAVClient.ts │ │ │ │ ├── command │ │ │ │ ├── types │ │ │ │ │ ├── ClamaAVCommandType.ts │ │ │ │ │ └── ClamAVCommand.ts │ │ │ │ ├── data-transformer │ │ │ │ │ └── ClamAVCommandDataTransformer.ts │ │ │ │ └── factory │ │ │ │ │ ├── errors │ │ │ │ │ └── ClamAVCommandFactoryError.ts │ │ │ │ │ └── ClamAVCommandFactory.ts │ │ │ │ └── index.ts │ │ ├── service │ │ │ ├── io-parameters │ │ │ │ ├── ServiceOutputParameters.ts │ │ │ │ └── ServiceInputParameters.ts │ │ │ ├── service │ │ │ │ └── IService.ts │ │ │ ├── index.ts │ │ │ └── errors │ │ │ │ └── ServiceInputParametersValidationError.ts │ │ ├── configuration │ │ │ ├── index.ts │ │ │ ├── errors │ │ │ │ └── ConfigError.ts │ │ │ ├── parser │ │ │ │ └── EnvParser.ts │ │ │ └── config │ │ │ │ └── Config.ts │ │ ├── types │ │ │ └── PromiseCallback.ts │ │ └── base-errors │ │ │ └── RequestValidationError.ts │ ├── infrastructure │ │ ├── response │ │ │ ├── index.ts │ │ │ ├── code │ │ │ │ └── ServerResponseCode.ts │ │ │ └── response │ │ │ │ └── ServerResponse.ts │ │ ├── module │ │ │ ├── scanner │ │ │ │ ├── ScanTokens.ts │ │ │ │ └── ScanModule.ts │ │ │ ├── RootModule.ts │ │ │ └── infrastructure │ │ │ │ └── InfrastructureModule.ts │ │ ├── interceptor │ │ │ ├── LoggerInterceptor.ts │ │ │ └── ErrorHandlerInterceptor.ts │ │ └── server │ │ │ └── ServerApplication.ts │ ├── presentation │ │ └── rest-api-interface │ │ │ ├── documentation │ │ │ ├── scanner │ │ │ │ ├── ping │ │ │ │ │ ├── PingResponseDataModel.ts │ │ │ │ │ └── PingResponse.ts │ │ │ │ ├── sync-scan │ │ │ │ │ ├── SyncScanBody.ts │ │ │ │ │ ├── SyncScanResponse.ts │ │ │ │ │ └── SyncScanResponseDataModel.ts │ │ │ │ └── get-version │ │ │ │ │ ├── GetVersionResponse.ts │ │ │ │ │ └── GetVersionResponseDataModel.ts │ │ │ └── common │ │ │ │ └── BaseResponseModel.ts │ │ │ └── ScanController.ts │ ├── application │ │ └── scanner │ │ │ ├── io-parameters │ │ │ ├── output │ │ │ │ ├── PingScannerOutputParameters.ts │ │ │ │ ├── SyncScanOutputParameters.ts │ │ │ │ └── GetScannerVersionOutputParameters.ts │ │ │ └── input │ │ │ │ └── SyncScanInputParameters.ts │ │ │ ├── index.ts │ │ │ └── service │ │ │ ├── PingScannerService.ts │ │ │ ├── GetScannerVersionService.ts │ │ │ └── SyncScanService.ts │ └── bootstrap.ts ├── Dockerfile ├── test │ ├── .helper │ │ ├── SetupEnv.ts │ │ └── MockHelper.ts │ └── unit │ │ ├── core │ │ ├── service │ │ │ ├── errors │ │ │ │ └── ServiceInputParametersValidationError.spec.ts │ │ │ └── io-parameters │ │ │ │ └── ServiceInputParameters.spec.ts │ │ ├── configuration │ │ │ ├── errors │ │ │ │ └── ConfigError.spec.ts │ │ │ └── config │ │ │ │ └── Config.spec.ts │ │ └── lib │ │ │ ├── clamav │ │ │ ├── client │ │ │ │ ├── errors │ │ │ │ │ └── ClamAVClientError.spec.ts │ │ │ │ ├── parser │ │ │ │ │ └── ClamAVClientResponseParser.spec.ts │ │ │ │ └── ClamAVClient.spec.ts │ │ │ └── command │ │ │ │ ├── data-transformer │ │ │ │ └── ClamAVCommandDataTransformer.spec.ts │ │ │ │ └── factory │ │ │ │ ├── errors │ │ │ │ └── ClamAVCommandFactoryError.spec.ts │ │ │ │ └── ClamAVCommandFactory.spec.ts │ │ │ └── logger │ │ │ └── logger │ │ │ └── CoreLogger.spec.ts │ │ ├── application │ │ └── scanner │ │ │ └── service │ │ │ ├── PingScannerService.spec.ts │ │ │ ├── GetScannerVersionService.spec.ts │ │ │ └── SyncScanService.spec.ts │ │ └── infrastructure │ │ └── response │ │ └── response │ │ └── ServerResponse.spec.ts ├── env │ ├── .env │ └── unit-test.env ├── tsconfig.json ├── scripts │ ├── compiler.sh │ └── compiler-local.sh ├── jest-unit.json ├── tslint.json └── package.json ├── scanner └── clamav │ └── docker │ ├── docker-entrypoint.sh │ ├── clamd.conf │ ├── Dockerfile │ ├── talos.pub │ └── freshclam.conf ├── .gitignore ├── env └── api.env ├── sonar-project.properties ├── docker-compose.yml ├── LICENSE ├── .github └── workflows │ └── build.yml └── README.md /assets/clean-file.txt: -------------------------------------------------------------------------------- 1 | Clean file -------------------------------------------------------------------------------- /assets/infected-file.txt: -------------------------------------------------------------------------------- 1 | X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/logger/index.ts: -------------------------------------------------------------------------------- 1 | export { CoreLogger } from './logger/CoreLogger'; 2 | -------------------------------------------------------------------------------- /scanner/clamav/docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -m 4 | 5 | freshclam -d & 6 | clamd 7 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/service/io-parameters/ServiceOutputParameters.ts: -------------------------------------------------------------------------------- 1 | export class ServiceOutputParameters {} 2 | -------------------------------------------------------------------------------- /assets/readme.api-documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvarentsov/virus-scanner/HEAD/assets/readme.api-documentation.png -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/configuration/index.ts: -------------------------------------------------------------------------------- 1 | export { ConfigError } from './errors/ConfigError'; 2 | export { Config } from './config/Config'; 3 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/types/ClamAVPingDetails.ts: -------------------------------------------------------------------------------- 1 | export type ClamAVPingDetails = { 2 | 3 | Message: string; 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/types/PromiseCallback.ts: -------------------------------------------------------------------------------- 1 | declare type ResolveCallback = (value: T) => void; 2 | 3 | declare type RejectCallback = (error: Error) => void; 4 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/types/ClamAVScanStatus.ts: -------------------------------------------------------------------------------- 1 | export enum ClamAVScanStatus { 2 | 3 | CLEAN = 'CLEAN', 4 | 5 | INFECTED = 'INFECTED', 6 | 7 | } 8 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/response/index.ts: -------------------------------------------------------------------------------- 1 | export { ServerResponseCode } from './code/ServerResponseCode'; 2 | export { ServerResponse } from './response/ServerResponse'; 3 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/logger/transport/index.ts: -------------------------------------------------------------------------------- 1 | export { WinstonLoggerTransport } from './impl/WinstonLoggerTransport'; 2 | export { ILoggerTransport } from './ILoggerTransport'; 3 | -------------------------------------------------------------------------------- /nodejs-rest-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY dist /usr/src/app 6 | 7 | RUN yarn install --production 8 | 9 | CMD ["node", "bootstrap.js"] 10 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/types/ClamAVVersionDetails.ts: -------------------------------------------------------------------------------- 1 | export type ClamAVVersionDetails = { 2 | 3 | ClamAV: string; 4 | 5 | SignatureDatabase: { version: string, buildTime: string } 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/types/ClamAVConnectionOptions.ts: -------------------------------------------------------------------------------- 1 | export type ClamAVConnectionOptions = { 2 | 3 | host: string; 4 | 5 | port: number; 6 | 7 | timeoutInMs?: number; 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/command/types/ClamaAVCommandType.ts: -------------------------------------------------------------------------------- 1 | export enum ClamaAVCommandType { 2 | 3 | PING = 'PING', 4 | 5 | VERSION = 'VERSION', 6 | 7 | INSTREAM = 'INSTREAM', 8 | 9 | } 10 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/.helper/SetupEnv.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as path from 'path'; 3 | 4 | const envPath: string = path.resolve(__dirname, '../../env/unit-test.env'); 5 | 6 | dotenv.config({ path: envPath }); 7 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/types/ClamAVScanDetails.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVScanStatus } from './ClamAVScanStatus'; 2 | 3 | export type ClamAVScanDetails = { 4 | 5 | Message: string; 6 | 7 | Status: ClamAVScanStatus 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Intellij ### 2 | 3 | .idea/* 4 | 5 | ### NodeJS Rest Client ### 6 | 7 | nodejs-rest-client/dist/ 8 | nodejs-rest-client/node_modules/ 9 | nodejs-rest-client/src/node_modules/ 10 | nodejs-rest-client/unit-coverage/ 11 | 12 | test-report.xml 13 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/command/types/ClamAVCommand.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | 3 | export type ClamAVCommand = { 4 | 5 | name: string; 6 | 7 | prefix?: string; 8 | 9 | postfix?: string; 10 | 11 | data?: Readable; 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/ping/PingResponseDataModel.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class PingResponseDataModel { 4 | 5 | @ApiProperty({ type: 'string' }) 6 | public readonly message: string; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/sync-scan/SyncScanBody.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class SyncScanBody { 4 | 5 | @ApiProperty({ type: 'string', format: 'binary' }) 6 | public readonly file: string; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /scanner/clamav/docker/clamd.conf: -------------------------------------------------------------------------------- 1 | LogFile /dev/stdout 2 | LogTime yes 3 | LogClean yes 4 | LogSyslog no 5 | LogVerbose yes 6 | DatabaseDirectory /var/lib/clamav 7 | LocalSocket /var/run/clamav/clamd.socket 8 | TCPSocket 3310 9 | Foreground yes 10 | 11 | MaxScanSize 1000M 12 | MaxFileSize 1000M 13 | StreamMaxLength 1000M -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/module/scanner/ScanTokens.ts: -------------------------------------------------------------------------------- 1 | export enum ScanTokens { 2 | 3 | SyncScanService = 'SyncScanService', 4 | 5 | AsyncScanService = 'AsyncScanService', 6 | 7 | PingScannerService = 'PingScannerService', 8 | 9 | GetScannerVersionService = 'GetScannerVersionService', 10 | 11 | } 12 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/module/RootModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScanModule } from './scanner/ScanModule'; 3 | import { InfrastructureModule } from './infrastructure/InfrastructureModule'; 4 | 5 | @Module({ 6 | imports: [ 7 | InfrastructureModule, 8 | ScanModule 9 | ], 10 | }) 11 | export class RootModule {} 12 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/index.ts: -------------------------------------------------------------------------------- 1 | export { ClamAVClient } from './client/ClamAVClient'; 2 | 3 | export { ClamAVConnectionOptions } from './client/types/ClamAVConnectionOptions'; 4 | 5 | export { ClamAVScanDetails } from './client/types/ClamAVScanDetails'; 6 | export { ClamAVPingDetails } from './client/types/ClamAVPingDetails'; 7 | export { ClamAVVersionDetails } from './client/types/ClamAVVersionDetails'; 8 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/service/service/IService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceInputParameters, ServiceOutputParameters } from '..'; 2 | 3 | type ServiceInput = ServiceInputParameters | void; 4 | type ServiceOutput = ServiceOutputParameters | void; 5 | 6 | export interface IService { 7 | 8 | execute(inputParameters?: TInput): Promise; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/logger/transport/ILoggerTransport.ts: -------------------------------------------------------------------------------- 1 | export interface ILoggerTransport { 2 | 3 | log(message: string, context?: string): void; 4 | 5 | error(message: string, trace?: string, context?: string): void; 6 | 7 | warn(message: string, context?: string): void; 8 | 9 | debug(message: string, context?: string): void; 10 | 11 | verbose(message: string, context?: string): void; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/ping/PingResponse.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponseModel } from '../../common/BaseResponseModel'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { PingResponseDataModel } from './PingResponseDataModel'; 4 | 5 | export class PingResponse extends BaseResponseModel { 6 | 7 | @ApiProperty({ type: PingResponseDataModel }) 8 | public readonly data: PingResponseDataModel; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /env/api.env: -------------------------------------------------------------------------------- 1 | API_HOST=0.0.0.0 2 | API_PORT=1337 3 | API_BASE_PATH=api 4 | 5 | # 0 | 1 - Run application in cluster mode (https://nodejs.org/api/cluster.html#cluster_how_it_works) 6 | API_CLUSTER_ENABLE=0 7 | 8 | CLAMAV_HOST=scanner 9 | CLAMAV_PORT=3310 10 | 11 | # In miliseconds 12 | CLAMAV_TIMEOUT=60_000 13 | 14 | # TEXT | JSON 15 | LOG_FORMAT=TEXT 16 | 17 | # 0 | 1 18 | LOG_DISABLE_COLORS=1 19 | 20 | # In bytes - See limits in clamd.conf 21 | MAX_SYNC_SCAN_FILE_SIZE=31_457_280 -------------------------------------------------------------------------------- /nodejs-rest-client/env/.env: -------------------------------------------------------------------------------- 1 | API_HOST=127.0.0.1 2 | API_PORT=3005 3 | API_BASE_PATH=api 4 | 5 | # 0 | 1 - Run application in cluster mode (https://nodejs.org/api/cluster.html#cluster_how_it_works) 6 | API_CLUSTER_ENABLE=0 7 | 8 | CLAMAV_HOST=127.0.0.1 9 | CLAMAV_PORT=3310 10 | 11 | # In miliseconds 12 | CLAMAV_TIMEOUT=60_000 13 | 14 | # TEXT | JSON 15 | LOG_FORMAT=TEXT 16 | 17 | # 0 | 1 18 | LOG_DISABLE_COLORS=0 19 | 20 | # In bytes - See limits in clamd.conf 21 | MAX_SYNC_SCAN_FILE_SIZE=31_457_280 -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/sync-scan/SyncScanResponse.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponseModel } from '../../common/BaseResponseModel'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { SyncScanResponseDataModel } from './SyncScanResponseDataModel'; 4 | 5 | export class SyncScanResponse extends BaseResponseModel { 6 | 7 | @ApiProperty({ type: SyncScanResponseDataModel }) 8 | public readonly data: SyncScanResponseDataModel; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /nodejs-rest-client/env/unit-test.env: -------------------------------------------------------------------------------- 1 | API_HOST=127.0.0.1 2 | API_PORT=3005 3 | API_BASE_PATH=api 4 | 5 | # 0 | 1 - Run application in cluster mode (https://nodejs.org/api/cluster.html#cluster_how_it_works) 6 | API_CLUSTER_ENABLE=0 7 | 8 | CLAMAV_HOST=127.0.0.1 9 | CLAMAV_PORT=3310 10 | 11 | # In miliseconds 12 | CLAMAV_TIMEOUT=60_000 13 | 14 | # TEXT | JSON 15 | LOG_FORMAT=JSON 16 | 17 | # 0 | 1 18 | LOG_DISABLE_COLORS=1 19 | 20 | # In bytes - See limits in clamd.conf 21 | MAX_SYNC_SCAN_FILE_SIZE=31_457_280 -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/get-version/GetVersionResponse.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponseModel } from '../../common/BaseResponseModel'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { GetVersionResponseDataModel } from './GetVersionResponseDataModel'; 4 | 5 | export class GetVersionResponse extends BaseResponseModel { 6 | 7 | @ApiProperty({ type: GetVersionResponseDataModel }) 8 | public readonly data: GetVersionResponseDataModel; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/service/index.ts: -------------------------------------------------------------------------------- 1 | // Base Service 2 | 3 | export { IService } from './service/IService'; 4 | 5 | // Service Errors 6 | 7 | export { ServiceInputParametersValidationError } from './errors/ServiceInputParametersValidationError'; 8 | 9 | // IO Parameters 10 | 11 | export { 12 | ServiceInputParametersValidationDetails, 13 | ServiceInputParameters 14 | } from './io-parameters/ServiceInputParameters'; 15 | 16 | export { ServiceOutputParameters } from './io-parameters/ServiceOutputParameters'; 17 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/sync-scan/SyncScanResponseDataModel.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ClamAVScanStatus } from '../../../../../core/lib/clamav/client/types/ClamAVScanStatus'; 3 | 4 | export class SyncScanResponseDataModel { 5 | 6 | @ApiProperty({ type: 'string' }) 7 | public readonly message: string; 8 | 9 | @ApiProperty({ enum: [ClamAVScanStatus.CLEAN, ClamAVScanStatus.INFECTED] }) 10 | public readonly status: ClamAVScanStatus; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/scanner/get-version/GetVersionResponseDataModel.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ClamAVScanStatus } from '../../../../../core/lib/clamav/client/types/ClamAVScanStatus'; 3 | 4 | export class GetVersionResponseDataModel { 5 | 6 | @ApiProperty({ type: 'string' }) 7 | public readonly message: string; 8 | 9 | @ApiProperty({ enum: [ClamAVScanStatus.CLEAN, ClamAVScanStatus.INFECTED] }) 10 | public readonly status: ClamAVScanStatus; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/documentation/common/BaseResponseModel.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class BaseResponseModel { 4 | 5 | @ApiProperty({type: 'number'}) 6 | public readonly code: number; 7 | 8 | @ApiProperty({type: 'string'}) 9 | public readonly message: string; 10 | 11 | @ApiProperty({type: 'number', description: ' Timestamp in milliseconds'}) 12 | public readonly timestamp: number; 13 | 14 | @ApiProperty({type: 'object'}) 15 | public readonly data: {}; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/response/code/ServerResponseCode.ts: -------------------------------------------------------------------------------- 1 | export type ResponseCodeDescription = { code: number, message: string }; 2 | 3 | export class ServerResponseCode { 4 | 5 | public static readonly SUCCESS: ResponseCodeDescription = { 6 | code : 200, 7 | message: 'Success.' 8 | }; 9 | 10 | public static readonly REQUEST_VALIDATION_ERROR: ResponseCodeDescription = { 11 | code : 400, 12 | message: 'Request validation error.' 13 | }; 14 | 15 | public static readonly INTERNAL_ERROR: ResponseCodeDescription = { 16 | code : 500, 17 | message: 'Internal error.' 18 | }; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/io-parameters/output/PingScannerOutputParameters.ts: -------------------------------------------------------------------------------- 1 | import { ServiceOutputParameters } from '../../../../core/service'; 2 | import { ClamAVPingDetails } from '../../../../core/lib/clamav'; 3 | 4 | export class PingScannerOutputParameters extends ServiceOutputParameters { 5 | 6 | public readonly message: string; 7 | 8 | private constructor(pingDetails: ClamAVPingDetails) { 9 | super(); 10 | this.message = pingDetails.Message; 11 | } 12 | 13 | public static create(pingDetails: ClamAVPingDetails): PingScannerOutputParameters { 14 | return new PingScannerOutputParameters(pingDetails); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/module/infrastructure/InfrastructureModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; 3 | import { ErrorHandlerInterceptor } from '../../interceptor/ErrorHandlerInterceptor'; 4 | import { LoggerInterceptor } from '../../interceptor/LoggerInterceptor'; 5 | 6 | @Module({ 7 | providers: [ 8 | { 9 | provide : APP_FILTER, 10 | useClass: ErrorHandlerInterceptor, 11 | }, 12 | { 13 | provide : APP_INTERCEPTOR, 14 | useClass: LoggerInterceptor, 15 | }, 16 | ] 17 | }) 18 | export class InfrastructureModule {} 19 | -------------------------------------------------------------------------------- /nodejs-rest-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": "./src", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": [ 8 | "es2018" 9 | ], 10 | "types": [ 11 | "reflect-metadata", 12 | "jest", 13 | "node" 14 | ], 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "allowJs": false, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "removeComments": true, 21 | "sourceMap": true 22 | }, 23 | "typeRoots": [ 24 | "./src/core/types" 25 | ], 26 | "include": [ 27 | "./src/**/*.ts", 28 | "*.json" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=pvarentsov 2 | sonar.projectKey=pvarentsov_virus-scanner 3 | sonar.projectVersion=1.0.0 4 | 5 | sonar.sources=nodejs-rest-client/src 6 | sonar.tests=nodejs-rest-client/test 7 | sonar.test=nodejs-rest-client/test/**/*.spec.ts 8 | sonar.coverage.exclusions=\ 9 | nodejs-rest-client/src/infrastructure/interceptor/**,\ 10 | nodejs-rest-client/src/infrastructure/module/**,\ 11 | nodejs-rest-client/src/infrastructure/server/**,\ 12 | nodejs-rest-client/src/presentation/**,\ 13 | nodejs-rest-client/src/bootstrap.ts 14 | sonar.junit.reportPaths=nodejs-rest-client/unit-coverage/junit.xml 15 | sonar.typescript.lcov.reportPaths=nodejs-rest-client/unit-coverage/lcov.info 16 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/index.ts: -------------------------------------------------------------------------------- 1 | // Service 2 | 3 | export { SyncScanService } from './service/SyncScanService'; 4 | export { PingScannerService } from './service/PingScannerService'; 5 | export { GetScannerVersionService } from './service/GetScannerVersionService'; 6 | 7 | // IO Parameters 8 | 9 | export { SyncScanInputParameters } from './io-parameters/input/SyncScanInputParameters'; 10 | export { SyncScanOutputParameters } from './io-parameters/output/SyncScanOutputParameters'; 11 | 12 | export { PingScannerOutputParameters } from './io-parameters/output/PingScannerOutputParameters'; 13 | 14 | export { GetScannerVersionOutputParameters } from './io-parameters/output/GetScannerVersionOutputParameters'; 15 | -------------------------------------------------------------------------------- /nodejs-rest-client/scripts/compiler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | RUN_DIR=$(pwd) 6 | 7 | SCRIPT_DIR=$( 8 | cd $(dirname "$0") 9 | pwd 10 | ) 11 | 12 | clear_dist() { 13 | rm -rf ./dist/* 14 | } 15 | 16 | run_tsc() { 17 | ./node_modules/.bin/tsc --skipLibCheck 18 | } 19 | 20 | copy_configuration_files() { 21 | cp ./package.json ./dist/package.json 22 | } 23 | 24 | install_dependencies() { 25 | cd ./dist 26 | yarn install --production 27 | cd .. 28 | } 29 | 30 | compile() { 31 | clear_dist 32 | run_tsc 33 | copy_configuration_files 34 | install_dependencies 35 | } 36 | 37 | start() { 38 | cd "$SCRIPT_DIR" && cd .. 39 | compile 40 | cd "$RUN_DIR" 41 | } 42 | 43 | start 44 | -------------------------------------------------------------------------------- /nodejs-rest-client/scripts/compiler-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | RUN_DIR=$(pwd) 6 | 7 | SCRIPT_DIR=$( 8 | cd $(dirname "$0") 9 | pwd 10 | ) 11 | 12 | clear_dist() { 13 | rm -rf `find ./dist/* | grep -v "node_modules" | grep -v "yarn.lock"` 14 | } 15 | 16 | run_tsc() { 17 | ./node_modules/.bin/tsc --skipLibCheck 18 | } 19 | 20 | copy_configuration_files() { 21 | cp ./package.json ./dist/package.json 22 | } 23 | 24 | install_dependencies() { 25 | cd ./dist 26 | yarn install --production 27 | cd .. 28 | } 29 | 30 | compile() { 31 | clear_dist 32 | run_tsc 33 | copy_configuration_files 34 | install_dependencies 35 | } 36 | 37 | start() { 38 | cd "$SCRIPT_DIR" && cd .. 39 | compile 40 | cd "$RUN_DIR" 41 | } 42 | 43 | start 44 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/configuration/errors/ConfigError.ts: -------------------------------------------------------------------------------- 1 | export class ConfigError extends Error { 2 | 3 | public readonly message: string; 4 | 5 | private constructor(message: string) { 6 | super(); 7 | 8 | this.name = this.constructor.name; 9 | this.message = message; 10 | 11 | Error.captureStackTrace(this, this.constructor); 12 | } 13 | 14 | public static createVariableNotSetError(variable: string): ConfigError { 15 | const message: string = `${variable} not set.`; 16 | return new ConfigError(message); 17 | } 18 | 19 | public static createVariableParsingError(variable: string): ConfigError { 20 | const message: string = `${variable} parsing error.`; 21 | return new ConfigError(message); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/base-errors/RequestValidationError.ts: -------------------------------------------------------------------------------- 1 | export class RequestValidationError extends Error { 2 | 3 | public readonly message: string; 4 | 5 | private constructor(message: string) { 6 | super(); 7 | 8 | this.name = this.constructor.name; 9 | this.message = message; 10 | 11 | Error.captureStackTrace(this, this.constructor); 12 | } 13 | 14 | public static create(message: string): RequestValidationError { 15 | return new RequestValidationError(message); 16 | } 17 | 18 | public static FILE_SIZE_EXCEEDED_MESSAGE = (limit: number): string => { 19 | return `File size exceeded. Limit is ${limit} bytes.`; 20 | } 21 | 22 | public static MULTIPART_FORM_EMPTY_MESSAGE = (): string => { 23 | return `Multipart form is empty.`; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/io-parameters/output/SyncScanOutputParameters.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVScanStatus } from '../../../../core/lib/clamav/client/types/ClamAVScanStatus'; 2 | import { ServiceOutputParameters } from '../../../../core/service'; 3 | import { ClamAVScanDetails } from '../../../../core/lib/clamav'; 4 | 5 | export class SyncScanOutputParameters extends ServiceOutputParameters { 6 | 7 | public readonly message: string; 8 | 9 | public readonly status: ClamAVScanStatus; 10 | 11 | private constructor(scanDetails: ClamAVScanDetails) { 12 | super(); 13 | 14 | this.message = scanDetails.Message; 15 | this.status = scanDetails.Status; 16 | } 17 | 18 | public static create(scanDetails: ClamAVScanDetails): SyncScanOutputParameters { 19 | return new SyncScanOutputParameters(scanDetails); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /nodejs-rest-client/jest-unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "verbose": true, 5 | "testEnvironment": "node", 6 | "testRegex": "(/test/unit/.*|(\\.|/)(spec))\\.ts?$", 7 | "coverageReporters": [ 8 | "json-summary", 9 | "text", 10 | "lcov" 11 | ], 12 | "coverageDirectory": "./unit-coverage", 13 | "collectCoverageFrom": [ 14 | "./src/**/*.{ts,js}", 15 | "!./src/bootstrap.{ts,js}", 16 | "!./src/presentation/**", 17 | "!./src/infrastructure/interceptor/**", 18 | "!./src/infrastructure/module/**", 19 | "!./src/infrastructure/server/**" 20 | ], 21 | "transform": { 22 | "^.+\\.(t|j)s$": "ts-jest" 23 | }, 24 | "setupFiles": [ 25 | "./test/.helper/SetupEnv.ts" 26 | ], 27 | "reporters": [ 28 | "default", 29 | "jest-junit" 30 | ], 31 | "testResultsProcessor": "jest-sonar-reporter" 32 | } 33 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/module/scanner/ScanModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScanController } from '../../../presentation/rest-api-interface/ScanController'; 3 | import { GetScannerVersionService, PingScannerService, SyncScanService } from '../../../application/scanner'; 4 | import { ScanTokens } from './ScanTokens'; 5 | 6 | @Module({ 7 | controllers: [ 8 | ScanController 9 | ], 10 | providers: [ 11 | { 12 | provide : ScanTokens.SyncScanService, 13 | useClass: SyncScanService 14 | }, 15 | { 16 | provide : ScanTokens.PingScannerService, 17 | useClass: PingScannerService 18 | }, 19 | { 20 | provide : ScanTokens.GetScannerVersionService, 21 | useClass: GetScannerVersionService 22 | }, 23 | ] 24 | }) 25 | export class ScanModule {} 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | scanner: 6 | restart: always 7 | build: 8 | context: scanner/clamav/docker 9 | dockerfile: Dockerfile 10 | image: varentsovpavel/virus-scanner-clamav 11 | ports: 12 | - 3310:3310 13 | expose: 14 | - 3310 15 | networks: 16 | virus-scanner-network: 17 | aliases: 18 | - virus-scanner-network 19 | 20 | api: 21 | restart: always 22 | build: 23 | context: nodejs-rest-client/ 24 | dockerfile: Dockerfile 25 | image: varentsovpavel/virus-scanner-api 26 | depends_on: 27 | - scanner 28 | ports: 29 | - 1337:1337 30 | expose: 31 | - 1337 32 | env_file: 33 | - env/api.env 34 | networks: 35 | virus-scanner-network: 36 | aliases: 37 | - virus-scanner-network 38 | 39 | networks: 40 | 41 | virus-scanner-network: 42 | driver: bridge 43 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/errors/ClamAVClientError.ts: -------------------------------------------------------------------------------- 1 | export class ClamAVClientError extends Error { 2 | 3 | public readonly message: string; 4 | 5 | private constructor(message: string) { 6 | super(); 7 | 8 | this.name = this.constructor.name; 9 | this.message = message; 10 | 11 | Error.captureStackTrace(this, this.constructor); 12 | } 13 | 14 | public static createConnectionTimedOutError(): ClamAVClientError { 15 | const message: string = 'Connection to ClamAV timed out.'; 16 | return new ClamAVClientError(message); 17 | } 18 | 19 | public static createScanAbortedError(reason: string): ClamAVClientError { 20 | let message: string = `Scan aborted.`; 21 | 22 | if (reason.length > 0) { 23 | message = message + ` Reason: ${reason}.`; 24 | } 25 | 26 | return new ClamAVClientError(message); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/service/PingScannerService.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVClient, ClamAVConnectionOptions, ClamAVPingDetails } from '../../../core/lib/clamav'; 2 | import { IService } from '../../../core/service'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { PingScannerOutputParameters } from '..'; 5 | import { Config } from '../../../core/configuration'; 6 | 7 | @Injectable() 8 | export class PingScannerService implements IService { 9 | 10 | public async execute(): Promise { 11 | const connectionOptions: ClamAVConnectionOptions = { 12 | host : Config.CLAMAV_HOST, 13 | port : Config.CLAMAV_PORT, 14 | timeoutInMs: Config.CLAMAV_TIMEOUT, 15 | }; 16 | 17 | const pingDetails: ClamAVPingDetails = await ClamAVClient.ping(connectionOptions); 18 | 19 | return PingScannerOutputParameters.create(pingDetails); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/service/errors/ServiceInputParametersValidationError.ts: -------------------------------------------------------------------------------- 1 | import { ServiceInputParametersValidationDetails } from '..'; 2 | 3 | export class ServiceInputParametersValidationError extends Error { 4 | 5 | public readonly message: string; 6 | 7 | private readonly details: ServiceInputParametersValidationDetails; 8 | 9 | private constructor(details: ServiceInputParametersValidationDetails) { 10 | super(); 11 | 12 | this.name = this.constructor.name; 13 | this.message = this.constructor.name; 14 | this.details = details; 15 | 16 | Error.captureStackTrace(this, this.constructor); 17 | } 18 | 19 | public static create(details: ServiceInputParametersValidationDetails): ServiceInputParametersValidationError { 20 | return new ServiceInputParametersValidationError(details); 21 | } 22 | 23 | public getDetails(): ServiceInputParametersValidationDetails { 24 | return this.details; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/io-parameters/input/SyncScanInputParameters.ts: -------------------------------------------------------------------------------- 1 | import { ServiceInputParameters } from '../../../../core/service'; 2 | import { Readable } from 'stream'; 3 | import { IsInstance, IsNumber } from 'class-validator'; 4 | 5 | export class SyncScanInputParameters extends ServiceInputParameters { 6 | 7 | @IsInstance(Readable) 8 | public readonly file: Readable; 9 | 10 | @IsNumber() 11 | public readonly fileSizeInBytes: number; 12 | 13 | constructor(file: Readable, fileSizeInBytes: number) { 14 | super(); 15 | 16 | this.file = file; 17 | this.fileSizeInBytes = fileSizeInBytes; 18 | } 19 | 20 | public static async create(file: Readable, fileSizeInBytes: number): Promise { 21 | const inputParameters: SyncScanInputParameters = new SyncScanInputParameters(file, fileSizeInBytes); 22 | await inputParameters.validate(); 23 | 24 | return inputParameters; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/io-parameters/output/GetScannerVersionOutputParameters.ts: -------------------------------------------------------------------------------- 1 | import { ServiceOutputParameters } from '../../../../core/service'; 2 | import { ClamAVVersionDetails } from '../../../../core/lib/clamav'; 3 | 4 | export class GetScannerVersionOutputParameters extends ServiceOutputParameters { 5 | 6 | public readonly clamAV: string; 7 | 8 | public readonly signatureDatabase: { version: string, buildTime: string }; 9 | 10 | private constructor(versionDetails: ClamAVVersionDetails) { 11 | super(); 12 | 13 | this.clamAV = versionDetails.ClamAV; 14 | 15 | this.signatureDatabase = { 16 | version : versionDetails.SignatureDatabase.version, 17 | buildTime: versionDetails.SignatureDatabase.buildTime 18 | }; 19 | } 20 | 21 | public static create(versionDetails: ClamAVVersionDetails): GetScannerVersionOutputParameters { 22 | return new GetScannerVersionOutputParameters(versionDetails); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/service/GetScannerVersionService.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVClient, ClamAVConnectionOptions, ClamAVVersionDetails } from '../../../core/lib/clamav'; 2 | import { IService } from '../../../core/service'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { GetScannerVersionOutputParameters } from '..'; 5 | import { Config } from '../../../core/configuration'; 6 | 7 | @Injectable() 8 | export class GetScannerVersionService implements IService { 9 | 10 | public async execute(): Promise { 11 | const connectionOptions: ClamAVConnectionOptions = { 12 | host : Config.CLAMAV_HOST, 13 | port : Config.CLAMAV_PORT, 14 | timeoutInMs: Config.CLAMAV_TIMEOUT, 15 | }; 16 | 17 | const versionDetails: ClamAVVersionDetails = await ClamAVClient.getVersion(connectionOptions); 18 | 19 | return GetScannerVersionOutputParameters.create(versionDetails); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/configuration/parser/EnvParser.ts: -------------------------------------------------------------------------------- 1 | import { ConfigError } from '..'; 2 | 3 | export class EnvParser { 4 | 5 | public static parseString(variable: string): T { 6 | const value: string | undefined = process.env[variable]; 7 | 8 | if (value === undefined) { 9 | throw ConfigError.createVariableNotSetError(variable); 10 | } 11 | 12 | /*tslint:disable-next-line*/ 13 | const result: any = value; 14 | 15 | return result; 16 | } 17 | 18 | public static parseNumber(variable: string): number { 19 | const value: string | undefined = process.env[variable]; 20 | 21 | if (value === undefined) { 22 | throw ConfigError.createVariableNotSetError(variable); 23 | } 24 | 25 | const parsedValue: number = parseInt(value.replace(new RegExp('_', 'g'), ''), 10); 26 | 27 | if (isNaN(parsedValue)) { 28 | throw ConfigError.createVariableParsingError(variable); 29 | } 30 | 31 | return parsedValue; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/command/data-transformer/ClamAVCommandDataTransformer.ts: -------------------------------------------------------------------------------- 1 | import { Transform, TransformCallback } from 'stream'; 2 | 3 | export class ClamAVCommandDataTransformer { 4 | 5 | public static readonly INSTREAM = (): Transform => { 6 | const transform: Transform = new Transform(); 7 | 8 | transform._transform = (chunk: Buffer, encoding: string, callback: TransformCallback): void => { 9 | const chunkLengthPart: Buffer = Buffer.alloc(4); 10 | chunkLengthPart.writeUInt32BE(chunk.length, 0); 11 | 12 | const chunkPart: Buffer = chunk; 13 | 14 | transform.push(chunkLengthPart); 15 | transform.push(chunkPart); 16 | 17 | callback(); 18 | }; 19 | 20 | transform._flush = (callback: TransformCallback): void => { 21 | const zeroLengthPart: Buffer = Buffer.alloc(4); 22 | zeroLengthPart.writeUInt32BE(0, 0); 23 | 24 | transform.push(zeroLengthPart); 25 | 26 | callback(); 27 | }; 28 | 29 | return transform; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pavel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/interceptor/LoggerInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { ServerResponse } from '../response'; 4 | import { Request } from 'express'; 5 | import { tap } from 'rxjs/operators'; 6 | import { CoreLogger } from '../../core/lib/logger'; 7 | 8 | @Injectable() 9 | export class LoggerInterceptor implements NestInterceptor { 10 | 11 | public intercept(context: ExecutionContext, next: CallHandler): Observable { 12 | const request: Request = context.switchToHttp().getRequest(); 13 | const requestStartDate: number = Date.now(); 14 | 15 | return next.handle().pipe(tap((): void => { 16 | const requestFinishDate: number = Date.now(); 17 | 18 | const message: string = 19 | `Method: ${request.method}; ` + 20 | `Path: ${request.path}; ` + 21 | `SpentTime: ${requestFinishDate - requestStartDate}ms`; 22 | 23 | CoreLogger.log(message, LoggerInterceptor.name); 24 | })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/service/errors/ServiceInputParametersValidationError.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ServiceInputParametersValidationDetails, 3 | ServiceInputParametersValidationError 4 | } from '../../../../../src/core/service'; 5 | 6 | describe('ServiceInputParametersValidationError', () => { 7 | 8 | describe(`Create ServiceInputParametersValidationError instance`, () => { 9 | 10 | it(`Expect ServiceInputParametersValidationError with required attributes`, () => { 11 | 12 | const details: ServiceInputParametersValidationDetails = { 13 | context: 'SyncScanInputParameters', 14 | details: [ 15 | {property: 'fileSizeInBytes', errors: ['must be positive integer']} 16 | ] 17 | }; 18 | 19 | const error: ServiceInputParametersValidationError = ServiceInputParametersValidationError.create(details); 20 | 21 | expect(error).toBeInstanceOf(ServiceInputParametersValidationError); 22 | expect(error.message).toBe('ServiceInputParametersValidationError'); 23 | expect(error.getDetails()).toEqual(details); 24 | }); 25 | 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/configuration/errors/ConfigError.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigError } from '../../../../../src/core/configuration'; 2 | 3 | describe('ConfigError', () => { 4 | 5 | describe(`Create "Variable not set error" `, () => { 6 | 7 | it(`When variable is HOST, expect ConfigError says that HOST variable is not set`, () => { 8 | const expectedErrorMessage: string = 'HOST not set.'; 9 | 10 | const error: ConfigError = ConfigError.createVariableNotSetError('HOST'); 11 | 12 | expect(error).toBeInstanceOf(ConfigError); 13 | expect(error.message).toBe(expectedErrorMessage); 14 | }); 15 | 16 | }); 17 | 18 | describe(`Create "Variable parsing error"`, () => { 19 | 20 | it(`When variable is HOST, expect ConfigError says there is an error when parsing a HOST variable`, () => { 21 | const expectedErrorMessage: string = 'HOST parsing error.'; 22 | 23 | const error: ConfigError = ConfigError.createVariableParsingError('HOST'); 24 | 25 | expect(error).toBeInstanceOf(ConfigError); 26 | expect(error.message).toBe(expectedErrorMessage); 27 | }); 28 | 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/command/factory/errors/ClamAVCommandFactoryError.ts: -------------------------------------------------------------------------------- 1 | import { ClamaAVCommandType } from '../../types/ClamaAVCommandType'; 2 | 3 | export class ClamAVCommandFactoryError extends Error { 4 | 5 | public readonly message: string; 6 | 7 | private constructor(message: string) { 8 | super(); 9 | 10 | this.name = this.constructor.name; 11 | this.message = message; 12 | 13 | Error.captureStackTrace(this, this.constructor); 14 | } 15 | 16 | public static createCommandValidationError( 17 | options: { commandType: ClamaAVCommandType, needData: boolean } 18 | 19 | ): ClamAVCommandFactoryError { 20 | 21 | let message: string = `${options.commandType} command requires the data.`; 22 | 23 | if (!options.needData) { 24 | message = `${options.commandType} command does't require the data.`; 25 | } 26 | 27 | return new ClamAVCommandFactoryError(message); 28 | } 29 | 30 | public static createUnknownCommandError(unknownCommand: string): ClamAVCommandFactoryError { 31 | const message: string = `Unknown command: ${unknownCommand}.`; 32 | return new ClamAVCommandFactoryError(message); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/configuration/config/Config.ts: -------------------------------------------------------------------------------- 1 | import { EnvParser } from '../parser/EnvParser'; 2 | 3 | export class Config { 4 | 5 | // API 6 | 7 | public static readonly API_HOST: string = EnvParser.parseString('API_HOST'); 8 | 9 | public static readonly API_PORT: number = EnvParser.parseNumber('API_PORT'); 10 | 11 | public static readonly API_BASE_PATH: string = EnvParser.parseString('API_BASE_PATH'); 12 | 13 | public static readonly API_CLUSTER_ENABLE: number = EnvParser.parseNumber('API_CLUSTER_ENABLE'); 14 | 15 | // ClamAV 16 | 17 | public static readonly CLAMAV_HOST: string = EnvParser.parseString('CLAMAV_HOST'); 18 | 19 | public static readonly CLAMAV_PORT: number = EnvParser.parseNumber('CLAMAV_PORT'); 20 | 21 | public static readonly CLAMAV_TIMEOUT: number = EnvParser.parseNumber('CLAMAV_TIMEOUT'); 22 | 23 | // Logs 24 | 25 | public static readonly LOG_FORMAT: 'TEXT'|'JSON' = EnvParser.parseString('LOG_FORMAT'); 26 | 27 | public static readonly LOG_DISABLE_COLORS: number = EnvParser.parseNumber('LOG_DISABLE_COLORS'); 28 | 29 | // Files 30 | 31 | public static readonly MAX_SYNC_SCAN_FILE_SIZE: number = EnvParser.parseNumber('MAX_SYNC_SCAN_FILE_SIZE'); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/response/response/ServerResponse.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponseCode } from '..'; 2 | 3 | export class ServerResponse { 4 | 5 | public readonly code: number; 6 | 7 | public readonly message: string; 8 | 9 | public readonly timestamp: number; 10 | 11 | public readonly data: {}; 12 | 13 | private constructor(code: number, message: string, data?: {}) { 14 | this.code = code; 15 | this.message = message; 16 | this.data = data || {}; 17 | this.timestamp = Date.now(); 18 | } 19 | 20 | public static createSuccessResponse(data?: {}, message?: string): ServerResponse { 21 | const resultCode: number = ServerResponseCode.SUCCESS.code; 22 | const resultMessage: string = message || ServerResponseCode.SUCCESS.message; 23 | 24 | return new ServerResponse(resultCode, resultMessage, data); 25 | } 26 | 27 | public static createErrorResponse(code?: number, message?: string, data?: {}): ServerResponse { 28 | const resultCode: number = code || ServerResponseCode.INTERNAL_ERROR.code; 29 | const resultMessage: string = message || ServerResponseCode.INTERNAL_ERROR.message; 30 | 31 | return new ServerResponse(resultCode, resultMessage, data); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /nodejs-rest-client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [] 8 | }, 9 | "jsRules": {}, 10 | "rules": { 11 | "no-any": true, 12 | "eofline": true, 13 | "quotemark": [true, "single"], 14 | "indent": [true, "spaces", 4], 15 | "member-access": true, 16 | "ordered-imports": false, 17 | "max-line-length": [true, 120], 18 | "no-trailing-whitespace": false, 19 | "no-inferrable-types": false, 20 | "object-literal-sort-keys": false, 21 | "array-type": [true, "array-simple"], 22 | "member-ordering": false, 23 | "interface-name": [true, "always-prefix"], 24 | "no-default-export": false, 25 | "interface-over-type-literal": false, 26 | "trailing-comma": false, 27 | "object-literal-shorthand": false, 28 | "no-console": false, 29 | "typedef-whitespace": false, 30 | "typedef": [ 31 | true, 32 | "call-signature", 33 | "arrow-call-signature", 34 | "parameter", 35 | "arrow-parameter", 36 | "property-declaration", 37 | "variable-declaration", 38 | "member-variable-declaration", 39 | "object-destructuring", 40 | "array-destructuring" 41 | ] 42 | }, 43 | "rulesDirectory": [] 44 | } 45 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/service/io-parameters/ServiceInputParameters.ts: -------------------------------------------------------------------------------- 1 | import { validate, ValidationError } from 'class-validator'; 2 | import { ServiceInputParametersValidationError } from '..'; 3 | 4 | export class ServiceInputParameters { 5 | 6 | public async validate(context?: string): Promise { 7 | const validationErrors: ValidationError[] = await validate(this); 8 | const validationContext: string = context || this.constructor.name; 9 | 10 | if (validationErrors.length > 0) { 11 | const details: ServiceInputParametersValidationDetails = { 12 | context: validationContext, 13 | details : [] 14 | }; 15 | 16 | for (const validationError of validationErrors) { 17 | const dtoValidationMessage: { property: string, errors: string[] } = { 18 | property: validationError.property, 19 | errors : validationError.constraints ? Object.values(validationError.constraints) : [] 20 | }; 21 | details.details.push(dtoValidationMessage); 22 | } 23 | 24 | throw ServiceInputParametersValidationError.create(details); 25 | } 26 | } 27 | 28 | } 29 | 30 | export type ServiceInputParametersValidationDetails = { 31 | context: string; 32 | details: Array<{ property: string, errors: string[] }> 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x] 16 | 17 | defaults: 18 | run: 19 | working-directory: ./nodejs-rest-client 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | # Disabling shallow clone is recommended 25 | # for improving relevancy of reporting 26 | fetch-depth: 0 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install libraries 32 | run: yarn install 33 | 34 | - name: Run linter 35 | run: yarn lint 36 | 37 | - name: Build application 38 | run: yarn build 39 | 40 | - name: Run unit tests 41 | run: yarn test:unit:coverage 42 | 43 | - name: SonarCloud Scan 44 | uses: sonarsource/sonarcloud-github-action@master 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 48 | 49 | - name: Push docker images 50 | run: | 51 | docker login -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWORD }} 52 | docker-compose build 53 | docker-compose push 54 | docker logout 55 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/application/scanner/service/SyncScanService.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVClient, ClamAVConnectionOptions, ClamAVScanDetails } from '../../../core/lib/clamav'; 2 | import { IService } from '../../../core/service'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { SyncScanInputParameters, SyncScanOutputParameters } from '..'; 5 | import { Config } from '../../../core/configuration'; 6 | import { RequestValidationError } from '../../../core/base-errors/RequestValidationError'; 7 | 8 | @Injectable() 9 | export class SyncScanService implements IService { 10 | 11 | public async execute(inputParameters: SyncScanInputParameters): Promise { 12 | const fileSizeLimit: number = Config.MAX_SYNC_SCAN_FILE_SIZE; 13 | 14 | if (inputParameters.fileSizeInBytes > fileSizeLimit) { 15 | const errorMessage: string = RequestValidationError.FILE_SIZE_EXCEEDED_MESSAGE(fileSizeLimit); 16 | throw RequestValidationError.create(errorMessage); 17 | } 18 | 19 | const connectionOptions: ClamAVConnectionOptions = { 20 | host : Config.CLAMAV_HOST, 21 | port : Config.CLAMAV_PORT, 22 | timeoutInMs: Config.CLAMAV_TIMEOUT, 23 | }; 24 | 25 | const scanDetails: ClamAVScanDetails = await ClamAVClient.scanStream(inputParameters.file, connectionOptions); 26 | 27 | return SyncScanOutputParameters.create(scanDetails); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/service/io-parameters/ServiceInputParameters.spec.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { SyncScanInputParameters } from '../../../../../src/application/scanner'; 3 | import { MockHelper } from '../../../../.helper/MockHelper'; 4 | import { ServiceInputParametersValidationError } from '../../../../../src/core/service'; 5 | 6 | describe('ServiceInputParameters', () => { 7 | 8 | describe(`validate`, () => { 9 | 10 | const testDescription: string = 11 | `When input "fileSizeInBytes" is not number, ` + 12 | `expect ServiceInputParametersValidationError says that "fileSizeInBytes" must be number`; 13 | 14 | it(testDescription, async () => { 15 | const file: Readable = MockHelper.createReadStream(); 16 | const fileSizeInBytes: unknown = '42 bytes'; 17 | 18 | try { 19 | await SyncScanInputParameters.create(file, fileSizeInBytes as number); 20 | 21 | } catch (error) { 22 | 23 | const catchError: ServiceInputParametersValidationError = error; 24 | 25 | expect(catchError).toBeInstanceOf(ServiceInputParametersValidationError); 26 | expect(catchError.getDetails().context).toBe(SyncScanInputParameters.name); 27 | expect(catchError.getDetails().details.length).toBe(1); 28 | expect(catchError.getDetails().details[0].property).toBe('fileSizeInBytes'); 29 | } 30 | }); 31 | 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/client/errors/ClamAVClientError.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVClientError } from '../../../../../../../src/core/lib/clamav/client/errors/ClamAVClientError'; 2 | 3 | describe('ClamAVClientError', () => { 4 | 5 | describe(`Create "Connection timed out error" `, () => { 6 | 7 | it(`Expect ClamAVClientError says that the connection to ClamAV was timed out`, () => { 8 | const expectedErrorMessage: string = 'Connection to ClamAV timed out.'; 9 | 10 | const error: ClamAVClientError = ClamAVClientError.createConnectionTimedOutError(); 11 | 12 | expect(error).toBeInstanceOf(ClamAVClientError); 13 | expect(error.message).toBe(expectedErrorMessage); 14 | }); 15 | 16 | }); 17 | 18 | describe(`Create "Scan aborted error"`, () => { 19 | 20 | const scanAbortedDescription: { when: string, expect: string } = { 21 | when : `When reason is "Internal error"`, 22 | expect: `expect ClamAVClientError says that the scanning was aborted due to "Intern ERROR"` 23 | }; 24 | 25 | it(`${scanAbortedDescription.when}, ${scanAbortedDescription.expect}`, () => { 26 | const expectedErrorMessage: string = 'Scan aborted. Reason: Internal error.'; 27 | 28 | const error: ClamAVClientError = ClamAVClientError.createScanAbortedError('Internal error'); 29 | 30 | expect(error).toBeInstanceOf(ClamAVClientError); 31 | expect(error.message).toBe(expectedErrorMessage); 32 | }); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/logger/logger/CoreLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { ILoggerTransport, WinstonLoggerTransport } from '../transport'; 3 | 4 | const LoggerTransport: ILoggerTransport = WinstonLoggerTransport.create(); 5 | 6 | export class CoreLogger extends Logger { 7 | 8 | public log(message: string, context?: string): void { 9 | CoreLogger.log(message, context); 10 | } 11 | 12 | public error(message: string, trace?: string, context?: string): void { 13 | CoreLogger.error(message, trace, context); 14 | } 15 | 16 | public warn(message: string, context?: string): void { 17 | CoreLogger.warn(message, context); 18 | } 19 | 20 | public debug(message: string, context?: string): void { 21 | CoreLogger.debug(message, context); 22 | } 23 | 24 | public verbose(message: string, context?: string): void { 25 | CoreLogger.verbose(message, context); 26 | } 27 | 28 | public static log(message: string, context?: string): void { 29 | LoggerTransport.log(message, context); 30 | } 31 | 32 | public static error(message: string, trace?: string, context?: string): void { 33 | LoggerTransport.error(message, trace, context); 34 | } 35 | 36 | public static warn(message: string, context?: string): void { 37 | LoggerTransport.warn(message, context); 38 | } 39 | 40 | public static debug(message: string, context?: string): void { 41 | LoggerTransport.debug(message, context); 42 | } 43 | 44 | public static verbose(message: string, context?: string): void { 45 | LoggerTransport.verbose(message, context); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { ServerApplication } from './infrastructure/server/ServerApplication'; 2 | import { Config } from './core/configuration'; 3 | import { CoreLogger } from './core/lib/logger'; 4 | import { fork, isMaster, on, Worker } from 'cluster'; 5 | import { cpus } from 'os'; 6 | 7 | (async (): Promise => { 8 | if (!Config.API_CLUSTER_ENABLE) { 9 | await runApplication(); 10 | 11 | } else { 12 | await spawnApplicationWorkers(); 13 | await runApplicationWorkers(); 14 | } 15 | })(); 16 | 17 | async function runApplication(): Promise { 18 | const serverApplication: ServerApplication = ServerApplication.create(); 19 | await serverApplication.bootstrap(); 20 | } 21 | 22 | async function spawnApplicationWorkers(): Promise { 23 | if (isMaster) { 24 | const cpuCount: number = cpus().length; 25 | 26 | for (let workerIndex: number = 0; workerIndex < cpuCount; workerIndex++) { 27 | fork(); 28 | } 29 | 30 | on('exit', (worker: Worker, code: number, signal: string): void => { 31 | const codeMessagePart: string = `code: ${code || 'unknown'}`; 32 | const signalMessagePart: string = `signal: ${signal || 'unknown'}`; 33 | 34 | const errorMassage: string = 35 | `Worker on PID: ${worker.process.pid} ` + 36 | `is down with ${codeMessagePart}; ${signalMessagePart}`; 37 | 38 | CoreLogger.error(errorMassage); 39 | 40 | fork(); 41 | }); 42 | } 43 | } 44 | 45 | async function runApplicationWorkers(): Promise { 46 | if (!isMaster) { 47 | await runApplication(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/.helper/MockHelper.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { Socket } from 'net'; 3 | import Mitm = require('mitm'); 4 | 5 | export class MockHelper { 6 | 7 | public static createReadStream(buffer?: Buffer): Readable { 8 | const data: Buffer = buffer || Buffer.from('11111111'); 9 | const readStream: Readable = new Readable(); 10 | 11 | readStream.push(data); 12 | readStream.push(null); 13 | 14 | return readStream; 15 | } 16 | 17 | public static createBuffer(options: { sizeInBytes: number, UInt8Data: number[] }): Buffer { 18 | if (options.sizeInBytes !== options.UInt8Data.length) { 19 | throw new Error('MockHelper: UInt8Data length does not match the sizeInBytes'); 20 | } 21 | 22 | const sourceBuffer: Buffer = Buffer.alloc(options.sizeInBytes); 23 | 24 | options.UInt8Data.forEach( 25 | (UInt8Chunk: number, index: number) => sourceBuffer.writeUInt8(UInt8Chunk, index) 26 | ); 27 | 28 | return sourceBuffer; 29 | } 30 | 31 | public static createClamAVServer(options: { sendOnConnection: string, delayInMs: number }): IMockClamAVServer { 32 | const clamAVServer: IMockClamAVServer = Mitm(); 33 | 34 | clamAVServer.on('connection', (socket: Socket) => { 35 | setTimeout(() => { 36 | socket.write(options.sendOnConnection); 37 | socket.emit('end'); 38 | }, options.delayInMs); 39 | }); 40 | 41 | return clamAVServer; 42 | } 43 | 44 | } 45 | 46 | export interface IMockClamAVServer { 47 | 48 | on(event: 'connection', callback: (socket: Socket) => void): void; 49 | 50 | disable(): void; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/server/ServerApplication.ts: -------------------------------------------------------------------------------- 1 | import { NestExpressApplication } from '@nestjs/platform-express'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { RootModule } from '../module/RootModule'; 4 | import { Config } from '../../core/configuration'; 5 | import { CoreLogger } from '../../core/lib/logger'; 6 | import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger'; 7 | 8 | export class ServerApplication { 9 | 10 | private readonly port: number = Config.API_PORT; 11 | 12 | private readonly host: string = Config.API_HOST; 13 | 14 | public static create(): ServerApplication { 15 | return new ServerApplication(); 16 | } 17 | 18 | public async bootstrap(): Promise { 19 | const app: NestExpressApplication = await NestFactory.create( 20 | RootModule, 21 | { logger: Config.API_CLUSTER_ENABLE ? false : new CoreLogger() } 22 | ); 23 | 24 | this.buildAPIDocumentation(app); 25 | 26 | await app.listen(this.port, this.host); 27 | 28 | this.log(); 29 | } 30 | 31 | public buildAPIDocumentation(app: NestExpressApplication): void { 32 | const apiBasePath: string = Config.API_BASE_PATH; 33 | 34 | const title: string = 'Virus Scanner'; 35 | const description: string = 'Virus Scanner API description'; 36 | 37 | const options: Omit = new DocumentBuilder() 38 | .setTitle(title) 39 | .setDescription(description) 40 | .build(); 41 | 42 | const document: OpenAPIObject = SwaggerModule.createDocument(app, options); 43 | 44 | SwaggerModule.setup(`${apiBasePath}/documentation`, app, document); 45 | } 46 | 47 | public log(): void { 48 | CoreLogger.log(`Server started on host: ${this.host}; port: ${this.port};`, ServerApplication.name); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/application/scanner/service/PingScannerService.spec.ts: -------------------------------------------------------------------------------- 1 | import { PingScannerOutputParameters, PingScannerService } from '../../../../../src/application/scanner'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { ScanTokens } from '../../../../../src/infrastructure/module/scanner/ScanTokens'; 4 | import { IService } from '../../../../../src/core/service'; 5 | import { IMockClamAVServer, MockHelper } from '../../../../.helper/MockHelper'; 6 | 7 | describe('PingScannerService', () => { 8 | const serverDelayInMs: number = 0; 9 | 10 | let mockClamAVServer: IMockClamAVServer; 11 | let service: IService; 12 | 13 | afterEach(() => { 14 | mockClamAVServer.disable(); 15 | }); 16 | 17 | beforeAll(async () => { 18 | const module: TestingModule = await Test.createTestingModule({ 19 | providers: [ 20 | { 21 | provide : ScanTokens.PingScannerService, 22 | useClass : PingScannerService 23 | }, 24 | ], 25 | }).compile(); 26 | 27 | service = module.get>(ScanTokens.PingScannerService); 28 | }); 29 | 30 | describe(`Execute`, () => { 31 | 32 | it('Expect PingScannerOutputParameters object with PONG message', async () => { 33 | const mockReceivedData: string = 'PONG'; 34 | 35 | mockClamAVServer = MockHelper.createClamAVServer({ 36 | sendOnConnection: mockReceivedData, 37 | delayInMs : serverDelayInMs 38 | }); 39 | 40 | const expectedResult: PingScannerOutputParameters = PingScannerOutputParameters.create({ Message: 'PONG' }); 41 | 42 | const result: PingScannerOutputParameters = await service.execute(); 43 | 44 | expect(result).toEqual(expectedResult); 45 | }); 46 | 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/application/scanner/service/GetScannerVersionService.spec.ts: -------------------------------------------------------------------------------- 1 | import { GetScannerVersionOutputParameters, GetScannerVersionService, } from '../../../../../src/application/scanner'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { ScanTokens } from '../../../../../src/infrastructure/module/scanner/ScanTokens'; 4 | import { IService } from '../../../../../src/core/service'; 5 | import { IMockClamAVServer, MockHelper } from '../../../../.helper/MockHelper'; 6 | 7 | describe('GetScannerVersionService', () => { 8 | const serverDelayInMs: number = 0; 9 | 10 | let mockClamAVServer: IMockClamAVServer; 11 | let service: IService; 12 | 13 | afterEach(() => { 14 | mockClamAVServer.disable(); 15 | }); 16 | 17 | beforeAll(async () => { 18 | const module: TestingModule = await Test.createTestingModule({ 19 | providers: [ 20 | { 21 | provide : ScanTokens.GetScannerVersionService, 22 | useClass : GetScannerVersionService 23 | }, 24 | ], 25 | }).compile(); 26 | 27 | service = module.get>(ScanTokens.GetScannerVersionService); 28 | }); 29 | 30 | describe(`Execute`, () => { 31 | 32 | it('Expect GetScannerVersionOutputParameters object with version details', async () => { 33 | const mockReceivedData: string = 'ClamAV 0.102.0/25000/Wed Jan 01 00:00:00 2019'; 34 | 35 | mockClamAVServer = MockHelper.createClamAVServer({ 36 | sendOnConnection: mockReceivedData, 37 | delayInMs : serverDelayInMs 38 | }); 39 | 40 | const expectedResult: GetScannerVersionOutputParameters = GetScannerVersionOutputParameters.create({ 41 | ClamAV : '0.102.0', 42 | SignatureDatabase: { version: '25000', buildTime: 'Wed Jan 01 00:00:00 2019' } 43 | }); 44 | 45 | const result: GetScannerVersionOutputParameters = await service.execute(); 46 | 47 | expect(result).toEqual(expectedResult); 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/command/data-transformer/ClamAVCommandDataTransformer.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockHelper } from '../../../../../../.helper/MockHelper'; 2 | import { Readable } from 'stream'; 3 | import { 4 | ClamAVCommandDataTransformer 5 | } from '../../../../../../../src/core/lib/clamav/command/data-transformer/ClamAVCommandDataTransformer'; 6 | 7 | describe('ClamAVCommandDataTransformer', () => { 8 | 9 | describe(`Transform INSTREAM stream`, () => { 10 | 11 | const testDescription: string = 12 | `When stream contains buffer = 11111111, ` + 13 | `expect transformed stream contains buffer with:\n` + 14 | ` 1. "Data Length Part" where length = 1 byte in the 32 bit format;\n` + 15 | ` 2. "Data Part" where data = 11111111;\n` + 16 | ` 3. "Data End Flag Part" where flag = 0 in the 32 bit format.`; 17 | 18 | it(testDescription, async () => { 19 | const sourceBuffer: Buffer = MockHelper.createBuffer({ sizeInBytes: 1, UInt8Data: [255] }); 20 | const sourceReadStream: Readable = MockHelper.createReadStream(sourceBuffer); 21 | 22 | const expectedBuffer: Buffer = MockHelper.createBuffer({ 23 | sizeInBytes: 9, 24 | UInt8Data : [0, 0, 0, 1, 255, 0, 0, 0, 0] 25 | }); 26 | 27 | const transformedReadStream: Readable = sourceReadStream.pipe(ClamAVCommandDataTransformer.INSTREAM()); 28 | const transformedBuffer: Buffer = await pipeStreamToBuffer(transformedReadStream); 29 | 30 | expect(transformedBuffer).toEqual(expectedBuffer); 31 | }); 32 | 33 | }); 34 | 35 | }); 36 | 37 | async function pipeStreamToBuffer(readStream: Readable): Promise { 38 | const buffer: Buffer = await new Promise( 39 | (resolve: (value: Buffer) => void, reject: (e: Error) => void): void => { 40 | 41 | const chunks: Buffer[] = []; 42 | 43 | readStream.on('data', (chunk: Buffer) => chunks.push(chunk)); 44 | readStream.on('end', () => resolve(Buffer.concat(chunks))); 45 | readStream.on('error', reject); 46 | } 47 | ); 48 | 49 | return buffer; 50 | } 51 | -------------------------------------------------------------------------------- /nodejs-rest-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-rest-client", 3 | "version": "1.0.0", 4 | "description": "REST client for virus scanner", 5 | "author": "Pavel Varentsov (varentsovpavel@gmail.com)", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "./scripts/compiler.sh", 9 | "build:local": "./scripts/compiler-local.sh", 10 | "copy:env": "cp ./env/.env ./dist/", 11 | "start": "node ./dist/bootstrap.js", 12 | "start:local": "yarn copy:env && cd ./dist && node -r dotenv/config bootstrap.js", 13 | "lint": "./node_modules/.bin/tslint --project tsconfig.json --config tslint.json", 14 | "test:unit": "./node_modules/.bin/jest --config jest-unit.json", 15 | "test:unit:coverage": "./node_modules/.bin/jest --coverage --config jest-unit.json", 16 | "test:unit:badges": "./node_modules/.bin/jest-coverage-badges input \"./unit-coverage/coverage-summary.json\" output \"./badges\"", 17 | "lib:check": "./node_modules/.bin/ncu", 18 | "lib:upgrade": "./node_modules/.bin/ncu -u && yarn install" 19 | }, 20 | "engines": { 21 | "node": ">=12" 22 | }, 23 | "engineStrict": true, 24 | "dependencies": { 25 | "@nestjs/common": "7.6.17", 26 | "@nestjs/core": "7.6.17", 27 | "@nestjs/platform-express": "7.6.17", 28 | "@nestjs/swagger": "4.8.0", 29 | "busboy": "0.3.1", 30 | "class-validator": "0.13.1", 31 | "dotenv": "10.0.0", 32 | "reflect-metadata": "0.1.13", 33 | "rxjs": "7.1.0", 34 | "swagger-ui-express": "4.1.6", 35 | "winston": "3.3.3" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/testing": "7.6.17", 39 | "@types/busboy": "0.2.3", 40 | "@types/express": "4.17.12", 41 | "@types/jest": "26.0.23", 42 | "@types/mitm": "1.3.3", 43 | "@types/node": "15.12.2", 44 | "hook-std": "2.0.0", 45 | "jest": "27.0.4", 46 | "jest-coverage-badges": "1.1.2", 47 | "jest-junit": "12.2.0", 48 | "jest-sonar-reporter": "2.0.0", 49 | "mitm": "1.7.2", 50 | "npm-check-updates": "11.6.0", 51 | "ts-jest": "27.0.3", 52 | "ts-node": "10.0.0", 53 | "tslint": "6.1.3", 54 | "typescript": "4.3.2" 55 | }, 56 | "jest-junit": { 57 | "outputDirectory": "./unit-coverage" 58 | }, 59 | "jestSonar": { 60 | "sonar56x": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/configuration/config/Config.spec.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../../../../src/core/configuration'; 2 | 3 | describe('Config', () => { 4 | 5 | describe(`Config's initialization with environment variables`, () => { 6 | 7 | it('When environment variables are exposed, expect API_HOST to be initialized as string', () => { 8 | expect(typeof Config.API_HOST === 'string').toBeTruthy(); 9 | }); 10 | 11 | it('When environment variables are exposed, expect API_PORT to be initialized as number', () => { 12 | expect(typeof Config.API_PORT === 'number').toBeTruthy(); 13 | }); 14 | 15 | it('When environment variables are exposed, expect API_BASE_PATH to be initialized as string', () => { 16 | expect(typeof Config.API_BASE_PATH === 'string').toBeTruthy(); 17 | }); 18 | 19 | it('When environment variables are exposed, expect API_CLUSTER_ENABLE to be initialized as number', () => { 20 | expect(typeof Config.API_CLUSTER_ENABLE === 'number').toBeTruthy(); 21 | }); 22 | 23 | it('When environment variables are exposed, expect CLAMAV_HOST to be initialized as string', () => { 24 | expect(typeof Config.CLAMAV_HOST === 'string').toBeTruthy(); 25 | }); 26 | 27 | it('When environment variables are exposed, expect CLAMAV_PORT to be initialized as number', () => { 28 | expect(typeof Config.CLAMAV_PORT === 'number').toBeTruthy(); 29 | }); 30 | 31 | it('When environment variables are exposed, expect CLAMAV_TIMEOUT to be initialized as number', () => { 32 | expect(typeof Config.CLAMAV_TIMEOUT === 'number').toBeTruthy(); 33 | }); 34 | 35 | it('When environment variables are exposed, expect LOG_FORMAT to be initialized as string', () => { 36 | expect(typeof Config.LOG_FORMAT === 'string').toBeTruthy(); 37 | }); 38 | 39 | it('When environment variables are exposed, expect LOG_DISABLE_COLORS to be initialized as number', () => { 40 | expect(typeof Config.LOG_DISABLE_COLORS === 'number').toBeTruthy(); 41 | }); 42 | 43 | it('When environment variables are exposed, expect MAX_SYNC_SCAN_FILE_SIZE to be initialized as number', () => { 44 | expect(typeof Config.MAX_SYNC_SCAN_FILE_SIZE === 'number').toBeTruthy(); 45 | }); 46 | 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /scanner/clamav/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ENV CLAMAV_VERSION=0.103.2 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # Install dependencies 7 | 8 | RUN apt-get update && \ 9 | apt-get upgrade -y && \ 10 | apt-get install --no-install-recommends -y \ 11 | build-essential \ 12 | gnupg \ 13 | dirmngr \ 14 | openssl \ 15 | libssl-dev \ 16 | libxml2-dev \ 17 | libxml2 \ 18 | libbz2-dev \ 19 | bzip2 \ 20 | zlib1g \ 21 | zlib1g-dev \ 22 | gettext \ 23 | autoconf \ 24 | libjson-c-dev \ 25 | ncurses-dev \ 26 | libpcre3-dev \ 27 | check \ 28 | valgrind \ 29 | libcurl4-openssl-dev \ 30 | ca-certificates \ 31 | wget && \ 32 | apt-get clean && \ 33 | rm -rf /var/lib/apt/lists/* 34 | 35 | COPY talos.pub /tmp/talos.pub 36 | 37 | # Download and build ClamAV 38 | 39 | RUN wget -nv https://www.clamav.net/downloads/production/clamav-${CLAMAV_VERSION}.tar.gz && \ 40 | wget -nv https://www.clamav.net/downloads/production/clamav-${CLAMAV_VERSION}.tar.gz.sig && \ 41 | gpg --import /tmp/talos.pub && \ 42 | gpg --decrypt clamav-${CLAMAV_VERSION}.tar.gz.sig && \ 43 | tar xzf clamav-${CLAMAV_VERSION}.tar.gz && \ 44 | cd clamav-${CLAMAV_VERSION} && \ 45 | ./configure && \ 46 | make && make install && \ 47 | rm -rf /clamav-${CLAMAV_VERSION} && \ 48 | rm -rf /tmp/talos.pub 49 | 50 | # Add ClamAV user 51 | 52 | RUN groupadd -r clamav && \ 53 | useradd -r -g clamav -u 1000 clamav -d /var/lib/clamav && \ 54 | mkdir -p /var/lib/clamav && \ 55 | mkdir /usr/local/share/clamav && \ 56 | chown -R clamav:clamav /var/lib/clamav /usr/local/share/clamav 57 | 58 | # Configure ClamAV 59 | 60 | RUN chown clamav:clamav -R /usr/local/etc/ 61 | COPY --chown=clamav:clamav ./*.conf /usr/local/etc/ 62 | 63 | RUN cp /usr/local/lib/libclamav.so.* /usr/lib/x86_64-linux-gnu && \ 64 | cp /usr/local/lib/libfreshclam.so.* /usr/lib/x86_64-linux-gnu && \ 65 | cp /usr/local/lib/libclammspack.so.* /usr/lib/x86_64-linux-gnu && \ 66 | cp /usr/local/lib/libclamunrar_iface.so.* /usr/lib/x86_64-linux-gnu && \ 67 | cp /usr/local/lib/libclamunrar.so.* /usr/lib/x86_64-linux-gnu 68 | 69 | RUN freshclam && \ 70 | chown clamav:clamav /var/lib/clamav/*.cvd 71 | 72 | # Add permissions 73 | 74 | RUN mkdir /var/run/clamav && \ 75 | chown clamav:clamav /var/run/clamav && \ 76 | chmod 750 /var/run/clamav 77 | 78 | USER 1000 79 | 80 | VOLUME /var/lib/clamav 81 | 82 | COPY --chown=clamav:clamav docker-entrypoint.sh / 83 | 84 | ENTRYPOINT ["/docker-entrypoint.sh"] 85 | 86 | EXPOSE 3310 87 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/command/factory/errors/ClamAVCommandFactoryError.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVCommandFactoryError } from '../../../../../../../../src/core/lib/clamav/command/factory/errors/ClamAVCommandFactoryError'; 2 | import { ClamaAVCommandType } from '../../../../../../../../src/core/lib/clamav/command/types/ClamaAVCommandType'; 3 | 4 | describe('ClamAVCommandFactoryError', () => { 5 | 6 | describe(`Create command validation error`, () => { 7 | 8 | const instreamWithDataDescription: { when: string, expect: string } = { 9 | when : `When commandType is INSTREAM and needData is truthy`, 10 | expect: `expect ClamAVCommandFactoryError says that INSTREAM command requires the data` 11 | }; 12 | 13 | it(`${instreamWithDataDescription.when}, ${instreamWithDataDescription.expect}`, () => { 14 | const expectedErrorMessage: string = 'INSTREAM command requires the data.'; 15 | 16 | const errorOptions: { commandType: ClamaAVCommandType, needData: boolean } = { 17 | commandType: ClamaAVCommandType.INSTREAM, 18 | needData : true, 19 | }; 20 | 21 | const error: ClamAVCommandFactoryError 22 | = ClamAVCommandFactoryError.createCommandValidationError(errorOptions); 23 | 24 | expect(error).toBeInstanceOf(ClamAVCommandFactoryError); 25 | expect(error.message).toBe(expectedErrorMessage); 26 | }); 27 | 28 | const pingWithoutDataDescription: { when: string, expect: string } = { 29 | when : `When commandType is PING and needData is falsy`, 30 | expect: `expect ClamAVCommandFactoryError says that PING command does't require the data` 31 | }; 32 | 33 | it(`${pingWithoutDataDescription.when}, ${pingWithoutDataDescription.expect}`, () => { 34 | const expectedErrorMessage: string = `PING command does't require the data.`; 35 | 36 | const errorOptions: { commandType: ClamaAVCommandType, needData: boolean } = { 37 | commandType: ClamaAVCommandType.PING, 38 | needData : false, 39 | }; 40 | 41 | const error: ClamAVCommandFactoryError 42 | = ClamAVCommandFactoryError.createCommandValidationError(errorOptions); 43 | 44 | expect(error).toBeInstanceOf(ClamAVCommandFactoryError); 45 | expect(error.message).toBe(expectedErrorMessage); 46 | }); 47 | 48 | }); 49 | 50 | describe(`Create unknown command error`, () => { 51 | 52 | it(`When command is UNKNOWN, expect ClamAVCommandFactoryError says that this command is unknown`, () => { 53 | const expectedErrorMessage: string = `Unknown command: UNKNOWN.`; 54 | 55 | const error: ClamAVCommandFactoryError = ClamAVCommandFactoryError.createUnknownCommandError('UNKNOWN'); 56 | 57 | expect(error).toBeInstanceOf(ClamAVCommandFactoryError); 58 | expect(error.message).toBe(expectedErrorMessage); 59 | }); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/client/parser/ClamAVClientResponseParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVPingDetails, ClamAVScanDetails, ClamAVVersionDetails } from '../../../../../../../src/core/lib/clamav'; 2 | import { ClamAVScanStatus } from '../../../../../../../src/core/lib/clamav/client/types/ClamAVScanStatus'; 3 | import { ClamAVClientResponseParser } from '../../../../../../../src/core/lib/clamav/client/parser/ClamAVClientResponseParser'; 4 | 5 | describe('ClamAVClientResponseParser', () => { 6 | 7 | describe(`Parse scan details`, () => { 8 | 9 | const cleanMsg: string = 'stream: OK'; 10 | 11 | it(`When details contain "${cleanMsg}", expect ClamAVScanDetails object with clean status`, () => { 12 | const expectedScanDetails: ClamAVScanDetails = { 13 | Message: 'Ok', 14 | Status : ClamAVScanStatus.CLEAN, 15 | }; 16 | 17 | const scanDetails: ClamAVScanDetails = ClamAVClientResponseParser.parseScanDetails(cleanMsg); 18 | 19 | expect(scanDetails).toEqual(expectedScanDetails); 20 | }); 21 | 22 | const infectedMsg: string = 'stream: Some-Signature FOUND'; 23 | 24 | it(`When details contain "${infectedMsg}", expect ClamAVScanDetails object with infected status`, () => { 25 | const expectedScanDetails: ClamAVScanDetails = { 26 | Message: 'Some-Signature found', 27 | Status : ClamAVScanStatus.INFECTED, 28 | }; 29 | 30 | const scanDetails: ClamAVScanDetails = ClamAVClientResponseParser.parseScanDetails(infectedMsg); 31 | 32 | expect(scanDetails).toEqual(expectedScanDetails); 33 | }); 34 | 35 | }); 36 | 37 | describe(`Parse ping details`, () => { 38 | 39 | it(`When details contain "PONG\\n", expect ClamAVPingDetails object with PONG message`, () => { 40 | const expectedPingDetails: ClamAVPingDetails = { 41 | Message: 'PONG' 42 | }; 43 | 44 | const message: string = 'PONG\n'; 45 | 46 | const pingDetails: ClamAVPingDetails = ClamAVClientResponseParser.parsePingDetails(message); 47 | 48 | expect(pingDetails).toEqual(expectedPingDetails); 49 | }); 50 | 51 | }); 52 | 53 | describe(`Parse version details`, () => { 54 | 55 | const testDescription: { when: string, expect: string } = { 56 | when : `When details contain "ClamAV '0.100.1/25000/Jan 01.01.2020\\n"`, 57 | expect: `expect ClamAVVersionDetails object with parsed attributes' values` 58 | }; 59 | 60 | it(`${testDescription.when}, ${testDescription.expect}`, () => { 61 | 62 | const expectedVersionDetails: ClamAVVersionDetails = { 63 | ClamAV : '0.100.1', 64 | SignatureDatabase: { version: '25000', buildTime: 'Jan 01.01.2020' } 65 | }; 66 | 67 | const message: string = `ClamAV 0.100.1/25000/Jan 01.01.2020\n`; 68 | 69 | const versionDetails: ClamAVVersionDetails = ClamAVClientResponseParser.parseVersionDetails(message); 70 | 71 | expect(versionDetails).toEqual(expectedVersionDetails); 72 | }); 73 | 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virus Scanner 2 | 3 | This project is a virus scanner as a microservice with REST interface. It consists of ClamAV and NodeJS client for ClamAV. 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](./LICENSE) 6 | ![Build](https://github.com/pvarentsov/virus-scanner/workflows/Build/badge.svg) 7 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pvarentsov_virus-scanner&metric=alert_status)](https://sonarcloud.io/dashboard?id=pvarentsov_virus-scanner) 8 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pvarentsov_virus-scanner&metric=coverage)](https://sonarcloud.io/dashboard?id=pvarentsov_virus-scanner) 9 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=pvarentsov_virus-scanner&metric=ncloc)](https://sonarcloud.io/dashboard?id=pvarentsov_virus-scanner) 10 | 11 | ## ClamAV 12 | 13 | [ClamAV](https://www.clamav.net/) is used as a docker image. It runs: 14 | * `clamd` daemon socket listening on port 3310; 15 | * `freshclam` signature database update tool in the background. 16 | 17 | ## REST Client 18 | 19 | REST Client is a NodeJS application based on [NestJS](https://nestjs.com/) framework. 20 | 21 | ## How-To 22 | 23 | To run microservice you can use `docker compose`. It will run Scanner service on port 3310 and API service on port 1337. 24 | 25 | Usage: 26 | 27 | 1. `docker-compose pull` 28 | 2. `docker-compose up -d` 29 | 30 |
31 | 32 | The API documentation will be available on endpoint GET http://localhost:1337/api/documentation 33 | 34 |
35 | 36 | ![API documentation](./assets/readme.api-documentation.png) 37 |
38 | 39 | 40 | ## Configuring 41 | 42 | REST Client configuring is based on environment variables. See all variables [here](nodejs-rest-client/env/.env). In current implementation all variables are required and must be exposed before starting the REST Client. 43 | 44 | ClamAV uses [clamd.conf](scanner/clamav/docker/clamd.conf) and [freshclam.conf](scanner/clamav/docker/freshclam.conf) configuration files. You can change these before building the docker image. See the full documentation for [clamd.conf](https://linux.die.net/man/5/clamd.conf) and [freshclam.conf](https://linux.die.net/man/5/freshclam.conf). 45 | 46 | ## Local Development 47 | 48 | To develop locally you can run ClamAV container in the background: `docker-compose up -d scanner`. 49 | 50 | To run REST Client manually on your machine need to: 51 | * have NodeJS with version `> 11` and `yarn` installed globally 52 | * enter to the client directory - `cd ./nodejs-rest-client` 53 | * install dependencies - `yarn install` 54 | * build project - `yarn build` 55 | * run project - `yarn start` 56 | 57 | To run unit tests use: 58 | * `yarn test:unit` 59 | * `yarn test:unit:coverage` 60 | 61 | You can run client with variables from [.env](nodejs-rest-client/env/.env). Use `yarn start:local` command instead `yarn start`. 62 | 63 | ## Notes 64 | 65 | * This project is not production-ready. 66 | * The running of the `clamd` and `freshclam` can take a little time. So clamav can be unavailable the first 20-30 seconds after starting. 67 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/infrastructure/interceptor/ErrorHandlerInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { ServerResponse, ServerResponseCode } from '../response'; 4 | import { ServiceInputParametersValidationDetails, ServiceInputParametersValidationError } from '../../core/service'; 5 | import { ClamAVClientError } from '../../core/lib/clamav/client/errors/ClamAVClientError'; 6 | import { ClamAVCommandFactoryError } from '../../core/lib/clamav/command/factory/errors/ClamAVCommandFactoryError'; 7 | import { RequestValidationError } from '../../core/base-errors/RequestValidationError'; 8 | import { CoreLogger } from '../../core/lib/logger'; 9 | 10 | @Catch() 11 | export class ErrorHandlerInterceptor implements ExceptionFilter { 12 | 13 | public catch(error: Error, host: ArgumentsHost): void { 14 | const request: Request = host.switchToHttp().getRequest(); 15 | const response: Response = host.switchToHttp().getResponse(); 16 | 17 | let errorResponse: ServerResponse = ServerResponse.createErrorResponse(); 18 | 19 | errorResponse = this.handleRequestValidationError(error, errorResponse); 20 | errorResponse = this.handleClamAVError(error, errorResponse); 21 | 22 | const message: string = 23 | `Method: ${request.method}; ` + 24 | `Path: ${request.path};`; 25 | 26 | CoreLogger.error(message, error.stack, ErrorHandlerInterceptor.name); 27 | 28 | response.json(errorResponse); 29 | } 30 | 31 | private handleRequestValidationError(error: Error, errorResponse: ServerResponse): ServerResponse { 32 | if (error instanceof RequestValidationError) { 33 | const code: number = ServerResponseCode.REQUEST_VALIDATION_ERROR.code; 34 | const message: string = error.message || ServerResponseCode.REQUEST_VALIDATION_ERROR.message; 35 | 36 | errorResponse = ServerResponse.createErrorResponse(code, message); 37 | } 38 | if (error instanceof ServiceInputParametersValidationError) { 39 | const code: number = ServerResponseCode.REQUEST_VALIDATION_ERROR.code; 40 | const message: string = ServerResponseCode.REQUEST_VALIDATION_ERROR.message; 41 | const data: ServiceInputParametersValidationDetails = error.getDetails(); 42 | 43 | errorResponse = ServerResponse.createErrorResponse(code, message, data); 44 | } 45 | 46 | return errorResponse; 47 | } 48 | 49 | private handleClamAVError(error: Error, errorResponse: ServerResponse): ServerResponse { 50 | if (error instanceof ClamAVClientError) { 51 | const code: number = ServerResponseCode.INTERNAL_ERROR.code; 52 | const message: string = error.message; 53 | 54 | errorResponse = ServerResponse.createErrorResponse(code, message); 55 | } 56 | if (error instanceof ClamAVCommandFactoryError) { 57 | const code: number = ServerResponseCode.INTERNAL_ERROR.code; 58 | const message: string = error.message; 59 | 60 | errorResponse = ServerResponse.createErrorResponse(code, message); 61 | } 62 | 63 | return errorResponse; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/infrastructure/response/response/ServerResponse.spec.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponse, ServerResponseCode } from '../../../../../src/infrastructure/response'; 2 | 3 | describe('ServerResponse', () => { 4 | 5 | describe(`createSuccessResponse`, () => { 6 | 7 | const withoutInputArgumentsTestDescription: string = 8 | 'When input arguments are not set, ' + 9 | 'expect success response with default code and default message'; 10 | 11 | it(withoutInputArgumentsTestDescription, async () => { 12 | const successResponse: ServerResponse = ServerResponse.createSuccessResponse(); 13 | 14 | expect(successResponse.code).toBe(ServerResponseCode.SUCCESS.code); 15 | expect(successResponse.message).toBe(ServerResponseCode.SUCCESS.message); 16 | expect(successResponse.data).toEqual({}); 17 | expect(new Date(successResponse.timestamp)).toBeInstanceOf(Date); 18 | }); 19 | 20 | const withInputArgumentsTestDescription: string = 21 | 'When data and message are set, ' + 22 | 'expect success response with custom message and custom data'; 23 | 24 | it(withInputArgumentsTestDescription, async () => { 25 | const data: { status: string } = { status: 'Ok' }; 26 | const message: string = 'Ok'; 27 | 28 | const successResponse: ServerResponse = ServerResponse.createSuccessResponse(data, message); 29 | 30 | expect(successResponse.code).toBe(ServerResponseCode.SUCCESS.code); 31 | expect(successResponse.message).toBe(message); 32 | expect(successResponse.data).toEqual(data); 33 | expect(new Date(successResponse.timestamp)).toBeInstanceOf(Date); 34 | }); 35 | 36 | }); 37 | 38 | describe(`createErrorResponse`, () => { 39 | 40 | const withoutInputArgumentsTestDescription: string = 41 | 'When input arguments are not set, ' + 42 | 'expect error response with default code and default message'; 43 | 44 | it(withoutInputArgumentsTestDescription, async () => { 45 | const errorResponse: ServerResponse = ServerResponse.createErrorResponse(); 46 | 47 | expect(errorResponse.code).toBe(ServerResponseCode.INTERNAL_ERROR.code); 48 | expect(errorResponse.message).toBe(ServerResponseCode.INTERNAL_ERROR.message); 49 | expect(errorResponse.data).toEqual({}); 50 | expect(new Date(errorResponse.timestamp)).toBeInstanceOf(Date); 51 | }); 52 | 53 | const withInputArgumentsTestDescription: string = 54 | 'When data, code and message are set, ' + 55 | 'expect error response with custom message, custom code and custom data'; 56 | 57 | it(withInputArgumentsTestDescription, async () => { 58 | const data: { status: string } = { status: 'Request validation error' }; 59 | const message: string = ServerResponseCode.REQUEST_VALIDATION_ERROR.message; 60 | const code: number = ServerResponseCode.REQUEST_VALIDATION_ERROR.code; 61 | 62 | const errorResponse: ServerResponse = ServerResponse.createErrorResponse(code, message, data); 63 | 64 | expect(errorResponse.code).toBe(code); 65 | expect(errorResponse.message).toBe(message); 66 | expect(errorResponse.data).toEqual(data); 67 | expect(new Date(errorResponse.timestamp)).toBeInstanceOf(Date); 68 | }); 69 | 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/parser/ClamAVClientResponseParser.ts: -------------------------------------------------------------------------------- 1 | import { ClamAVPingDetails, ClamAVScanDetails, ClamAVVersionDetails } from '../..'; 2 | import { ClamAVScanStatus } from '../types/ClamAVScanStatus'; 3 | 4 | export class ClamAVClientResponseParser { 5 | 6 | /** 7 | * Available message values: 8 | * * stream: OK 9 | * * stream: Some-Signature FOUND 10 | */ 11 | public static parseScanDetails(message: string): ClamAVScanDetails { 12 | const parsedMessage: string = ClamAVClientResponseParser.clearNoise(message) 13 | .replace('stream: ', '') 14 | .replace('FOUND', 'found') 15 | .replace('OK', 'Ok') 16 | .replace('\n', ''); 17 | 18 | const status: ClamAVScanStatus = message.includes('OK') && !message.includes('FOUND') 19 | ? ClamAVScanStatus.CLEAN 20 | : ClamAVScanStatus.INFECTED; 21 | 22 | return { Message: parsedMessage, Status: status }; 23 | } 24 | 25 | /** 26 | * Available values: 27 | * * PONG\n 28 | */ 29 | public static parsePingDetails(message: string): ClamAVPingDetails { 30 | const parsedMessage: string = ClamAVClientResponseParser 31 | .clearNoise(message) 32 | .replace('\n', ''); 33 | 34 | return { Message: parsedMessage }; 35 | } 36 | 37 | /** 38 | * Available values: 39 | * * ClamAV 0.102.0/25000/Wed Jan 01 00:00:00 2019\n 40 | */ 41 | public static parseVersionDetails(message: string): ClamAVVersionDetails { 42 | let clamAVVersion: string = ''; 43 | let signatureDatabaseVersion: string = ''; 44 | let signatureDatabaseBuildTime: string = ''; 45 | 46 | // Parsing of the ClamAV version 47 | 48 | const cleanMessage: string = ClamAVClientResponseParser.clearNoise(message); 49 | 50 | const clamAVVersionRegexp: RegExp = new RegExp(`ClamAV ([0-9.]*)\\/`, 'g'); 51 | const matchesWithClamAVVersion: string[] | null = cleanMessage.match(clamAVVersionRegexp); 52 | 53 | if (matchesWithClamAVVersion && matchesWithClamAVVersion.length > 0) { 54 | clamAVVersion = matchesWithClamAVVersion[0] 55 | .replace('ClamAV ', '') 56 | .replace('/', ''); 57 | } 58 | 59 | // Parsing of the Signature Database version 60 | 61 | const signatureDatabaseVersionRegexp: RegExp = new RegExp(`ClamAV ${clamAVVersion}\\/[0-9]+\\/`, 'g'); 62 | const matchesWithSignatureDatabaseVersion: string[] | null = cleanMessage.match(signatureDatabaseVersionRegexp); 63 | 64 | if (matchesWithSignatureDatabaseVersion && matchesWithSignatureDatabaseVersion.length > 0) { 65 | signatureDatabaseVersion = matchesWithSignatureDatabaseVersion[0] 66 | .replace(`ClamAV ${clamAVVersion}/`, '') 67 | .replace('/', ''); 68 | } 69 | 70 | // Parsing of the Signature Database Build Time 71 | 72 | if (clamAVVersion && signatureDatabaseVersion) { 73 | signatureDatabaseBuildTime = cleanMessage 74 | .replace(`ClamAV ${clamAVVersion}/${signatureDatabaseVersion}/`, '') 75 | .replace('\n', ''); 76 | } 77 | 78 | const versionDetails: ClamAVVersionDetails = { 79 | ClamAV : clamAVVersion, 80 | SignatureDatabase: { version: signatureDatabaseVersion, buildTime: signatureDatabaseBuildTime} 81 | }; 82 | 83 | return versionDetails; 84 | } 85 | 86 | public static clearNoise(message: string): string { 87 | return message 88 | .replace(new RegExp('\\u0000', 'g'), ''); 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /scanner/clamav/docker/talos.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGBjkiwBEADgJTEabt5zCareK9pJJswGU62smrq3uOaaDhtgztj3bxRY/UGT 4 | jypxMee1S/fGWQZQy52lFOXLud5gFC5QU8Yk+7EAsh2ZJSKtWUw8/iMxZ4vsrKVV 5 | QQRLTqMUY16R6/8UzdIT/hD6CbgWgiXF4NH5AGleNqjkF4TXrGof0AK0veekZYJV 6 | WWStqJR/cIiG0nxDQ87RWfeZgrULZmA8uii22po7rGGzxT0byb83dKK+7IoJ/6B/ 7 | ZlI0PmzuJ9/Xp6Mmm//sdPEqRwedt2aGrvtdF79xYJ1tDhOVMpID0aPdURBwlliq 8 | fyKGaIUEa1ke+Dy7sQF8i3zY7ce6PZOtbsts9xsJLvF98VhRsFy0vProPv1mVbiU 9 | PoxxPTnyLeGUm27amIMl4NfX4a8Hdu+ExzKprqWo3Ir08HQzNt6QoFghDIpi9nm4 10 | k327CJzJv/g2dq5kY/KU6wFHbdH3zP7u+p9DDqKJYFebPCvwM1hMxPdLqemTsfob 11 | kJ4iXcAXjpMqwXX9m0lyQcRHdIdc99yyCUMdPNfapLgY7rOahsS16795/5KSrCuF 12 | h2RcoAWUjh6sGjgGIY4Hy1qQwp3t6X/L6TOhDkBDWId5bTKFR9NqrVprOVsUutbs 13 | 0TOqLyH4GXCpE9vzg8DX7FTdRiCTpbyQ7VuSxRN/vAyVRP4chrABNfvh/QARAQAB 14 | tDtUYWxvcyAoVGFsb3MsIENpc2NvIFN5c3RlbXMgSW5jLikgPHJlc2VhcmNoQHNv 15 | dXJjZWZpcmUuY29tPokCPgQTAQIAKAUCYGOSLAIbAwUJA8JnAAYLCQgHAwIGFQgC 16 | CQoLBBYCAwECHgECF4AACgkQYJsCTys+3QfbLg//eZ0yCLr957FtztVlLIHYLpJn 17 | LIl8m+hu3KeUTIwvMoCLiw48cWqFZaJS9PTmrraSj5SKMDnAYFl4O0fhHfQiWDjb 18 | sZ32hQni1PcqxoXqSnkXD7mXjcPH2WuNnQM5WZoAD2VmksqRT57I/K2omW/sjaVe 19 | Nbq3GSOy8WThibswxzioDHtTPFa0/Ah2qq8OkcVJuTwCS1xkLijJc3jx/pOBHWFA 20 | BA4VX5pwcSou/woJ+ySsgBGEo5hOsd0r7h3a0O8EiuGulHTqQt87rVWGv0JKhnub 21 | FULr/ld8+d1zGvJL3OzFG6udjWjw3QqsLDZa94G1ksZWgqr/RgexlSYuxPW+lKUC 22 | QkgotLaEKQC4cpBLRcJEjWyrf4IjoJvkFrUtPsVH9VStICUQATyXARNVWbnJHq3Y 23 | qynCXSB4NZvdo9BF6Tx3FA+ZUjK4/X/UsjL/Hmv99huBctQsWL7gQCoSw9YOt4qs 24 | /As6fgPaNpYb9woJqNMEQNmrhfnnX9PGaM5dM769/E5vF67mkhBNqVJ0+4gyrpTU 25 | T7Pmavrc3T4aSSde8eG6zSlmW8wM5xELfK5TeTexBKGAaDV8c2BkfenRO8OvBSvr 26 | Gz+Xp/YzO9uGUPnbMsTVtxClmzmEj/MVpvtRdEo+dbVOSy8nk3XCu7jMjpojggPv 27 | YQ+4CZYxYpW1T2hSFxGJAhwEEAECAAYFAmBjkn0ACgkQ8T+eFrylv628FA/7BEZI 28 | mkl9Poxf1Omdzq+AGa5/1mnJ39jobc+wMBBww2baJX0YnYcZX0xfm5db/SaWbK7y 29 | EGWRnMAG0qt32zE3lwrLAlMcJ4a5RuSIIob1FDHXZ0YE0196FBJqsnMoheV6s9FL 30 | W5wj0Yhsbjo88MyhQrbHwnaZZ86EIdXlx3lcjSNStdMtzikBNFDQGb80OFMDU3Ab 31 | c0epVagX7tYI4vTHS1IK6SJ+E+UdXPN3Ci5HHvA9dMp6GRwTYgbvMwkjH7bPD47b 32 | /gOvAUj5+34p4dN7Lvy8VTvRiTGsk7EpY1zUibmjjpZbonH+wZu/hp7mPbUExtzC 33 | 2tZ6TES7lAq+vDlrg0rJuMDAJTemICBwd3waeEdPtNIBycfTf1qKevarrQPByeM9 34 | Cghw0PIh9Z5woNOoeTdvoyLm4kRSUqqPpAHiqgnC3nVVZc//NdeuoDDcstgYqsoK 35 | bSqW/1xfFjAE5f+a5uuu3I+6oVF83e+fLCuoVr0l1jdGx8jEo0F0TiP7qJ4HE9zs 36 | HsfQ8A7rlQFDU/DXQibrwVioVsVbX4D3cYTAQ3hwZHFZ4yF79fnyQuVis8dVjw0b 37 | w42rD0enmlxJ8MdZ6Xuq9pu74fQhH9vPhX5sUFU9uXmvYdeHm3IaP3U59y7Azapt 38 | +Rf7YvmXTCsfH9Rlw1MZKHfOFoMozqJs7sqDfGi5Ag0EYGOSLAEQAM5kdheiwStz 39 | nKiaIWaO+0PBA8bAv2xG7qW/Di85xdcH9miHZM9+lx/iZoOBC9wZC9eatV4Hcukf 40 | f700a/LGZSYVDvHvdEWbTv6ZwvHzbxuc1Kv8cLYopRUfOAwMYOmXriMLxVmd3fcf 41 | PNsfPRqfkaZRdkm7qTbPDeKpSL157HbUG64Eej3cOViq49Hy9L6jtfjtZVxX7Oav 42 | jnEpyezG6qSIAkvD6O7JYg3yfkr4sa44qohq9lDfjWpoXMebu0WsIyW11hm+7KMr 43 | BMHjlNgXppu0+ryeKfQiFjPDBd9aflnHy2e8aHef9S5349thNGzjV3TNMV6A6oAN 44 | 2XQ7pgj5DTwMZtHFCjdEHIyfGCAgQQL0/MaFzKwuw/l/m31smZgItAZXYY1xoC2g 45 | h7LTPZ/3t2VVVof4TNXDc+pUNgY6bwPBksuhsX8qsldDr5q3jdHZsjlycpL38Z4E 46 | ZNg3BqxJlVseB395ZOQ6FCtHGh6rpsYQZDj1QWcUyev8NHSbSNRMS2/Nn5bT3KgE 47 | WEWrmOxp3iMmunBqmnt2/xJ83PKRTbSKgcG+Y/+DtnleHpRueRUPC/5XX0DNznSj 48 | F10vAh4XtBKGBNaHU9VvnMXlYeJ9kCMdSs7cM4FfLFMtPkFwpDYhvQRAEwt11RV6 49 | bGo5ZCgGrHGIBlNk6ZSO1hP15hUtkWU7ABEBAAGJAiUEGAECAA8FAmBjkiwCGwwF 50 | CQPCZwAACgkQYJsCTys+3QfI7Q//Sb2yotfcsG5Q2FkHRBE85su01c6pewImV9bo 51 | fNhATSQ37yVHUDrchm+kY6Pq5Tdgg+eAMcYz2yv9JhFxJyzgI0viQrkjD7oXeRTG 52 | Z0CvzxHhTakAOADXAnYtwmJglEBTCCbUZ968kQkdBxEaUjVWPCMyIceRr8kUfiCj 53 | X51+DLESy8b5lOBhprO6vDukk/rmDruIpJPhJ3f89gsp2Ry7gk7a5ENIuVEElLK6 54 | OPBZhC3dDZwsvm5CYb62+U/b1xtmElpgGbNJCjxvAZiJ0WN2zfBXan+SJ4I9NFUw 55 | 9jvSURvDV24s4YPhkbZuOIqQEEYF8QMZ1VJlsr7BoWIXrdKDNJbmEVyx3UiYXKD1 56 | BVXCQADPu8G8EPuo4yAfWymJAOJbAqNF2Op6+sC7/v8Xcgc3PGGyu23cZwikfCAg 57 | V+beywTPI5+eVV5F/rpxXOlvNxT0NOg3UOeQ9GvCbD5ZcuDzmhqso0eMABeq5K5X 58 | B12xlWNaTZsIt8Dim4uKaKMGeB+6iygkHITbay0sMUo0dX6nT27bjX5dTBo/vnVA 59 | PYuCS6rh8ojalR1fYFKA1zdeSaJ2EW5KmgC9yedylSbHdQ+LjSY3t/Ut4RYaekID 60 | eGmVoQkJkL7gIAs8NOYwG3ayr0AtmeMagAMy94NH5ufVgFk+QPmXpzS7rMLQ3Is1 61 | ZOuWNrQ= 62 | =NP/0 63 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/logger/transport/impl/WinstonLoggerTransport.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, Logger, LoggerOptions, transports } from 'winston'; 2 | import { ILoggerTransport } from '../ILoggerTransport'; 3 | import { Config } from '../../../../configuration'; 4 | 5 | export class WinstonLoggerTransport implements ILoggerTransport { 6 | 7 | private readonly logger: Logger; 8 | 9 | private readonly pid: number; 10 | 11 | private constructor() { 12 | this.pid = process.pid; 13 | this.logger = createLogger(WinstonLoggerTransport.OPTIONS); 14 | } 15 | 16 | public log(message: string, context?: string): void { 17 | this.logger.info(message, { 18 | context: context || WinstonLoggerTransport.DEFAULT_CONTEXT, 19 | pid : this.pid 20 | }); 21 | } 22 | 23 | public error(message: string, trace?: string, context?: string): void { 24 | this.logger.error(message, { 25 | context: context || WinstonLoggerTransport.DEFAULT_CONTEXT, 26 | trace : trace, 27 | pid : this.pid 28 | }); 29 | } 30 | 31 | public warn(message: string, context?: string): void { 32 | this.logger.warn(message, { 33 | context: context || WinstonLoggerTransport.DEFAULT_CONTEXT, 34 | pid : this.pid 35 | }); 36 | } 37 | 38 | public debug(message: string, context?: string): void { 39 | this.logger.debug(message, { 40 | context: context || WinstonLoggerTransport.DEFAULT_CONTEXT, 41 | pid : this.pid 42 | }); 43 | } 44 | 45 | public verbose(message: string, context?: string): void { 46 | this.logger.verbose(message, { 47 | context: context || WinstonLoggerTransport.DEFAULT_CONTEXT, 48 | pid : this.pid }); 49 | } 50 | 51 | public static create(): WinstonLoggerTransport { 52 | return new WinstonLoggerTransport(); 53 | } 54 | 55 | private static readonly DEFAULT_CONTEXT: string = 'Global'; 56 | 57 | private static readonly OPTIONS: LoggerOptions = { 58 | level: 'debug', 59 | transports: [ new transports.Console({}) ], 60 | format : format.combine( 61 | format.timestamp({ format: `MM/DD/YYYY HH:mm:ss` }), 62 | WinstonLoggerTransport.chooseLogColor(), 63 | WinstonLoggerTransport.chooseLogFormat(), 64 | ), 65 | }; 66 | 67 | /*tslint:disable-next-line*/ 68 | private static buildMessageTemplate(info: any): string { 69 | const timestamp: string = `[${info.timestamp}]`; 70 | 71 | const level: string = `${WinstonLoggerTransport.alignValue(info.level, 7)}`; 72 | const pid: string = WinstonLoggerTransport.alignValue(`[${info.pid}]`, 8); 73 | 74 | const context: string = `[${info.context}]`; 75 | const message: string = `${info.message}`; 76 | const trace: string = info.trace ? `\n\n${info.trace}\n` : ''; 77 | 78 | return `${timestamp} ${pid} ${level} ${context} ${message}${trace}`; 79 | } 80 | 81 | private static alignValue(value: string | number, maxWidth: number): string { 82 | const colorRegexp: RegExp = new RegExp( 83 | `[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`, 'g' 84 | ); 85 | 86 | const parsedValue: string = `${value}`.replace(colorRegexp, ''); 87 | 88 | const valueLength: number = parsedValue.length; 89 | const lengthDiff: number = maxWidth - valueLength; 90 | 91 | let resultValue: string = `${value}`; 92 | 93 | if (lengthDiff > 0) { 94 | resultValue = resultValue.padEnd(resultValue.length + lengthDiff); 95 | } 96 | 97 | return resultValue; 98 | } 99 | 100 | /*tslint:disable-next-line*/ 101 | private static chooseLogFormat(): any { 102 | if (Config.LOG_FORMAT === 'JSON') { 103 | return format.json(); 104 | } 105 | 106 | /*tslint:disable-next-line*/ 107 | return format.printf((info: any): string => WinstonLoggerTransport.buildMessageTemplate(info)); 108 | } 109 | 110 | /*tslint:disable-next-line*/ 111 | private static chooseLogColor(): any { 112 | if (Config.LOG_DISABLE_COLORS) { 113 | return format.uncolorize(); 114 | } 115 | 116 | return format.colorize({ all: true }); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/command/factory/ClamAVCommandFactory.ts: -------------------------------------------------------------------------------- 1 | import { ClamaAVCommandType } from '../types/ClamaAVCommandType'; 2 | import { Readable, Transform } from 'stream'; 3 | import { ClamAVCommandFactoryError } from './errors/ClamAVCommandFactoryError'; 4 | import { ClamAVCommandDataTransformer } from '../data-transformer/ClamAVCommandDataTransformer'; 5 | import { ClamAVCommand } from '../types/ClamAVCommand'; 6 | 7 | export class ClamAVCommandFactory { 8 | 9 | private readonly commandType: ClamaAVCommandType; 10 | 11 | private readonly commandRule: ClamAVCommandRule; 12 | 13 | private readonly commandPrefix: string | undefined; 14 | 15 | private readonly commandPostfix: string | undefined; 16 | 17 | private readonly data: Readable | undefined; 18 | 19 | private constructor(type: ClamaAVCommandType, data?: Readable) { 20 | this.commandType = type; 21 | this.commandRule = ClamAVCommandFactory.findCommandRule(this.commandType); 22 | 23 | this.data = data; 24 | 25 | if (this.commandRule.needPrefix) { 26 | this.commandPrefix = ClamAVCommandFactory.commandPrefix; 27 | } 28 | if (this.commandRule.needPostfix) { 29 | this.commandPostfix = ClamAVCommandFactory.commandPostfix; 30 | } 31 | } 32 | 33 | public static createCommand(commandType: ClamaAVCommandType, data?: Readable): ClamAVCommand { 34 | const commandFactory: ClamAVCommandFactory = new ClamAVCommandFactory(commandType, data); 35 | commandFactory.validateCommand(); 36 | 37 | const command: ClamAVCommand = { 38 | name : commandType, 39 | prefix : commandFactory.commandPrefix, 40 | postfix: commandFactory.commandPostfix, 41 | data : ClamAVCommandFactory.transformData(commandType, data) 42 | }; 43 | 44 | return command; 45 | } 46 | 47 | private validateCommand(): void { 48 | if (!this.data && this.commandRule.needData) { 49 | throw ClamAVCommandFactoryError.createCommandValidationError( 50 | { commandType: this.commandType, needData: true } 51 | ); 52 | } 53 | if (this.data && !this.commandRule.needData) { 54 | throw ClamAVCommandFactoryError.createCommandValidationError( 55 | { commandType: this.commandType, needData: false } 56 | ); 57 | } 58 | } 59 | 60 | private static findCommandRule(type: ClamaAVCommandType): ClamAVCommandRule { 61 | const rule: ClamAVCommandRule | undefined = ClamAVCommandFactory.commandRules[type]; 62 | 63 | if (!rule) { 64 | throw ClamAVCommandFactoryError.createUnknownCommandError(type); 65 | } 66 | 67 | return rule; 68 | } 69 | 70 | private static transformData(commandType: ClamaAVCommandType, data?: Readable): Readable | undefined { 71 | let resultData: Readable | undefined; 72 | 73 | if (data instanceof Readable) { 74 | resultData = data; 75 | 76 | const transformer: (() => Transform) | undefined = ClamAVCommandFactory.dataTransformers[commandType]; 77 | 78 | if (transformer) { 79 | resultData = data.pipe(transformer()); 80 | } 81 | } 82 | 83 | return resultData; 84 | } 85 | 86 | private static readonly commandPrefix: string = 'z'; 87 | 88 | private static readonly commandPostfix: string = '\0'; 89 | 90 | private static readonly commandRules: ClamAVCommandRules = { 91 | [ClamaAVCommandType.PING]: { 92 | needPrefix : false, 93 | needPostfix: false, 94 | needData : false, 95 | }, 96 | 97 | [ClamaAVCommandType.VERSION]: { 98 | needPrefix : false, 99 | needPostfix: false, 100 | needData : false, 101 | }, 102 | 103 | [ClamaAVCommandType.INSTREAM]: { 104 | needPrefix : true, 105 | needPostfix: true, 106 | needData : true, 107 | }, 108 | }; 109 | 110 | private static readonly dataTransformers: ClamAVDataTransformers = { 111 | [ClamaAVCommandType.INSTREAM]: ClamAVCommandDataTransformer.INSTREAM, 112 | }; 113 | 114 | } 115 | 116 | type ClamAVCommandRule = { needPrefix: boolean, needPostfix: boolean, needData: boolean }; 117 | 118 | type ClamAVCommandRules = { [command: string]: ClamAVCommandRule }; 119 | 120 | type ClamAVDataTransformers = { [command: string]: () => Transform }; 121 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/application/scanner/service/SyncScanService.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SyncScanInputParameters, 3 | SyncScanOutputParameters, 4 | SyncScanService, 5 | } from '../../../../../src/application/scanner'; 6 | import { Test, TestingModule } from '@nestjs/testing'; 7 | import { ScanTokens } from '../../../../../src/infrastructure/module/scanner/ScanTokens'; 8 | import { IService } from '../../../../../src/core/service'; 9 | import { IMockClamAVServer, MockHelper } from '../../../../.helper/MockHelper'; 10 | import { ClamAVScanStatus } from '../../../../../src/core/lib/clamav/client/types/ClamAVScanStatus'; 11 | import { Readable } from 'stream'; 12 | import { RequestValidationError } from '../../../../../src/core/base-errors/RequestValidationError'; 13 | import { Config } from '../../../../../src/core/configuration'; 14 | 15 | describe('SyncScanService', () => { 16 | const serverDelayInMs: number = 10; 17 | 18 | let mockClamAVServer: IMockClamAVServer; 19 | let service: IService; 20 | 21 | afterEach(() => { 22 | mockClamAVServer.disable(); 23 | }); 24 | 25 | beforeAll(async () => { 26 | const module: TestingModule = await Test.createTestingModule({ 27 | providers: [ 28 | { 29 | provide : ScanTokens.SyncScanService, 30 | useClass : SyncScanService 31 | }, 32 | ], 33 | }).compile(); 34 | 35 | service = module.get>(ScanTokens.SyncScanService); 36 | }); 37 | 38 | describe(`Execute`, () => { 39 | 40 | it('Expect SyncScanOutputParameters object with CLEAN status', async () => { 41 | const mockReceivedData: string = 'stream: OK'; 42 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(1)); 43 | 44 | mockClamAVServer = MockHelper.createClamAVServer({ 45 | sendOnConnection: mockReceivedData, 46 | delayInMs : serverDelayInMs 47 | }); 48 | 49 | const expectedResult: SyncScanOutputParameters = SyncScanOutputParameters.create({ 50 | Message: 'Ok', 51 | Status : ClamAVScanStatus.CLEAN 52 | }); 53 | 54 | const inputParameters: SyncScanInputParameters = await SyncScanInputParameters.create(mockReadStream, 1); 55 | const result: SyncScanOutputParameters = await service.execute(inputParameters); 56 | 57 | expect(result).toEqual(expectedResult); 58 | }); 59 | 60 | it('Expect SyncScanOutputParameters object with INFECTED status', async () => { 61 | const mockReceivedData: string = 'stream: Some-Signature FOUND'; 62 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(1)); 63 | 64 | mockClamAVServer = MockHelper.createClamAVServer({ 65 | sendOnConnection: mockReceivedData, 66 | delayInMs : serverDelayInMs 67 | }); 68 | 69 | const expectedResult: SyncScanOutputParameters = SyncScanOutputParameters.create({ 70 | Message: 'Some-Signature found', 71 | Status : ClamAVScanStatus.INFECTED 72 | }); 73 | 74 | const inputParameters: SyncScanInputParameters = await SyncScanInputParameters.create(mockReadStream, 1); 75 | const result: SyncScanOutputParameters = await service.execute(inputParameters); 76 | 77 | expect(result).toEqual(expectedResult); 78 | }); 79 | 80 | const requestValidationErrorTestDescription: string = 81 | `When the file size exceeded the value of the MAX_SYNC_SCAN_FILE_SIZE environment variable, ` + 82 | `expect RequestValidationError says that file size was exceeded`; 83 | 84 | it(requestValidationErrorTestDescription, async () => { 85 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(1)); 86 | 87 | const expectedError: RequestValidationError = RequestValidationError.create( 88 | RequestValidationError.FILE_SIZE_EXCEEDED_MESSAGE(Config.MAX_SYNC_SCAN_FILE_SIZE) 89 | ); 90 | 91 | const inputParameters: SyncScanInputParameters = await SyncScanInputParameters.create( 92 | mockReadStream, 93 | Config.MAX_SYNC_SCAN_FILE_SIZE + 1 94 | ); 95 | 96 | await expect(service.execute(inputParameters)).rejects.toEqual(expectedError); 97 | }); 98 | 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/command/factory/ClamAVCommandFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClamaAVCommandType } from '../../../../../../../src/core/lib/clamav/command/types/ClamaAVCommandType'; 2 | import { ClamAVCommandFactory } from '../../../../../../../src/core/lib/clamav/command/factory/ClamAVCommandFactory'; 3 | import { ClamAVCommand } from '../../../../../../../src/core/lib/clamav/command/types/ClamAVCommand'; 4 | import { Readable } from 'stream'; 5 | import { 6 | ClamAVCommandFactoryError 7 | } from '../../../../../../../src/core/lib/clamav/command/factory/errors/ClamAVCommandFactoryError'; 8 | import { MockHelper } from '../../../../../../.helper/MockHelper'; 9 | 10 | describe('ClamAVCommandFactory', () => { 11 | 12 | describe(`Create PING command`, () => { 13 | 14 | it('When commandType is PING and data is not set, expect PING ClamAVCommand object', () => { 15 | const expectedCommand: ClamAVCommand = { 16 | name : ClamaAVCommandType.PING, 17 | prefix : undefined, 18 | postfix: undefined, 19 | data : undefined 20 | }; 21 | 22 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand(ClamaAVCommandType.PING); 23 | 24 | expect(command).toEqual(expectedCommand); 25 | }); 26 | 27 | it('When commandType is PING and data is set, expect it throws ClamAVCommandFactoryError', () => { 28 | const expectedError: ClamAVCommandFactoryError = ClamAVCommandFactoryError.createCommandValidationError({ 29 | commandType: ClamaAVCommandType.PING, 30 | needData : false 31 | }); 32 | 33 | const mockReadStream: Readable = MockHelper.createReadStream(); 34 | 35 | const createCommandFunction: () => void = (): void => { 36 | ClamAVCommandFactory.createCommand(ClamaAVCommandType.PING, mockReadStream); 37 | }; 38 | 39 | expect(createCommandFunction).toThrow(expectedError); 40 | }); 41 | 42 | }); 43 | 44 | describe(`Create VERSION command`, () => { 45 | 46 | it('When commandType is VERSION and data is not set, expect VERSION ClamAVCommand object', () => { 47 | const expectedCommand: ClamAVCommand = { 48 | name : ClamaAVCommandType.VERSION, 49 | prefix : undefined, 50 | postfix: undefined, 51 | data : undefined 52 | }; 53 | 54 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand(ClamaAVCommandType.VERSION); 55 | 56 | expect(command).toEqual(expectedCommand); 57 | }); 58 | 59 | it('When commandType is VERSION and data is set, expect it throws ClamAVCommandFactoryError', () => { 60 | const expectedError: ClamAVCommandFactoryError = ClamAVCommandFactoryError.createCommandValidationError({ 61 | commandType: ClamaAVCommandType.VERSION, 62 | needData : false 63 | }); 64 | 65 | const mockReadStream: Readable = MockHelper.createReadStream(); 66 | 67 | const createCommandFunction: () => void = (): void => { 68 | ClamAVCommandFactory.createCommand(ClamaAVCommandType.VERSION, mockReadStream); 69 | }; 70 | 71 | expect(createCommandFunction).toThrow(expectedError); 72 | }); 73 | 74 | }); 75 | 76 | describe(`Create INSTREAM command`, () => { 77 | 78 | it('When commandType is INSTREAM and data is set, expect INSTREAM ClamAVCommand object', () => { 79 | const mockReadStream: Readable = MockHelper.createReadStream(); 80 | 81 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand( 82 | ClamaAVCommandType.INSTREAM, 83 | mockReadStream 84 | ); 85 | 86 | expect(command.name).toBe(ClamaAVCommandType.INSTREAM); 87 | expect(command.prefix).toBe('z'); 88 | expect(command.postfix).toBe('\0'); 89 | expect(command.data).toBeInstanceOf(Readable); 90 | }); 91 | 92 | it('When commandType is INSTREAM and data is not set, expect it throws ClamAVCommandFactoryError', () => { 93 | const expectedError: ClamAVCommandFactoryError = ClamAVCommandFactoryError.createCommandValidationError({ 94 | commandType: ClamaAVCommandType.INSTREAM, 95 | needData : true 96 | }); 97 | 98 | const createCommandFunction: () => void = (): void => { 99 | ClamAVCommandFactory.createCommand(ClamaAVCommandType.INSTREAM); 100 | }; 101 | 102 | expect(createCommandFunction).toThrow(expectedError); 103 | }); 104 | 105 | }); 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/presentation/rest-api-interface/ScanController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpCode, Inject, Post, Req } from '@nestjs/common'; 2 | import { IService } from '../../core/service'; 3 | import { 4 | GetScannerVersionOutputParameters, 5 | PingScannerOutputParameters, 6 | SyncScanInputParameters, 7 | SyncScanOutputParameters 8 | } from '../../application/scanner'; 9 | import * as Busboy from 'busboy'; 10 | import { Request } from 'express'; 11 | import { Readable } from 'stream'; 12 | import { ScanTokens } from '../../infrastructure/module/scanner/ScanTokens'; 13 | import { ServerResponse } from '../../infrastructure/response'; 14 | import { RequestValidationError } from '../../core/base-errors/RequestValidationError'; 15 | import { Config } from '../../core/configuration'; 16 | import { ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger'; 17 | import { SyncScanResponse } from './documentation/scanner/sync-scan/SyncScanResponse'; 18 | import { PingResponse } from './documentation/scanner/ping/PingResponse'; 19 | import { GetVersionResponse } from './documentation/scanner/get-version/GetVersionResponse'; 20 | import { SyncScanBody } from './documentation/scanner/sync-scan/SyncScanBody'; 21 | import IBusboy = busboy.Busboy; 22 | 23 | @Controller(`${Config.API_BASE_PATH}/scanner`) 24 | @ApiTags('Scanner') 25 | export class ScanController { 26 | 27 | constructor( 28 | @Inject(ScanTokens.SyncScanService) 29 | private readonly syncScanService: IService, 30 | 31 | @Inject(ScanTokens.PingScannerService) 32 | private readonly pingScannerService: IService, 33 | 34 | @Inject(ScanTokens.GetScannerVersionService) 35 | private readonly getScannerVersionService: IService, 36 | ) {} 37 | 38 | @Post('sync-scan') 39 | @HttpCode(200) 40 | @ApiConsumes('multipart/form-data') 41 | @ApiBody({type: SyncScanBody}) 42 | @ApiResponse({status: 200, type: SyncScanResponse}) 43 | public async syncScan(@Req() request: Request): Promise { 44 | return new Promise( 45 | (resolve: ResolveCallback, reject: RejectCallback): void => { 46 | 47 | const busboy: IBusboy = new Busboy({ headers: request.headers, limits: { files: 1 } }); 48 | const fieldNames: string[] = []; 49 | 50 | busboy.on('file', async (fieldName: string, fileInputStream: Readable): Promise => { 51 | try { 52 | fieldNames.push(fieldName); 53 | 54 | const fileSize: number = parseInt(request.headers['content-length']!, 10); 55 | 56 | const syncScanInputParameters: SyncScanInputParameters 57 | = await SyncScanInputParameters.create(fileInputStream, fileSize); 58 | 59 | const syncScanOutputParameters: SyncScanOutputParameters 60 | = await this.syncScanService.execute(syncScanInputParameters); 61 | 62 | const response: ServerResponse = ServerResponse.createSuccessResponse(syncScanOutputParameters); 63 | 64 | resolve(response); 65 | 66 | } catch (err) { 67 | fileInputStream.resume(); 68 | reject(err); 69 | } 70 | }); 71 | 72 | this.handleBusboyFinishEvent(busboy, fieldNames, reject); 73 | 74 | request.pipe(busboy); 75 | } 76 | ); 77 | } 78 | 79 | @Post('ping') 80 | @HttpCode(200) 81 | @ApiResponse({status: 200, type: PingResponse}) 82 | public async ping(): Promise { 83 | const pingDetails: PingScannerOutputParameters = await this.pingScannerService.execute(); 84 | return ServerResponse.createSuccessResponse(pingDetails); 85 | } 86 | 87 | @Get('version') 88 | @HttpCode(200) 89 | @ApiResponse({status: 200, type: GetVersionResponse}) 90 | public async getVersion(): Promise { 91 | const versionDetails: GetScannerVersionOutputParameters = await this.getScannerVersionService.execute(); 92 | return ServerResponse.createSuccessResponse(versionDetails); 93 | } 94 | 95 | private handleBusboyFinishEvent = ( 96 | busboy: IBusboy, 97 | fieldNames: string[], 98 | reject: RejectCallback 99 | 100 | ): void => { 101 | 102 | busboy.on('finish', (): void => { 103 | if (fieldNames.length === 0) { 104 | const errorMessage: string = RequestValidationError.MULTIPART_FORM_EMPTY_MESSAGE(); 105 | reject(RequestValidationError.create(errorMessage)); 106 | } 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/logger/logger/CoreLogger.spec.ts: -------------------------------------------------------------------------------- 1 | import * as hookStd from 'hook-std'; 2 | import { CoreLogger } from '../../../../../../src/core/lib/logger'; 3 | 4 | type Message = { 5 | context : string; 6 | pid : number; 7 | level : string; 8 | message : string; 9 | timestamp: string; 10 | trace? : string; 11 | }; 12 | 13 | describe('CoreLogger', () => { 14 | 15 | describe(`log`, () => { 16 | 17 | const baseLogTestDescription: string = 18 | 'When message is "Hello!" and context is not set, ' + 19 | 'expect log with "info" level, "Hello!" message and "Global" context'; 20 | 21 | it(baseLogTestDescription, async () => { 22 | await masterTest('log', 'Hello!'); 23 | }); 24 | 25 | const logWithCustomContextTestDescription: string = 26 | 'When message is "Hello!" and context is "Greeting", ' + 27 | 'expect log with "info" level, "Hello!" message and "Greeting" context'; 28 | 29 | it(logWithCustomContextTestDescription, async () => { 30 | await masterTest('log', 'Hello!', 'Greeting'); 31 | }); 32 | 33 | }); 34 | 35 | describe(`error`, () => { 36 | 37 | const baseErrorTestDescription: string = 38 | 'When message is "Internal Error!" and context is not set, ' + 39 | 'expect log with "error" level, "Internal Error!" message and "Global" context'; 40 | 41 | it(baseErrorTestDescription, async () => { 42 | await masterTest('error', 'Internal Error!'); 43 | }); 44 | 45 | const errorWithCustomContextAndStackTraceTestDescription: string = 46 | 'When message is "Internal Error!", context is "InternalError" and trace is set, ' + 47 | 'expect log with "error" level, "Internal Error!" message, "InternalError" context and stack trace'; 48 | 49 | it(errorWithCustomContextAndStackTraceTestDescription, async () => { 50 | const error: Error = new Error('Internal Error!'); 51 | await masterTest('error', 'Internal Error!', 'InternalError', error.stack); 52 | }); 53 | 54 | }); 55 | 56 | describe(`warn`, () => { 57 | 58 | const baseWarnTestDescription: string = 59 | 'When message is "Warning!" and context is not set, ' + 60 | 'expect log with "warn" level, "Warning!" message and "Global" context'; 61 | 62 | it(baseWarnTestDescription, async () => { 63 | await masterTest('warn', 'Warning!'); 64 | }); 65 | 66 | const warnWithCustomContextTestDescription: string = 67 | 'When message is "Warning!" and context is "Warning", ' + 68 | 'expect log with "warn" level, "Warning!" message and "Warning" context'; 69 | 70 | it(warnWithCustomContextTestDescription, async () => { 71 | await masterTest('warn', 'Warning!', 'Warning'); 72 | }); 73 | 74 | }); 75 | 76 | describe(`debug`, () => { 77 | 78 | const baseDebugTestDescription: string = 79 | 'When message is "Debug!" and context is not set, ' + 80 | 'expect log with "debug" level, "Debug!" message and "Global" context'; 81 | 82 | it(baseDebugTestDescription, async () => { 83 | await masterTest('debug', 'Debug!'); 84 | }); 85 | 86 | const debugWithCustomContextTestDescription: string = 87 | 'When message is "Debug!" and context is "Debug", ' + 88 | 'expect log with "debug" level, "Debug!" message and "Debug" context'; 89 | 90 | it(debugWithCustomContextTestDescription, async () => { 91 | await masterTest('debug', 'Debug!', 'Debug'); 92 | }); 93 | 94 | }); 95 | 96 | describe(`verbose`, () => { 97 | 98 | const baseVerboseTestDescription: string = 99 | 'When message is "Verbose!" and context is not set, ' + 100 | 'expect log with "verbose" level, "Verbose!" message and "Global" context'; 101 | 102 | it(baseVerboseTestDescription, async () => { 103 | await masterTest('verbose', 'Verbose!'); 104 | }); 105 | 106 | const verboseWithCustomContextTestDescription: string = 107 | 'When message is "Verbose!" and context is "Verbose", ' + 108 | 'expect log with "verbose" level, "Verbose!" message and "Verbose" context'; 109 | 110 | it(verboseWithCustomContextTestDescription, async () => { 111 | await masterTest('verbose', 'Verbose!', 'Verbose'); 112 | }); 113 | 114 | }); 115 | 116 | }); 117 | 118 | async function masterTest( 119 | method: keyof CoreLogger, 120 | message: string, 121 | context?: string, 122 | trace?: string 123 | 124 | ): Promise { 125 | 126 | const expectedLevel: string = method !== 'log' ? method : 'info'; 127 | const expectedContext: string = context || 'Global'; 128 | const expectedMessage: string = message; 129 | const expectedPid: number = process.pid; 130 | const expectedTrace: string | undefined = trace; 131 | 132 | const hookStdPromise: hookStd.HookPromise = hookStd.stdout((receivedMessage: string) => { 133 | hookStdPromise.unhook(); 134 | 135 | const messageObject: Message = JSON.parse(receivedMessage); 136 | const logDate: Date = new Date(Date.parse(messageObject.timestamp)); 137 | 138 | expect(messageObject.context).toBe(expectedContext); 139 | expect(messageObject.pid).toBe(expectedPid); 140 | expect(messageObject.level).toBe(expectedLevel); 141 | expect(messageObject.message).toBe(expectedMessage); 142 | expect(messageObject.trace).toBe(expectedTrace); 143 | expect(logDate).toBeInstanceOf(Date); 144 | }); 145 | 146 | const logger: CoreLogger = new CoreLogger(); 147 | 148 | if (method === 'error') { 149 | logger[method](message, trace, context); 150 | } else { 151 | logger[method](message, context); 152 | } 153 | 154 | await hookStdPromise; 155 | } 156 | -------------------------------------------------------------------------------- /nodejs-rest-client/test/unit/core/lib/clamav/client/ClamAVClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClamAVClient, 3 | ClamAVConnectionOptions, 4 | ClamAVPingDetails, 5 | ClamAVScanDetails, 6 | ClamAVVersionDetails 7 | } from '../../../../../../src/core/lib/clamav'; 8 | import { IMockClamAVServer, MockHelper } from '../../../../../.helper/MockHelper'; 9 | import { Readable } from 'stream'; 10 | import { ClamAVScanStatus } from '../../../../../../src/core/lib/clamav/client/types/ClamAVScanStatus'; 11 | import { ClamAVClientError } from '../../../../../../src/core/lib/clamav/client/errors/ClamAVClientError'; 12 | 13 | let mockClamAVServer: IMockClamAVServer; 14 | 15 | afterEach(() => { 16 | mockClamAVServer.disable(); 17 | }); 18 | 19 | describe('ClamAVClient', () => { 20 | const connectionOptions: ClamAVConnectionOptions = { host: 'localhost', port: 42 }; 21 | const serverDelayInMs: number = 10; 22 | 23 | describe(`Send SCAN command`, () => { 24 | 25 | it('Expect parsed SCAN details object with CLEAN status', async () => { 26 | const mockReceivedData: string = 'stream: OK\0\n'; 27 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(1)); 28 | 29 | mockClamAVServer = MockHelper.createClamAVServer({ 30 | sendOnConnection: mockReceivedData, 31 | delayInMs : serverDelayInMs 32 | }); 33 | 34 | const expectedDetails: ClamAVScanDetails = { 35 | Message: 'Ok', 36 | Status : ClamAVScanStatus.CLEAN 37 | }; 38 | 39 | const scanDetails: ClamAVScanDetails = await ClamAVClient.scanStream(mockReadStream, connectionOptions); 40 | 41 | expect(scanDetails).toEqual(expectedDetails); 42 | }); 43 | 44 | it('Expect parsed SCAN details object with INFECTED status', async () => { 45 | const mockReceivedData: string = 'stream: Some-Signature FOUND\0\n'; 46 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(1)); 47 | 48 | mockClamAVServer = MockHelper.createClamAVServer({ 49 | sendOnConnection: mockReceivedData, 50 | delayInMs : serverDelayInMs 51 | }); 52 | 53 | const expectedDetails: ClamAVScanDetails = { 54 | Message: 'Some-Signature found', 55 | Status : ClamAVScanStatus.INFECTED 56 | }; 57 | 58 | const scanDetails: ClamAVScanDetails = await ClamAVClient.scanStream(mockReadStream, connectionOptions); 59 | 60 | expect(scanDetails).toEqual(expectedDetails); 61 | }); 62 | 63 | const scanAbortedTestDescription: string = 64 | `When the client receives a response but the data has not yet been sent, ` + 65 | `expect ClamAVClientError says that the scanning was aborted due to "Internal ERROR"`; 66 | 67 | it(scanAbortedTestDescription, async () => { 68 | const mockReceivedData: string = 'Internal ERROR'; 69 | const mockReadStream: Readable = MockHelper.createReadStream(Buffer.alloc(10)); 70 | 71 | mockClamAVServer = MockHelper.createClamAVServer({ 72 | sendOnConnection: mockReceivedData, 73 | delayInMs : 0 74 | }); 75 | 76 | const expectedError: ClamAVClientError = ClamAVClientError.createScanAbortedError('Internal ERROR'); 77 | 78 | await expect(ClamAVClient.scanStream(mockReadStream, connectionOptions)).rejects.toEqual(expectedError); 79 | }); 80 | 81 | }); 82 | 83 | describe(`Send PING command`, () => { 84 | 85 | it('Expect parsed PONG details object', async () => { 86 | const mockReceivedData: string = 'PONG\0\n'; 87 | 88 | mockClamAVServer = MockHelper.createClamAVServer({ 89 | sendOnConnection: mockReceivedData, 90 | delayInMs : serverDelayInMs 91 | }); 92 | 93 | const expectedDetails: ClamAVPingDetails = { Message: 'PONG' }; 94 | 95 | const pingDetails: ClamAVPingDetails = await ClamAVClient.ping(connectionOptions); 96 | 97 | expect(pingDetails).toEqual(expectedDetails); 98 | }); 99 | 100 | const connectionTimedOutTestDescription: string = 101 | `When the server response time exceeded the "timeoutInMs" option, ` + 102 | `expect ClamAVClientError says the connection to ClamAV server was timed out`; 103 | 104 | it(connectionTimedOutTestDescription, async () => { 105 | const mockReceivedData: string = 'Internal ERROR'; 106 | 107 | mockClamAVServer = MockHelper.createClamAVServer({ 108 | sendOnConnection: mockReceivedData, 109 | delayInMs : 20 110 | }); 111 | 112 | const expectedError: ClamAVClientError = ClamAVClientError.createConnectionTimedOutError(); 113 | 114 | const scanConnectionOptions: ClamAVConnectionOptions = { ...connectionOptions, timeoutInMs: 10 }; 115 | 116 | await expect(ClamAVClient.ping(scanConnectionOptions)).rejects.toEqual(expectedError); 117 | }); 118 | 119 | }); 120 | 121 | describe(`Send VERSION command`, () => { 122 | 123 | it('Expect parsed VERSION details object', async () => { 124 | const mockReceivedData: string = 'ClamAV 0.102.0/25000/Wed Jan 01 00:00:00 2019\0\n'; 125 | 126 | mockClamAVServer = MockHelper.createClamAVServer({ 127 | sendOnConnection: mockReceivedData, 128 | delayInMs : serverDelayInMs 129 | }); 130 | 131 | const expectedDetails: ClamAVVersionDetails = { 132 | ClamAV : '0.102.0', 133 | SignatureDatabase: { version: '25000', buildTime: 'Wed Jan 01 00:00:00 2019' } 134 | }; 135 | 136 | const versionDetails: ClamAVVersionDetails = await ClamAVClient.getVersion(connectionOptions); 137 | 138 | expect(versionDetails).toEqual(expectedDetails); 139 | }); 140 | 141 | }); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /nodejs-rest-client/src/core/lib/clamav/client/ClamAVClient.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { createConnection, Socket } from 'net'; 3 | import { ClamAVCommandFactory } from '../command/factory/ClamAVCommandFactory'; 4 | import { ClamaAVCommandType } from '../command/types/ClamaAVCommandType'; 5 | import { ClamAVScanDetails } from './types/ClamAVScanDetails'; 6 | import { ClamAVPingDetails } from './types/ClamAVPingDetails'; 7 | import { ClamAVVersionDetails } from './types/ClamAVVersionDetails'; 8 | import { ClamAVClientResponseParser } from './parser/ClamAVClientResponseParser'; 9 | import { ClamAVClientError } from './errors/ClamAVClientError'; 10 | import { ClamAVConnectionOptions } from './types/ClamAVConnectionOptions'; 11 | import { ClamAVCommand } from '../command/types/ClamAVCommand'; 12 | 13 | export class ClamAVClient { 14 | 15 | private readonly host: string; 16 | 17 | private readonly port: number; 18 | 19 | private readonly timeoutInMs: number; 20 | 21 | private readonly parsedCommand: string; 22 | 23 | private readonly inputData: { isFinished: boolean, stream: Readable } | undefined; 24 | 25 | private readonly commandResponseChunks: Buffer[]; 26 | 27 | private constructor(host: string, port: number, timeoutInMs: number | undefined, command: ClamAVCommand) { 28 | this.host = host; 29 | this.port = port; 30 | this.timeoutInMs = timeoutInMs || ClamAVClient.DEFAULT_TIMEOUT_IN_MS; 31 | this.parsedCommand = ClamAVClient.parseCommand(command); 32 | 33 | if (command.data) { 34 | this.inputData = { isFinished: false, stream: command.data }; 35 | } 36 | 37 | this.commandResponseChunks = []; 38 | } 39 | 40 | public static async scanStream(stream: Readable, options: ClamAVConnectionOptions): Promise { 41 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand(ClamaAVCommandType.INSTREAM, stream); 42 | const client: ClamAVClient = new ClamAVClient(options.host, options.port, options.timeoutInMs, command); 43 | 44 | const responseMessage: string = await client.sendCommand(); 45 | 46 | return ClamAVClientResponseParser.parseScanDetails(responseMessage); 47 | } 48 | 49 | public static async ping(options: ClamAVConnectionOptions): Promise { 50 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand(ClamaAVCommandType.PING); 51 | const client: ClamAVClient = new ClamAVClient(options.host, options.port, options.timeoutInMs, command); 52 | 53 | const responseMessage: string = await client.sendCommand(); 54 | 55 | return ClamAVClientResponseParser.parsePingDetails(responseMessage); 56 | } 57 | 58 | public static async getVersion(options: ClamAVConnectionOptions): Promise { 59 | const command: ClamAVCommand = ClamAVCommandFactory.createCommand(ClamaAVCommandType.VERSION); 60 | const client: ClamAVClient = new ClamAVClient(options.host, options.port, options.timeoutInMs, command); 61 | 62 | const responseMessage: string = await client.sendCommand(); 63 | 64 | return ClamAVClientResponseParser.parseVersionDetails(responseMessage); 65 | } 66 | 67 | private async sendCommand(): Promise { 68 | return new Promise((resolve: (value: string) => void, reject: (error: Error) => void): void => { 69 | 70 | const connectTimer: NodeJS.Timeout = setTimeout( 71 | (): void => { 72 | socket.destroy(ClamAVClientError.createConnectionTimedOutError()); 73 | 74 | if (this.inputData) { 75 | this.inputData.stream.destroy(); 76 | } 77 | }, 78 | this.timeoutInMs 79 | ); 80 | 81 | const socketOnConnectListener: () => void = (): void => { 82 | socket.write(this.parsedCommand); 83 | 84 | const inputData: { isFinished: boolean, stream: Readable } | undefined = this.inputData; 85 | 86 | if (inputData) { 87 | inputData.stream.addListener('end', (): void => { 88 | inputData.isFinished = true; 89 | inputData.stream.destroy(); 90 | }); 91 | 92 | inputData.stream.addListener('error', reject); 93 | 94 | inputData.stream.pipe(socket); 95 | } 96 | }; 97 | 98 | const socketOnDataListener: (chunk: Buffer) => void = (chunk: Buffer): void => { 99 | clearTimeout(connectTimer); 100 | 101 | if (this.inputData && !this.inputData.stream.isPaused()) { 102 | this.inputData.stream.pause(); 103 | } 104 | 105 | this.commandResponseChunks.push(chunk); 106 | }; 107 | 108 | const socketOnEndListener: () => void = (): void => { 109 | clearTimeout(connectTimer); 110 | 111 | const commandResultBuffer: Buffer = Buffer.concat(this.commandResponseChunks); 112 | 113 | const inputData: { isFinished: boolean, stream: Readable } | undefined = this.inputData; 114 | 115 | if (inputData && !inputData.isFinished) { 116 | inputData.stream.destroy(); 117 | 118 | const commandResult: string = commandResultBuffer.toString('utf-8'); 119 | const cleanCommandResult: string = ClamAVClientResponseParser.clearNoise(commandResult); 120 | 121 | reject(ClamAVClientError.createScanAbortedError(cleanCommandResult)); 122 | } 123 | 124 | resolve(commandResultBuffer.toString('utf-8')); 125 | }; 126 | 127 | const socket: Socket = createConnection({ host: this.host, port: this.port }); 128 | 129 | socket.setTimeout(this.timeoutInMs); 130 | socket.addListener('connect', socketOnConnectListener); 131 | socket.addListener('data', socketOnDataListener); 132 | socket.addListener('end', socketOnEndListener); 133 | socket.addListener('error', reject); 134 | }); 135 | } 136 | 137 | private static parseCommand(command: ClamAVCommand): string { 138 | let parsedCommand: string = command.name; 139 | 140 | if (command.prefix) { 141 | parsedCommand = command.prefix + parsedCommand; 142 | } 143 | if (command.postfix) { 144 | parsedCommand = parsedCommand + command.postfix; 145 | } 146 | 147 | return parsedCommand; 148 | } 149 | 150 | private static readonly DEFAULT_TIMEOUT_IN_MS: number = 5000; 151 | 152 | } 153 | -------------------------------------------------------------------------------- /scanner/clamav/docker/freshclam.conf: -------------------------------------------------------------------------------- 1 | ## 2 | ## Example config file for freshclam 3 | ## Please read the freshclam.conf(5) manual before editing this file. 4 | ## 5 | 6 | 7 | # Comment or remove the line below. 8 | 9 | # Path to the database directory. 10 | # WARNING: It must match clamd.conf's directive! 11 | # Default: hardcoded (depends on installation options) 12 | DatabaseDirectory /var/lib/clamav 13 | 14 | # Path to the log file (make sure it has proper permissions) 15 | # Default: disabled 16 | # UpdateLogFile /dev/stdout 17 | 18 | # Maximum size of the log file. 19 | # Value of 0 disables the limit. 20 | # You may use 'M' or 'm' for megabytes (1M = 1m = 1048576 bytes) 21 | # and 'K' or 'k' for kilobytes (1K = 1k = 1024 bytes). 22 | # in bytes just don't use modifiers. If LogFileMaxSize is enabled, 23 | # log rotation (the LogRotate option) will always be enabled. 24 | # Default: 1M 25 | # LogFileMaxSize 2M 26 | 27 | # Log time with each message. 28 | # Default: no 29 | LogTime yes 30 | 31 | # Enable verbose logging. 32 | # Default: no 33 | LogVerbose yes 34 | 35 | # Use system logger (can work together with UpdateLogFile). 36 | # Default: no 37 | LogSyslog no 38 | 39 | # Specify the type of syslog messages - please refer to 'man syslog' 40 | # for facility names. 41 | # Default: LOG_LOCAL6 42 | # LogFacility LOG_MAIL 43 | 44 | # Enable log rotation. Always enabled when LogFileMaxSize is enabled. 45 | # Default: no 46 | # LogRotate yes 47 | 48 | # This option allows you to save the process identifier of the daemon 49 | # Default: disabled 50 | # PidFile /var/run/freshclam.pid 51 | 52 | # By default when started freshclam drops privileges and switches to the 53 | # "clamav" user. This directive allows you to change the database owner. 54 | # Default: clamav (may depend on installation options) 55 | DatabaseOwner clamav 56 | 57 | # Use DNS to verify virus database version. Freshclam uses DNS TXT records 58 | # to verify database and software versions. With this directive you can change 59 | # the database verification domain. 60 | # WARNING: Do not touch it unless you're configuring freshclam to use your 61 | # own database verification domain. 62 | # Default: current.cvd.clamav.net 63 | # DNSDatabaseInfo current.cvd.clamav.net 64 | 65 | # Uncomment the following line and replace XY with your country 66 | # code. See http://www.iana.org/cctld/cctld-whois.htm for the full list. 67 | # You can use db.XY.ipv6.clamav.net for IPv6 connections. 68 | DatabaseMirror db.uk.clamav.net 69 | 70 | # database.clamav.net is a round-robin record which points to our most 71 | # reliable mirrors. It's used as a fall back in case db.XY.clamav.net is 72 | # not working. DO NOT TOUCH the following line unless you know what you 73 | # are doing. 74 | DatabaseMirror database.clamav.net 75 | 76 | # How many attempts to make before giving up. 77 | # Default: 3 (per mirror) 78 | # MaxAttempts 5 79 | 80 | # With this option you can control scripted updates. It's highly recommended 81 | # to keep it enabled. 82 | # Default: yes 83 | # ScriptedUpdates yes 84 | 85 | # By default freshclam will keep the local databases (.cld) uncompressed to 86 | # make their handling faster. With this option you can enable the compression; 87 | # the change will take effect with the next database update. 88 | # Default: no 89 | # CompressLocalDatabase no 90 | 91 | # With this option you can provide custom sources (http:// or file://) for 92 | # database files. This option can be used multiple times. 93 | # Default: no custom URLs 94 | # DatabaseCustomURL http://myserver.com/mysigs.ndb 95 | # DatabaseCustomURL file:///mnt/nfs/local.hdb 96 | 97 | # This option allows you to easily point freshclam to private mirrors. 98 | # If PrivateMirror is set, freshclam does not attempt to use DNS 99 | # to determine whether its databases are out-of-date, instead it will 100 | # use the If-Modified-Since request or directly check the headers of the 101 | # remote database files. For each database, freshclam first attempts 102 | # to download the CLD file. If that fails, it tries to download the 103 | # CVD file. This option overrides DatabaseMirror, DNSDatabaseInfo 104 | # and ScriptedUpdates. It can be used multiple times to provide 105 | # fall-back mirrors. 106 | # Default: disabled 107 | # PrivateMirror mirror1.mynetwork.com 108 | # PrivateMirror mirror2.mynetwork.com 109 | 110 | # Number of database checks per day. 111 | # Default: 12 (every two hours) 112 | # Checks 24 113 | 114 | # Proxy settings 115 | # Default: disabled 116 | # HTTPProxyServer myproxy.com 117 | # HTTPProxyPort 1234 118 | # HTTPProxyUsername myusername 119 | # HTTPProxyPassword mypass 120 | 121 | # If your servers are behind a firewall/proxy which applies User-Agent 122 | # filtering you can use this option to force the use of a different 123 | # User-Agent header. 124 | # Default: clamav/version_number 125 | # HTTPUserAgent SomeUserAgentIdString 126 | 127 | # Use aaa.bbb.ccc.ddd as client address for downloading databases. Useful for 128 | # multi-homed systems. 129 | # Default: Use OS'es default outgoing IP address. 130 | # LocalIPAddress aaa.bbb.ccc.ddd 131 | 132 | # Send the RELOAD command to clamd. 133 | # Default: no 134 | # NotifyClamd /path/to/clamd.conf 135 | 136 | # Run command after successful database update. 137 | # Default: disabled 138 | # OnUpdateExecute command 139 | 140 | # Run command when database update process fails. 141 | # Default: disabled 142 | # OnErrorExecute command 143 | 144 | # Run command when freshclam reports outdated version. 145 | # In the command string %v will be replaced by the new version number. 146 | # Default: disabled 147 | # OnOutdatedExecute command 148 | 149 | # Don't fork into background. 150 | # Default: no 151 | Foreground yes 152 | 153 | # Enable debug messages in libclamav. 154 | # Default: no 155 | # Debug yes 156 | 157 | # Timeout in seconds when connecting to database server. 158 | # Default: 30 159 | # ConnectTimeout 60 160 | 161 | # Timeout in seconds when reading from database server. 162 | # Default: 30 163 | # ReceiveTimeout 60 164 | 165 | # With this option enabled, freshclam will attempt to load new 166 | # databases into memory to make sure they are properly handled 167 | # by libclamav before replacing the old ones. 168 | # Default: yes 169 | # TestDatabases yes 170 | 171 | # When enabled freshclam will submit statistics to the ClamAV Project about 172 | # the latest virus detections in your environment. The ClamAV maintainers 173 | # will then use this data to determine what types of malware are the most 174 | # detected in the field and in what geographic area they are. 175 | # Freshclam will connect to clamd in order to get recent statistics. 176 | # Default: no 177 | # SubmitDetectionStats /path/to/clamd.conf 178 | 179 | # Country of origin of malware/detection statistics (for statistical 180 | # purposes only). The statistics collector at ClamAV.net will look up 181 | # your IP address to determine the geographical origin of the malware 182 | # reported by your installation. If this installation is mainly used to 183 | # scan data which comes from a different location, please enable this 184 | # option and enter a two-letter code (see http://www.iana.org/domains/root/db/) 185 | # of the country of origin. 186 | # Default: disabled 187 | # DetectionStatsCountry country-code 188 | 189 | # This option enables support for our "Personal Statistics" service. 190 | # When this option is enabled, the information on malware detected by 191 | # your clamd installation is made available to you through our website. 192 | # To get your HostID, log on http://www.stats.clamav.net and add a new 193 | # host to your host list. Once you have the HostID, uncomment this option 194 | # and paste the HostID here. As soon as your freshclam starts submitting 195 | # information to our stats collecting service, you will be able to view 196 | # the statistics of this clamd installation by logging into 197 | # http://www.stats.clamav.net with the same credentials you used to 198 | # generate the HostID. For more information refer to: 199 | # http://www.clamav.net/documentation.html#cctts 200 | # This feature requires SubmitDetectionStats to be enabled. 201 | # Default: disabled 202 | # DetectionStatsHostID unique-id 203 | 204 | # This option enables support for Google Safe Browsing. When activated for 205 | # the first time, freshclam will download a new database file (safebrowsing.cvd) 206 | # which will be automatically loaded by clamd and clamscan during the next 207 | # reload, provided that the heuristic phishing detection is turned on. This 208 | # database includes information about websites that may be phishing sites or 209 | # possible sources of malware. When using this option, it's mandatory to run 210 | # freshclam at least every 30 minutes. 211 | # Freshclam uses the ClamAV's mirror infrastructure to distribute the 212 | # database and its updates but all the contents are provided under Google's 213 | # terms of use. See http://www.google.com/transparencyreport/safebrowsing 214 | # and http://www.clamav.net/documentation.html#safebrowsing 215 | # for more information. 216 | # Default: disabled 217 | # SafeBrowsing yes 218 | 219 | # This option enables downloading of bytecode.cvd, which includes additional 220 | # detection mechanisms and improvements to the ClamAV engine. 221 | # Default: enabled 222 | # Bytecode yes 223 | 224 | # Download an additional 3rd party signature database distributed through 225 | # the ClamAV mirrors. 226 | # This option can be used multiple times. 227 | # ExtraDatabase dbname1 228 | # ExtraDatabase dbname2 --------------------------------------------------------------------------------