binstat 1.0.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.
- binstat/VERSION.txt +1 -0
- binstat/__info__.py +19 -0
- binstat/__init__.py +110 -0
- binstat/__main__.py +134 -0
- binstat/features.py +151 -0
- binstat/model.json +1 -0
- binstat/model.py +112 -0
- binstat-1.0.0.dist-info/METADATA +756 -0
- binstat-1.0.0.dist-info/RECORD +13 -0
- binstat-1.0.0.dist-info/WHEEL +5 -0
- binstat-1.0.0.dist-info/entry_points.txt +2 -0
- binstat-1.0.0.dist-info/licenses/LICENSE +674 -0
- binstat-1.0.0.dist-info/top_level.txt +1 -0
binstat/VERSION.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
binstat/__info__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""BinStat package information.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
__y = str(datetime.now().year)
|
|
9
|
+
__s = "2026"
|
|
10
|
+
|
|
11
|
+
__author__ = "Alexandre D'Hondt"
|
|
12
|
+
__copyright__ = "© {} Alexandre D'Hondt".format([__y, __s + "-" + __y][__y != __s])
|
|
13
|
+
__license__ = "GPLv3 (https://www.gnu.org/licenses/gpl-3.0.fr.html)"
|
|
14
|
+
__reference__ = "https://doi.org/10.5769/J201101003"
|
|
15
|
+
__source__ = "https://github.com/dhondta/binstat"
|
|
16
|
+
|
|
17
|
+
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
|
|
18
|
+
__version__ = f.read().strip()
|
|
19
|
+
|
binstat/__init__.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from .features import *
|
|
6
|
+
from .features import __all__ as _features
|
|
7
|
+
from .model import *
|
|
8
|
+
from .model import __all__ as _model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["BinStat", "DEFAULT_MODEL", "THRESHOLD"] + _features + _model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_MODEL = os.path.join(os.path.dirname(__file__), "model.json")
|
|
15
|
+
THRESHOLD = .5
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _files(*paths):
|
|
19
|
+
""" Yield the files from the input paths, walking folders recursively (sorted, for reproducibility). """
|
|
20
|
+
for path in paths:
|
|
21
|
+
path = str(path)
|
|
22
|
+
if os.path.isdir(path):
|
|
23
|
+
for root, dirs, files in os.walk(path):
|
|
24
|
+
dirs.sort()
|
|
25
|
+
for f in sorted(files):
|
|
26
|
+
yield os.path.join(root, f)
|
|
27
|
+
else:
|
|
28
|
+
yield path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BinStat:
|
|
32
|
+
def __init__(self, model=None, threshold=THRESHOLD, logger=None, **kwargs):
|
|
33
|
+
""" Configure the detector with a model (Model instance or path to a JSON model ; None for the default one)
|
|
34
|
+
and the minimum ratio of packed blocks for an executable to be packed. """
|
|
35
|
+
if not 0. <= threshold <= 1.:
|
|
36
|
+
raise ValueError("threshold must be between 0 and 1")
|
|
37
|
+
self.model = model if isinstance(model, Model) else Model.load(model or DEFAULT_MODEL)
|
|
38
|
+
self.threshold = threshold
|
|
39
|
+
self.logger = logger
|
|
40
|
+
|
|
41
|
+
def blocks(self, executable):
|
|
42
|
+
""" Classify each block of the input executable (1 = packed, 0 = not packed). """
|
|
43
|
+
return self.model.predict(file_features(str(executable), **self.model.features_kwargs))
|
|
44
|
+
|
|
45
|
+
def detect(self, executable):
|
|
46
|
+
""" Analyze the input executable file: it is packed when the ratio of its packed blocks reaches the
|
|
47
|
+
threshold. """
|
|
48
|
+
r = self.ratio(executable)
|
|
49
|
+
return r if r == "?" else bool(r >= self.threshold)
|
|
50
|
+
|
|
51
|
+
def ratio(self, executable):
|
|
52
|
+
""" Compute the ratio of packed blocks of the input executable. """
|
|
53
|
+
try:
|
|
54
|
+
blocks = self.blocks(executable)
|
|
55
|
+
except OSError as e:
|
|
56
|
+
self._log(f"{executable}: {e.strerror or e}", OSError)
|
|
57
|
+
return "?"
|
|
58
|
+
if len(blocks) == 0:
|
|
59
|
+
self._log(f"{executable} has no block to analyze", ValueError)
|
|
60
|
+
return "?"
|
|
61
|
+
r = float(blocks.mean())
|
|
62
|
+
if self.logger:
|
|
63
|
+
self.logger.debug(f"{blocks.sum()}/{len(blocks)} blocks recognized as packed ({r:.2%}, threshold: "
|
|
64
|
+
f"{self.threshold:.2%})")
|
|
65
|
+
return r
|
|
66
|
+
|
|
67
|
+
def _log(self, msg, exception, level="error"):
|
|
68
|
+
""" Helper for either logging a message if a logger is defined or raising an exception. """
|
|
69
|
+
if self.logger:
|
|
70
|
+
getattr(self.logger, level)(msg)
|
|
71
|
+
else:
|
|
72
|
+
raise exception(msg)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def train(cls, packed, not_packed, ngram=1, block_size=BLOCK_SIZE, minkowski_order=MINKOWSKI_ORDER, boost=30,
|
|
76
|
+
balanced=False, max_blocks=None, max_leaf_nodes=None, min_samples_leaf=1, seed=None, threshold=THRESHOLD,
|
|
77
|
+
logger=None, **kwargs):
|
|
78
|
+
""" Train a detector on packed and not packed executables, labelling all the blocks of an executable with its
|
|
79
|
+
own label.
|
|
80
|
+
|
|
81
|
+
:param packed: list of packed executables or folders holding them
|
|
82
|
+
:param not_packed: list of not packed executables or folders holding them
|
|
83
|
+
:param balanced: weight the blocks so that both classes weigh the same (packed blocks usually outnumber
|
|
84
|
+
the others)
|
|
85
|
+
:param max_blocks: maximum number of blocks taken (at random) from each executable (None for all)
|
|
86
|
+
:return: BinStat instance (its model can be saved with .model.save(path))
|
|
87
|
+
"""
|
|
88
|
+
fkw = {'ngram': ngram, 'block_size': block_size, 'minkowski_order': minkowski_order}
|
|
89
|
+
rng, X, y = np.random.default_rng(seed), [], []
|
|
90
|
+
for label, paths in [(1, packed), (0, not_packed)]:
|
|
91
|
+
for path in _files(*paths):
|
|
92
|
+
try:
|
|
93
|
+
F = file_features(path, **fkw)
|
|
94
|
+
except OSError as e:
|
|
95
|
+
if logger:
|
|
96
|
+
logger.warning(f"{path}: {e.strerror or e}")
|
|
97
|
+
continue
|
|
98
|
+
if max_blocks is not None and len(F) > max_blocks:
|
|
99
|
+
F = F[np.sort(rng.choice(len(F), max_blocks, replace=False))]
|
|
100
|
+
X.append(F)
|
|
101
|
+
y.append(np.full(len(F), label))
|
|
102
|
+
if logger:
|
|
103
|
+
logger.debug(f"{path}: {len(F)} blocks ({['not packed', 'packed'][label]})")
|
|
104
|
+
X, y = np.vstack(X or [np.zeros((0, len(FEATURES)))]), np.concatenate(y or [np.zeros(0, dtype="int64")])
|
|
105
|
+
if logger:
|
|
106
|
+
logger.info(f"training on {len(y)} blocks ({int(y.sum())} packed, {int(len(y) - y.sum())} not packed)")
|
|
107
|
+
w = (len(y) / (2 * np.bincount(y, minlength=2)))[y] if balanced and len(set(y)) == 2 else None
|
|
108
|
+
m = train(X, y, boost=boost, max_leaf_nodes=max_leaf_nodes, min_samples_leaf=min_samples_leaf, seed=seed,
|
|
109
|
+
sample_weight=w, balanced=balanced, **fkw)
|
|
110
|
+
return cls(m, threshold, logger)
|
binstat/__main__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import hashlib
|
|
3
|
+
import logging
|
|
4
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
5
|
+
from ast import literal_eval
|
|
6
|
+
from os.path import exists
|
|
7
|
+
from time import perf_counter
|
|
8
|
+
|
|
9
|
+
from .__info__ import __author__, __copyright__, __license__, __reference__, __source__, __version__
|
|
10
|
+
from .__init__ import BinStat, BLOCK_SIZE, MINKOWSKI_ORDER, NGRAMS, THRESHOLD, file_features
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def valid_file(path):
|
|
14
|
+
if not exists(path):
|
|
15
|
+
raise ValueError("input file does not exist")
|
|
16
|
+
return path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Positive:
|
|
20
|
+
def __init__(self, *types, maximum=None, zero=True):
|
|
21
|
+
self._types, self._max, self._zero = types, maximum, zero
|
|
22
|
+
|
|
23
|
+
def __call__(self, string):
|
|
24
|
+
try:
|
|
25
|
+
n = literal_eval(string)
|
|
26
|
+
except (SyntaxError, ValueError):
|
|
27
|
+
raise ValueError(string)
|
|
28
|
+
if not isinstance(n, self._types) or n < 0. or not self._zero and n == 0 or \
|
|
29
|
+
self._max is not None and n > self._max:
|
|
30
|
+
raise ValueError(string)
|
|
31
|
+
return self._types[0](n)
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return "positive %s" % "|".join(map(lambda x: x.__name__, self._types))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _features_args(parser):
|
|
38
|
+
parser.add_argument("--block-size", type=Positive(int, zero=False), default=BLOCK_SIZE,
|
|
39
|
+
help="size of the blocks, in bytes (default: %d)" % BLOCK_SIZE)
|
|
40
|
+
parser.add_argument("--minkowski-order", type=Positive(float, int, zero=False), default=MINKOWSKI_ORDER,
|
|
41
|
+
help="order (lambda) of Minkowski's distance (default: %d)" % MINKOWSKI_ORDER)
|
|
42
|
+
parser.add_argument("--ngram", type=int, choices=NGRAMS, default=1, help="n-gram size (default: 1)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _detect(args):
|
|
46
|
+
r = BinStat(args.model, args.threshold, args.logger)
|
|
47
|
+
return r.ratio(args.path) if args.ratio else r.detect(args.path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _features(args):
|
|
51
|
+
with open(args.path, "rb") as f:
|
|
52
|
+
md5 = hashlib.md5(f.read()).hexdigest()
|
|
53
|
+
kw = {k: getattr(args, k) for k in ["block_size", "minkowski_order", "ngram"]}
|
|
54
|
+
# same format as the C5.0 data file of the paper: the measures, the label, the block ID and the MD5 of the file
|
|
55
|
+
return "\n".join(",".join(["%f" % v for v in row] + [args.label, f"BLOCK{i}", md5])
|
|
56
|
+
for i, row in enumerate(file_features(args.path, **kw)))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _train(args):
|
|
60
|
+
kw = {k: getattr(args, k) for k in ["balanced", "block_size", "boost", "max_blocks", "max_leaf_nodes",
|
|
61
|
+
"minkowski_order", "ngram", "seed"]}
|
|
62
|
+
BinStat.train(args.packed, args.not_packed, logger=args.logger, **kw).model.save(args.output)
|
|
63
|
+
args.logger.info(f"model saved to {args.output}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
""" Tool's main function """
|
|
68
|
+
descr = "BinStat {}\n\nAuthor : {}\nCopyright: {}\nLicense : {}\nReference: {}\nSource : {}\n" \
|
|
69
|
+
"\nThis tool computes statistics on the n-grams of each block of an executable and classifies the blocks" \
|
|
70
|
+
" with boosted decision trees to determine if the executable is packed or not.\n\n"
|
|
71
|
+
descr = descr.format(__version__, __author__, __copyright__, __license__, __reference__, __source__)
|
|
72
|
+
examples = "usage examples:\n- " + "\n- ".join([
|
|
73
|
+
"binstat detect program.exe",
|
|
74
|
+
"binstat detect program.exe --threshold 0.6 --ratio",
|
|
75
|
+
"binstat features program.exe --label yes",
|
|
76
|
+
"binstat train -p packed/ -n not-packed/ -o model.json --max-blocks 100",
|
|
77
|
+
])
|
|
78
|
+
parser = ArgumentParser(description=descr, epilog=examples, formatter_class=RawTextHelpFormatter)
|
|
79
|
+
# extra arguments are accepted after each command
|
|
80
|
+
common = ArgumentParser(add_help=False)
|
|
81
|
+
extra = common.add_argument_group("extra arguments")
|
|
82
|
+
extra.add_argument("-b", "--benchmark", action="store_true",
|
|
83
|
+
help="enable benchmarking, output in seconds (default: False)")
|
|
84
|
+
extra.add_argument("-v", "--verbose", action="store_true", help="display debug information (default: False)")
|
|
85
|
+
sparsers = parser.add_subparsers(dest="command", required=True, help="command to be executed")
|
|
86
|
+
cmd = lambda name, help: sparsers.add_parser(name, help=help, description=help, parents=[common])
|
|
87
|
+
detect = cmd("detect", "detect whether an executable is packed")
|
|
88
|
+
detect.add_argument("path", type=valid_file, help="path to the executable file")
|
|
89
|
+
detect.add_argument("-m", "--model", help="path to a JSON model (default: the model shipped with BinStat)")
|
|
90
|
+
detect.add_argument("-r", "--ratio", action="store_true",
|
|
91
|
+
help="output the ratio of packed blocks instead of the decision")
|
|
92
|
+
detect.add_argument("-t", "--threshold", type=Positive(float, int, maximum=1), default=THRESHOLD,
|
|
93
|
+
help="minimum ratio of packed blocks for the executable to be packed (default: %.1f)" %
|
|
94
|
+
THRESHOLD)
|
|
95
|
+
features = cmd("features", "compute the measures of each block (in C5.0 data format)")
|
|
96
|
+
features.add_argument("path", type=valid_file, help="path to the executable file")
|
|
97
|
+
_features_args(features)
|
|
98
|
+
features.add_argument("--label", choices=["yes", "no", "?"], default="?",
|
|
99
|
+
help="label of the blocks, 'yes' meaning packed (default: ?, i.e. unknown)")
|
|
100
|
+
train = cmd("train", "train a model on packed and not packed executables")
|
|
101
|
+
train.add_argument("-n", "--not-packed", action="extend", nargs="+", required=True,
|
|
102
|
+
help="not packed executables or folders holding them")
|
|
103
|
+
train.add_argument("-o", "--output", required=True, help="path to the output JSON model")
|
|
104
|
+
train.add_argument("-p", "--packed", action="extend", nargs="+", required=True,
|
|
105
|
+
help="packed executables or folders holding them")
|
|
106
|
+
_features_args(train)
|
|
107
|
+
train.add_argument("--balanced", action="store_true",
|
|
108
|
+
help="weight the blocks so that packed and not packed blocks weigh the same (default: False)")
|
|
109
|
+
train.add_argument("--boost", type=Positive(int, zero=False), default=30,
|
|
110
|
+
help="number of boosting trials (default: 30)")
|
|
111
|
+
train.add_argument("--max-blocks", type=Positive(int, zero=False),
|
|
112
|
+
help="maximum number of blocks taken at random from each executable (default: all)")
|
|
113
|
+
train.add_argument("--max-leaf-nodes", type=Positive(int, zero=False),
|
|
114
|
+
help="maximum number of leaves of each tree (default: unlimited)")
|
|
115
|
+
train.add_argument("--seed", type=int, help="random seed, for reproducible models")
|
|
116
|
+
args = parser.parse_args()
|
|
117
|
+
logging.basicConfig()
|
|
118
|
+
args.logger = logging.getLogger("binstat")
|
|
119
|
+
args.logger.setLevel([logging.INFO, logging.DEBUG][args.verbose])
|
|
120
|
+
code = 0
|
|
121
|
+
# execute the tool
|
|
122
|
+
if args.benchmark:
|
|
123
|
+
t1 = perf_counter()
|
|
124
|
+
try:
|
|
125
|
+
r = {'detect': _detect, 'features': _features, 'train': _train}[args.command](args)
|
|
126
|
+
dt = str(perf_counter() - t1) if args.benchmark else ""
|
|
127
|
+
if r is not None:
|
|
128
|
+
print(str(r))
|
|
129
|
+
if dt != "":
|
|
130
|
+
print(dt)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
args.logger.exception(e)
|
|
133
|
+
code = 1
|
|
134
|
+
return code
|
binstat/features.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Statistical module of BinStat: the thirteen block measures of Park et al.
|
|
3
|
+
|
|
4
|
+
Each block of the input is described by the frequencies X_f of its n-gram values f (0 <= f < M, with M = 256**n). Except
|
|
5
|
+
Simpson's index and the entropy, the measures compare the frequencies of consecutive n-gram values, X_f and X_{f+1},
|
|
6
|
+
for 0 <= f < M - 1. As a block holds at most 1024 distinct n-grams, only the non-zero frequencies are computed, so that
|
|
7
|
+
bigrams to four-grams cost the same as unigrams.
|
|
8
|
+
|
|
9
|
+
The divergences (Kullback-Leibler, Jensen-Shannon and Itakura-Saito) use decimal logarithms: with these, the measures
|
|
10
|
+
match the sample line given in the paper for a UPX-packed block, while the entropy is in bits as stated.
|
|
11
|
+
"""
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["BLOCK_SIZE", "FEATURES", "MINKOWSKI_ORDER", "NGRAMS", "block_features", "file_features"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BLOCK_SIZE = 1024
|
|
19
|
+
FEATURES = ["simpson", "canberra", "minkowski", "manhattan", "chebyshev", "bray_curtis", "angular_separation",
|
|
20
|
+
"correlation", "entropy", "kullback_leibler", "jensen_shannon", "itakura_saito", "total_variation"]
|
|
21
|
+
MINKOWSKI_ORDER = 3
|
|
22
|
+
NGRAMS = (1, 2, 3, 4)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _div(a, b):
|
|
26
|
+
""" Element-wise a / b, with 0 where b is 0. """
|
|
27
|
+
out = np.zeros_like(a, dtype="float64")
|
|
28
|
+
np.divide(a, b, out=out, where=b != 0)
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _histograms(data, ngram, block_size):
|
|
33
|
+
""" Count the n-grams of each block.
|
|
34
|
+
|
|
35
|
+
N-grams do not cross block boundaries ; the last block may be shorter than block_size, and is ignored when it is
|
|
36
|
+
shorter than the n-gram.
|
|
37
|
+
|
|
38
|
+
:return: (number of n-grams per block, sorted keys block * M + value of the n-grams, their counts)
|
|
39
|
+
"""
|
|
40
|
+
a = np.frombuffer(data, dtype="uint8")
|
|
41
|
+
n_blocks = (len(a) - ngram) // block_size + 1 if len(a) >= ngram else 0
|
|
42
|
+
if n_blocks == 0:
|
|
43
|
+
return np.zeros(0, dtype="int64"), np.zeros(0, dtype="int64"), np.zeros(0, dtype="int64")
|
|
44
|
+
# value of the n-gram starting at each position (big-endian, so that f and f+1 differ in their last byte)
|
|
45
|
+
start = np.arange(len(a) - ngram + 1, dtype="int64")
|
|
46
|
+
values = np.zeros(len(start), dtype="int64")
|
|
47
|
+
for j in range(ngram):
|
|
48
|
+
values = (values << 8) | a[j:len(a) - ngram + 1 + j]
|
|
49
|
+
# keep the n-grams lying entirely inside their block
|
|
50
|
+
keep = start % block_size <= block_size - ngram
|
|
51
|
+
block = start[keep] // block_size
|
|
52
|
+
counts_per_block = np.bincount(block, minlength=n_blocks)
|
|
53
|
+
if ngram == 1:
|
|
54
|
+
# fast path: a dense histogram of 256 values per block
|
|
55
|
+
dense = np.bincount(block * 256 + values[keep], minlength=n_blocks * 256)
|
|
56
|
+
keys = np.flatnonzero(dense)
|
|
57
|
+
return counts_per_block, keys, dense[keys]
|
|
58
|
+
keys, counts = np.unique(block * 256 ** ngram + values[keep], return_counts=True)
|
|
59
|
+
return counts_per_block, keys, counts
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _measures(n_per_block, keys, counts, ngram, minkowski_order=MINKOWSKI_ORDER):
|
|
63
|
+
""" Compute the thirteen measures of each block from its sparse n-gram histogram. """
|
|
64
|
+
m, nb = 256 ** ngram, len(n_per_block)
|
|
65
|
+
bsum = lambda block, w: np.bincount(block, weights=w, minlength=nb)
|
|
66
|
+
N = n_per_block.astype("float64")
|
|
67
|
+
blk, val = keys // m, keys % m
|
|
68
|
+
x = counts / N[blk]
|
|
69
|
+
# pairs (X_f, X_{f+1}) where at least one of both is non-zero ; all the other pairs are (0, 0)
|
|
70
|
+
f = np.union1d(keys[val <= m - 2], keys[val >= 1] - 1)
|
|
71
|
+
pb = f // m
|
|
72
|
+
i = np.searchsorted(keys, f)
|
|
73
|
+
i[i == len(keys)] = 0
|
|
74
|
+
xa = np.where(keys[i] == f, x[i], 0.)
|
|
75
|
+
j = np.searchsorted(keys, f + 1)
|
|
76
|
+
j[j == len(keys)] = 0
|
|
77
|
+
xb = np.where(keys[j] == f + 1, x[j], 0.)
|
|
78
|
+
d = np.abs(xa - xb)
|
|
79
|
+
both = (xa > 0) & (xb > 0)
|
|
80
|
+
r = np.where(both, xa / np.where(both, xb, 1.), 1.)
|
|
81
|
+
mix = (xa + xb) / 2
|
|
82
|
+
# correlation coefficient, with the mean frequency 1/M over the M values ; each (0, 0) pair contributes 1/M^2
|
|
83
|
+
# to each sum
|
|
84
|
+
mean, zero_pairs = 1. / m, (m - 1) - bsum(pb, None)
|
|
85
|
+
cov = bsum(pb, (xa - mean) * (xb - mean)) + zero_pairs * mean ** 2
|
|
86
|
+
va = bsum(pb, (xa - mean) ** 2) + zero_pairs * mean ** 2
|
|
87
|
+
vb = bsum(pb, (xb - mean) ** 2) + zero_pairs * mean ** 2
|
|
88
|
+
chebyshev = np.zeros(nb)
|
|
89
|
+
np.maximum.at(chebyshev, pb, d)
|
|
90
|
+
manhattan = bsum(pb, d)
|
|
91
|
+
return np.column_stack([
|
|
92
|
+
# 1) Simpson's index
|
|
93
|
+
_div(bsum(blk, counts * (counts - 1.)), N * (N - 1)),
|
|
94
|
+
# 2) Canberra's distance (pairs (0, 0) are skipped)
|
|
95
|
+
bsum(pb, _div(d, xa + xb)),
|
|
96
|
+
# 3) Minkowski's distance of order lambda
|
|
97
|
+
bsum(pb, d ** minkowski_order) ** (1. / minkowski_order),
|
|
98
|
+
# 4) Manhattan's distance
|
|
99
|
+
manhattan,
|
|
100
|
+
# 5) Chebyshev's distance
|
|
101
|
+
chebyshev,
|
|
102
|
+
# 6) Bray-Curtis' distance
|
|
103
|
+
_div(manhattan, bsum(pb, xa + xb)),
|
|
104
|
+
# 7) Angular separation
|
|
105
|
+
_div(bsum(pb, xa * xb), np.sqrt(bsum(pb, xa ** 2) * bsum(pb, xb ** 2))),
|
|
106
|
+
# 8) Correlation coefficient
|
|
107
|
+
_div(cov, np.sqrt(va * vb)),
|
|
108
|
+
# 9) Entropy (bits)
|
|
109
|
+
-bsum(blk, x * np.log2(x)),
|
|
110
|
+
# 10) Kullback-Leibler's divergence (pairs where X_f or X_{f+1} is zero are skipped)
|
|
111
|
+
bsum(pb, np.where(both, xa * np.log10(r), 0.)),
|
|
112
|
+
# 11) Jensen-Shannon's divergence
|
|
113
|
+
bsum(pb, (_xlogy(xa, mix) + _xlogy(xb, mix)) / 2),
|
|
114
|
+
# 12) Itakura-Saito's divergence (pairs where X_f or X_{f+1} is zero are skipped)
|
|
115
|
+
bsum(pb, np.where(both, r - np.log10(r) - 1, 0.)),
|
|
116
|
+
# 13) Total variation
|
|
117
|
+
manhattan / 2,
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _xlogy(x, y):
|
|
122
|
+
""" x * log10(x / y), with 0 where x is 0. """
|
|
123
|
+
out = np.zeros_like(x)
|
|
124
|
+
nz = x > 0
|
|
125
|
+
out[nz] = x[nz] * np.log10(x[nz] / y[nz])
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def block_features(data, ngram=1, block_size=BLOCK_SIZE, minkowski_order=MINKOWSKI_ORDER):
|
|
130
|
+
""" Compute the thirteen measures of each block of the input bytes.
|
|
131
|
+
|
|
132
|
+
:param data: bytes to analyze
|
|
133
|
+
:param ngram: n-gram size, from 1 (unigrams) to 4 (four-grams)
|
|
134
|
+
:param block_size: size of the blocks, in bytes
|
|
135
|
+
:param minkowski_order: order (lambda) of Minkowski's distance
|
|
136
|
+
:return: array of shape (number of blocks, 13), with columns in the order of FEATURES
|
|
137
|
+
"""
|
|
138
|
+
if ngram not in NGRAMS:
|
|
139
|
+
raise ValueError(f"n-gram size must be one of {', '.join(map(str, NGRAMS))}")
|
|
140
|
+
if block_size < ngram:
|
|
141
|
+
raise ValueError("block size must be at least the n-gram size")
|
|
142
|
+
n_per_block, keys, counts = _histograms(bytes(data), ngram, block_size)
|
|
143
|
+
if len(n_per_block) == 0:
|
|
144
|
+
return np.zeros((0, len(FEATURES)))
|
|
145
|
+
return _measures(n_per_block, keys, counts, ngram, minkowski_order)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def file_features(path, **kwargs):
|
|
149
|
+
""" Compute the thirteen measures of each block of a file (see block_features). """
|
|
150
|
+
with open(path, "rb") as f:
|
|
151
|
+
return block_features(f.read(), **kwargs)
|