carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1#!/bin/bash
2# đŸ¤– scanner-selftest: prove, on THIS engine, that clamd never answers OK for
3# an archive it could only partly scan.
4#
5# scanner-selftest.service runs this every time clamd starts, and the TCP
6# listener is bound to that unit, so a failure here keeps port 3310 shut. It
7# talks to clamd on its unix socket: the port it guards is not open yet.
8#
9# Why it exists: with ClamAV's packaged limits, a zip member that expands past
10# MaxFileSize is trimmed and the trimmed copy is scanned and answered OK, with
11# no limit raised. The box is configured so that cannot happen (both limits
12# equal, AlertExceedsMax on). This checks that the configuration still does
13# what it did when it was chosen, on whatever version is installed today.
14#
15# Fixtures are sized for a 100 MiB scan limit. Members are written from a temp
16# file so their zip headers are ordinary; streamed members get zip64 headers,
17# which clamd does not open at all.
18set -u
19sock=/run/clamav/clamd.ctl
20
21if [ "${SCANNER_SELFTEST_FORCE_FAIL:-}" = 1 ]; then
22 echo "scanner-selftest: FAIL (forced, for the acceptance check)"
23 exit 1
24fi
25
26d=$(mktemp -d /var/tmp/scanner-selftest.XXXXXX) || exit 1
27trap 'rm -rf "$d"' EXIT
28echo "LocalSocket $sock" > "$d/clamdscan.conf"
29
30python3 - "$d" <<'PY' || { echo "scanner-selftest: FAIL (could not build fixtures)"; exit 1; }
31import io, gzip, os, random, sys, tarfile, zipfile
32d = sys.argv[1]
33fx = os.path.join(d, 'fx')
34os.mkdir(fx)
35EICAR = rb'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
36
37def block(seed):
38 # 1 MiB: a 16 KiB random block repeated. It compresses about 140 to 1 and
39 # differs per seed, so clamd's clean cache cannot wave a member through.
40 r = random.Random(seed)
41 return bytes(r.getrandbits(8) for _ in range(16384)) * 64
42
43def bigfile(name, mb, seed):
44 p = os.path.join(d, name)
45 blk = block(seed)
46 with open(p, 'wb') as f:
47 for _ in range(mb):
48 f.write(blk)
49 return p
50
51def mkzip(name, build):
52 with zipfile.ZipFile(os.path.join(fx, name), 'w', zipfile.ZIP_DEFLATED) as z:
53 build(z)
54
55open(os.path.join(fx, '01-clean.txt'), 'wb').write(b'hello world\n')
56open(os.path.join(fx, '02-eicar.txt'), 'wb').write(EICAR)
57big = bigfile('big.bin', 500, 1)
58mid = bigfile('mid.bin', 60, 5)
59mkzip('03-zip-one-500.zip', lambda z: z.write(big, 'big.bin'))
60with zipfile.ZipFile(os.path.join(d, 'inner.zip'), 'w', zipfile.ZIP_DEFLATED) as z:
61 z.write(big, 'big.bin')
62mkzip('04-zip-nested-500.zip', lambda z: z.write(os.path.join(d, 'inner.zip'), 'inner.zip'))
63def many(z):
64 for i in range(30):
65 z.write(bigfile('m.bin', 20, 100 + i), 'm%02d.bin' % i)
66mkzip('05-zip-30x20.zip', many)
67with gzip.open(os.path.join(fx, '06-gzip-500.gz'), 'wb') as g:
68 blk = block(2)
69 for _ in range(500):
70 g.write(blk)
71with tarfile.open(os.path.join(fx, '07-targz-5x40.tar.gz'), 'w:gz') as t:
72 for i in range(5):
73 data = block(200 + i) * 40
74 ti = tarfile.TarInfo('t%d.bin' % i)
75 ti.size = len(data)
76 t.addfile(ti, io.BytesIO(data))
77def big_then_eicar(z):
78 z.write(big, 'big.bin')
79 z.writestr('eicar.com', EICAR)
80mkzip('08-zip-500-then-eicar.zip', big_then_eicar)
81def mid_then_eicar(z):
82 z.write(mid, 'mid.bin')
83 z.writestr('eicar.com', EICAR)
84mkzip('09-zip-60-then-eicar.zip', mid_then_eicar)
85mkzip('10-zip-one-60.zip', lambda z: z.write(mid, 'mid.bin'))
86PY
87
88# What each fixture must answer. "limit" is the one that matters: an OK there
89# is a truncated scan signed clean.
90want() {
91 case $1 in
92 01 | 10) echo ok ;;
93 02 | 09) echo infected ;;
94 *) echo limit ;;
95 esac
96}
97
98# >>> classify
99# What a reply says: ok, infected, limit, timeout or error. The signature is
100# read as internal/clamd reads it: drop the trailing " FOUND", keep what follows
101# the last ": " (clamdscan puts the file path there, not "stream"). A limit is
102# Heuristics.Limits.Exceeded itself or a name under it, dot included, so a
103# lookalike such as Win.Heuristics.Limits.Exceeded.Fake is malware. MaxScanTime
104# is a timeout: it says the engine ran out of time, not that a limit held, so
105# it never matches what a fixture wants.
106classify() {
107 local sig
108 case $1 in
109 *" FOUND")
110 sig=${1% FOUND}
111 sig=${sig##*: }
112 case $sig in
113 Heuristics.Limits.Exceeded.MaxScanTime) echo timeout ;;
114 Heuristics.Limits.Exceeded | Heuristics.Limits.Exceeded.*) echo limit ;;
115 *) echo infected ;;
116 esac
117 ;;
118 *": OK") echo ok ;;
119 *) echo error ;;
120 esac
121}
122# <<< classify
123
124fail=0
125for f in "$d"/fx/*; do
126 n=$(basename "$f")
127 exp=$(want "${n%%-*}")
128 reply=$(clamdscan --config-file="$d/clamdscan.conf" --stream --no-summary "$f" 2>&1 | tail -1)
129 got=$(classify "$reply")
130 if [ "$got" = "$exp" ]; then
131 echo "scanner-selftest: pass $n ($got)"
132 elif [ "$got" = timeout ]; then
133 echo "scanner-selftest: FAIL $n timeout, not a verdict (the engine is too slow or overloaded): $reply"
134 fail=1
135 else
136 echo "scanner-selftest: FAIL $n want $exp, got $got: $reply"
137 fail=1
138 fi
139done
140
141if [ $fail -eq 0 ]; then
142 echo "scanner-selftest: PASS"
143else
144 echo "scanner-selftest: FAIL"
145fi
146exit $fail
147