mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-20 02:23:20 +00:00
c15c9c48f8
Run against the dev stack with real ClamAV and queue workers, and a code review looking for ways around the scanner. Quarantine now stays quarantined until somebody releases the file. A rescan only touches files people can download, and changes nothing when the scanner cannot answer or scanning is off. Before, an old infected file rescanned while clamd restarted went through the "allow" policy and became downloadable. The daily missing-files check leaves quarantined files alone, so a storage outage no longer brings one back as a fresh upload. A file longer than clamd's StreamMaxLength is "too large" again. clamd answers and hangs up; the next write raised a warning that became an exception before the answer was read, so the file was recorded as "scanner down" and retried past the unscannable policy. The production compose example gives clamd the settings it needs. On its own defaults an encrypted zip comes back clean. The Test button now sends a password-protected zip and fails when it is called clean, and says when an address answers but is not ClamAV. Saving the settings restarts the queue workers, which kept the old values in memory. New scan runs --all, as its name says, and is refused while scans are queued. A retry scheduled for later no longer counts as a scan in progress. Also: quarantine respects client scope for listing, release and notifications; a zip built before a file was quarantined is refused; public comments and version links skip unavailable files; a client no longer sees their own quarantined or missing upload; a file whose bytes return is scanned at once; clamd listens on IPv6 too, so its container health check passes.
101 lines
3.9 KiB
PHP
101 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Files\Console;
|
|
|
|
use App\Modules\Audit\Action;
|
|
use App\Modules\Audit\ActivityLogger;
|
|
use App\Modules\Files\Jobs\ScanFileJob;
|
|
use App\Modules\Files\MissingFileScanner;
|
|
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;
|
|
|
|
/**
|
|
* Checks daily that the files this installation lists are actually there.
|
|
*
|
|
* Its own command rather than part of scanning, because a missing file is
|
|
* not a virus question and an installation with no scanner has exactly
|
|
* the same problem. It was only ever noticed when something tried to read
|
|
* the bytes — a download, or a scan — which means the first person to
|
|
* find out was a client clicking a link.
|
|
*
|
|
* Recovery is part of the job: storage comes back, and a library that
|
|
* kept insisting every file was gone would be its own bug.
|
|
*/
|
|
class CheckMissingFilesCommand extends Command
|
|
{
|
|
protected $signature = 'projectsend:check-missing-files';
|
|
|
|
protected $description = 'Check that every file in the database is still on disk (runs daily)';
|
|
|
|
public function handle(MissingFileScanner $scanner, ActivityLogger $activity, ScanningConfig $scanning): int
|
|
{
|
|
$gone = $scanner->scan();
|
|
$newlyGone = 0;
|
|
|
|
foreach (array_chunk($gone, 200) as $chunk) {
|
|
// Not a quarantined file. It is unavailable already, and
|
|
// marking it missing would wipe the threat name and then, when
|
|
// the bytes came back, send it round as a fresh upload — out
|
|
// of quarantine with nobody having released it.
|
|
$candidates = File::query()
|
|
->whereIn('id', $chunk)
|
|
->whereNotIn('scan_status', [
|
|
ScanStatus::Missing->value,
|
|
ScanStatus::Infected->value,
|
|
ScanStatus::UnscannableBlocked->value,
|
|
])
|
|
->get();
|
|
|
|
foreach ($candidates as $file) {
|
|
// Stamped like any other verdict: this is the moment the
|
|
// file was last looked at, and without it a missing file
|
|
// never appears in the Activity list — which is exactly
|
|
// where somebody watching would look for it.
|
|
$file->forceFill([
|
|
'scan_status' => ScanStatus::Missing,
|
|
'scan_note' => null,
|
|
'scanned_at' => now(),
|
|
])->save();
|
|
|
|
$activity->logSystem(Action::FileMissing, ['id' => $file->id, 'name' => $file->name]);
|
|
$newlyGone++;
|
|
}
|
|
}
|
|
|
|
$back = $scanner->recovered();
|
|
|
|
foreach (array_chunk($back, 200) as $chunk) {
|
|
// Back to the start rather than to whatever it was before:
|
|
// nothing here knows what the scanner had decided about bytes
|
|
// that have since been away, and re-checking them is cheap
|
|
// next to trusting a verdict about a file that left.
|
|
File::query()->whereIn('id', $chunk)->update($scanning->enabled()
|
|
? ['scan_status' => ScanStatus::Pending->value, 'scan_note' => null]
|
|
: ['scan_status' => ScanStatus::NotScanned->value, 'scan_note' => NotScannedReason::BeforeScanning->value]);
|
|
|
|
// Straight to the scanner rather than left for the hourly
|
|
// sweep, which kept a file that had come back unavailable for
|
|
// up to an hour for no reason.
|
|
if ($scanning->enabled()) {
|
|
foreach ($chunk as $id) {
|
|
ScanFileJob::dispatch($id);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->info(sprintf(
|
|
'%d file(s) are missing from storage (%d newly), %d came back.',
|
|
count($gone),
|
|
$newlyGone,
|
|
count($back),
|
|
));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|