diff --git a/backend/build.gradle b/backend/build.gradle index 70e9613..72fed37 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -30,6 +30,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' compileOnly 'org.projectlombok:lombok' + implementation 'xyz.capybara:clamav-client:2.1.2' annotationProcessor 'org.projectlombok:lombok' } diff --git a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java index e67de4f..d06fc11 100644 --- a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java +++ b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java @@ -25,14 +25,17 @@ public class CustomQuoteRequestController { private final CustomQuoteRequestRepository requestRepo; private final CustomQuoteRequestAttachmentRepository attachmentRepo; + private final com.printcalculator.service.ClamAVService clamAVService; // TODO: Inject Storage Service private static final String STORAGE_ROOT = "storage_requests"; public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo, - CustomQuoteRequestAttachmentRepository attachmentRepo) { + CustomQuoteRequestAttachmentRepository attachmentRepo, + com.printcalculator.service.ClamAVService clamAVService) { this.requestRepo = requestRepo; this.attachmentRepo = attachmentRepo; + this.clamAVService = clamAVService; } // 1. Create Custom Quote Request @@ -68,6 +71,9 @@ public class CustomQuoteRequestController { for (MultipartFile file : files) { if (file.isEmpty()) continue; + // Scan for virus + clamAVService.scan(file.getInputStream()); + CustomQuoteRequestAttachment attachment = new CustomQuoteRequestAttachment(); attachment.setRequest(request); attachment.setOriginalFilename(file.getOriginalFilename()); diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index 2bc9dd9..2d4e637 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -28,6 +28,7 @@ public class OrderController { private final QuoteSessionRepository quoteSessionRepo; private final QuoteLineItemRepository quoteLineItemRepo; private final CustomerRepository customerRepo; + private final com.printcalculator.service.ClamAVService clamAVService; // TODO: Inject Storage Service or use a base path property private static final String STORAGE_ROOT = "storage_orders"; @@ -36,12 +37,14 @@ public class OrderController { OrderItemRepository orderItemRepo, QuoteSessionRepository quoteSessionRepo, QuoteLineItemRepository quoteLineItemRepo, - CustomerRepository customerRepo) { + CustomerRepository customerRepo, + com.printcalculator.service.ClamAVService clamAVService) { this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; this.quoteSessionRepo = quoteSessionRepo; this.quoteLineItemRepo = quoteLineItemRepo; this.customerRepo = customerRepo; + this.clamAVService = clamAVService; } @@ -229,6 +232,9 @@ public class OrderController { if (!item.getOrder().getId().equals(orderId)) { return ResponseEntity.badRequest().build(); } + + // Scan for virus + clamAVService.scan(file.getInputStream()); // Ensure path logic String relativePath = item.getStoredRelativePath(); diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteController.java b/backend/src/main/java/com/printcalculator/controller/QuoteController.java index 018b613..45369c1 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteController.java @@ -22,15 +22,17 @@ public class QuoteController { private final SlicerService slicerService; private final QuoteCalculator quoteCalculator; private final PrinterMachineRepository machineRepo; + private final com.printcalculator.service.ClamAVService clamAVService; // Defaults (using aliases defined in ProfileManager) private static final String DEFAULT_FILAMENT = "pla_basic"; private static final String DEFAULT_PROCESS = "standard"; - public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) { + public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo, com.printcalculator.service.ClamAVService clamAVService) { this.slicerService = slicerService; this.quoteCalculator = quoteCalculator; this.machineRepo = machineRepo; + this.clamAVService = clamAVService; } @PostMapping("/api/quote") @@ -99,6 +101,9 @@ public class QuoteController { return ResponseEntity.badRequest().build(); } + // Scan for virus + clamAVService.scan(file.getInputStream()); + // Fetch Default Active Machine PrinterMachine machine = machineRepo.findFirstByIsActiveTrue() .orElseThrow(() -> new IOException("No active printer found in database")); diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java index ce4261d..b58c224 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -40,6 +40,7 @@ public class QuoteSessionController { private final QuoteCalculator quoteCalculator; private final PrinterMachineRepository machineRepo; private final com.printcalculator.repository.PricingPolicyRepository pricingRepo; + private final com.printcalculator.service.ClamAVService clamAVService; // Defaults private static final String DEFAULT_FILAMENT = "pla_basic"; @@ -50,13 +51,15 @@ public class QuoteSessionController { SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo, - com.printcalculator.repository.PricingPolicyRepository pricingRepo) { + com.printcalculator.repository.PricingPolicyRepository pricingRepo, + com.printcalculator.service.ClamAVService clamAVService) { this.sessionRepo = sessionRepo; this.lineItemRepo = lineItemRepo; this.slicerService = slicerService; this.quoteCalculator = quoteCalculator; this.machineRepo = machineRepo; this.pricingRepo = pricingRepo; + this.clamAVService = clamAVService; } // 1. Start a new empty session @@ -99,6 +102,9 @@ public class QuoteSessionController { private QuoteLineItem addItemToSession(QuoteSession session, MultipartFile file, com.printcalculator.dto.PrintSettingsDto settings) throws IOException { if (file.isEmpty()) throw new IOException("File is empty"); + // Scan for virus + clamAVService.scan(file.getInputStream()); + // 1. Define Persistent Storage Path // Structure: storage_quotes/{sessionId}/{uuid}.{ext} String storageDir = "storage_quotes/" + session.getId(); diff --git a/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..360d5f5 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/exception/GlobalExceptionHandler.java @@ -0,0 +1,27 @@ +package com.printcalculator.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(VirusDetectedException.class) + public ResponseEntity handleVirusDetectedException( + VirusDetectedException ex, WebRequest request) { + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", ex.getMessage()); + body.put("error", "Virus Detected"); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } +} diff --git a/backend/src/main/java/com/printcalculator/exception/VirusDetectedException.java b/backend/src/main/java/com/printcalculator/exception/VirusDetectedException.java new file mode 100644 index 0000000..6b64216 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/exception/VirusDetectedException.java @@ -0,0 +1,7 @@ +package com.printcalculator.exception; + +public class VirusDetectedException extends RuntimeException { + public VirusDetectedException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/printcalculator/service/ClamAVService.java b/backend/src/main/java/com/printcalculator/service/ClamAVService.java new file mode 100644 index 0000000..dc6532a --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/ClamAVService.java @@ -0,0 +1,64 @@ +package com.printcalculator.service; + +import com.printcalculator.exception.VirusDetectedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import xyz.capybara.clamav.ClamavClient; +import xyz.capybara.clamav.commands.scan.result.ScanResult; + +import java.io.InputStream; +import java.util.Collection; +import java.util.Map; + +@Service +public class ClamAVService { + + private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class); + + private final ClamavClient clamavClient; + private final boolean enabled; + + public ClamAVService( + @Value("${clamav.host:clamav}") String host, + @Value("${clamav.port:3310}") int port, + @Value("${clamav.enabled:true}") boolean enabled + ) { + this.enabled = enabled; + ClamavClient client = null; + try { + if (enabled) { + logger.info("Initializing ClamAV client at {}:{}", host, port); + client = new ClamavClient(host, port); + } + } catch (Exception e) { + logger.error("Failed to initialize ClamAV client: " + e.getMessage()); + } + this.clamavClient = client; + } + + public boolean scan(InputStream inputStream) { + if (!enabled || clamavClient == null) { + return true; + } + try { + ScanResult result = clamavClient.scan(inputStream); + if (result instanceof ScanResult.OK) { + return true; + } else if (result instanceof ScanResult.VirusFound) { + Map> viruses = ((ScanResult.VirusFound) result).getFoundViruses(); + logger.warn("VIRUS DETECTED: {}", viruses); + throw new VirusDetectedException("Virus detected in the uploaded file: " + viruses); + } else { + logger.warn("Unknown scan result: {}. Allowing file (FAIL-OPEN)", result); + return true; + } + } catch (VirusDetectedException e) { + throw e; + } catch (Exception e) { + logger.error("Error scanning file with ClamAV. Allowing file (FAIL-OPEN)", e); + return true; + } + } +} diff --git a/deploy/envs/dev.env b/deploy/envs/dev.env index 0364437..88f337d 100644 --- a/deploy/envs/dev.env +++ b/deploy/envs/dev.env @@ -7,4 +7,7 @@ TAG=dev BACKEND_PORT=18002 FRONTEND_PORT=18082 +CLAMAV_HOST=192.168.1.147 +CLAMAV_PORT=3310 +CLAMAV_ENABLED=true diff --git a/deploy/envs/int.env b/deploy/envs/int.env index 79f7a35..1353b58 100644 --- a/deploy/envs/int.env +++ b/deploy/envs/int.env @@ -7,4 +7,7 @@ TAG=int BACKEND_PORT=18001 FRONTEND_PORT=18081 +CLAMAV_HOST=192.168.1.147 +CLAMAV_PORT=3310 +CLAMAV_ENABLED=true diff --git a/deploy/envs/prod.env b/deploy/envs/prod.env index 878558b..a91bbcb 100644 --- a/deploy/envs/prod.env +++ b/deploy/envs/prod.env @@ -7,4 +7,7 @@ TAG=prod BACKEND_PORT=8000 FRONTEND_PORT=80 +CLAMAV_HOST=192.168.1.147 +CLAMAV_PORT=3310 +CLAMAV_ENABLED=true diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 47df56f..da3d8cc 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -10,6 +10,9 @@ services: - DB_URL=${DB_URL} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} + - CLAMAV_HOST=${CLAMAV_HOST} + - CLAMAV_PORT=${CLAMAV_PORT} + - CLAMAV_ENABLED=${CLAMAV_ENABLED} - TEMP_DIR=/app/temp - PROFILES_DIR=/app/profiles restart: always diff --git a/docker-compose.yml b/docker-compose.yml index faf3593..f347611 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,11 @@ services: - MARKUP_PERCENT=20 - TEMP_DIR=/app/temp - PROFILES_DIR=/app/profiles + - CLAMAV_HOST=clamav + - CLAMAV_PORT=3310 depends_on: - db + - clamav restart: unless-stopped frontend: @@ -49,5 +52,13 @@ services: - postgres_data:/var/lib/postgresql/data restart: unless-stopped + clamav: + platform: linux/amd64 + image: clamav/clamav:latest + container_name: print-calculator-clamav + ports: + - "3310:3310" + restart: unless-stopped + volumes: postgres_data: