SUPPORT-8771:init av-service

This commit is contained in:
adel.kalimullin 2024-12-09 14:56:41 +03:00
parent 8fb7e98705
commit 0a7183c091
8 changed files with 268 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/

55
pom.xml Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.micord.av.service</groupId>
<artifactId>av-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>av-service</name>
<description>av-service</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,13 @@
package ru.micord.av.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AvServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AvServiceApplication.class, args);
}
}

View file

@ -0,0 +1,38 @@
package ru.micord.av.service.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.av.service.model.AvResponse;
import ru.micord.av.service.service.FileScanService;
/**
* @author Adel Kalimullin
*/
@RestController
@RequestMapping("/scans")
public class FileScanController {
private final FileScanService fileScanService;
public FileScanController(FileScanService fileScanService) {
this.fileScanService = fileScanService;
}
@PostMapping()
public ResponseEntity<?> scanFile( @RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().build();
}
try {
AvResponse result = fileScanService.scanFile(file);
return ResponseEntity.ok(result);
}
catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View file

@ -0,0 +1,17 @@
package ru.micord.av.service.model;
import java.util.Map;
/**
* @author Adel Kalimullin
*/
public record AvResponse(String completed, String created, Integer progress,
Map<String, Scan> scan_result, String status, String[] verdicts) {
public record Scan(String started, String stopped, Threat[] threats, String verdict) {
public static final String VERDICT_CLEAN = "clean";
public static final String VERDICT_INFECTED = "infected";
public record Threat(String name, String object) {
}
}
}

View file

@ -0,0 +1,79 @@
package ru.micord.av.service.service;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.av.service.model.AvResponse;
import ru.micord.av.service.util.AvUtils;
/**
* @author Adel Kalimullin
*/
@Service
public class FileScanService {
@Value("${file.upload.directory}")
public String uploadDirectory;
public AvResponse scanFile(MultipartFile file){
File tempFile = null;
LocalDateTime startTime;
LocalDateTime stopTime;
try {
tempFile = saveFile(file);
startTime = LocalDateTime.now();
String rawResult = runKeslScan(tempFile);
stopTime = LocalDateTime.now();
return AvUtils.parseKeslResponse(rawResult, startTime, stopTime,tempFile,file.getOriginalFilename());
}
catch (Exception e) {
throw new RuntimeException("Ошибка при сканировании файла: " + file.getOriginalFilename(), e);
}
finally {
deleteFile(tempFile);
}
}
private File saveFile(MultipartFile file) throws IOException {
Path uploadPath = Paths.get(uploadDirectory);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Path tempFile = Files.createTempFile(uploadPath, "kesl-upload-", ".tmp");
file.transferTo(tempFile.toFile());
return tempFile.toFile();
}
private String runKeslScan(File file) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(
AvUtils.KESL_CONTROL,
AvUtils.KESL_SCAN,
file.getAbsolutePath()
);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
try (InputStream inputStream = process.getInputStream()) {
String result = new String(inputStream.readAllBytes());
process.waitFor();
return result;
}
}
private void deleteFile(File file) {
if (file != null && file.exists()) {
try {
Files.delete(file.toPath());
}
catch (IOException e) {
throw new RuntimeException("Ошибка удаления временного файла: " + file.getAbsolutePath(), e);
}
}
}
}

View file

@ -0,0 +1,62 @@
package ru.micord.av.service.util;
import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ru.micord.av.service.model.AvResponse;
/**
* @author Adel Kalimullin
*/
public class AvUtils {
public static String KESL_CONTROL = "kesl-control";
public static String KESL_SCAN = "--scan-file";
public static AvResponse parseKeslResponse(String rawResult, LocalDateTime startTime,
LocalDateTime stopTime, File file, String originalFilename) {
Map<String, AvResponse.Scan> scanResults = new HashMap<>();
List<String> verdicts = new ArrayList<>();
List<AvResponse.Scan.Threat> threats = new ArrayList<>();
String verdict = null;
for (String line : rawResult.split("\n")) {
if (line.startsWith("Infected objects")) {
verdict = extractInteger(line) > 0
? AvResponse.Scan.VERDICT_INFECTED
: AvResponse.Scan.VERDICT_CLEAN;
if (AvResponse.Scan.VERDICT_INFECTED.equals(verdict))
threats.add(new AvResponse.Scan.Threat("", file.getAbsolutePath()));
}
if (line.startsWith("Scan errors") && extractInteger(line) > 0) {
verdict = "error";
}
}
if (verdict != null) {
scanResults.put(originalFilename, new AvResponse.Scan(startTime.toString(), stopTime.toString(),
threats.toArray(new AvResponse.Scan.Threat[0]), verdict
));
verdicts.add(verdict);
}
return new AvResponse(startTime.toString(), stopTime.toString(), 100, scanResults, "completed",
verdicts.toArray(new String[0])
);
}
private static int extractInteger(String line) {
String[] parts = line.split(":");
if (parts.length > 1) {
try {
return Integer.parseInt(parts[1].trim());
}
catch (NumberFormatException e) {
throw new RuntimeException(e);
}
}
return 0;
}
}

View file

@ -0,0 +1,3 @@
spring.application.name=ervu-lkrp-kesl
server.port=8080
file.upload.directory = /tmp/uploaded_files