malsearch 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- malsearch/VERSION.txt +1 -0
- malsearch/__info__.py +15 -0
- malsearch/__init__.py +90 -0
- malsearch/__main__.py +60 -0
- malsearch/clients/__common__.py +162 -0
- malsearch/clients/__init__.py +11 -0
- malsearch/clients/malpedia.py +16 -0
- malsearch/clients/malshare.py +15 -0
- malsearch/clients/malwarebazaar.py +16 -0
- malsearch/clients/triage.py +21 -0
- malsearch/clients/virusshare.py +15 -0
- malsearch/clients/virustotal.py +21 -0
- malsearch-0.1.0.dist-info/LICENSE +674 -0
- malsearch-0.1.0.dist-info/METADATA +726 -0
- malsearch-0.1.0.dist-info/RECORD +18 -0
- malsearch-0.1.0.dist-info/WHEEL +5 -0
- malsearch-0.1.0.dist-info/entry_points.txt +2 -0
- malsearch-0.1.0.dist-info/top_level.txt +1 -0
malsearch/VERSION.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
malsearch/__info__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""MalSearch package information.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
__author__ = "Alexandre D'Hondt"
|
|
8
|
+
__copyright__ = "© 2024 A. D'Hondt"
|
|
9
|
+
__email__ = "alexandre.dhondt@gmail.com"
|
|
10
|
+
__license__ = "GPLv3 (https://www.gnu.org/licenses/gpl-3.0.fr.html)"
|
|
11
|
+
__source__ = "https://github.com/packing-box/malsearch"
|
|
12
|
+
|
|
13
|
+
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
|
|
14
|
+
__version__ = f.read().strip()
|
|
15
|
+
|
malsearch/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .clients import *
|
|
3
|
+
from .clients import __all__ as _clients
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = ["download_sample", "download_samples"] + _clients
|
|
7
|
+
|
|
8
|
+
_CLIENTS_MAP = {n.lower(): globals()[n] for n in _clients}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _valid_conf(path):
|
|
12
|
+
from configparser import ConfigParser
|
|
13
|
+
from os.path import exists, expanduser
|
|
14
|
+
path = expanduser(path)
|
|
15
|
+
if not exists(path):
|
|
16
|
+
raise ValueError("configuration file does not exist")
|
|
17
|
+
conf = ConfigParser()
|
|
18
|
+
try:
|
|
19
|
+
conf.read(path)
|
|
20
|
+
conf.path = path
|
|
21
|
+
except:
|
|
22
|
+
raise ValueError("invalid configuration file")
|
|
23
|
+
return conf
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def download_sample(hash, config=None, **kwargs):
|
|
27
|
+
import logging
|
|
28
|
+
logger = logging.getLogger("malsearch")
|
|
29
|
+
if config is None:
|
|
30
|
+
logger.error("no configuration file provided")
|
|
31
|
+
logger.info(f"you can create one at {config} manually (INI format with section 'API keys')")
|
|
32
|
+
else:
|
|
33
|
+
import datetime as dt
|
|
34
|
+
from os.path import exists, join
|
|
35
|
+
p = join(kwargs.get('output_dir', "."), hash)
|
|
36
|
+
if exists(p) and not kwargs.get('overwrite'):
|
|
37
|
+
logger.info(f"'{p}' already exists")
|
|
38
|
+
return
|
|
39
|
+
if isinstance(config, str):
|
|
40
|
+
config = _valid_conf(config)
|
|
41
|
+
clients = []
|
|
42
|
+
for n in config['API keys']:
|
|
43
|
+
if n in (kwargs.get('skip') or []):
|
|
44
|
+
logger.debug(f"{n} skipped")
|
|
45
|
+
continue
|
|
46
|
+
if config.has_section("Disabled"):
|
|
47
|
+
t = config['Disabled'].get(n)
|
|
48
|
+
if t is not None:
|
|
49
|
+
try:
|
|
50
|
+
if dt.datetime.strptime(t, "%d/%m/%Y %H:%M:%S") < dt.datetime.now():
|
|
51
|
+
from contextlib import nullcontext
|
|
52
|
+
with kwargs.get('lock') or nullcontext():
|
|
53
|
+
config['Disabled'].pop(n)
|
|
54
|
+
with open(config.path, 'w') as f:
|
|
55
|
+
config.write(f)
|
|
56
|
+
else:
|
|
57
|
+
logger.warning(f"{n} is disabled until {t}")
|
|
58
|
+
continue
|
|
59
|
+
except ValueError:
|
|
60
|
+
logger.warning(f"{n} is disabled")
|
|
61
|
+
continue
|
|
62
|
+
clients.append(n)
|
|
63
|
+
if len(clients) == 0:
|
|
64
|
+
logger.warning("no download client available/enabled")
|
|
65
|
+
logger.debug(f"clients: {', '.join(clients)}")
|
|
66
|
+
for n in clients:
|
|
67
|
+
logger.debug(f"trying {n}...")
|
|
68
|
+
cls = _CLIENTS_MAP[n]
|
|
69
|
+
if cls.__base__.__name__ == "API":
|
|
70
|
+
kwargs['api_key'] = config['API keys'].get(n)
|
|
71
|
+
client = cls(config=config, **kwargs)
|
|
72
|
+
try:
|
|
73
|
+
client.get_file_by_hash(hash)
|
|
74
|
+
if hasattr(client, "content") and client.content is not None and len(client.content) > 0:
|
|
75
|
+
logger.debug("found sample !")
|
|
76
|
+
return
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
logger.debug(e)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.exception(e)
|
|
81
|
+
logger.warning(f"could not find the sample with hash {hash}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def download_samples(*hashes, max_workers=5, **kwargs):
|
|
85
|
+
from concurrent.futures import ThreadPoolExecutor as Pool
|
|
86
|
+
from threading import Lock
|
|
87
|
+
kwargs['lock'] = Lock()
|
|
88
|
+
with Pool(max_workers=max_workers) as executor:
|
|
89
|
+
for h in hashes:
|
|
90
|
+
executor.submit(download_sample, h.lower(), **kwargs)
|
malsearch/__main__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _parser(name, description, examples):
|
|
6
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
7
|
+
descr = f"{name} {__version__}\n\nAuthor : {__author__} ({__email__})\nCopyright: {__copyright__}\nLicense :" \
|
|
8
|
+
f" {__license__}\nSource : {__source__}\n\n{description}.\n\n"
|
|
9
|
+
return ArgumentParser(description=descr, formatter_class=RawTextHelpFormatter, add_help=False,
|
|
10
|
+
epilog="usage examples:\n- " + "\n- ".join(examples) if len(examples) > 0 else None)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _setup(parser):
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
if hasattr(args, "verbose"):
|
|
16
|
+
import logging
|
|
17
|
+
logging.basicConfig(level=[logging.INFO, logging.DEBUG][args.verbose])
|
|
18
|
+
if not args.disable_cache:
|
|
19
|
+
from requests_cache import install_cache
|
|
20
|
+
install_cache("malsearch",
|
|
21
|
+
use_cache_dir=True, # Save files in the default user cache dir
|
|
22
|
+
cache_control=True, # Use Cache-Control response headers for expiration, if available
|
|
23
|
+
expire_after=86400, # Otherwise expire responses after one day
|
|
24
|
+
allowable_codes=[404], # Cache 400 responses as a solemn reminder of your failures
|
|
25
|
+
allowable_methods=['GET', 'POST'], # Cache whatever HTTP methods you want
|
|
26
|
+
ignored_parameters=['api_key'], # Don't match this request param, and redact if from the cache
|
|
27
|
+
match_headers=['Accept-Language'], # Cache a different response per language
|
|
28
|
+
stale_if_error=True, # In case of request errors, use stale cache data if possible
|
|
29
|
+
)
|
|
30
|
+
return args
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
from os import makedirs
|
|
35
|
+
from .__init__ import _valid_conf, download_samples
|
|
36
|
+
from .clients.__common__ import _valid_hash
|
|
37
|
+
parser = _parser("MalSearch", "This tool is aimed to search for malware samples across some public databases", [])
|
|
38
|
+
parser.add_argument("sample_hash", type=_valid_hash, nargs="*", help="input hash")
|
|
39
|
+
parser.add_argument("-f", "--from-file", help="get hashes from the target file (newline-separated list)")
|
|
40
|
+
opt = parser.add_argument_group("optional arguments")
|
|
41
|
+
opt.add_argument("-c", "--config", default="~/.malsearch.conf", type=_valid_conf, help="INI configuration file")
|
|
42
|
+
opt.add_argument("-o", "--output-dir", default=".", help="output directory for downloaded samples")
|
|
43
|
+
opt.add_argument("-s", "--skip", nargs="*", help="skip the specified clients while downloading samples")
|
|
44
|
+
opt.add_argument("-u", "--unpacked", action="store_true",
|
|
45
|
+
help="if available and target sample is packed, download unpacked version too")
|
|
46
|
+
opt.add_argument("--disable-cache", action="store_true", help="disable requests cache")
|
|
47
|
+
extra = parser.add_argument_group("extra arguments")
|
|
48
|
+
extra.add_argument("-h", "--help", action="help", help="show this help message and exit")
|
|
49
|
+
extra.add_argument("-v", "--verbose", action="store_true", help="display debug information (default: False)")
|
|
50
|
+
args = _setup(parser)
|
|
51
|
+
if args.from_file is not None:
|
|
52
|
+
with open(args.from_file) as f:
|
|
53
|
+
for h in f.readlines():
|
|
54
|
+
args.sample_hash.append(_valid_hash(h.strip()))
|
|
55
|
+
makedirs(args.output_dir, exist_ok=True)
|
|
56
|
+
if len(args.sample_hash) > 0:
|
|
57
|
+
download_samples(*args.sample_hash, **vars(args))
|
|
58
|
+
else:
|
|
59
|
+
import logging
|
|
60
|
+
logging.getLogger("malsearch").warning("nothing to download")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["API", "Web"]
|
|
6
|
+
|
|
7
|
+
_MIN_BACKOFF = 300
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("malsearch")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _valid_hash(hash, *types):
|
|
13
|
+
import re
|
|
14
|
+
types_map = {'md5': 32, 'sha1': 40, 56: 'sha224', 'sha256': 64, 'sha384': 96, 'sha512': 128}
|
|
15
|
+
pattern = "|".join(f"[0-9a-f]{{{types_map[t]}}}" for t in (types or types_map.keys()))
|
|
16
|
+
if re.match(r"{}".format(f"^{pattern}$"), hash, re.I) is None:
|
|
17
|
+
raise ValueError("hash type not supported")
|
|
18
|
+
return hash.lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hashtype(*types):
|
|
22
|
+
def _wrapper(f):
|
|
23
|
+
from functools import wraps
|
|
24
|
+
@wraps(f)
|
|
25
|
+
def _subwrapper(*args, **kwargs):
|
|
26
|
+
_valid_hash(args[0] if isinstance(args[0], str) else args[1], *types)
|
|
27
|
+
return f(*args, **kwargs)
|
|
28
|
+
return _subwrapper
|
|
29
|
+
return _wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Base:
|
|
33
|
+
def __init__(self, **kwargs):
|
|
34
|
+
self.name = self.__class__.__name__.lower()
|
|
35
|
+
for k, v in kwargs.items():
|
|
36
|
+
setattr(self, f"_{k}", v)
|
|
37
|
+
|
|
38
|
+
def _decode(self, codec):
|
|
39
|
+
try:
|
|
40
|
+
import codext as codecs
|
|
41
|
+
except ImportError:
|
|
42
|
+
import codecs
|
|
43
|
+
try:
|
|
44
|
+
self.content = codecs.decode(self.content, codec)
|
|
45
|
+
except ValueError as e:
|
|
46
|
+
logger.debug(f"cannot decode ({e})")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.exception(e)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def _save(self, filename):
|
|
52
|
+
from os.path import join
|
|
53
|
+
try:
|
|
54
|
+
c = self.content
|
|
55
|
+
with open(join(self._output_dir, filename), 'wb') as f:
|
|
56
|
+
f.write(c)
|
|
57
|
+
except AttributeError:
|
|
58
|
+
logger.debug("cannot save (no content downloaded)")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.exception(e)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def _unzip(self, password=None):
|
|
64
|
+
from io import BytesIO
|
|
65
|
+
try:
|
|
66
|
+
with BytesIO(self.content) as buffer:
|
|
67
|
+
from pyzipper import AESZipFile
|
|
68
|
+
from zipfile import ZipFile
|
|
69
|
+
for ZipClass in [ZipFile, AESZipFile]:
|
|
70
|
+
try:
|
|
71
|
+
with ZipClass(buffer) as zip_file:
|
|
72
|
+
if password:
|
|
73
|
+
zip_file.setpassword(password)
|
|
74
|
+
with zip_file.open(zip_file.namelist()[0]) as file:
|
|
75
|
+
self.content = file.read()
|
|
76
|
+
logger.debug("unzipped with {ZipClass}")
|
|
77
|
+
break
|
|
78
|
+
except NotImplementedError:
|
|
79
|
+
continue
|
|
80
|
+
except AttributeError:
|
|
81
|
+
logger.debug("cannot unzip (no content downloaded)")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.exception(e)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class API(_Base):
|
|
88
|
+
def __request(self, path, **kwargs):
|
|
89
|
+
from requests import exceptions, get, post
|
|
90
|
+
for i in ["headers", "params"]:
|
|
91
|
+
n = getattr(self, f"_api_key_{i.rstrip('s')}", None)
|
|
92
|
+
if n:
|
|
93
|
+
d = kwargs.pop(i, {}) or {}
|
|
94
|
+
d[n] = self._api_key
|
|
95
|
+
kwargs[i] = d
|
|
96
|
+
n = getattr(self, "_auth_method", None)
|
|
97
|
+
if n:
|
|
98
|
+
kwargs['headers'] = kwargs['headers'] or {}
|
|
99
|
+
kwargs['headers']['Authorization'] = f"{self._auth_method} {self._api_key}"
|
|
100
|
+
r = (get if 'params' in kwargs else post)(f"{self.url}/{path}", **kwargs)
|
|
101
|
+
try:
|
|
102
|
+
r.raise_for_status()
|
|
103
|
+
self.content = r.content
|
|
104
|
+
except exceptions.RequestException as e:
|
|
105
|
+
if r.status_code == 403:
|
|
106
|
+
import datetime as dt
|
|
107
|
+
from contextlib import nullcontext
|
|
108
|
+
with kwargs.get('lock') or nullcontext():
|
|
109
|
+
if not self._config.has_section("Disabled"):
|
|
110
|
+
self._config.add_section("Disabled")
|
|
111
|
+
boff = dt.timedelta(seconds=getattr(self, "backoff", _MIN_BACKOFF))
|
|
112
|
+
self._config['Disabled'][self.name] = dt.datetime.strftime(dt.datetime.now() + boff,
|
|
113
|
+
"%d/%m/%Y %H:%M:%S")
|
|
114
|
+
with open(self._config.path, 'w') as f:
|
|
115
|
+
self._config.write(f)
|
|
116
|
+
logger.exception(e)
|
|
117
|
+
try:
|
|
118
|
+
self.json = r.json()
|
|
119
|
+
except:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def _get(self, path, params=None, headers=None):
|
|
123
|
+
""" Perform a GET request.
|
|
124
|
+
|
|
125
|
+
:param path: API endpoint (relative path).
|
|
126
|
+
:param params: Dictionary of URL parameters.
|
|
127
|
+
:param headers: Optional dictionary of request headers.
|
|
128
|
+
:return: JSON data or error message.
|
|
129
|
+
"""
|
|
130
|
+
self.__request(path, params=params, headers=headers)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def _post(self, path, data=None, json=None, headers=None):
|
|
134
|
+
""" Perform a POST request.
|
|
135
|
+
|
|
136
|
+
:param path: API endpoint (relative path).
|
|
137
|
+
:param data: Dictionary or list of tuples for form-encoded data.
|
|
138
|
+
:param json: Dictionary to send JSON data.
|
|
139
|
+
:param headers: Optional dictionary of request headers.
|
|
140
|
+
:return: JSON data or error message.
|
|
141
|
+
"""
|
|
142
|
+
self.__request(path, data=data, json=json, headers=headers)
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Web(_Base):
|
|
147
|
+
def _get(self, path, params=None, headers=None):
|
|
148
|
+
""" Perform a GET request.
|
|
149
|
+
|
|
150
|
+
:param path: API endpoint (relative path).
|
|
151
|
+
:param params: Dictionary of URL parameters.
|
|
152
|
+
:param headers: Optional dictionary of request headers.
|
|
153
|
+
:return: BeautifulSoup-parsed HTML object or None.
|
|
154
|
+
"""
|
|
155
|
+
from bs4 import BeautifulSoup
|
|
156
|
+
from requests import exceptions, get
|
|
157
|
+
try:
|
|
158
|
+
r = get(path, params=params, headers=headers)
|
|
159
|
+
r.raise_for_status()
|
|
160
|
+
return BeautifulSoup(r.content, "html.parser")
|
|
161
|
+
except exceptions.RequestException as e:
|
|
162
|
+
return
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .malpedia import Malpedia
|
|
3
|
+
from .malshare import MalShare
|
|
4
|
+
from .malwarebazaar import MalwareBazaar
|
|
5
|
+
from .triage import Triage
|
|
6
|
+
from .virusshare import VirusShare
|
|
7
|
+
from .virustotal import VirusTotal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ["Malpedia", "MalShare", "MalwareBazaar", "Triage", "VirusShare", "VirusTotal"]
|
|
11
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["Malpedia"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Malpedia(API):
|
|
9
|
+
doc = "https://malpedia.caad.fkie.fraunhofer.de/usage/api"
|
|
10
|
+
url = "https://malpedia.caad.fkie.fraunhofer.de/api"
|
|
11
|
+
_auth_method = "APIToken"
|
|
12
|
+
|
|
13
|
+
@hashtype("md5", "sha256")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
self._get(f"get/sample/{hash}/zip")._save(hash)
|
|
16
|
+
#TODO: parse ZIP for unpacked version
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["MalShare"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MalShare(API):
|
|
9
|
+
doc = "https://malshare.com/doc.php"
|
|
10
|
+
url = "https://malshare.com"
|
|
11
|
+
_api_key_param = "api_key"
|
|
12
|
+
|
|
13
|
+
@hashtype("md5", "sha1", "sha256")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
self._get("api.php", params={'action': "getfile", 'hash': hash})._save(hash)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["MalwareBazaar"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MalwareBazaar(API):
|
|
9
|
+
doc = "https://bazaar.abuse.ch/api"
|
|
10
|
+
url = "https://mb-api.abuse.ch/api/v1"
|
|
11
|
+
_api_key_header = "API-KEY"
|
|
12
|
+
|
|
13
|
+
@hashtype("sha256")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
# daily limit: 2000/IP/day
|
|
16
|
+
self._post("", data={'query': "get_file", f'sha256_hash': hash})._unzip(b"infected")._save(hash)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["Triage"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Triage(API):
|
|
9
|
+
doc = "https://tria.ge/docs"
|
|
10
|
+
url = "https://tria.ge/api/v0"
|
|
11
|
+
_auth_method = "Bearer"
|
|
12
|
+
|
|
13
|
+
@hashtype("md5", "sha1", "sha256", "sha512")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
hashtype = {32: "md5", 40: "sha1", 64: "sha256", 128: "sha512"}[len(hash)]
|
|
16
|
+
self._get("search", params={'query': f"{hashtype}:{hash}"})
|
|
17
|
+
try:
|
|
18
|
+
sample_id = self.json['data'][0]['id']
|
|
19
|
+
self._get(f"samples/{sample_id}/sample")._save(hash)
|
|
20
|
+
except IndexError:
|
|
21
|
+
delattr(self, "content")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["VirusShare"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VirusShare(API):
|
|
9
|
+
doc = "https://virusshare.com/apiv2_reference"
|
|
10
|
+
url = "https://virusshare.com/apiv2"
|
|
11
|
+
_api_key_param = "apikey"
|
|
12
|
+
|
|
13
|
+
@hashtype("md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
self._get("download", params={'hash': hash})._save(hash)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
from .__common__ import hashtype, API
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["VirusTotal"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VirusTotal(API):
|
|
9
|
+
doc = "https://docs.virustotal.com/reference/overview"
|
|
10
|
+
url = "https://www.virustotal.com/api/v3"
|
|
11
|
+
_api_key_header = "X-Apikey"
|
|
12
|
+
|
|
13
|
+
@hashtype("md5", "sha1", "sha256")
|
|
14
|
+
def get_file_by_hash(self, hash):
|
|
15
|
+
if self._unpacked:
|
|
16
|
+
data = self._get(f"/files/{hash}")
|
|
17
|
+
for n in data['data']['attributes']['names']:
|
|
18
|
+
if n.endswith("_unpacked"):
|
|
19
|
+
hash2, _ = n.split("_", 1)
|
|
20
|
+
self._get(f"/files/{hash2}/download")._save(hash2)
|
|
21
|
+
self._get(f"files/{hash}/download")._unzip(b"infected")._save(hash)
|