+22
0x0-vscan.service
+22
0x0-vscan.service
···
1
+
[Unit]
2
+
Description=Scan 0x0 files with ClamAV
3
+
After=remote-fs.target clamd.service
4
+
5
+
[Service]
6
+
Type=oneshot
7
+
User=nullptr
8
+
WorkingDirectory=/path/to/0x0
9
+
BindPaths=/path/to/0x0
10
+
11
+
Environment=FLASK_APP=fhost
12
+
ExecStart=/usr/bin/flask vscan
13
+
ProtectProc=noaccess
14
+
ProtectSystem=strict
15
+
ProtectHome=tmpfs
16
+
PrivateTmp=true
17
+
PrivateUsers=true
18
+
ProtectKernelLogs=true
19
+
LockPersonality=true
20
+
21
+
[Install]
22
+
WantedBy=multi-user.target
+9
0x0-vscan.timer
+9
0x0-vscan.timer
+16
README.rst
+16
README.rst
···
59
59
* `PyAV <https://github.com/PyAV-Org/PyAV>`_
60
60
61
61
62
+
Virus Scanning
63
+
--------------
64
+
65
+
0x0 can scan its files with ClamAV’s daemon. As this can take a long time
66
+
for larger files, this does not happen immediately but instead every time
67
+
you run the ``vscan`` command. It is recommended to configure a systemd
68
+
timer or cronjob to do this periodically. Examples are included::
69
+
70
+
0x0-vscan.service
71
+
0x0-vscan.timer
72
+
73
+
Remember to adjust your size limits in clamd.conf!
74
+
75
+
This feature requires the `clamd module <https://pypi.org/project/clamd/>`_.
76
+
77
+
62
78
Network Security Considerations
63
79
-------------------------------
64
80
+63
-1
fhost.py
+63
-1
fhost.py
···
22
22
from flask import Flask, abort, make_response, redirect, request, send_from_directory, url_for, Response, render_template
23
23
from flask_sqlalchemy import SQLAlchemy
24
24
from flask_migrate import Migrate
25
-
from sqlalchemy import and_
25
+
from sqlalchemy import and_, or_
26
26
from jinja2.exceptions import *
27
27
from jinja2 import ChoiceLoader, FileSystemLoader
28
28
from hashlib import sha256
···
32
32
import os
33
33
import sys
34
34
import time
35
+
import datetime
35
36
import typing
36
37
import requests
37
38
import secrets
···
70
71
FHOST_UPLOAD_BLACKLIST = None,
71
72
NSFW_DETECT = False,
72
73
NSFW_THRESHOLD = 0.608,
74
+
VSCAN_SOCKET = None,
75
+
VSCAN_QUARANTINE_PATH = "quarantine",
76
+
VSCAN_IGNORE = [
77
+
"Eicar-Test-Signature",
78
+
"PUA.Win.Packer.XmMusicFile",
79
+
],
80
+
VSCAN_INTERVAL = datetime.timedelta(days=7),
73
81
URL_ALPHABET = "DEQhd2uFteibPwq0SWBInTpA_jcZL5GKz3YCR14Ulk87Jors9vNHgfaOmMXy6Vx-",
74
82
)
75
83
···
131
139
expiration = db.Column(db.BigInteger)
132
140
mgmt_token = db.Column(db.String)
133
141
secret = db.Column(db.String)
142
+
last_vscan = db.Column(db.DateTime)
134
143
135
144
def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token):
136
145
self.sha256 = sha256
···
591
600
max_exp = app.config.get("FHOST_MAX_EXPIRATION", 365 * 24 * 60 * 60 * 1000)
592
601
max_size = app.config.get("MAX_CONTENT_LENGTH", 256 * 1024 * 1024)
593
602
return min_exp + int((-max_exp + min_exp) * (filesize / max_size - 1) ** 3)
603
+
604
+
def do_vscan(f):
605
+
if f["path"].is_file():
606
+
with open(f["path"], "rb") as scanf:
607
+
try:
608
+
f["result"] = list(app.config["VSCAN_SOCKET"].instream(scanf).values())[0]
609
+
except:
610
+
f["result"] = ("SCAN FAILED", None)
611
+
else:
612
+
f["result"] = ("FILE NOT FOUND", None)
613
+
614
+
return f
615
+
616
+
@app.cli.command("vscan")
617
+
def vscan():
618
+
if not app.config["VSCAN_SOCKET"]:
619
+
print("""Error: Virus scanning enabled but no connection method specified.
620
+
Please set VSCAN_SOCKET.""")
621
+
sys.exit(1)
622
+
623
+
qp = Path(app.config["VSCAN_QUARANTINE_PATH"])
624
+
qp.mkdir(parents=True, exist_ok=True)
625
+
626
+
from multiprocessing import Pool
627
+
with Pool() as p:
628
+
if isinstance(app.config["VSCAN_INTERVAL"], datetime.timedelta):
629
+
scandate = datetime.datetime.now() - app.config["VSCAN_INTERVAL"]
630
+
res = File.query.filter(or_(File.last_vscan < scandate,
631
+
File.last_vscan == None),
632
+
File.removed == False)
633
+
else:
634
+
res = File.query.filter(File.last_vscan == None, File.removed == False)
635
+
636
+
work = [{"path" : f.getpath(), "name" : f.getname(), "id" : f.id} for f in res]
637
+
638
+
results = []
639
+
for i, r in enumerate(p.imap_unordered(do_vscan, work)):
640
+
if r["result"][0] != "OK":
641
+
print(f"{r['name']}: {r['result'][0]} {r['result'][1] or ''}")
642
+
643
+
found = False
644
+
if r["result"][0] == "FOUND":
645
+
if not r["result"][1] in app.config["VSCAN_IGNORE"]:
646
+
r["path"].rename(qp / r["name"])
647
+
found = True
648
+
649
+
results.append({
650
+
"id" : r["id"],
651
+
"last_vscan" : None if r["result"][0] == "SCAN FAILED" else datetime.datetime.now(),
652
+
"removed" : found})
653
+
654
+
db.session.bulk_update_mappings(File, results)
655
+
db.session.commit()
+31
instance/config.example.py
+31
instance/config.example.py
···
168
168
NSFW_THRESHOLD = 0.608
169
169
170
170
171
+
# If you want to scan files for viruses using ClamAV, specify the socket used
172
+
# for connections here. You will need the clamd module.
173
+
# Since this can take a very long time on larger files, it is not done
174
+
# immediately but every time you run the vscan command. It is recommended to
175
+
# configure a systemd timer or cronjob to do this periodically.
176
+
# Remember to adjust your size limits in clamd.conf!
177
+
#
178
+
# Example:
179
+
# from clamd import ClamdUnixSocket
180
+
# VSCAN_SOCKET = ClamdUnixSocket("/run/clamav/clamd-socket")
181
+
182
+
# This is the directory that files flagged as malicious are moved to.
183
+
# Relative paths are resolved relative to the working directory
184
+
# of the 0x0 process.
185
+
VSCAN_QUARANTINE_PATH = "quarantine"
186
+
187
+
# Since updated virus definitions might catch some files that were previously
188
+
# reported as clean, you may want to rescan old files periodically.
189
+
# Set this to a datetime.timedelta to specify the frequency, or None to
190
+
# disable rescanning.
191
+
from datetime import timedelta
192
+
VSCAN_INTERVAL = timedelta(days=7)
193
+
194
+
# Some files flagged by ClamAV are usually not malicious, especially if the
195
+
# DetectPUA option is enabled in clamd.conf. This is a list of signatures
196
+
# that will be ignored.
197
+
VSCAN_IGNORE = [
198
+
"Eicar-Test-Signature",
199
+
"PUA.Win.Packer.XmMusicFile",
200
+
]
201
+
171
202
# A list of all characters which can appear in a URL
172
203
#
173
204
# If this list is too short, then URLs can very quickly become long.
+26
migrations/versions/5cee97aab219_.py
+26
migrations/versions/5cee97aab219_.py
···
1
+
"""add date of last virus scan
2
+
3
+
Revision ID: 5cee97aab219
4
+
Revises: e2e816056589
5
+
Create Date: 2022-12-10 16:39:56.388259
6
+
7
+
"""
8
+
9
+
# revision identifiers, used by Alembic.
10
+
revision = '5cee97aab219'
11
+
down_revision = 'e2e816056589'
12
+
13
+
from alembic import op
14
+
import sqlalchemy as sa
15
+
16
+
17
+
def upgrade():
18
+
# ### commands auto generated by Alembic - please adjust! ###
19
+
op.add_column('file', sa.Column('last_vscan', sa.DateTime(), nullable=True))
20
+
# ### end Alembic commands ###
21
+
22
+
23
+
def downgrade():
24
+
# ### commands auto generated by Alembic - please adjust! ###
25
+
op.drop_column('file', 'last_vscan')
26
+
# ### end Alembic commands ###