Merge branch 'feature/SUPPORT-8841_pass_protect_files_for_dev' into develop

# Conflicts:
#	README.md
This commit is contained in:
Alexandr Shalaginov 2025-01-15 09:11:52 +03:00
commit f9204bf4f7
8 changed files with 90 additions and 107 deletions

View file

@ -22,16 +22,36 @@
## **Конфигурация и запуск** ## **Конфигурация и запуск**
### **1. Конфигурация параметров для сервиса** ### **1. Конфигурация параметров для сервиса**
Для корректного запуска сервиса добавьте следующие параметры в файл `application.properties`: Для корректного запуска сервиса добавьте следующие параметры в файл `application.yaml`:
```properties ```yaml
file.upload.directory=/tmp/uploaded_files # Путь для хранения временных файлов file:
spring.servlet.multipart.max-file-size=10MB # Максимальный размер загружаемого файла upload:
spring.servlet.multipart.max-request-size=10MB # Максимальный размер запроса directory: '/tmp/uploaded_files' # Путь для хранения временных файлов
server.port=8080 # Порт, на котором работает сервис spring:
servlet:
multipart:
max-file-size: 10MB # Максимальный размер загружаемого файла
max-request-size: 10MB # Максимальный размер запроса
server:
port: 8080 # Порт, на котором работает сервис
```
Если KESL использует русскую локализацию, то следует в файл `application.yaml` добавить следующий параметр:
```yaml
password:
protected:
result:
name: 'Объекты, защищенные паролем'
```
Для английской локализации:
```yaml
password:
protected:
result:
name: 'Password-protected objects'
``` ```
### **2. Запуск JAR-файла с конфигурационным файлом** ### **2. Запуск JAR-файла с конфигурационным файлом**
```bash ```bash
java -jar app.jar --spring.config.location=file:/path/to/your/application.properties java -jar app.jar --spring.config.location=file:/path/to/your/application.yaml -Dfile.encoding=UTF8
``` ```
Ссылка на av-service задаётся в перекладчике (сервис ervu-lkrp-av) в параметре AV_REST_ADDRESS. Пример - AV_REST_ADDRESS=http://10.10.10.10:8080/av-wrapper/scan-file Ссылка на av-service задаётся в перекладчике (сервис ervu-lkrp-av) в параметре AV_REST_ADDRESS. Пример - AV_REST_ADDRESS=http://10.10.10.10:8080/av-wrapper/scan-file

View file

@ -8,7 +8,7 @@ Type=simple
Restart=on-failure Restart=on-failure
User=root User=root
WorkingDirectory=/opt/av-service WorkingDirectory=/opt/av-service
ExecStart=/usr/bin/java -jar av-service.jar ExecStart=/usr/bin/java -jar av-service.jar -Dfile.encoding=UTF8
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -22,6 +22,12 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-exec</artifactId>
<version>1.12</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>

View file

