From 2cae337f6964dc160a0a77df5e2ad68aaa927723 Mon Sep 17 00:00:00 2001
From: Predhumeau Manon <manon.predhumeau@irstea.fr>
Date: Thu, 19 Oct 2017 13:34:32 +0200
Subject: [PATCH] Add antivirus analyse (xenelop quahog)

---
 Listener/VirusScannerListener.php           | 54 +++++++++------
 Resources/config/services.yml               | 28 ++++++--
 Tests/Listener/VirusScannerListenerTest.php | 73 ++++++++++-----------
 composer.json                               |  3 +-
 4 files changed, 92 insertions(+), 66 deletions(-)

diff --git a/Listener/VirusScannerListener.php b/Listener/VirusScannerListener.php
index 7c784c82..f1dd77a8 100644
--- a/Listener/VirusScannerListener.php
+++ b/Listener/VirusScannerListener.php
@@ -6,9 +6,10 @@
 
 namespace Irstea\FileUploadBundle\Listener;
 
-use CL\Tissue\Adapter\AdapterInterface;
+use Xenolope\Quahog\Client;
 use Irstea\FileUploadBundle\Event\FileUploadCompleteEvent;
 use Irstea\FileUploadBundle\Exception\RejectedFileException;
+use Xenolope\Quahog\Exception\ConnectionException;
 
 /**
  * Description of AntivirusListener.
@@ -16,42 +17,57 @@ use Irstea\FileUploadBundle\Exception\RejectedFileException;
 class VirusScannerListener
 {
     /**
-     * @var AdapterInterface
+     * @var Client
      */
-    private $scanner;
+    private $client;
+
 
     /**
-     * @param AdapterInterface $scanner
+     * @param Client $client
      */
-    public function __construct(AdapterInterface $scanner)
+    public function __construct(Client $client)
     {
-        $this->scanner = $scanner;
+        $this->client = $client;
+
+        try {
+            // Check clamd server's state
+            $this->client->ping();
+
+            // Reload the virus database
+            $this->client->reload();
+        } catch (ConnectionException $connectionException) {
+            $this->client->shutdown();
+            $this->client = null;
+        }
     }
 
