Virenpruefung an clamd anbinden, wo clamd tatsaechlich lauscht
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Die Adresse zeigte auf /var/run/clamav/clamd.sock in einem geteilten Volume. Das offizielle clamav-Image legt seinen Unix-Socket aber unter /tmp im eigenen Container an - nachgemessen, das Verzeichnis im Volume bleibt leer. Jeder Verbindungsversuch lief damit in ENOENT, und die Statusseite meldete die Virenpruefung dauerhaft als gestoert. Folgenreich war das nicht nur fuer die Anzeige: ohne erreichbaren Scanner bleiben Uploads auf "Pruefung unvollstaendig" liegen und brauchen die Freigabe von Hand. Die Kette hat also gehalten, aber niemand kam durch. Dasselbe Image oeffnet ab Werk TCPSocket 3310. Darueber laeuft die Anbindung jetzt; der Port ist im Stack nicht veroeffentlicht und nur im internen Netz erreichbar. CLAMAV_ADDRESS nimmt weiterhin einen Pfad an, falls irgendwo doch ein Unix-Socket erreichbar ist. Das Socket-Volume faellt weg, es enthielt nie etwas. Der Scanner sieht ausserdem die Dateiablage nicht mehr: uebertragen wird per INSTREAM durch die Verbindung, gelesen hat er dort nie etwas. Geprueft gegen ein laufendes clamd, einschliesslich EICAR. Diese Pruefung bleibt als abschaltbarer Teil der Suite stehen: CLAMAV_LIVE=clamav:3310 pnpm --filter @afis/web test pruefkette Die uebrigen neuen Pruefungen decken das Zerlegen der Adresse ab. Sie haetten den Fehler nicht gefunden - eine Adresse, die auf nichts zeigt, ist syntaktisch tadellos. Nur eine echte Verbindung beantwortet das.
This commit is contained in:
+4
-1
@@ -52,7 +52,10 @@ BOOTSTRAP_ADMIN_IDS=
|
|||||||
|
|
||||||
# --- Dateien und Prüfkette ----------------------------------------------------
|
# --- Dateien und Prüfkette ----------------------------------------------------
|
||||||
FILES_DIR=/data/files
|
FILES_DIR=/data/files
|
||||||
CLAMAV_SOCKET=/var/run/clamav/clamd.sock
|
# clamd: host:port im Stack, oder ein Pfad, wenn ein Unix-Socket erreichbar ist.
|
||||||
|
# Das offizielle Image legt seinen Socket unter /tmp im eigenen Container ab,
|
||||||
|
# deshalb TCP - der Port ist nur im Stack-Netz erreichbar, nicht nach aussen.
|
||||||
|
CLAMAV_ADDRESS=clamav:3310
|
||||||
PDF_SERVICE_URL=http://pdf:3000
|
PDF_SERVICE_URL=http://pdf:3000
|
||||||
|
|
||||||
# --- Cloudflare Tunnel --------------------------------------------------------
|
# --- Cloudflare Tunnel --------------------------------------------------------
|
||||||
|
|||||||
+8
-4
@@ -45,7 +45,6 @@ services:
|
|||||||
HOSTNAME: '0.0.0.0'
|
HOSTNAME: '0.0.0.0'
|
||||||
volumes:
|
volumes:
|
||||||
- files:/data/files
|
- files:/data/files
|
||||||
- clamav_socket:/var/run/clamav
|
|
||||||
# Links der Port auf dem Host, rechts der im Container. Nur der linke ist
|
# Links der Port auf dem Host, rechts der im Container. Nur der linke ist
|
||||||
# frei waehlbar - ueber WEB_PORT in der .env, damit fuer eine andere
|
# frei waehlbar - ueber WEB_PORT in der .env, damit fuer eine andere
|
||||||
# Portwahl keine verfolgte Datei angefasst werden muss.
|
# Portwahl keine verfolgte Datei angefasst werden muss.
|
||||||
@@ -131,10 +130,16 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
CLAMAV_NO_MILTERD: 'true'
|
CLAMAV_NO_MILTERD: 'true'
|
||||||
FRESHCLAM_CHECKS: '2'
|
FRESHCLAM_CHECKS: '2'
|
||||||
|
# Nur die Signaturen. Der Scanner bekommt die Daten ueber INSTREAM durch
|
||||||
|
# die Verbindung gereicht und braucht die Dateiablage deshalb nicht zu
|
||||||
|
# sehen - siehe lib/files/clamav.ts. Ein Socket-Volume gibt es nicht mehr:
|
||||||
|
# das offizielle Image legt seinen Unix-Socket unter /tmp im eigenen
|
||||||
|
# Container an, das geteilte Verzeichnis blieb leer, und die Virenpruefung
|
||||||
|
# meldete sich dauerhaft als gestoert. Die Anbindung laeuft ueber
|
||||||
|
# TCPSocket 3310, den dasselbe Image ab Werk oeffnet. Der Port ist nicht
|
||||||
|
# veroeffentlicht und nur im Stack-Netz erreichbar.
|
||||||
volumes:
|
volumes:
|
||||||
- clamav_db:/var/lib/clamav
|
- clamav_db:/var/lib/clamav
|
||||||
- clamav_socket:/var/run/clamav
|
|
||||||
- files:/data/files
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'clamdscan --ping 1 || exit 1']
|
test: ['CMD-SHELL', 'clamdscan --ping 1 || exit 1']
|
||||||
interval: 60s
|
interval: 60s
|
||||||
@@ -245,4 +250,3 @@ volumes:
|
|||||||
pgdata:
|
pgdata:
|
||||||
files:
|
files:
|
||||||
clamav_db:
|
clamav_db:
|
||||||
clamav_socket:
|
|
||||||
|
|||||||
@@ -99,9 +99,16 @@ info "Dateiablage unter ${FILES_DIR_LOKAL}"
|
|||||||
|
|
||||||
# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in
|
# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in
|
||||||
# der Quarantäne - sie gehen nicht ungeprüft durch. Siehe docs/07.
|
# der Quarantäne - sie gehen nicht ungeprüft durch. Siehe docs/07.
|
||||||
CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)"
|
CLAMAV_ADRESSE="$(grep -E '^CLAMAV_ADDRESS=' .env 2>/dev/null | cut -d= -f2- || true)"
|
||||||
[ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \
|
CLAMAV_ADRESSE="${CLAMAV_ADRESSE:-clamav:3310}"
|
||||||
|| warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand"
|
clamd_erreichbar() {
|
||||||
|
case "$CLAMAV_ADRESSE" in
|
||||||
|
/*) [ -S "$CLAMAV_ADRESSE" ] ;;
|
||||||
|
*) (exec 3<>"/dev/tcp/${CLAMAV_ADRESSE%:*}/${CLAMAV_ADRESSE##*:}") 2>/dev/null ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
clamd_erreichbar \
|
||||||
|
|| warn "Kein clamd unter ${CLAMAV_ADRESSE}: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand"
|
||||||
command -v qpdf >/dev/null \
|
command -v qpdf >/dev/null \
|
||||||
|| warn "qpdf fehlt: PDF-Uploads landen auf 'Prüfung unvollständig' (im Container ist es enthalten)"
|
|| warn "qpdf fehlt: PDF-Uploads landen auf 'Prüfung unvollständig' (im Container ist es enthalten)"
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@ const schema = z.object({
|
|||||||
.transform((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
.transform((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
||||||
|
|
||||||
FILES_DIR: z.string().default('/data/files'),
|
FILES_DIR: z.string().default('/data/files'),
|
||||||
CLAMAV_SOCKET: z.string().default('/var/run/clamav/clamd.sock'),
|
/** `host:port` oder Pfad zu einem Unix-Socket. Siehe lib/files/clamav.ts. */
|
||||||
|
CLAMAV_ADDRESS: z.string().default('clamav:3310'),
|
||||||
PDF_SERVICE_URL: z.url().optional(),
|
PDF_SERVICE_URL: z.url().optional(),
|
||||||
|
|
||||||
TZ: z.string().default('Europe/Berlin'),
|
TZ: z.string().default('Europe/Berlin'),
|
||||||
|
|||||||
+42
-9
@@ -1,14 +1,50 @@
|
|||||||
import net from 'node:net'
|
import net from 'node:net'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anbindung an clamd über den Unix-Socket im geteilten Volume.
|
* Anbindung an clamd.
|
||||||
*
|
*
|
||||||
* Es wird **INSTREAM** benutzt, nicht SCAN mit einem Pfad: dann muss der
|
* Es wird **INSTREAM** benutzt, nicht SCAN mit einem Pfad: dann muss der
|
||||||
* Scanner die Datei nicht sehen können, und die Anbindung funktioniert auch,
|
* Scanner die Datei nicht sehen können, und die Anbindung funktioniert auch,
|
||||||
* wenn clamd anderswo läuft. Das Protokoll ist simpel - Länge als 4 Byte,
|
* wenn clamd anderswo läuft. Das Protokoll ist simpel - Länge als 4 Byte,
|
||||||
* Nutzdaten, dann eine Null-Länge als Abschluss.
|
* Nutzdaten, dann eine Null-Länge als Abschluss.
|
||||||
|
*
|
||||||
|
* Die Adresse ist entweder `host:port` oder ein Pfad zu einem Unix-Socket.
|
||||||
|
* Vorgabe ist TCP, weil das offizielle clamav-Image seinen Unix-Socket unter
|
||||||
|
* `/tmp/clamd.sock` anlegt und nicht dort, wo ein geteiltes Volume ihn
|
||||||
|
* sichtbar machen könnte - ein Socket-Volume blieb schlicht leer und die
|
||||||
|
* Virenprüfung meldete sich dauerhaft als gestört. `TCPSocket 3310` ist in
|
||||||
|
* demselben Image ab Werk offen, und im Stack ist der Port nicht nach aussen
|
||||||
|
* veröffentlicht.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** `host:port` oder ein absoluter Pfad. Getrennt, damit es prüfbar ist. */
|
||||||
|
export type ClamavAddress = { path: string } | { host: string; port: number }
|
||||||
|
|
||||||
|
export function parseClamavAddress(address: string): ClamavAddress {
|
||||||
|
const sauber = address.trim()
|
||||||
|
if (sauber.startsWith('/') || sauber.startsWith('.')) return { path: sauber }
|
||||||
|
|
||||||
|
// Von hinten trennen, damit eine IPv6-Adresse in Klammern heil bleibt.
|
||||||
|
const trenner = sauber.lastIndexOf(':')
|
||||||
|
if (trenner <= 0) throw new Error(`clamd-Adresse ohne Port: ${address}`)
|
||||||
|
|
||||||
|
const port = Number(sauber.slice(trenner + 1))
|
||||||
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
||||||
|
throw new Error(`clamd-Adresse mit ungültigem Port: ${address}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = sauber.slice(0, trenner).replace(/^\[|\]$/g, '')
|
||||||
|
if (host.length === 0) throw new Error(`clamd-Adresse ohne Host: ${address}`)
|
||||||
|
return { host, port }
|
||||||
|
}
|
||||||
|
|
||||||
|
function verbinden(address: string): net.Socket {
|
||||||
|
const ziel = parseClamavAddress(address)
|
||||||
|
return 'path' in ziel
|
||||||
|
? net.createConnection(ziel.path)
|
||||||
|
: net.createConnection({ host: ziel.host, port: ziel.port })
|
||||||
|
}
|
||||||
|
|
||||||
export type ScanResult =
|
export type ScanResult =
|
||||||
| { ok: true; clean: true }
|
| { ok: true; clean: true }
|
||||||
| { ok: true; clean: false; signature: string }
|
| { ok: true; clean: false; signature: string }
|
||||||
@@ -18,7 +54,7 @@ export type ScanResult =
|
|||||||
const CHUNK = 64 * 1024
|
const CHUNK = 64 * 1024
|
||||||
|
|
||||||
export function scanBuffer(
|
export function scanBuffer(
|
||||||
socketPath: string,
|
address: string,
|
||||||
content: Uint8Array,
|
content: Uint8Array,
|
||||||
timeoutMs = 30_000,
|
timeoutMs = 30_000,
|
||||||
): Promise<ScanResult> {
|
): Promise<ScanResult> {
|
||||||
@@ -26,7 +62,7 @@ export function scanBuffer(
|
|||||||
let settled = false
|
let settled = false
|
||||||
let antwort = ''
|
let antwort = ''
|
||||||
|
|
||||||
const socket = net.createConnection(socketPath)
|
const socket = verbinden(address)
|
||||||
const done = (result: ScanResult) => {
|
const done = (result: ScanResult) => {
|
||||||
if (settled) return
|
if (settled) return
|
||||||
settled = true
|
settled = true
|
||||||
@@ -69,11 +105,8 @@ export function scanBuffer(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Fragt clamd, ob er da ist. Antwort auf zPING ist "PONG". */
|
||||||
* Fragt clamd über den Unix-Socket. Der Socket liegt im geteilten Volume
|
export function pingClamav(address: string, timeoutMs = 2000): Promise<boolean> {
|
||||||
* clamav_socket, siehe compose.yaml. Antwort auf zPING ist "PONG".
|
|
||||||
*/
|
|
||||||
export function pingClamav(socketPath: string, timeoutMs = 2000): Promise<boolean> {
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let settled = false
|
let settled = false
|
||||||
const done = (ok: boolean) => {
|
const done = (ok: boolean) => {
|
||||||
@@ -83,7 +116,7 @@ export function pingClamav(socketPath: string, timeoutMs = 2000): Promise<boolea
|
|||||||
resolve(ok)
|
resolve(ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = net.createConnection(socketPath)
|
const socket = verbinden(address)
|
||||||
socket.setTimeout(timeoutMs)
|
socket.setTimeout(timeoutMs)
|
||||||
socket.on('connect', () => socket.write('zPING\0'))
|
socket.on('connect', () => socket.write('zPING\0'))
|
||||||
socket.on('data', (chunk) => done(chunk.toString().includes('PONG')))
|
socket.on('data', (chunk) => done(chunk.toString().includes('PONG')))
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export async function runCheckChain(
|
|||||||
|
|
||||||
// 2. Virenscanner.
|
// 2. Virenscanner.
|
||||||
if (!options.skipScan) {
|
if (!options.skipScan) {
|
||||||
const scan = await scanBuffer(getEnv().CLAMAV_SOCKET, inhalt)
|
const scan = await scanBuffer(getEnv().CLAMAV_ADDRESS, inhalt)
|
||||||
if (!scan.ok) {
|
if (!scan.ok) {
|
||||||
// Nicht blockiert, sondern unentschieden: die Datei ist nicht als böse
|
// Nicht blockiert, sondern unentschieden: die Datei ist nicht als böse
|
||||||
// erkannt, sie ist ungeprüft. Ein Administrator kann sie freigeben.
|
// erkannt, sie ist ungeprüft. Ein Administrator kann sie freigeben.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async function checkDiscordGateway(): Promise<{ status: ComponentState; detail?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkClamav(): Promise<{ status: ComponentState; detail?: string }> {
|
async function checkClamav(): Promise<{ status: ComponentState; detail?: string }> {
|
||||||
const ok = await pingClamav(getEnv().CLAMAV_SOCKET)
|
const ok = await pingClamav(getEnv().CLAMAV_ADDRESS)
|
||||||
return ok ? { status: 'up' } : { status: 'down', detail: 'clamd antwortet nicht' }
|
return ok ? { status: 'up' } : { status: 'down', detail: 'clamd antwortet nicht' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ vi.mock('@/lib/db', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/lib/env', () => ({
|
vi.mock('@/lib/env', () => ({
|
||||||
getEnv: () => ({ CLAMAV_SOCKET: '/var/run/clamav/clamd.sock' }),
|
getEnv: () => ({ CLAMAV_ADDRESS: 'clamav:3310' }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/lib/files/clamav', () => ({ pingClamav: vi.fn(async () => true) }))
|
vi.mock('@/lib/files/clamav', () => ({ pingClamav: vi.fn(async () => true) }))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import sharp from 'sharp'
|
|||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { acceptUpload, loadDeliverable, releaseAttachment } from '@/lib/files/service'
|
import { acceptUpload, loadDeliverable, releaseAttachment } from '@/lib/files/service'
|
||||||
import { runCheckChain } from '@/lib/files/process'
|
import { runCheckChain } from '@/lib/files/process'
|
||||||
|
import { parseClamavAddress, pingClamav, scanBuffer } from '@/lib/files/clamav'
|
||||||
import { absolutePath } from '@/lib/files/paths'
|
import { absolutePath } from '@/lib/files/paths'
|
||||||
import { AccessError } from '@/lib/errors'
|
import { AccessError } from '@/lib/errors'
|
||||||
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
||||||
@@ -19,6 +20,79 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
|||||||
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
||||||
const suite = url ? describe : describe.skip
|
const suite = url ? describe : describe.skip
|
||||||
|
|
||||||
|
describe('Adresse des Virenscanners', () => {
|
||||||
|
/**
|
||||||
|
* Der Grund für diese Prüfungen: die Adresse stand als Pfad auf ein
|
||||||
|
* geteiltes Volume in der Konfiguration, das offizielle clamav-Image legt
|
||||||
|
* seinen Unix-Socket aber unter /tmp im eigenen Container an. Das
|
||||||
|
* Verzeichnis blieb leer, jeder Verbindungsversuch lief in ENOENT, und die
|
||||||
|
* Virenprüfung meldete sich dauerhaft als gestört. Ein Tippfehler in der
|
||||||
|
* Adresse darf nicht wieder still enden.
|
||||||
|
*/
|
||||||
|
it('liest host:port', () => {
|
||||||
|
expect(parseClamavAddress('clamav:3310')).toEqual({ host: 'clamav', port: 3310 })
|
||||||
|
expect(parseClamavAddress(' 127.0.0.1:3310 ')).toEqual({ host: '127.0.0.1', port: 3310 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt eine IPv6-Adresse heil', () => {
|
||||||
|
expect(parseClamavAddress('[::1]:3310')).toEqual({ host: '::1', port: 3310 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erkennt einen Unix-Socket am Pfad', () => {
|
||||||
|
expect(parseClamavAddress('/tmp/clamd.sock')).toEqual({ path: '/tmp/clamd.sock' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('wirft bei Unsinn, statt still ins Leere zu greifen', () => {
|
||||||
|
for (const eingabe of ['clamav', 'clamav:', ':3310', 'clamav:0', 'clamav:99999', '']) {
|
||||||
|
expect(() => parseClamavAddress(eingabe), eingabe).toThrow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('meldet einen nicht erreichbaren Scanner als nicht erreichbar', async () => {
|
||||||
|
// Port 1 ist reserviert und lauscht nirgends. Kein Absturz, ein `false`.
|
||||||
|
expect(await pingClamav('127.0.0.1:1', 1000)).toBe(false)
|
||||||
|
expect(await pingClamav('/gibt/es/nicht.sock', 1000)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gegen einen laufenden Scanner. Standardmässig übersprungen, weil auf dem
|
||||||
|
* Entwicklungsrechner keiner läuft.
|
||||||
|
*
|
||||||
|
* Diese Prüfungen hätten den Fehler gefunden, den die obigen nicht finden
|
||||||
|
* konnten: eine Adresse, die auf nichts zeigt, ist syntaktisch tadellos. Nur
|
||||||
|
* eine echte Verbindung beantwortet die Frage, ob die Prüfkette wirklich hält.
|
||||||
|
*
|
||||||
|
* CLAMAV_LIVE=clamav:3310 pnpm --filter @afis/web test pruefkette
|
||||||
|
*/
|
||||||
|
const live = process.env.CLAMAV_LIVE
|
||||||
|
const liveSuite = live ? describe : describe.skip
|
||||||
|
|
||||||
|
liveSuite('Gegen einen laufenden Scanner', () => {
|
||||||
|
it('antwortet auf die Anfrage', async () => {
|
||||||
|
expect(await pingClamav(live!, 5000)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lässt harmlosen Inhalt durch', async () => {
|
||||||
|
expect(await scanBuffer(live!, Buffer.from('harmloser Text'))).toEqual({
|
||||||
|
ok: true,
|
||||||
|
clean: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erkennt die Testsignatur', async () => {
|
||||||
|
// EICAR ist kein Schadprogramm, sondern die vereinbarte Zeichenfolge, an
|
||||||
|
// der jeder Scanner anschlagen muss. Damit lässt sich prüfen, dass die
|
||||||
|
// Kette wirklich prüft und nicht nur eine Verbindung aufbaut.
|
||||||
|
const eicar = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
|
||||||
|
expect(await scanBuffer(live!, Buffer.from(eicar))).toEqual({
|
||||||
|
ok: true,
|
||||||
|
clean: false,
|
||||||
|
signature: 'Eicar-Test-Signature',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const marke = `kette-${Date.now()}`
|
const marke = `kette-${Date.now()}`
|
||||||
|
|
||||||
function perms(
|
function perms(
|
||||||
|
|||||||
Reference in New Issue
Block a user