@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.micord.av.service.model.ScanResult;
import ru.micord.av.service.service.FileScanService; import ru.micord.av.service.service.FileScanService;
/** /**
@ -32,11 +31,10 @@ public class FileScanController {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
try { try {
ScanResult result = fileScanService.scanFile(file); int result = fileScanService.scanFile(file);
LOGGER.info("Scan result for file {}: status - {}, verdicts - {}", LOGGER.info("Scan result for file {}: exitCode - {}",
file.getOriginalFilename(), file.getOriginalFilename(),
result.status(), result);
String.join(", ", result.verdicts()));
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} }
catch (Exception e) { catch (Exception e) {

View file

@ -1,17 +0,0 @@
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) {
}
}
}

View file

@ -2,23 +2,20 @@ package ru.micord.av.service.service;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.util.concurrent.TimeoutException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
import ru.micord.av.service.exception.AvException; import ru.micord.av.service.exception.AvException;
import ru.micord.av.service.model.ScanResult;
/** /**
* @author Adel Kalimullin * @author Adel Kalimullin
@ -30,21 +27,16 @@ public class FileScanService {
private static final Logger LOGGER = LoggerFactory.getLogger(FileScanService.class); private static final Logger LOGGER = LoggerFactory.getLogger(FileScanService.class);
@Value("${file.upload.directory}") @Value("${file.upload.directory}")
private String uploadDirectory; private String uploadDirectory;
@Value("${password.protected.result.name:Объекты, защищенные паролем}")
private String passwordProtectedResultName;
public ScanResult scanFile(MultipartFile file) { public int scanFile(MultipartFile file) {
File tempFile = null; File tempFile = null;
LocalDateTime startTime;
LocalDateTime stopTime;
try { try {
tempFile = saveFile(file); tempFile = saveFile(file);
startTime = LocalDateTime.now(); return runKeslScan(tempFile, file.getOriginalFilename());
String rawResult = runKeslScan(tempFile);
stopTime = LocalDateTime.now();
return parseKeslResponse(rawResult, startTime, stopTime, tempFile,
file.getOriginalFilename()
);
} }
catch (Exception e) { catch (IOException | InterruptedException | TimeoutException e) {
throw new AvException("Error scanning file: " + file.getOriginalFilename(), e); throw new AvException("Error scanning file: " + file.getOriginalFilename(), e);
} }
finally { finally {
@ -61,72 +53,49 @@ public class FileScanService {
private File saveFile(MultipartFile file) throws IOException { private File saveFile(MultipartFile file) throws IOException {
Path uploadPath = Paths.get(uploadDirectory); Path uploadPath = Paths.get(uploadDirectory);
if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath);
Files.createDirectories(uploadPath); Path tempFilePath = Files.createTempFile(uploadPath, "kesl-upload-", ".tmp");
} File tempFile = tempFilePath.toFile();
Path tempFile = Files.createTempFile(uploadPath, "kesl-upload-", ".tmp"); file.transferTo(tempFile);
file.transferTo(tempFile.toFile()); return tempFile;
return tempFile.toFile();
} }
private String runKeslScan(File file) throws IOException, InterruptedException { private int runKeslScan(File file, String originalFileName)
ProcessBuilder processBuilder = new ProcessBuilder(KESL_CONTROL, KESL_SCAN, throws IOException, InterruptedException, TimeoutException {
file.getAbsolutePath() ProcessResult processResult = new ProcessExecutor()
); .command(KESL_CONTROL, KESL_SCAN, file.getAbsolutePath())
processBuilder.redirectErrorStream(true); .redirectOutput(Slf4jStream.of(getClass()).asDebug())
Process process = processBuilder.start(); .readOutput(true)
try (InputStream inputStream = process.getInputStream()) { .execute();
String result = new String(inputStream.readAllBytes()); String processOutput = processResult.outputUTF8();
int exitCode = process.waitFor(); int exitCode = processResult.getExitValue();
if (exitCode != 0 && exitCode != 72) {
throw new AvException("KESL error, exit code: " + exitCode); if (exitCode != 0 && exitCode != 72) {
} throw new AvException(
return result; "KESL error scanning file: " + originalFileName + ", exit code: " + exitCode);
} }
checkScanResult(processOutput, originalFileName);
return exitCode;
} }
private ScanResult parseKeslResponse(String rawResult, LocalDateTime startTime, private void checkScanResult(String result, String originalFileName) {
LocalDateTime stopTime, File file, String originalFilename) { if (!result.contains(passwordProtectedResultName)) {
Map<String, ScanResult.Scan> scanResults = new HashMap<>(); throw new AvException(String.format(
List<String> verdicts = new ArrayList<>(); "File scan result doesn't contains \"%s\", "
List<ScanResult.Scan.Threat> threats = new ArrayList<>(); + "please check property \"password.protected.result.name\" is correct",
String verdict = null; passwordProtectedResultName
));
}
for (String line : rawResult.split("\n")) { for (String line : result.split("\n")) {
if (line.startsWith("Infected objects")) { String[] lineParts = line.split(":");
verdict = extractInteger(line) > 0 if (lineParts.length > 1) {
? ScanResult.Scan.VERDICT_INFECTED if (lineParts[0].startsWith(passwordProtectedResultName)
: ScanResult.Scan.VERDICT_CLEAN; && Integer.parseInt(lineParts[1].trim()) > 0) {
if (ScanResult.Scan.VERDICT_INFECTED.equals(verdict)) throw new AvException("Detected password-protected file: " + originalFileName);
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;
} }
} }

View file

@ -1 +0,0 @@
file.upload.directory = /tmp/uploaded_files

View file

@ -0,0 +1,8 @@
file:
upload:
directory: '/tmp/uploaded_files'
password:
protected:
result:
name: 'Объекты, защищенные паролем'