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