mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 01:25:09 +00:00
e9496dc357
An infected file now goes somewhere rather than nowhere. Staff holding the new release_quarantined_files permission get a Quarantine screen listing what was refused, who uploaded it, and what the scanner called it. They can delete it as they always could, or release it — which needs a written reason, a password confirmation on top of the permission, and lands in the activity log under their name. Only the administrator role holds that permission by default. Deciding a threat report is wrong is a different judgement from deciding a file is no longer needed, which is why it is not delete_files. Two notifications, two audiences: staff who can act on it, and the person who uploaded it — for whom this is how they learn their own machine has something on it. The people the file was shared with are deliberately not told about a file they never received. `projectsend:scan-files` runs hourly: it re-queues files still waiting, and re-scans the ones that went out unscanned while the scanner was unreachable, since it may be back. With --existing it also works through a library uploaded before scanning was switched on, paced by a setting so it does not starve today's uploads. A file that was downloadable before it was caught says so on the screen, with its download count, because that is the case where somebody may already have a copy.
95 lines
3.3 KiB
PHP
95 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Console;
|
|
|
|
use App\Modules\Files\Jobs\ScanFileJob;
|
|
use App\Modules\Files\Models\File;
|
|
use App\Modules\Files\Scanning\NotScannedReason;
|
|
use App\Modules\Files\Scanning\ScanningConfig;
|
|
use App\Modules\Files\Scanning\ScanStatus;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
/**
|
|
* Sends files back to the scanner: the ones still waiting, the ones that
|
|
* went through unscanned because it was down, and — when asked — the
|
|
* library that was already here before any of this existed.
|
|
*
|
|
* Hourly rather than daily. A file stuck pending is a file nobody can
|
|
* download, and an installation set to hold has no other way forward
|
|
* once its worker restarted and the job with it.
|
|
*/
|
|
class ScanFilesCommand extends Command
|
|
{
|
|
protected $signature = 'projectsend:scan-files
|
|
{--existing : also work through files that were never scanned because scanning was off}';
|
|
|
|
protected $description = 'Scan files that are waiting, were missed, or were never checked (runs hourly)';
|
|
|
|
public function handle(ScanningConfig $config): int
|
|
{
|
|
if (! $config->enabled()) {
|
|
$this->info('Virus scanning is switched off.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$waiting = $this->dispatchFor(File::query()->where('scan_status', ScanStatus::Pending));
|
|
|
|
// Allowed through while the scanner was unreachable. Now that it
|
|
// may be back, they are asked again — a file found infected at
|
|
// this point is quarantined like any other, and its quarantine
|
|
// notice says it was available in the meantime.
|
|
$missed = $this->dispatchFor(
|
|
File::query()
|
|
->where('scan_status', ScanStatus::NotScanned)
|
|
->where('scan_note', NotScannedReason::ScannerUnavailable->value)
|
|
);
|
|
|
|
$this->info("Re-queued {$waiting} waiting file(s) and {$missed} that were missed while the scanner was down.");
|
|
|
|
if ($this->option('existing')) {
|
|
// Paced, because this can be a whole library at once and the
|
|
// scanner is also serving today's uploads. An hour's worth per
|
|
// run, since that is how often this command runs.
|
|
$limit = $config->existingScanRatePerMinute() * 60;
|
|
|
|
$old = $this->dispatchFor(
|
|
File::query()
|
|
->where('scan_status', ScanStatus::NotScanned)
|
|
->where('scan_note', NotScannedReason::BeforeScanning->value),
|
|
$limit,
|
|
);
|
|
|
|
$this->info("Queued {$old} file(s) that had never been scanned.");
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @param Builder<File> $query
|
|
*/
|
|
private function dispatchFor(Builder $query, ?int $limit = null): int
|
|
{
|
|
if ($limit !== null) {
|
|
$query->limit($limit);
|
|
}
|
|
|
|
$ids = $query->orderBy('id')->pluck('id');
|
|
|
|
foreach ($ids as $id) {
|
|
// Back to pending first: the job only acts on a pending file,
|
|
// which is what stops two runs of this command from scanning
|
|
// the same file twice.
|
|
File::query()->whereKey($id)->update(['scan_status' => ScanStatus::Pending->value, 'scan_note' => null]);
|
|
|
|
ScanFileJob::dispatch((int) $id);
|
|
}
|
|
|
|
return $ids->count();
|
|
}
|
|
}
|