[mirror of https://git.0x0.st/mia/0x0] No-bullshit file hosting and URL shortening service https://0x0.st

Add support for ClamAV

+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
··· 1 + [Unit] 2 + Description=Scan 0x0 files with ClamAV 3 + 4 + [Timer] 5 + OnCalendar=hourly 6 + Persistent=true 7 + 8 + [Install] 9 + WantedBy=timers.target
+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
··· 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
··· 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
··· 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 ###