diff --git a/.gitignore b/.gitignore index 549e00a..8f922b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ HELP.md + target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f267431 --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.0 + + + ru.micord.av.service + av-service + 0.0.1-SNAPSHOT + av-service + av-service + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/ru/micord/av/service/AvServiceApplication.java b/src/main/java/ru/micord/av/service/AvServiceApplication.java new file mode 100644 index 0000000..7fe9aa8 --- /dev/null +++ b/src/main/java/ru/micord/av/service/AvServiceApplication.java @@ -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); + } + +} diff --git a/src/main/java/ru/micord/av/service/controller/FileScanController.java b/src/main/java/ru/micord/av/service/controller/FileScanController.java new file mode 100644 index 0000000..00b4d94 --- /dev/null +++ b/src/main/java/ru/micord/av/service/controller/FileScanController.java @@ -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(); + } + } +} diff --git a/src/main/java/ru/micord/av/service/model/AvResponse.java b/src/main/java/ru/micord/av/service/model/AvResponse.java new file mode 100644 index 0000000..3acab86 --- /dev/null +++ b/src/main/java/ru/micord/av/service/model/AvResponse.java @@ -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 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) { + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/micord/av/service/service/FileScanService.java b/src/main/java/ru/micord/av/service/service/FileScanService.java new file mode 100644 index 0000000..4e1278e --- /dev/null +++ b/src/main/java/ru/micord/av/service/service/FileScanService.java @@ -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); + } + } + } +} diff --git a/src/main/java/ru/micord/av/service/util/AvUtils.java b/src/main/java/ru/micord/av/service/util/AvUtils.java new file mode 100644 index 0000000..7272d23 --- /dev/null +++ b/src/main/java/ru/micord/av/service/util/AvUtils.java @@ -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 scanResults = new HashMap<>(); + List verdicts = new ArrayList<>(); + List 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; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..40a8f84 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=ervu-lkrp-kesl +server.port=8080 +file.upload.directory = /tmp/uploaded_files