Initial bootstrap: cpanel-importer sanitization sandbox
Skeleton for the cpanel-importer Docker container — a one-shot
sandbox the WHP panel invokes BEFORE extracting a customer cpmove
tarball. See cpanel-import-container-spec.md (in /workspace/) for the
full design.
What this ships in v1.0:
- Dockerfile: almalinux:10-minimal + PHP 8.4 (Remi) + ClamAV 1.4 +
SaneSecurity Foxhole.PHP rules + tar/mariadb-client/rsync. Runs as
UID 999 (whp-import) via the panel-side --user 999:999 flag.
- scripts/entrypoint.sh: validates env, runs (optional) freshclam,
drives extract -> scan-files -> scan-dbs -> rsync -> report.json.
- scripts/extract.sh + scripts/lib/scan-symlinks.php: pre-extract
symlink scan ported standalone from
web-files/libs/CpanelBackupImporter.php (the existing 2026-05-29
whp02 destruction-vector fix). Aborts with exit 3 before tar runs
if any DANGEROUS symlink is found.
- scripts/scan-files.php: ClamAV walk + classify-and-action. v1.0
ships with an empty cleaner registry — every hit is
QUARANTINE_ONLY. Cleaner hooks are stubbed for v1.1.
- scripts/scan-dbs.php: regex MyISAM -> InnoDB rewrite (always
applied), WordPress identification, and ONE WP content scan check
(siteurl_external_domain). v1.1 will grow the check set.
- scripts/lib/safety-net.php: container-narrow open_basedir
allow-list, much tighter than the panel-side one.
- .gitea/workflows/build-push.yaml: builds + smoke-tests +
PHP-syntax-checks + bash-syntax-checks before pushing to
repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer.
- tests/build-fixtures.sh: builds cpmove-clean.tar.gz (benign WP
dump) and cpmove-alfa.tar.gz (the ALFA-shell symlink-to-/etc
vector) for local end-to-end testing.
- README.md / CONTRIBUTING.md: docker-run invocation, bind-mount
catalog, report.json schema, how to add a cleaner pattern or a WP
scan signature.
Local acceptance test results:
- clean fixture -> status=completed, 3 MyISAM->InnoDB, no flags, 0
- ALFA fixture -> exit 1, status=failed, failed_stage=extract,
"tarball contains dangerous symlinks; aborting" on stderr
- compromised-siteurl fixture -> imported_into_new_server=false,
.flagged file written, summary_for_panel.show_alert=true
Image size: 197 MB compressed (gzipped docker save), ~397 MB unique
layers extracted. Well under the spec's 600 MB compressed / 1.2 GB
extracted budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:57 -07:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* scan-files.php — ClamAV scan + classify-and-action orchestrator.
|
|
|
|
|
*
|
|
|
|
|
* v1.0: quarantine-on-every-hit. No auto-cleaners enabled. The cleaner
|
|
|
|
|
* registry (KNOWN_REMOVABLE / REMOVABLE_WITH_BACKUP) is stubbed below
|
|
|
|
|
* for v1.1 expansion; see CONTRIBUTING.md for how to wire one in.
|
|
|
|
|
*
|
|
|
|
|
* Usage:
|
|
|
|
|
* scan-files.php --extract <dir> --quarantine <dir> --report <out.json> --import-id <id>
|
|
|
|
|
*
|
|
|
|
|
* Exit codes:
|
|
|
|
|
* 0 — scan completed (regardless of how many hits)
|
|
|
|
|
* 1 — fatal scanner error (clamscan binary missing, signature DB unreadable)
|
|
|
|
|
* 2 — usage error
|
|
|
|
|
*
|
|
|
|
|
* Report shape: matches spec §3, e.g.:
|
|
|
|
|
* {
|
|
|
|
|
* "files_scanned": N,
|
|
|
|
|
* "files_clean": N,
|
|
|
|
|
* "files_cleaned": 0, // always 0 in v1.0 — no cleaners yet
|
|
|
|
|
* "files_quarantined": N,
|
|
|
|
|
* "actions": [ { path, signature, action, cleaner, backup } ]
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
require __DIR__ . '/lib/safety-net.php';
|
|
|
|
|
|
|
|
|
|
const SCANNER_VERSION = '1.0.0';
|
|
|
|
|
|
|
|
|
|
$opts = getopt('', ['extract:', 'quarantine:', 'report:', 'import-id:']);
|
|
|
|
|
foreach (['extract', 'quarantine', 'report', 'import-id'] as $k) {
|
|
|
|
|
if (!isset($opts[$k])) {
|
|
|
|
|
fwrite(STDERR, "usage: scan-files.php --extract <dir> --quarantine <dir> --report <out.json> --import-id <id>\n");
|
|
|
|
|
exit(2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$extractDir = rtrim($opts['extract'], '/');
|
|
|
|
|
$quarantineDir = rtrim($opts['quarantine'], '/');
|
|
|
|
|
$reportPath = $opts['report'];
|
|
|
|
|
$importId = $opts['import-id'];
|
|
|
|
|
|
|
|
|
|
if (!is_dir($extractDir)) {
|
|
|
|
|
fwrite(STDERR, "scan-files: extract dir does not exist: $extractDir\n");
|
|
|
|
|
exit(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@mkdir($quarantineDir, 0750, true);
|
|
|
|
|
|
|
|
|
|
fwrite(STDERR, "scan-files: starting (extract=$extractDir, quarantine=$quarantineDir)\n");
|
|
|
|
|
|
|
|
|
|
// -- v1.0 cleaner registry (intentionally empty) ----------------------------
|
|
|
|
|
//
|
|
|
|
|
// Each entry maps a ClamAV signature substring -> classification +
|
|
|
|
|
// cleaner callable. v1.0 ships empty so EVERY hit is classified as
|
|
|
|
|
// QUARANTINE_ONLY. See CONTRIBUTING.md "Adding an auto-cleaner pattern"
|
|
|
|
|
// for how to add a tested entry.
|
|
|
|
|
//
|
|
|
|
|
// Shape (v1.1+):
|
|
|
|
|
// $cleaners = [
|
|
|
|
|
// 'php-eval-base64-prefix' => [
|
|
|
|
|
// 'class' => 'KNOWN_REMOVABLE',
|
|
|
|
|
// 'match' => fn(string $sig): bool => str_contains($sig, 'PHP.Trojan.EvalB64'),
|
|
|
|
|
// 'clean' => fn(string $path): bool => /* rewrite file in place; return ok */,
|
|
|
|
|
// ],
|
|
|
|
|
// ];
|
|
|
|
|
$cleaners = [];
|
|
|
|
|
|
|
|
|
|
// -- run clamscan recursively over the extract dir --------------------------
|
|
|
|
|
|
|
|
|
|
// We use --infected so the output is only hits, and --recursive so we
|
|
|
|
|
// walk subdirectories. We deliberately do NOT use --remove (we never want
|
|
|
|
|
// clamscan unlinking files — we control quarantine).
|
|
|
|
|
//
|
|
|
|
|
// Output format per line on a hit:
|
|
|
|
|
// /tmp/extract/foo/bar.php: Some.Signature.Name FOUND
|
|
|
|
|
$cmd = sprintf(
|
|
|
|
|
'clamscan --infected --recursive --no-summary --stdout %s 2>/dev/null',
|
|
|
|
|
escapeshellarg($extractDir)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$fh = popen($cmd, 'r');
|
|
|
|
|
if (!$fh) {
|
|
|
|
|
fwrite(STDERR, "scan-files: failed to spawn clamscan\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$hits = [];
|
|
|
|
|
while (($line = fgets($fh)) !== false) {
|
|
|
|
|
$line = rtrim($line, "\r\n");
|
|
|
|
|
if ($line === '' || !str_ends_with($line, ' FOUND')) continue;
|
|
|
|
|
// Strip trailing ' FOUND'.
|
|
|
|
|
$body = substr($line, 0, -6);
|
|
|
|
|
$colon = strrpos($body, ': ');
|
|
|
|
|
if ($colon === false) continue;
|
|
|
|
|
$path = substr($body, 0, $colon);
|
|
|
|
|
$sig = substr($body, $colon + 2);
|
|
|
|
|
if (!str_starts_with($path, $extractDir)) {
|
|
|
|
|
// Defensive: shouldn't happen with our invocation.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$hits[] = ['path' => $path, 'signature' => $sig];
|
|
|
|
|
}
|
|
|
|
|
pclose($fh);
|
|
|
|
|
|
|
|
|
|
// File count — we need files_scanned for the report. clamscan's summary
|
|
|
|
|
// counting is suppressed; do a fast file count ourselves.
|
2026-05-31 11:40:44 -07:00
|
|
|
//
|
|
|
|
|
// Symlinks are skipped entirely:
|
|
|
|
|
// 1. cPanel cpmove tarballs contain symlinks with absolute targets that
|
|
|
|
|
// point at the SOURCE server's filesystem (e.g., /home/<user>/...)
|
|
|
|
|
// which don't exist inside the container. PHP's SplFileInfo::isFile()
|
|
|
|
|
// tries to follow the symlink, the resolved target is not under any
|
|
|
|
|
// open_basedir-allowed prefix, and PHP throws RuntimeException
|
|
|
|
|
// mid-iteration — aborting the whole scan.
|
|
|
|
|
// 2. clamscan itself handles symlinks via its own walk (default: does
|
|
|
|
|
// NOT follow them — same posture we want). Counting them here would
|
|
|
|
|
// double-count vs clamscan's signal anyway.
|
|
|
|
|
// 3. Quarantining a symlink-file is meaningless (it's a 0-byte fs entry
|
|
|
|
|
// whose target is the actual artifact).
|
|
|
|
|
//
|
|
|
|
|
// Use a CallbackFilterIterator that performs an lstat-based isLink() check
|
|
|
|
|
// BEFORE the iterator hands the entry off to RecursiveIteratorIterator's
|
|
|
|
|
// hasChildren / isFile follow-paths. is_link() is open_basedir-safe.
|
|
|
|
|
// The try/catch is a defense-in-depth belt: if any other fs-op throws
|
|
|
|
|
// (e.g. a symlink that races mid-walk), skip the entry rather than abort.
|
Initial bootstrap: cpanel-importer sanitization sandbox
Skeleton for the cpanel-importer Docker container — a one-shot
sandbox the WHP panel invokes BEFORE extracting a customer cpmove
tarball. See cpanel-import-container-spec.md (in /workspace/) for the
full design.
What this ships in v1.0:
- Dockerfile: almalinux:10-minimal + PHP 8.4 (Remi) + ClamAV 1.4 +
SaneSecurity Foxhole.PHP rules + tar/mariadb-client/rsync. Runs as
UID 999 (whp-import) via the panel-side --user 999:999 flag.
- scripts/entrypoint.sh: validates env, runs (optional) freshclam,
drives extract -> scan-files -> scan-dbs -> rsync -> report.json.
- scripts/extract.sh + scripts/lib/scan-symlinks.php: pre-extract
symlink scan ported standalone from
web-files/libs/CpanelBackupImporter.php (the existing 2026-05-29
whp02 destruction-vector fix). Aborts with exit 3 before tar runs
if any DANGEROUS symlink is found.
- scripts/scan-files.php: ClamAV walk + classify-and-action. v1.0
ships with an empty cleaner registry — every hit is
QUARANTINE_ONLY. Cleaner hooks are stubbed for v1.1.
- scripts/scan-dbs.php: regex MyISAM -> InnoDB rewrite (always
applied), WordPress identification, and ONE WP content scan check
(siteurl_external_domain). v1.1 will grow the check set.
- scripts/lib/safety-net.php: container-narrow open_basedir
allow-list, much tighter than the panel-side one.
- .gitea/workflows/build-push.yaml: builds + smoke-tests +
PHP-syntax-checks + bash-syntax-checks before pushing to
repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer.
- tests/build-fixtures.sh: builds cpmove-clean.tar.gz (benign WP
dump) and cpmove-alfa.tar.gz (the ALFA-shell symlink-to-/etc
vector) for local end-to-end testing.
- README.md / CONTRIBUTING.md: docker-run invocation, bind-mount
catalog, report.json schema, how to add a cleaner pattern or a WP
scan signature.
Local acceptance test results:
- clean fixture -> status=completed, 3 MyISAM->InnoDB, no flags, 0
- ALFA fixture -> exit 1, status=failed, failed_stage=extract,
"tarball contains dangerous symlinks; aborting" on stderr
- compromised-siteurl fixture -> imported_into_new_server=false,
.flagged file written, summary_for_panel.show_alert=true
Image size: 197 MB compressed (gzipped docker save), ~397 MB unique
layers extracted. Well under the spec's 600 MB compressed / 1.2 GB
extracted budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:57 -07:00
|
|
|
$filesScanned = 0;
|
2026-05-31 11:40:44 -07:00
|
|
|
$skippedLinks = 0;
|
|
|
|
|
$walkErrors = 0;
|
|
|
|
|
|
|
|
|
|
$rdi = new RecursiveDirectoryIterator(
|
|
|
|
|
$extractDir,
|
|
|
|
|
FilesystemIterator::SKIP_DOTS | FilesystemIterator::CURRENT_AS_PATHNAME
|
|
|
|
|
);
|
|
|
|
|
$filter = new RecursiveCallbackFilterIterator($rdi, function ($pathname, $key, $iterator) use (&$skippedLinks) {
|
|
|
|
|
// $pathname is a string when CURRENT_AS_PATHNAME is set.
|
|
|
|
|
if (is_link($pathname)) {
|
|
|
|
|
$skippedLinks++;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
$it = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::LEAVES_ONLY);
|
|
|
|
|
foreach ($it as $pathname) {
|
|
|
|
|
try {
|
|
|
|
|
// is_file() will follow symlinks, but we already filtered links
|
|
|
|
|
// out. For regular files this is a cheap stat.
|
|
|
|
|
if (is_file($pathname)) {
|
|
|
|
|
$filesScanned++;
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
// Belt: a race or filesystem oddity here shouldn't bomb the whole
|
|
|
|
|
// scanner. Log + continue.
|
|
|
|
|
$walkErrors++;
|
|
|
|
|
}
|
Initial bootstrap: cpanel-importer sanitization sandbox
Skeleton for the cpanel-importer Docker container — a one-shot
sandbox the WHP panel invokes BEFORE extracting a customer cpmove
tarball. See cpanel-import-container-spec.md (in /workspace/) for the
full design.
What this ships in v1.0:
- Dockerfile: almalinux:10-minimal + PHP 8.4 (Remi) + ClamAV 1.4 +
SaneSecurity Foxhole.PHP rules + tar/mariadb-client/rsync. Runs as
UID 999 (whp-import) via the panel-side --user 999:999 flag.
- scripts/entrypoint.sh: validates env, runs (optional) freshclam,
drives extract -> scan-files -> scan-dbs -> rsync -> report.json.
- scripts/extract.sh + scripts/lib/scan-symlinks.php: pre-extract
symlink scan ported standalone from
web-files/libs/CpanelBackupImporter.php (the existing 2026-05-29
whp02 destruction-vector fix). Aborts with exit 3 before tar runs
if any DANGEROUS symlink is found.
- scripts/scan-files.php: ClamAV walk + classify-and-action. v1.0
ships with an empty cleaner registry — every hit is
QUARANTINE_ONLY. Cleaner hooks are stubbed for v1.1.
- scripts/scan-dbs.php: regex MyISAM -> InnoDB rewrite (always
applied), WordPress identification, and ONE WP content scan check
(siteurl_external_domain). v1.1 will grow the check set.
- scripts/lib/safety-net.php: container-narrow open_basedir
allow-list, much tighter than the panel-side one.
- .gitea/workflows/build-push.yaml: builds + smoke-tests +
PHP-syntax-checks + bash-syntax-checks before pushing to
repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer.
- tests/build-fixtures.sh: builds cpmove-clean.tar.gz (benign WP
dump) and cpmove-alfa.tar.gz (the ALFA-shell symlink-to-/etc
vector) for local end-to-end testing.
- README.md / CONTRIBUTING.md: docker-run invocation, bind-mount
catalog, report.json schema, how to add a cleaner pattern or a WP
scan signature.
Local acceptance test results:
- clean fixture -> status=completed, 3 MyISAM->InnoDB, no flags, 0
- ALFA fixture -> exit 1, status=failed, failed_stage=extract,
"tarball contains dangerous symlinks; aborting" on stderr
- compromised-siteurl fixture -> imported_into_new_server=false,
.flagged file written, summary_for_panel.show_alert=true
Image size: 197 MB compressed (gzipped docker save), ~397 MB unique
layers extracted. Well under the spec's 600 MB compressed / 1.2 GB
extracted budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:57 -07:00
|
|
|
}
|
2026-05-31 11:40:44 -07:00
|
|
|
fwrite(STDERR, sprintf(
|
|
|
|
|
"scan-files: file walk: counted=%d, symlinks-skipped=%d, walk-errors=%d\n",
|
|
|
|
|
$filesScanned, $skippedLinks, $walkErrors
|
|
|
|
|
));
|
Initial bootstrap: cpanel-importer sanitization sandbox
Skeleton for the cpanel-importer Docker container — a one-shot
sandbox the WHP panel invokes BEFORE extracting a customer cpmove
tarball. See cpanel-import-container-spec.md (in /workspace/) for the
full design.
What this ships in v1.0:
- Dockerfile: almalinux:10-minimal + PHP 8.4 (Remi) + ClamAV 1.4 +
SaneSecurity Foxhole.PHP rules + tar/mariadb-client/rsync. Runs as
UID 999 (whp-import) via the panel-side --user 999:999 flag.
- scripts/entrypoint.sh: validates env, runs (optional) freshclam,
drives extract -> scan-files -> scan-dbs -> rsync -> report.json.
- scripts/extract.sh + scripts/lib/scan-symlinks.php: pre-extract
symlink scan ported standalone from
web-files/libs/CpanelBackupImporter.php (the existing 2026-05-29
whp02 destruction-vector fix). Aborts with exit 3 before tar runs
if any DANGEROUS symlink is found.
- scripts/scan-files.php: ClamAV walk + classify-and-action. v1.0
ships with an empty cleaner registry — every hit is
QUARANTINE_ONLY. Cleaner hooks are stubbed for v1.1.
- scripts/scan-dbs.php: regex MyISAM -> InnoDB rewrite (always
applied), WordPress identification, and ONE WP content scan check
(siteurl_external_domain). v1.1 will grow the check set.
- scripts/lib/safety-net.php: container-narrow open_basedir
allow-list, much tighter than the panel-side one.
- .gitea/workflows/build-push.yaml: builds + smoke-tests +
PHP-syntax-checks + bash-syntax-checks before pushing to
repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer.
- tests/build-fixtures.sh: builds cpmove-clean.tar.gz (benign WP
dump) and cpmove-alfa.tar.gz (the ALFA-shell symlink-to-/etc
vector) for local end-to-end testing.
- README.md / CONTRIBUTING.md: docker-run invocation, bind-mount
catalog, report.json schema, how to add a cleaner pattern or a WP
scan signature.
Local acceptance test results:
- clean fixture -> status=completed, 3 MyISAM->InnoDB, no flags, 0
- ALFA fixture -> exit 1, status=failed, failed_stage=extract,
"tarball contains dangerous symlinks; aborting" on stderr
- compromised-siteurl fixture -> imported_into_new_server=false,
.flagged file written, summary_for_panel.show_alert=true
Image size: 197 MB compressed (gzipped docker save), ~397 MB unique
layers extracted. Well under the spec's 600 MB compressed / 1.2 GB
extracted budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:57 -07:00
|
|
|
|
|
|
|
|
// -- classify + action each hit --------------------------------------------
|
|
|
|
|
|
|
|
|
|
$actions = [];
|
|
|
|
|
$cleaned = 0;
|
|
|
|
|
$quarantined = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($hits as $h) {
|
|
|
|
|
$path = $h['path'];
|
|
|
|
|
$sig = $h['signature'];
|
|
|
|
|
|
|
|
|
|
// v1.0 — every hit is QUARANTINE_ONLY because the cleaner registry
|
|
|
|
|
// is empty. Future work in v1.1 will iterate $cleaners and pick a
|
|
|
|
|
// matching cleaner.
|
|
|
|
|
$classification = 'QUARANTINE_ONLY';
|
|
|
|
|
foreach ($cleaners as $name => $entry) {
|
|
|
|
|
if (($entry['match'])($sig)) {
|
|
|
|
|
$classification = $entry['class'];
|
|
|
|
|
$cleanerName = $name;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$relPath = ltrim(substr($path, strlen($extractDir)), '/');
|
|
|
|
|
$qPath = $quarantineDir . '/' . $relPath;
|
|
|
|
|
|
|
|
|
|
if ($classification === 'QUARANTINE_ONLY') {
|
|
|
|
|
// Move the whole file to quarantine; remove from extract dir so
|
|
|
|
|
// the rsync to /host/sanitized/ does not include it.
|
|
|
|
|
@mkdir(dirname($qPath), 0750, true);
|
|
|
|
|
if (!@rename($path, $qPath)) {
|
|
|
|
|
// Fall back to copy + unlink (rename across mount boundaries
|
|
|
|
|
// sometimes EXDEVs even though /tmp and /host are both ours).
|
|
|
|
|
if (@copy($path, $qPath)) {
|
|
|
|
|
@unlink($path);
|
|
|
|
|
} else {
|
|
|
|
|
fwrite(STDERR, "scan-files: WARN failed to quarantine $path -> $qPath\n");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$quarantined++;
|
|
|
|
|
$actions[] = [
|
|
|
|
|
'path' => $relPath,
|
|
|
|
|
'signature' => $sig,
|
|
|
|
|
'action' => 'quarantined',
|
|
|
|
|
'cleaner' => null,
|
|
|
|
|
'backup' => $qPath,
|
|
|
|
|
];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// v1.1+ paths:
|
|
|
|
|
if ($classification === 'KNOWN_REMOVABLE' || $classification === 'REMOVABLE_WITH_BACKUP') {
|
|
|
|
|
// Backup first, then run the cleaner.
|
|
|
|
|
@mkdir(dirname($qPath), 0750, true);
|
|
|
|
|
$backup = $qPath . '.original';
|
|
|
|
|
if (!@copy($path, $backup)) {
|
|
|
|
|
fwrite(STDERR, "scan-files: backup before clean failed: $path; quarantining instead\n");
|
|
|
|
|
@rename($path, $qPath);
|
|
|
|
|
$quarantined++;
|
|
|
|
|
$actions[] = [
|
|
|
|
|
'path' => $relPath, 'signature' => $sig,
|
|
|
|
|
'action' => 'quarantined', 'cleaner' => null, 'backup' => $qPath,
|
|
|
|
|
];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$cleanerOk = ($cleaners[$cleanerName]['clean'])($path);
|
|
|
|
|
if (!$cleanerOk) {
|
|
|
|
|
// Cleaner refused; fall back to quarantine.
|
|
|
|
|
@rename($path, $qPath);
|
|
|
|
|
$quarantined++;
|
|
|
|
|
$actions[] = [
|
|
|
|
|
'path' => $relPath, 'signature' => $sig,
|
|
|
|
|
'action' => 'quarantined', 'cleaner' => $cleanerName, 'backup' => $qPath,
|
|
|
|
|
];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$cleaned++;
|
|
|
|
|
$actions[] = [
|
|
|
|
|
'path' => $relPath, 'signature' => $sig,
|
|
|
|
|
'action' => 'cleaned', 'cleaner' => $cleanerName, 'backup' => $backup,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$report = [
|
|
|
|
|
'scanner_version' => SCANNER_VERSION,
|
|
|
|
|
'import_id' => $importId,
|
|
|
|
|
'files_scanned' => $filesScanned,
|
|
|
|
|
'files_clean' => max(0, $filesScanned - count($hits)),
|
|
|
|
|
'files_cleaned' => $cleaned,
|
|
|
|
|
'files_quarantined' => $quarantined,
|
|
|
|
|
'actions' => $actions,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@file_put_contents($reportPath, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
|
|
|
|
|
|
|
|
|
fwrite(STDERR, sprintf(
|
|
|
|
|
"scan-files: done — scanned=%d clean=%d cleaned=%d quarantined=%d\n",
|
|
|
|
|
$filesScanned, $report['files_clean'], $cleaned, $quarantined
|
|
|
|
|
));
|
|
|
|
|
exit(0);
|