Context
We would like to integrate an antivirus into our application, which will scan the uploaded files and decide whether or not to save them in our file system.
Overview
We have a springboot application dockerized and deployed with Docker Swarm.
The architecture we adopt is the following: the antivirus will be hosted in a container and we will request it as a web service.
Docker layer
We recommend the use of ClamAV, a proven antivirus.
Therefore, we use an open source docker image to mount our container hosting ClamAV, the chosen image is mailu/clamav. Our docker-compose file looks like this :
version: "3.1"services:
api:
hostname: api
container_name: api
build: ./api/.
ports:
- "9999:9999"
depends_on:
- scan
scan:
hostname: scan
container_name: scan
image: mailu/clamav:1.7
ports:
- "3310:3310"
restart: always
Spring-boot layer
We may use this ClamAV Client, we could also integrate its source code in our project.
In this tutorial, we just use this dependency in our pom.xml file.
pom.xml dependency
<!-- A clamAV client implementation -->
<dependency>
<groupId>fi.solita.clamav</groupId>
<artifactId>clamav-client</artifactId>
<version>1.0.1</version>
</dependency>
A fileScan service
@Service
public class FileScanServiceClam implements FileScanService { private final ClamAVClient clamAVClient; public FileScanServiceClam(){
this.clamAVClient = new ClamAVClient("scan", 3310);
} public List<FileScanResponse> scanFiles(MultipartFile[] files) {
return Arrays.stream(files).map(multipartFile -> {
FileScanResponse fileScanResponse = new FileScanResponse();
long startTime = System.currentTimeMillis();
fileScanResponse.setUploadTime(startTime);
try {
byte[] response = this.clamAVClient.scan(multipartFile.getInputStream());
boolean status = ClamAVClient.isCleanReply(response);
fileScanResponse.setDetected(!status);
System.out.println("File Scanned = " + multipartFile.getOriginalFilename() + ", Clam AV Response = " + status);
} catch(Exception ex) {
System.out.println("Exception occurred while scanning using clam av = " + ex.getMessage());
fileScanResponse.setErrorMessage(ex.getMessage());
}
fileScanResponse.setFileName(multipartFile.getOriginalFilename());
fileScanResponse.setSize(multipartFile.getSize());
fileScanResponse.setScanTimeInMilliSec(System.currentTimeMillis() - startTime);
return fileScanResponse;
}).collect(Collectors.toList());
}
}
Service integration to a controller
@PostMapping("/upload")
public ScanResponse<List<FileScanResponse>> uploadFiles(@RequestParam("files") MultipartFile files) {
return new ScanResponse<>(fileScanServiceClam.scanFiles(new MultipartFile[]{files}));
}
Carrying out a functional test
It is possible to create a test file containing the following string : corrupted_file.txt
X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
PS: Don’t forget to add this file to the antivirus exceptions.
The test curl command :
$ curl -X POST <http://localhost:9999/entrypoint/upload> -H "accept: */*" -H "Content-Type: multipart/form-data" -F "files=@corrupted_file.txt;type=application/json"{"data":[{"fileName":"corrupted_file.txt","detected":true,"size":68,"scanTimeInMilliSec":3,"errorMessage":null,"hash":null,"uploadTime":1600246731991}],"metaData":null}
⇒ Detected is equal to true
Extra-link
An implementation : https://github.com/AmineBenaddiGitHub/rt-spbt-nginx-docker