-
Notifications
You must be signed in to change notification settings - Fork 5
/
clamav_rest.py
155 lines (123 loc) · 4.31 KB
/
clamav_rest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import logging, json_logging
import sys
import timeit
from functools import wraps
from secrets import compare_digest
from quart import Quart, request, jsonify, current_app, abort
from aioprometheus import Counter, Histogram, Registry, render
import clamd
app = Quart(__name__)
app.config.from_pyfile('config.py')
# Configure metrics
app.registry = Registry()
app.request_counter = Counter('requests', 'Number of overall requests.')
app.registry.register(app.request_counter)
app.scan_counter = Counter('scans', 'Number of overall virus scans.')
app.registry.register(app.scan_counter)
app.infection_counter = Counter('infections', 'Number of infected files found.')
app.registry.register(app.infection_counter)
app.scan_duration_histogram = Histogram('scan_duration', 'Histogram over virus scan duration.')
app.registry.register(app.scan_duration_histogram)
# Configure logging
if app.config['LOGJSON']:
do_not_log = ['/health', '/metrics']
json_logging.init_quart(enable_json=True)
json_logging.init_request_instrument(app, exclude_url_patterns=do_not_log)
logger = logging.getLogger('clamav-rest')
logger.setLevel(app.config['LOGLEVEL'])
logger.addHandler(logging.StreamHandler(sys.stdout))
# Configure clamd
try:
cd = clamd.ClamdAsyncNetworkSocket(
host=app.config['CLAMD_HOST'],
port=app.config['CLAMD_PORT']
)
except BaseException:
logger.exception('error bootstrapping clamd for network socket')
# https://gitlab.com/pgjones/quart-auth/-/issues/6#note_460844029
def auth_required(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if (
current_app.config['AUTH_USERNAME'] and
current_app.config['AUTH_PASSWORD']
):
auth = request.authorization
if (
auth is not None and
auth.type == 'basic' and
auth.username == current_app.config['AUTH_USERNAME'] and
compare_digest(auth.password, current_app.config['AUTH_PASSWORD'])
):
return await func(*args, **kwargs)
else:
abort(401)
else:
return await func(*args, **kwargs)
return wrapper
@app.route('/', methods=['POST'])
@auth_required
async def scan():
# Metric
app.request_counter.inc({'path': '/'})
files = await request.files
if len(files) != 1:
return 'Provide a single file', 400
_, file_data = list(files.items())[0]
logger.debug('Scanning {file_name}'.format(
file_name=file_data.filename
))
start_time = timeit.default_timer()
try:
resp = await cd.instream(file_data)
except clamd.ConnectionError as err:
logger.error('clamd.ConnectionError: {}'.format(err))
return 'Service Unavailable', 502
elapsed = timeit.default_timer() - start_time
# Metric
app.scan_duration_histogram.observe({'time': 'scan_duration'}, elapsed)
status, reason = resp['stream']
# Metric
if status != 'OK':
app.infection_counter.inc({'path': '/'})
response = {
'malware': False if status == 'OK' else True,
'reason': reason,
'time': elapsed
}
logger.info(
'Scanned {file_name}. Duration: {elapsed}. Infected: {status}'
.format(
file_name=file_data.filename,
elapsed=elapsed,
status=response['malware']
),
extra={'props': response}
)
# Metric
app.scan_counter.inc({'path': '/'})
return jsonify(response)
# Metrics endpoint
@app.route('/metrics')
async def handle_metrics():
content, http_headers = render(app.registry, request.headers.getlist('accept'))
return content, http_headers
# Liveness probe goes here
@app.route('/health/live', methods=['GET'])
def health_live():
return 'OK', 200
# Readyness probe goes here
@app.route('/health/ready', methods=['GET'])
async def health_ready():
try:
clamd_response = await cd.ping()
if 'PONG' in clamd_response:
return 'Service OK'
logger.error('expected PONG from clamd container')
return 'Service Down', 502
except clamd.ConnectionError:
logger.error('clamd.ConnectionError')
return 'Service Unavailable', 502
except BaseException as e:
logger.error(e)
return 'Service Unavailable', 500