Merge branch 'feature/SUPPORT-8771_av' into develop
This commit is contained in:
commit
a38ead8070
9 changed files with 282 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
HELP.md
|
HELP.md
|
||||||
|
|
||||||
target/
|
target/
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
!**/src/main/**/target/
|
!**/src/main/**/target/
|
||||||
|
|
|
||||||
41
pom.xml
Normal file
41
pom.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?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>1.0.0-SNAPSHOT</version>
|
||||||
|
<name>av-service</name>
|
||||||
|
<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>
|
||||||
13
src/main/java/ru/micord/av/service/AvServiceApplication.java
Normal file
13
src/main/java/ru/micord/av/service/AvServiceApplication.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package ru.micord.av.service.controller;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import ru.micord.av.service.model.ScanResult;
|
||||||
|
import ru.micord.av.service.service.FileScanService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Adel Kalimullin
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/av-wrapper")
|
||||||
|
public class FileScanController {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FileScanController.class);
|
||||||
|
private final FileScanService fileScanService;
|
||||||
|
|
||||||
|
public FileScanController(FileScanService fileScanService) {
|
||||||
|
this.fileScanService = fileScanService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/scan-file")
|
||||||
|
public ResponseEntity<?> scanFile(@RequestPart("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ScanResult result = fileScanService.scanFile(file);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
LOGGER.error(e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ru.micord.av.service.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Adel Kalimullin
|
||||||
|
*/
|
||||||
|
public class AvException extends RuntimeException {
|
||||||
|
public AvException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/ru/micord/av/service/model/ScanResult.java
Normal file
17
src/main/java/ru/micord/av/service/model/ScanResult.java
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ru.micord.av.service.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Adel Kalimullin
|
||||||
|
*/
|
||||||
|
public record ScanResult(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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/ru/micord/av/service/service/FileManager.java
Normal file
42
src/main/java/ru/micord/av/service/service/FileManager.java
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package ru.micord.av.service.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import ru.micord.av.service.exception.AvException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Adel Kalimullin
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class FileManager {
|
||||||
|
@Value("${file.upload.directory}")
|
||||||
|
public String uploadDirectory;
|
||||||
|
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteFile(File file) {
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
try {
|
||||||
|
Files.delete(file.toPath());
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new AvException("Ошибка удаления временного файла: " + file.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/main/java/ru/micord/av/service/service/FileScanService.java
Normal file
111
src/main/java/ru/micord/av/service/service/FileScanService.java
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package ru.micord.av.service.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import ru.micord.av.service.exception.AvException;
|
||||||
|
import ru.micord.av.service.model.ScanResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Adel Kalimullin
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class FileScanService {
|
||||||
|
public static String KESL_CONTROL = "kesl-control";
|
||||||
|
public static String KESL_SCAN = "--scan-file";
|
||||||
|
private final FileManager fileManager;
|
||||||
|
|
||||||
|
public FileScanService(FileManager fileManager) {
|
||||||
|
this.fileManager = fileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanResult scanFile(MultipartFile file) {
|
||||||
|
File tempFile = null;
|
||||||
|
LocalDateTime startTime;
|
||||||
|
LocalDateTime stopTime;
|
||||||
|
try {
|
||||||
|
tempFile = fileManager.saveFile(file);
|
||||||
|
startTime = LocalDateTime.now();
|
||||||
|
String rawResult = runKeslScan(tempFile);
|
||||||
|
stopTime = LocalDateTime.now();
|
||||||
|
return parseKeslResponse(rawResult, startTime, stopTime, tempFile,
|
||||||
|
file.getOriginalFilename()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new AvException("Ошибка при сканировании файла: " + file.getOriginalFilename(), e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fileManager.deleteFile(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String runKeslScan(File file) throws IOException, InterruptedException {
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(KESL_CONTROL, KESL_SCAN,
|
||||||
|
file.getAbsolutePath()
|
||||||
|
);
|
||||||
|
processBuilder.redirectErrorStream(true);
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
try (InputStream inputStream = process.getInputStream()) {
|
||||||
|
String result = new String(inputStream.readAllBytes());
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0 && exitCode != 72) {
|
||||||
|
throw new AvException("Ошибка KESL, код завершения: " + exitCode);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScanResult parseKeslResponse(String rawResult, LocalDateTime startTime,
|
||||||
|
LocalDateTime stopTime, File file, String originalFilename) {
|
||||||
|
Map<String, ScanResult.Scan> scanResults = new HashMap<>();
|
||||||
|
List<String> verdicts = new ArrayList<>();
|
||||||
|
List<ScanResult.Scan.Threat> threats = new ArrayList<>();
|
||||||
|
String verdict = null;
|
||||||
|
|
||||||
|
for (String line : rawResult.split("\n")) {
|
||||||
|
if (line.startsWith("Infected objects")) {
|
||||||
|
verdict = extractInteger(line) > 0
|
||||||
|
? ScanResult.Scan.VERDICT_INFECTED
|
||||||
|
: ScanResult.Scan.VERDICT_CLEAN;
|
||||||
|
if (ScanResult.Scan.VERDICT_INFECTED.equals(verdict))
|
||||||
|
threats.add(new ScanResult.Scan.Threat("", file.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
if (line.startsWith("Scan errors") && extractInteger(line) > 0) {
|
||||||
|
verdict = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verdict != null) {
|
||||||
|
scanResults.put(originalFilename,
|
||||||
|
new ScanResult.Scan(startTime.toString(), stopTime.toString(),
|
||||||
|
threats.toArray(new ScanResult.Scan.Threat[0]), verdict
|
||||||
|
)
|
||||||
|
);
|
||||||
|
verdicts.add(verdict);
|
||||||
|
}
|
||||||
|
return new ScanResult(startTime.toString(), stopTime.toString(), 100, scanResults, "completed",
|
||||||
|
verdicts.toArray(new String[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/main/resources/application.properties
Normal file
1
src/main/resources/application.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
file.upload.directory = /tmp/uploaded_files
|
||||||
Loading…
Add table
Add a link
Reference in a new issue