Skip to content

Commit

Permalink
feat: Add support for Symantec ICAP
Browse files Browse the repository at this point in the history
Signed-off-by: Cleopatra Enjeck M <[email protected]>
  • Loading branch information
enjeck committed Sep 12, 2024
1 parent 2c16990 commit 8a940de
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 4 deletions.
13 changes: 9 additions & 4 deletions js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,22 +135,27 @@ var antivirusSettings = antivirusSettings || {


function av_mode_show_options(str, mode = 'slow') {
if ( str === 'daemon' || str === 'kaspersky' || str === 'icap'){
if ( str === 'daemon' || str === 'kaspersky' || str === 'icap' || str === 'symantec'){
$('tr.av_socket, tr.av_path').hide(mode);
$('tr.av_host, tr.av_port').show(mode);
} else if ( str === 'socket' ) {
$('tr.av_socket').show(mode);
$('tr.av_path, tr.av_host, tr.av_port').hide(mode);
} else if (str === 'executable'){
$('tr.av_socket, tr.av_host, tr.av_port').hide(mode);
$('tr.av_socket, tr.av_host, tr.av_port, tr.av_password_action').hide(mode);
$('tr.av_path').show(mode);
}
if (str === 'icap'){
if (str === 'icap' || str === 'symantec'){
$('tr.av_icap_service, tr.av_icap_header, tr.av_icap_preset, tr.av_icap_mode, tr.av_icap_tls').show(mode);
} else {
$('tr.av_icap_service, tr.av_icap_header, tr.av_icap_preset, tr.av_icap_mode, tr.av_icap_tls').hide(mode);
}
if (str === 'kaspersky' || str === 'icap') {
if (str === 'symantec'){
$('tr.av_password_action').show(mode);
} else {
$('tr.av_password_action').hide(mode);
}
if (str === 'kaspersky' || str === 'icap' || str === 'symantec') {
$('#antivirus-advanced-wrapper').hide(mode);
} else {
$('#antivirus-advanced-wrapper').show(mode);
Expand Down
3 changes: 3 additions & 0 deletions lib/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @method ?string getAvCmdOptions()
* @method ?string getAvPath()
* @method ?string getAvInfectedAction()
* @method ?string getAvPasswordAction()
* @method ?string getAvStreamMaxLength()
* @method string getAvIcapMode()
* @method ?string getAvIcapRequestService()
Expand All @@ -34,6 +35,7 @@
* @method null setAvChunkSize(int $chunkSize)
* @method null setAvPath(string $avPath)
* @method null setAvInfectedAction(string $avInfectedAction)
* @method null setAvPasswordAction(string $avPasswordAction)
* @method null setAvIcapScanBackground(string $scanBackground)
* @method null setAvIcapMode(string $mode)
* @method null setAvIcapRequestService($reqService)
Expand Down Expand Up @@ -65,6 +67,7 @@ class AppConfig {
'av_icap_chunk_size' => '1048576',
'av_icap_connect_timeout' => '5',
'av_scan_first_bytes' => -1,
'av_password_action' => 'deny',
];

/**
Expand Down
3 changes: 3 additions & 0 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function __construct($appName, IRequest $request, AppConfig $appconfig, I
* @param string $avCmdOptions - extra command line options
* @param string $avPath - path to antivirus executable (Executable mode)
* @param string $avInfectedAction - action performed on infected files
* @param string $avPasswordAction - action performed on password protected files
* @param $avStreamMaxLength - reopen socket after bytes
* @param int $avMaxFileSize - file size limit
* @param int $avScanFirstBytes - scan size limit
Expand All @@ -62,6 +63,7 @@ public function save(
$avCmdOptions,
$avPath,
$avInfectedAction,
$avPasswordAction,
$avStreamMaxLength,
$avMaxFileSize,
$avScanFirstBytes,
Expand All @@ -77,6 +79,7 @@ public function save(
$this->settings->setAvCmdOptions($avCmdOptions);
$this->settings->setAvPath($avPath);
$this->settings->setAvInfectedAction($avInfectedAction);
$this->settings->setAvPasswordAction($avPasswordAction);
$this->settings->setAvStreamMaxLength($avStreamMaxLength);
$this->settings->setAvMaxFileSize($avMaxFileSize);
$this->settings->setAvScanFirstBytes($avScanFirstBytes);
Expand Down
164 changes: 164 additions & 0 deletions lib/ICAP/SymantecICAP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020 Robin Appelman <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files_Antivirus\Scanner;

use OCA\Files_Antivirus\AppConfig;
use OCA\Files_Antivirus\ICAP\ICAPClient;
use OCA\Files_Antivirus\ICAP\ICAPRequest;
use OCA\Files_Antivirus\ICAP\ICAPTlsClient;
use OCA\Files_Antivirus\Status;
use OCA\Files_Antivirus\StatusFactory;
use OCP\ICertificateManager;
use Psr\Log\LoggerInterface;

class SymantecICAP extends ScannerBase {
/** @var ICAPClient::MODE_REQ_MOD|ICAPClient::MODE_RESP_MOD */
private string $mode;
private ICAPClient $icapClient;
private ?ICAPRequest $icapRequest;
private string $service;
private string $virusHeader;
private int $chunkSize;
private bool $tls;
private string $passwordProtected;

public function __construct(
AppConfig $config,
LoggerInterface $logger,
StatusFactory $statusFactory,
ICertificateManager $certificateManager
) {
parent::__construct($config, $logger, $statusFactory);

$avHost = $this->appConfig->getAvHost();
$avPort = $this->appConfig->getAvPort();
$this->service = $config->getAvIcapRequestService();
$this->virusHeader = $config->getAvIcapResponseHeader();
$this->chunkSize = (int)$config->getAvChunkSize();
$this->mode = $config->getAvIcapMode();
$this->tls = $config->getAvIcapTls();
$this->passwordProtected = $config->getAvPasswordAction();

if (!($avHost && $avPort)) {
throw new \RuntimeException('The ICAP port and host are not set up.');
}
if ($this->tls) {
$this->icapClient = new ICAPTlsClient($avHost, (int)$avPort, (int)$config->getAvIcapConnectTimeout(), $certificateManager);
} else {
$this->icapClient = new ICAPClient($avHost, (int)$avPort, (int)$config->getAvIcapConnectTimeout());
}
}

public function initScanner() {
parent::initScanner();
$this->writeHandle = fopen("php://temp", 'w+');
$path = '/' . trim($this->path, '/');
if (str_contains($path, '.ocTransferId') && str_ends_with($path, '.part')) {
[$path] = explode('.ocTransferId', $path, 2);
}
$remote = $this->request?->getRemoteAddress();
$encodedPath = implode("/", array_map("rawurlencode", explode("/", $path)));
if ($this->mode === ICAPClient::MODE_REQ_MOD) {
$this->icapRequest = $this->icapClient->reqmod($this->service, [
'Allow' => 204,
"X-Client-IP" => $remote,
], [
"PUT $encodedPath HTTP/1.0",
"Host: nextcloud"
]);
} else {
$this->icapRequest = $this->icapClient->respmod($this->service, [
'Allow' => 204,
"X-Client-IP" => $remote,
], [
"GET $encodedPath HTTP/1.0",
"Host: nextcloud",
], [
"HTTP/1.0 200 OK",
"Content-Length: 1", // a dummy, non-zero, content length seems to be enough
]);
}
}

protected function writeChunk($chunk) {
if (ftell($this->writeHandle) > $this->chunkSize) {
$this->flushBuffer();
}
parent::writeChunk($chunk);
}

private function flushBuffer() {
rewind($this->writeHandle);
$data = stream_get_contents($this->writeHandle);
$this->icapRequest->write($data);
$this->writeHandle = fopen("php://temp", 'w+');
}

protected function scanBuffer() {
$this->flushBuffer();
$response = $this->icapRequest->finish();
$code = $response->getStatus()->getCode();
$unchecked_list = array("decode_error","max_archive_layers_exceeded");
$blocked_list = array("file_type_blocked", "file_extension_blocked");

$this->status->setNumericStatus(Status::SCANRESULT_CLEAN);
if ($code === 200 || $code === 204) {
// c-icap/clamav reports this header
$virus = $response->getIcapHeaders()[$this->virusHeader] ?? false;
if ($virus) {
$this->status->setNumericStatus(Status::SCANRESULT_INFECTED);
$this->status->setDetails($virus);
}

// kaspersky(pre 2020 product editions) and McAfee handling
$respHeader = $response->getResponseHeaders()['HTTP_STATUS'] ?? '';
if (\strpos($respHeader, '403 Forbidden') || \strpos($respHeader, '403 VirusFound')) {
$this->status->setNumericStatus(Status::SCANRESULT_INFECTED);
}
} elseif ($code === 202) {
$this->status->setNumericStatus(Status::SCANRESULT_UNCHECKED);
} elseif ($code === 500 && $response->getIcapHeaders()['X-Error-Code'] === 'password_protected') {
if ($this->passwordProtected === "accept") {
$this->status->setNumericStatus(Status::SCANRESULT_CLEAN);
} else {
$this->status->setNumericStatus(Status::SCANRESULT_INFECTED);
}
} elseif ($code === 500 && in_array($response->getIcapHeaders()['X-Error-Code'], $unchecked_list)) {
$this->status->setNumericStatus(Status::SCANRESULT_UNCHECKED);
} elseif ($code === 500 && in_array($response->getIcapHeaders()['X-Error-Code'], $blocked_list)) {
$this->status->setNumericStatus(Status::SCANRESULT_INFECTED);
} else {
throw new \RuntimeException('Invalid response from ICAP server');
}
}

protected function shutdownScanner() {
$this->scanBuffer();
}

public function setDebugCallback(callable $callback): void {
$this->icapClient->setDebugCallback($callback);
}
}
3 changes: 3 additions & 0 deletions lib/Scanner/ScannerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public function getScanner(string $path) {
case 'icap':
$scannerClass = ICAP::class;
break;
case 'symantec':
$scannerClass = SymantecICAP::class;
break;
default:
throw new \InvalidArgumentException('Application is misconfigured. Please check the settings at the admin page. Invalid mode: ' . $avMode);
}
Expand Down
6 changes: 6 additions & 0 deletions templates/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'socket' => $l->t('ClamAV Daemon (Socket)'),
'kaspersky' => $l->t('Kaspersky Daemon'),
'icap' => $l->t('ICAP server'),
'symantec' => $l->t('Symantec Icap'),
], $_['avMode'])) ?></select>
</td>
<td></td>
Expand Down Expand Up @@ -121,6 +122,11 @@
<td><select id="av_infected_action" name="avInfectedAction"><?php print_unescaped(html_select_options(['only_log' => $l->t('Only log'), 'delete' => $l->t('Delete file')], $_['avInfectedAction'])) ?></select></td>
<td></td>
</tr>
<tr class="av_password_action">
<td><label for="av_password_action"><?php p($l->t('Accept password protected files'));?></label></td>
<td><select id="av_password_action" name="avPasswordAction"><?php print_unescaped(html_select_options(['accept' => $l->t('Accept'), 'deny' => $l->t('Deny')], $_['avPasswordAction'])) ?></select></td>
<td></td>
</tr>
</table>
<input id="av_submit" type="submit" value="<?php p($l->t('Save'));?>" />
<span id="antivirus_save_msg"></span>
Expand Down
Loading

0 comments on commit 8a940de

Please sign in to comment.