+
     /**
      * @param FileUploadCompleteEvent $event
      */
     public function onFileUploadCompleted(FileUploadCompleteEvent $event)
     {
-        $file = $event->getUploadedFile();
-        $path = $file->getLocalPath();
-
-        $result = $this->scanner->scan([$path]);
+        if ($this->client != null) {
+            $file = $event->getUploadedFile();
+            $path = $file->getLocalPath();
 
-        $meta = $file->getMetadata();
+            $result = $this->client->scanFile($path);
+            $hasVirus = $result['status'] == Client::RESULT_FOUND;
 
-        $meta['virus_scanner'] = ['has_virus' => $result->hasVirus()];
+            $meta = $file->getMetadata();
+            $meta['virus_scanner'] = ['has_virus' => $hasVirus];
 
-        if ($result->hasVirus()) {
-            if (null !== $desc = $result->getDetections()[0]->getDescription()) {
-                $meta['virus_scanner']['detected'] = $desc;
+            if ($hasVirus) {
+                $desc = $result['reason'];
+                $meta['virus_scanner']['detected'] = $result['reason'];
             }
-        }
 
-        $file->setMetadata($meta);
+            $file->setMetadata($meta);
 
-        if ($result->hasVirus()) {
-            throw new RejectedFileException($file, $desc ? sprintf('Found the %s virus !', $desc) : 'Virus found !');
+            if ($hasVirus) {
+                throw new RejectedFileException($file, $desc ? sprintf('Found the %s virus !', $desc) : 'Virus found !');
+            }
+            $this->client->shutdown();
         }
     }
 }
diff --git a/Resources/config/services.yml b/Resources/config/services.yml
index 9b92167d..ec2f9b3a 100644
--- a/Resources/config/services.yml
+++ b/Resources/config/services.yml
@@ -10,6 +10,10 @@ parameters:
 
     irstea_file_upload.max_chunk_size: 0
 
+    irstea_file_upload.clamav_socket_path: unix:///var/run/clamav/clamd.ctl
+    irstea_file_upload.client_timeout: 30
+    irstea_file_upload.client_mode: PHP_NORMAL_READ
+
 services:
 
     # Le gestionnaire de fichiers
@@ -70,12 +74,24 @@ services:
             - "%irstea_file_upload.filesystem.name%"
 
     # Scanner anti-virus
-#    irstea_file_upload.virus_scanner:
-#        class: Irstea\FileUploadBundle\Listener\VirusScannerListener
-#        arguments:
-#            - @cl_tissue.scanner
-#        tags:
-#            - { name: kernel.event_listener, event: file_upload.complete, method: onFileUploadCompleted }
+    irstea_file_upload.socket:
+            class: Socket\Raw\Socket
+            arguments:
+                - "%irstea_file_upload.clamav_socket_path"
+
+    irstea_file_upload.client:
+            class: Xenolope\Quahog\Client
+            arguments:
+              - "@irstea_file_upload.socket"
+              - "%irstea_file_upload.client_timeout"
+              - "%irstea_file_upload.client_mode"
+
+    irstea_file_upload.virus_scanner:
+        class: Irstea\FileUploadBundle\Listener\VirusScannerListener
+        arguments:
+            - "@irstea_file_upload.client"
+        tags:
+            - { name: kernel.event_listener, event: file_upload.complete, method: onFileUploadCompleted }
 
     # Extension Twig
     irstea_file_upload.twig_extension:
diff --git a/Tests/Listener/VirusScannerListenerTest.php b/Tests/Listener/VirusScannerListenerTest.php
index 582da7f5..6b9ad06b 100644
--- a/Tests/Listener/VirusScannerListenerTest.php
+++ b/Tests/Listener/VirusScannerListenerTest.php
@@ -6,12 +6,12 @@
 
 namespace Irstea\FileUploadBundle\Tests\Listener;
 
-use CL\Tissue\Model\Detection;
-use CL\Tissue\Model\ScanResult;
 use Irstea\FileUploadBundle\Event\FileUploadCompleteEvent;
 use Irstea\FileUploadBundle\Listener\VirusScannerListener;
 use PHPUnit_Framework_MockObject_MockObject;
 use PHPUnit_Framework_TestCase;
+use Irstea\FileUploadBundle\Model\UploadedFileInterface;
+use Xenolope\Quahog\Client;
 
 /**
  * Generated by PHPUnit_SkeletonGenerator on 2015-01-29 at 14:43:16.
@@ -26,7 +26,7 @@ class VirusScannerListenerTest extends PHPUnit_Framework_TestCase
     /**
      * @var PHPUnit_Framework_MockObject_MockObject
      */
-    protected $scanner;
+    protected $client;
 
     /**
      * @var FileUploadCompleteEvent
@@ -39,42 +39,31 @@ class VirusScannerListenerTest extends PHPUnit_Framework_TestCase
     protected $file;
 
     /**
-     * {@inheritdoc}
+     * {@inheritDoc}
      */
     protected function setUp()
     {
-        self::markTestSkipped('No CL\Tissue');
-    }
+        $this->client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
 
-    public function testOnFileUploadCompletedResultOk()
-    {
-        $result = new ScanResult(
-            ['foopath'],
-            ['foopath'],
-            []
-        );
-        $this->scanner->expects(static::once())->method('scan')->with(['foopath'])->willReturn($result);
+        $this->file = $this->getMock(UploadedFileInterface::class);
+        $this->file->expects(static::once())->method('getLocalPath')->willReturn('foopath');
+        $this->file->expects(static::once())->method('getMetadata')->willReturn([]);
 
-        $this->file->expects(static::once())->method('setMetadata')->with(['virus_scanner' => ['has_virus' => false]]);
+        $this->event = new FileUploadCompleteEvent($this->file);
 
-        $this->listener->onFileUploadCompleted($this->event);
+        $this->listener = new VirusScannerListener($this->client);
     }
 
-    /**
-     * @expectedException \Irstea\FileUploadBundle\Exception\RejectedFileException
-     */
-    public function testOnFileUploadCompletedVirusFound()
+    public function testOnFileUploadCompletedResultOk()
     {
-        $result = new ScanResult(
-            ['foopath'],
-            ['foopath'],
-            [new Detection('foopath', Detection::TYPE_VIRUS, 'BAR')]
-        );
-        $this->scanner->expects(static::once())->method('scan')->with(['foopath'])->willReturn($result);
+        $result = [
+            'filename' => 'footpath',
+            'reason' => null,
+            'status' => Client::RESULT_OK
+        ];
 
-        $this->file->expects(static::once())->method('setMetadata')->with(
-            ['virus_scanner' => ['has_virus' => true, 'detected' => 'BAR']]
-        );
+        $this->client->expects(static::once())->method('scanFile')->with('foopath')->willReturn($result);
+        $this->file->expects(static::once())->method('setMetadata')->with(['virus_scanner' => ['has_virus' => false]]);
 
         $this->listener->onFileUploadCompleted($this->event);
     }
@@ -82,18 +71,22 @@ class VirusScannerListenerTest extends PHPUnit_Framework_TestCase
     /**
      * @expectedException \Irstea\FileUploadBundle\Exception\RejectedFileException
      */
-    public function testOnFileUploadCompletedVirusFoundNoDescription()
+    public function testOnFileUploadCompletedVirusFound()
     {
-        $result = new ScanResult(
-            ['foopath'],
-            ['foopath'],
-            [new Detection('foopath', Detection::TYPE_VIRUS)]
-        );
-        $this->scanner->expects(static::once())->method('scan')->with(['foopath'])->willReturn($result);
-
-        $this->file->expects(static::once())->method('setMetadata')->with(
-            ['virus_scanner' => ['has_virus' => true]]
-        );
+        $result = [
+            'filename' => 'footpath',
+            'reason' => 'Terrible virus',
+            'status' => Client::RESULT_FOUND
+        ];
+
+         $this->client->expects($this->any())
+            ->method('scanFile')
+            ->with('foopath')
+            ->willReturn($result);
+
+        $this->file->expects(static::once())
+            ->method('setMetadata')
+            ->with(['virus_scanner' => ['has_virus' => true, 'detected' => 'Terrible virus']]);
 
         $this->listener->onFileUploadCompleted($this->event);
     }
diff --git a/composer.json b/composer.json
index 6fa2391e..141f5fb8 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,8 @@
         "ramsey/uuid": "~2.8",
         "blueimp/jquery-file-upload": "~9.9",
         "white-october/pagerfanta-bundle": "~1.0",
-        "willdurand/js-translation-bundle": "~2.2"
+        "willdurand/js-translation-bundle": "~2.2",
+        "xenolope/quahog": "^2.1"
     },
     "require-dev": {
         "phpunit/phpunit": "^4.8"
-- 
GitLab