Merge remote-tracking branch 'origin/feature/SUPPORT-8841_pass_protect_files'
This commit is contained in:
commit
7fa3844866
7 changed files with 147 additions and 112 deletions
55
README.md
Normal file
55
README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# **av-service**
|
||||
|
||||
`av-service` — это сервис, взаимодействующий с Kaspersky Endpoint Security для Linux (KESL) на одной машине.
|
||||
|
||||
Основная задача сервиса — обеспечение защиты и анализа файлов с помощью интеграции с KESL.
|
||||
Сервис использует консольные команды для выполнения операций и взаимодействия с KESL, таких как запуск задач проверки и получение результатов анализа.
|
||||
---
|
||||
|
||||
## **Основные функции**
|
||||
- Проверка файлов на наличие угроз с использованием KESL.
|
||||
- Обработка файлов через POST-запросы.
|
||||
- Логирование результатов проверки.
|
||||
|
||||
---
|
||||
|
||||
## **Требования**
|
||||
- **Java 17**
|
||||
- **KESL** (установлен и настроен на одной машине с сервисом)
|
||||
|
||||
---
|
||||
|
||||
## **Конфигурация и запуск**
|
||||
|
||||
### **1. Конфигурация параметров для сервиса**
|
||||
Для корректного запуска сервиса добавьте следующие параметры в файл `application.yaml`:
|
||||
```yaml
|
||||
file:
|
||||
upload:
|
||||
directory: '/tmp/uploaded_files' # Путь для хранения временных файлов
|
||||
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-файла с конфигурационным файлом**
|
||||
```bash
|
||||
java -jar app.jar --spring.config.location=file:/path/to/your/application.yaml
|
||||
```
|
||||
2
pom.xml
2
pom.xml
|
|
@ -11,7 +11,7 @@
|
|||
</parent>
|
||||
<groupId>ru.micord.av.service</groupId>
|
||||
<artifactId>av-service</artifactId>
|
||||
<version>1.9.2</version>
|
||||
<version>1.10.2</version>
|
||||
<name>av-service</name>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
|
|
|
|||
|
|
@ -28,10 +28,15 @@ public class FileScanController {
|
|||
@PostMapping("/scan-file")
|
||||
public ResponseEntity<?> scanFile(@RequestPart("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
LOGGER.warn("The uploaded file is empty or missing.");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
try {
|
||||
ScanResult result = fileScanService.scanFile(file);
|
||||
LOGGER.info("Scan result for file {}: status - {}, verdicts - {}",
|
||||
file.getOriginalFilename(),
|
||||
result.status(),
|
||||
String.join(", ", result.verdicts()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ 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 java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
|
|
@ -19,93 +20,102 @@ import ru.micord.av.service.model.ScanResult;
|
|||
*/
|
||||
@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;
|
||||
}
|
||||
private static final String KESL_CONTROL = "kesl-control";
|
||||
private static final String KESL_SCAN = "--scan-file";
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FileScanService.class);
|
||||
@Value("${file.upload.directory}")
|
||||
private String uploadDirectory;
|
||||
@Value("${password.protected.result.name:Объекты, защищенные паролем}")
|
||||
private String passwordProtectedResultName;
|
||||
|
||||
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()
|
||||
);
|
||||
tempFile = saveFile(file);
|
||||
return runKeslScan(tempFile, file.getOriginalFilename());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new AvException("Ошибка при сканировании файла: " + file.getOriginalFilename(), e);
|
||||
catch (IOException | InterruptedException e) {
|
||||
throw new AvException("Error scanning file: " + file.getOriginalFilename(), e);
|
||||
}
|
||||
finally {
|
||||
fileManager.deleteFile(tempFile);
|
||||
try {
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
Files.delete(tempFile.toPath());
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.warn("Error deleting file: " + tempFile.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String runKeslScan(File file) throws IOException, InterruptedException {
|
||||
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 ScanResult runKeslScan(File file, String originalFileName) 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);
|
||||
|
||||
if (exitCode == 0) {
|
||||
LOGGER.debug("File {} scanned with result: {}", originalFileName, result);
|
||||
checkScanResult(result, originalFileName);
|
||||
return new ScanResult(
|
||||
null,
|
||||
null,
|
||||
100,
|
||||
null,
|
||||
"completed",
|
||||
new String[] {ScanResult.Scan.VERDICT_CLEAN}
|
||||
);
|
||||
}
|
||||
else if (exitCode == 72) {
|
||||
return new ScanResult(
|
||||
null,
|
||||
null,
|
||||
100,
|
||||
null,
|
||||
"completed",
|
||||
new String[] {ScanResult.Scan.VERDICT_INFECTED}
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw new AvException("KESL error scanning file "
|
||||
+ originalFileName
|
||||
+ ", exit code: " + 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;
|
||||
private void checkScanResult(String result, String originalFileName) {
|
||||
if (!result.contains(passwordProtectedResultName)) {
|
||||
throw new AvException(String.format("File scan result doesn't contains \"%s\", "
|
||||
+ "please check property \"password.protected.result.name\" is correct",
|
||||
passwordProtectedResultName));
|
||||
}
|
||||
|
||||
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";
|
||||
for (String line : result.split("\n")) {
|
||||
String[] lineParts = line.split(":");
|
||||
if (lineParts.length > 1) {
|
||||
if (lineParts[0].startsWith(passwordProtectedResultName)
|
||||
&& Integer.parseInt(lineParts[1].trim()) > 0) {
|
||||
throw new AvException("Detected password-protected file: " + originalFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 +0,0 @@
|
|||
file.upload.directory = /tmp/uploaded_files
|
||||
8
src/main/resources/application.yaml
Normal file
8
src/main/resources/application.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
file:
|
||||
upload:
|
||||
directory: '/tmp/uploaded_files'
|
||||
|
||||
password:
|
||||
protected:
|
||||
result:
|
||||
name: 'Объекты, защищенные паролем'
|
||||
Loading…
Add table
Add a link
Reference in a new issue