exeplot 0.4.2__tar.gz → 0.5.2__tar.gz

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.
Files changed (54) hide show
  1. {exeplot-0.4.2 → exeplot-0.5.2}/.coveragerc +1 -0
  2. {exeplot-0.4.2 → exeplot-0.5.2}/.github/workflows/python-package.yml +1 -1
  3. {exeplot-0.4.2 → exeplot-0.5.2}/PKG-INFO +18 -2
  4. {exeplot-0.4.2 → exeplot-0.5.2}/README.md +16 -0
  5. {exeplot-0.4.2 → exeplot-0.5.2}/docs/coverage.svg +1 -1
  6. exeplot-0.5.2/docs/pages/img/calc_packed_byte2.png +0 -0
  7. exeplot-0.5.2/docs/pages/img/calc_packed_entropy.png +0 -0
  8. exeplot-0.5.2/docs/pages/img/upx_calc_byte.png +0 -0
  9. exeplot-0.5.2/docs/pages/img/upx_calc_entropy.png +0 -0
  10. {exeplot-0.4.2 → exeplot-0.5.2}/pyproject.toml +1 -1
  11. exeplot-0.5.2/src/exeplot/VERSION.txt +1 -0
  12. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__conf__.py +3 -3
  13. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__main__.py +13 -4
  14. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/byte.py +32 -30
  15. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/diff.py +7 -6
  16. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/entropy.py +32 -32
  17. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/nested_pie.py +4 -3
  18. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/pie.py +7 -4
  19. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/utils.py +35 -14
  20. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/PKG-INFO +18 -2
  21. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/SOURCES.txt +4 -0
  22. {exeplot-0.4.2 → exeplot-0.5.2}/tests/test_others.py +6 -3
  23. {exeplot-0.4.2 → exeplot-0.5.2}/tests/test_plots.py +15 -0
  24. exeplot-0.4.2/src/exeplot/VERSION.txt +0 -1
  25. {exeplot-0.4.2 → exeplot-0.5.2}/.gitignore +0 -0
  26. {exeplot-0.4.2 → exeplot-0.5.2}/.readthedocs.yml +0 -0
  27. {exeplot-0.4.2 → exeplot-0.5.2}/LICENSE +0 -0
  28. {exeplot-0.4.2 → exeplot-0.5.2}/_config.yml +0 -0
  29. {exeplot-0.4.2 → exeplot-0.5.2}/docs/mkdocs.yml +0 -0
  30. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/css/extra.css +0 -0
  31. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_orig_entropy.png +0 -0
  32. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_byte.png +0 -0
  33. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_nested_pie.png +0 -0
  34. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/calc_packed_pie.png +0 -0
  35. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/icon.png +0 -0
  36. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/img/logo.png +0 -0
  37. {exeplot-0.4.2 → exeplot-0.5.2}/docs/pages/index.md +0 -0
  38. {exeplot-0.4.2 → exeplot-0.5.2}/docs/requirements.txt +0 -0
  39. {exeplot-0.4.2 → exeplot-0.5.2}/pytest.ini +0 -0
  40. {exeplot-0.4.2 → exeplot-0.5.2}/requirements.txt +0 -0
  41. {exeplot-0.4.2 → exeplot-0.5.2}/setup.cfg +0 -0
  42. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__info__.py +0 -0
  43. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/__init__.py +0 -0
  44. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/__common__.py +0 -0
  45. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/__init__.py +0 -0
  46. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot/plots/graph.py +0 -0
  47. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/dependency_links.txt +0 -0
  48. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/entry_points.txt +0 -0
  49. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/requires.txt +0 -0
  50. {exeplot-0.4.2 → exeplot-0.5.2}/src/exeplot.egg-info/top_level.txt +0 -0
  51. {exeplot-0.4.2 → exeplot-0.5.2}/tests/__init__.py +0 -0
  52. {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.elf +0 -0
  53. {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.exe +0 -0
  54. {exeplot-0.4.2 → exeplot-0.5.2}/tests/hello.macho +0 -0
@@ -21,3 +21,4 @@ exclude_lines =
21
21
  if self.type not in ["ELF", "MachO", "PE"]:
22
22
  if j <= i or start2 is None:
23
23
  if s != self.__size:
24
+ glob['_IMP'] = True
@@ -19,7 +19,7 @@ jobs:
19
19
  fail-fast: false
20
20
  matrix:
21
21
  os: [ubuntu-latest]
22
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
22
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
23
23
  steps:
24
24
  - uses: actions/checkout@v3
25
25
  - name: Set up Python ${{ matrix.python-version }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exeplot
3
- Version: 0.4.2
3
+ Version: 0.5.2
4
4
  Summary: Library for plotting executable samples supporting multiple formats
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -689,7 +689,7 @@ Classifier: Intended Audience :: Developers
689
689
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
690
690
  Classifier: Programming Language :: Python :: 3
691
691
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
692
- Requires-Python: <4,>=3.9
692
+ Requires-Python: <4,>=3.10
693
693
  Description-Content-Type: text/markdown
694
694
  License-File: LICENSE
695
695
  Requires-Dist: lief>=0.16.1
@@ -730,6 +730,14 @@ $ exeplot byte calc_packed.exe
730
730
 
731
731
  ![Byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte.png?raw=true)
732
732
 
733
+ Draw a simplified byte plot of `calc_packed.exe`:
734
+
735
+ ```sh
736
+ $ exeplot byte calc_packed.exe --no-title --no-legend
737
+ ```
738
+
739
+ ![Simplified byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte2.png?raw=true)
740
+
733
741
  Draw a pie plot of `calc_packed.exe`:
734
742
 
735
743
  ```sh
@@ -754,4 +762,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
754
762
 
755
763
  ![Entropy plot of `calc_orig.exe` and `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_orig_entropy.png?raw=true)
756
764
 
765
+ Draw a simplified entropy plot of `calc_packed.exe`:
766
+
767
+ ```sh
768
+ $ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
769
+ ```
770
+
771
+ ![Simplified entropy plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_entropy.png?raw=true)
772
+
757
773
 
@@ -26,6 +26,14 @@ $ exeplot byte calc_packed.exe
26
26
 
27
27
  ![Byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte.png?raw=true)
28
28
 
29
+ Draw a simplified byte plot of `calc_packed.exe`:
30
+
31
+ ```sh
32
+ $ exeplot byte calc_packed.exe --no-title --no-legend
33
+ ```
34
+
35
+ ![Simplified byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte2.png?raw=true)
36
+
29
37
  Draw a pie plot of `calc_packed.exe`:
30
38
 
31
39
  ```sh
@@ -50,4 +58,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
50
58
 
51
59
  ![Entropy plot of `calc_orig.exe` and `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_orig_entropy.png?raw=true)
52
60
 
61
+ Draw a simplified entropy plot of `calc_packed.exe`:
62
+
63
+ ```sh
64
+ $ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
65
+ ```
66
+
67
+ ![Simplified entropy plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_entropy.png?raw=true)
68
+
53
69
 
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 96.70%"><title>coverage: 96.70%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.70%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.70%</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 96.54%"><title>coverage: 96.54%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.54%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.54%</text></g></svg>
@@ -22,7 +22,7 @@ authors = [
22
22
  description = "Library for plotting executable samples supporting multiple formats"
23
23
  license = {file = "LICENSE"}
24
24
  keywords = ["python", "development", "programming", "executable-samples", "plot", "entropy", "cfg"]
25
- requires-python = ">=3.9,<4"
25
+ requires-python = ">=3.10,<4"
26
26
  classifiers = [
27
27
  "Development Status :: 5 - Production/Stable",
28
28
  "Environment :: Console",
@@ -0,0 +1 @@
1
+ 0.5.2
@@ -29,7 +29,7 @@ config = {
29
29
  numpy.int = numpy.int_ # dirty fix to "AttributeError: module 'numpy' has no attribute 'int'."
30
30
 
31
31
 
32
- def check_imports(*names):
32
+ def check_imports(*names) -> None:
33
33
  import warnings
34
34
  from inspect import currentframe
35
35
  glob = currentframe().f_back.f_globals
@@ -42,7 +42,7 @@ def check_imports(*names):
42
42
  glob['_IMP'] = False
43
43
 
44
44
 
45
- def configure(): # pragma: no cover
45
+ def configure() -> None: # pragma: no cover
46
46
  from configparser import ConfigParser
47
47
  from os.path import exists, expanduser
48
48
  path = expanduser("~/.exeplot.conf")
@@ -58,7 +58,7 @@ def configure(): # pragma: no cover
58
58
  plt.rcParams['font.family'] = config['font_family']
59
59
 
60
60
 
61
- def configure_fonts(**kw):
61
+ def configure_fonts(**kw) -> dict:
62
62
  import matplotlib
63
63
  matplotlib.rc('font', **{k.split("_")[1]: kw.pop(k, config[k]) for k in ['font_family', 'font_size']})
64
64
  kw['title-font'] = {'fontfamily': kw.pop('title_font_family', config['font_family']),
@@ -1,11 +1,12 @@
1
1
  # -*- coding: UTF-8 -*-
2
+ from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
3
+
2
4
  from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
3
5
  from .__init__ import *
4
6
  from .__init__ import __all__ as _plots
5
7
 
6
8
 
7
- def _parser(name, description, examples):
8
- from argparse import ArgumentParser, RawTextHelpFormatter
9
+ def _parser(name: str, description: str, examples: list[str]) -> ArgumentParser:
9
10
  descr = f"{name} {__version__}\n\nAuthor : {__author__} ({__email__})\nCopyright: {__copyright__}\nLicense :" \
10
11
  f" {__license__}\nSource : {__source__}\n\n{description}.\n\n"
11
12
  examples = [f"exeplot {e}" if not e.startswith("exeplot ") else e for e in examples]
@@ -13,7 +14,7 @@ def _parser(name, description, examples):
13
14
  epilog="usage examples:\n " + "\n ".join(examples) if len(examples) > 0 else None)
14
15
 
15
16
 
16
- def _setup(parser): # pragma: no cover
17
+ def _setup(parser: ArgumentParser) -> Namespace: # pragma: no cover
17
18
  args = parser.parse_args()
18
19
  if hasattr(args, "verbose"):
19
20
  import logging
@@ -22,7 +23,7 @@ def _setup(parser): # pragma: no cover
22
23
  return args
23
24
 
24
25
 
25
- def main():
26
+ def main() -> None: # pragma: no cover
26
27
  from os import makedirs
27
28
  parser = _parser("Exeplot", "This tool allows to plot executable sample(s) in different ways",
28
29
  ["byte binary.exe", "entropy binary1.exe binary2.exe --scale"])
@@ -34,6 +35,14 @@ def main():
34
35
  plot_func = globals()[plot]
35
36
  plot_parser = plot_func.__args__(plots.add_parser(plot, help=plot_func.__doc__.strip(), add_help=False))
36
37
  opt = plot_parser.add_argument_group("options")
38
+ if plot == "diff":
39
+ opt.add_argument("--no-colormap", action="store_true", help="do not display the color map (default: False)")
40
+ if plot == "entropy":
41
+ opt.add_argument("--no-entrypoint", action="store_true",
42
+ help="do not display the entry point (default: False)")
43
+ if plot in ["entropy", "pie"]:
44
+ opt.add_argument("--no-label", action="store_true", help="do not display the labels (default: False)")
45
+ opt.add_argument("--no-legend", action="store_true", help="do not display the legend (default: False)")
37
46
  opt.add_argument("--no-title", action="store_true", help="do not display the title (default: False)")
38
47
  extra = plot_parser.add_argument_group("extra arguments")
39
48
  extra.add_argument("-h", "--help", action="help", help="show this help message and exit")
@@ -53,39 +53,41 @@ def plot(executable, height=600, grayscale=False, **kwargs):
53
53
  pass
54
54
  max_w = sum(max_txt_w) + (n_cols - 1) * txt_spacing
55
55
  # draw a separator
56
- images.append(Image.new(m, (int(.05 * height), height), "white"))
56
+ if not kwargs.get('no_legend', False):
57
+ images.append(Image.new(m, (int(.05 * height), height), "white"))
57
58
  # draw a sections plot aside
58
59
  img = Image.new(m, (s, s), "white")
59
60
  # draw the legend with section names
60
- legend = Image.new(m, (max_w, height), "white")
61
- draw = ImageDraw.Draw(legend)
62
- _xy = lambda n, c: (txt_spacing + sum(max_txt_w[:c]) + len(max_txt_w[:c]) * txt_spacing, \
63
- txt_spacing + (n % n_lab_per_col) * (txt_spacing + txt_h))
64
- _c_func = [_rgb, _gs][grayscale]
65
- for i, name, start, end, color in binary:
66
- if start != end:
67
- x0, y0 = min(max(ceil(((start / factor) % s)) - 1, 0), s - 1), \
68
- min(max(ceil(start / s / factor) - 1, 0), s - 1)
69
- xN, yN = min(max(ceil(((end / factor) % s)) - 1, 0), s - 1), \
70
- min(max(ceil(end / s / factor) - 1, 0), s - 1)
71
- if y0 == yN:
72
- xN = min(max(x0 + 1, xN), s - 1)
73
- c = _c_func(color)
74
- for x in range(x0, s if y0 < yN else xN):
75
- img.putpixel((x, y0), c)
76
- for y in range(y0 + 1, yN):
77
- for x in range(0, s):
78
- img.putpixel((x, y), c)
79
- if yN > y0:
80
- for x in range(0, xN):
81
- img.putpixel((x, yN), c)
82
- # fill the legend with the current section name
83
- if name.startswith("TOTAL"):
84
- color = "black"
85
- draw.text(_xy(i, ceil((i + 1) / n_lab_per_col) - 1), name, fill=_c_func(color), font=font)
86
- images.append(img.resize((int(img.size[0] * sf * .2), height), resample=Image.Resampling.BOX))
87
- images.append(Image.new(m, (int(.03 * height), height), "white")) # draw another separator
88
- images.append(legend)
61
+ if not kwargs.get('no_legend', False):
62
+ legend = Image.new(m, (max_w, height), "white")
63
+ draw = ImageDraw.Draw(legend)
64
+ _xy = lambda n, c: (txt_spacing + sum(max_txt_w[:c]) + len(max_txt_w[:c]) * txt_spacing, \
65
+ txt_spacing + (n % n_lab_per_col) * (txt_spacing + txt_h))
66
+ _c_func = [_rgb, _gs][grayscale]
67
+ for i, name, start, end, color in binary:
68
+ if start != end:
69
+ x0, y0 = min(max(ceil(((start / factor) % s)) - 1, 0), s - 1), \
70
+ min(max(ceil(start / s / factor) - 1, 0), s - 1)
71
+ xN, yN = min(max(ceil(((end / factor) % s)) - 1, 0), s - 1), \
72
+ min(max(ceil(end / s / factor) - 1, 0), s - 1)
73
+ if y0 == yN:
74
+ xN = min(max(x0 + 1, xN), s - 1)
75
+ c = _c_func(color)
76
+ for x in range(x0, s if y0 < yN else xN):
77
+ img.putpixel((x, y0), c)
78
+ for y in range(y0 + 1, yN):
79
+ for x in range(0, s):
80
+ img.putpixel((x, y), c)
81
+ if yN > y0:
82
+ for x in range(0, xN):
83
+ img.putpixel((x, yN), c)
84
+ # fill the legend with the current section name
85
+ if name.startswith("TOTAL"):
86
+ color = "black"
87
+ draw.text(_xy(i, ceil((i + 1) / n_lab_per_col) - 1), name, fill=_c_func(color), font=font)
88
+ images.append(img.resize((int(img.size[0] * sf * .2), height), resample=Image.Resampling.BOX))
89
+ images.append(Image.new(m, (int(.03 * height), height), "white")) # draw another separator
90
+ images.append(legend)
89
91
  # combine images horizontally
90
92
  x, img = 0, Image.new(m, (sum(i.size[0] for i in images), height))
91
93
  for i in images:
@@ -133,13 +133,14 @@ def plot(executable, executable2, legend1="", legend2="", **kwargs):
133
133
  va="center")
134
134
  # ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
135
135
  logger.debug("> configuring the figure")
136
- cb = plt.colorbar(ScalarMappable(cmap=ListedColormap(colors, N=4)),
137
- location='bottom', ax=objs[-1], fraction=0.3, aspect=50, ticks=[0.125, 0.375, 0.625, 0.875])
138
- cb.set_ticklabels(['removed', 'modified', 'untouched', 'added'])
139
- cb.ax.tick_params(length=0)
140
- cb.outline.set_visible(False)
136
+ if not kwargs.get('no_colorbar', False):
137
+ cb = plt.colorbar(ScalarMappable(cmap=ListedColormap(colors, N=4)),
138
+ location='bottom', ax=objs[-1], fraction=0.3, aspect=50, ticks=[0.125, 0.375, 0.625, 0.875])
139
+ cb.set_ticklabels(['removed', 'modified', 'untouched', 'added'])
140
+ cb.ax.tick_params(length=0)
141
+ cb.outline.set_visible(False)
141
142
  plt.subplots_adjust(left=[.15, .02][legend1 == "" and legend2 == ""], bottom=.5/max(1.75, nf))
142
143
  h, l = (objs[int(title)] if nf+int(title) > 1 else objs).get_legend_handles_labels()
143
- if len(h) > 0:
144
+ if len(h) > 0 and not kwargs.get('no_legend', False):
144
145
  plt.figlegend(h, l, loc=[.8, .135], ncol=1, prop={'size': fs_ref*.7})
145
146
 
@@ -58,8 +58,7 @@ def data(executable, n_samples=N_SAMPLES, window_size=lambda s: 2*s, **kwargs):
58
58
  name = binary.section_names[section.name]
59
59
  start = max(data['sections'][-1][1] if len(data['sections']) > 0 else 0, int(section.offset // cs))
60
60
  rawsize = max(section.size, len(section.content)) # take section header's raw size but consider real size too
61
- max_end = min(max(start + MIN_ZONE_WIDTH, int((section.offset + rawsize) // cs)),
62
- len(data['entropy']) - 1)
61
+ max_end = min(max(start + MIN_ZONE_WIDTH, int((section.offset + rawsize) // cs)), len(data['entropy']) - 1)
63
62
  data['sections'].append(__d(int(min(start, max_end - MIN_ZONE_WIDTH)), int(max_end), name))
64
63
  # adjust the entry point (be sure that its position on the plot is within the EP section)
65
64
  if data['ep']:
@@ -121,36 +120,37 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
121
120
  fig.suptitle(f"Entropy per section of {d['type']} file: {target or d['name']}", x=x_t, y=y_t,
122
121
  ha="center", va="bottom", **kwargs['title-font'])
123
122
  # set the label and sublabel and display them
124
- try:
125
- label = labels[i]
126
- if isinstance(label, type(lambda: 0)):
127
- label = label(d)
128
- except:
129
- pass
130
123
  ref_point = .55
131
- if sublabel and not (isinstance(sublabel, str) and "ep" in sublabel and d['ep'] is None):
132
- if isinstance(sublabel, str):
133
- sublabel = SUBLABELS.get(sublabel)
134
- sl = sublabel(d) if isinstance(sublabel, type(lambda: 0)) else None
135
- if sl:
136
- nl, y_pos, f_color = len(sl.split("\n")), ref_point, "black"
137
- if label:
138
- f_size, f_color = fs_ref*.6 if nl <= 2 else fs_ref*.5, "gray"
139
- y_pos = max(0., ref_point - nl * [.16, .12, .09, .08][min(4, nl)-1])
140
- else:
141
- f_size = fs_ref * [.7, .6, .5][min(3, nl)-1]
142
- obj.text(s=sl, x=-420., y=y_pos, fontsize=f_size, color=f_color, ha="left", va="center")
143
- if label:
144
- y_pos = ref_point
145
- if sublabel:
146
- nl = len(sl.split("\n"))
147
- y_pos = min(1.3, ref_point + .3 + nl * [.16, .12, .09, .08][min(4, nl)-1])
148
- obj.text(s=label, x=-420., y=y_pos, fontsize=fs_ref, ha="left", va="center")
149
- h, h_midlen = d['hash'], round(len(d['hash'])/2+.5)
150
- h = f"{h[:h_midlen]}\n{h[h_midlen:]}"
151
- obj.text(s=h, x=-420., y=y_pos-.35, fontsize=fs_ref*.5, ha="left", va="center")
124
+ if not kwargs.get('no_label', False):
125
+ try:
126
+ label = labels[i]
127
+ if isinstance(label, type(lambda: 0)):
128
+ label = label(d)
129
+ except:
130
+ pass
131
+ if sublabel and not (isinstance(sublabel, str) and "ep" in sublabel and d['ep'] is None):
132
+ if isinstance(sublabel, str):
133
+ sublabel = SUBLABELS.get(sublabel)
134
+ sl = sublabel(d) if isinstance(sublabel, type(lambda: 0)) else None
135
+ if sl:
136
+ nl, y_pos, f_color = len(sl.split("\n")), ref_point, "black"
137
+ if label:
138
+ f_size, f_color = fs_ref*.6 if nl <= 2 else fs_ref*.5, "gray"
139
+ y_pos = max(0., ref_point - nl * [.16, .12, .09, .08][min(4, nl)-1])
140
+ else:
141
+ f_size = fs_ref * [.7, .6, .5][min(3, nl)-1]
142
+ obj.text(s=sl, x=-420., y=y_pos, fontsize=f_size, color=f_color, ha="left", va="center")
143
+ if label:
144
+ y_pos = ref_point
145
+ if sublabel:
146
+ nl = len(sl.split("\n"))
147
+ y_pos = min(1.3, ref_point + .3 + nl * [.16, .12, .09, .08][min(4, nl)-1])
148
+ obj.text(s=label, x=-420., y=y_pos, fontsize=fs_ref, ha="left", va="center")
149
+ h, h_midlen = d['hash'], round(len(d['hash'])/2+.5)
150
+ h = f"{h[:h_midlen]}\n{h[h_midlen:]}"
151
+ obj.text(s=h, x=-420., y=y_pos-.35, fontsize=fs_ref*.5, ha="left", va="center")
152
152
  # display the entry point
153
- if d['ep']:
153
+ if d['ep'] and not kwargs.get('no_entrypoint', False):
154
154
  obj.vlines(x=d['ep'][0], ymin=0, ymax=1, color="r", zorder=11).set_label("Entry point")
155
155
  obj.text(d['ep'][0], -.15, "______", c="r", ha="center", rotation=90, size=.8,
156
156
  bbox={'boxstyle': "rarrow", 'fc': "r", 'ec': "r", 'lw': 1})
@@ -166,7 +166,7 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
166
166
  color_cursor += 1
167
167
  # draw the section
168
168
  obj.fill_between(x, 0, 1, facecolor=c, alpha=.2)
169
- if name not in ["Headers", "Overlay"]:
169
+ if name not in ["Headers", "Overlay"] and not kwargs.get('no_label', False):
170
170
  if last is None or (start + end) // 2 - (last[0] + last[1]) // 2 > n // 12:
171
171
  pos_y = N_TOP
172
172
  else:
@@ -197,6 +197,6 @@ def plot(*filenames, labels=None, sublabel=None, scale=False, target=None, **kwa
197
197
  h, l = (objs[[0, 1][title]] if nf+[0, 1][title] > 1 else objs).get_legend_handles_labels()
198
198
  h.append(Patch(facecolor="black")), l.append("Headers")
199
199
  h.append(Patch(facecolor="lightgray")), l.append("Overlay")
200
- if len(h) > 0:
200
+ if len(h) > 0 and not kwargs.get('no_legend', False):
201
201
  plt.figlegend(h, l, loc=lloc, ncol=1 if lloc_side else len(l), prop={'size': fs_ref*.7})
202
202
 
@@ -32,9 +32,10 @@ def plot(executable, **kwargs):
32
32
  ax.pie(w, radius=1-i*size, colors=colors, startangle=90, wedgeprops={'width': size}, **pie_kw)
33
33
  # ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
34
34
  logger.debug("> configuring the figure")
35
- ncols = ceil(len(legend['colors']) / 12)
36
- ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
37
- bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(kwargs['config']['font_size']*.7))
35
+ if not kwargs.get('no_legend', False):
36
+ ncols = ceil(len(legend['colors']) / 12)
37
+ ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
38
+ bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(kwargs['config']['font_size']*.7))
38
39
  if not kwargs.get('no_title', False):
39
40
  fsp = plt.gcf().subplotpars
40
41
  plt.title(f"Nested pie plot of {binary.type} file: {binary.basename}", **kwargs['title-font'])
@@ -23,16 +23,19 @@ def plot(executable, donut=False, **kwargs):
23
23
  for _, _, data, _, colors, legend in binary._data():
24
24
  pass
25
25
  ncols, n = ceil(len(legend['colors']) / 12), sum(data)
26
- labels = [f"{100 * d / n:.1f}%" if round(d / n, 2) >= .02 and c != "white" else "" for d, c in zip(data, colors)]
27
26
  txt_kw = {k: v for k, v in kwargs['xlabel-font'].items()}
28
27
  txt_kw['color'] = "w"
29
28
  pie_kw = {'shadow': SHADOW} if kwargs['config']['shadow'] else {}
30
- _, texts = ax.pie(data, colors=colors, labels=labels, textprops=txt_kw, labeldistance=.55, startangle=90,
29
+ if not kwargs.get('no_label', False):
30
+ pie_kw['labels'] = [f"{100 * d / n:.1f}%" if round(d / n, 2) >= .02 and c != "white" else "" \
31
+ for d, c in zip(data, colors)]
32
+ _, texts = ax.pie(data, colors=colors, textprops=txt_kw, labeldistance=.55, startangle=90,
31
33
  wedgeprops={'width': [1., .55][donut]}, **pie_kw)
32
34
  # ---------------------------------------------- CONFIGURE THE FIGURE ----------------------------------------------
33
35
  logger.debug("> configuring the figure")
34
- ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
35
- bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(fs_ref*.7))
36
+ if not kwargs.get('no_legend', False):
37
+ ax.legend([plt.Rectangle((0, 0), 1, 1, color=c) for c in legend['colors']], legend['texts'], loc="center left",
38
+ bbox_to_anchor=(1, 0, 0.5, 1), ncol=ncols, fontsize=ceil(fs_ref*.7))
36
39
  plt.setp(texts, size=fs_ref*.8, weight="bold")
37
40
  if not kwargs.get('no_title', False):
38
41
  fsp = plt.gcf().subplotpars
@@ -1,5 +1,7 @@
1
1
  # -*- coding: UTF-8 -*-
2
+ import numpy as np
2
3
  from math import log2
4
+ from typing import Optional
3
5
 
4
6
 
5
7
  __all__ = ["ensure_str", "human_readable_size", "ngrams_counts", "ngrams_distribution", "shannon_entropy"]
@@ -7,7 +9,7 @@ __all__ = ["ensure_str", "human_readable_size", "ngrams_counts", "ngrams_distrib
7
9
  shannon_entropy = lambda b: -sum([p*log2(p) for p in [float(ctr)/len(b) for ctr in [b.count(c) for c in set(b)]]]) or 0.
8
10
 
9
11
 
10
- def ensure_str(s, encoding='utf-8', errors='strict'):
12
+ def ensure_str(s: str | bytes, encoding: str = "utf-8", errors: str = "strict") -> str:
11
13
  """ Ensure that an input string is decoded. """
12
14
  if isinstance(s, bytes):
13
15
  try:
@@ -19,7 +21,7 @@ def ensure_str(s, encoding='utf-8', errors='strict'):
19
21
  return s
20
22
 
21
23
 
22
- def human_readable_size(size, precision=0):
24
+ def human_readable_size(size: int, precision: int = 0) -> str:
23
25
  """ Display bytes' size in a human-readable format given a precision. """
24
26
  i, units = 0, ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
25
27
  while size >= 1024 and i < len(units)-1:
@@ -28,27 +30,45 @@ def human_readable_size(size, precision=0):
28
30
  return "%.*f%s" % (precision, size, units[i])
29
31
 
30
32
 
31
- def ngrams_counts(byte_obj, n=1, step=1):
33
+ def ngrams_counts(byte_obj: bytes | object, n: int = 1, step: int = 1) -> dict[bytes, int]:
32
34
  """ Output the Counter instance for an input byte sequence or byte object based on n-grams.
33
35
  If the input is a byte object, cache the result.
34
36
 
35
37
  :param byte_obj: byte sequence ('bytes') or byte object with "bytes" and "size" attributes (i.e. pathlib2.Path)
36
38
  :param n: n determining the size of n-grams, defaults to 1
37
39
  :param step: step for sliding the n-grams
40
+ :param start: number of bytes to start from
38
41
  """
39
- from collections import Counter
40
- if isinstance(byte_obj, (str, bytes)):
41
- return Counter(byte_obj[i:i+n] for i in range(0, len(byte_obj)-n+1, step))
42
- elif hasattr(byte_obj, "bytes") and hasattr(byte_obj, "size"):
43
- if not hasattr(byte_obj, "_ngram_counts_cache"):
44
- byte_obj._ngram_counts_cache = {}
45
- if n not in byte_obj._ngram_counts_cache.keys():
46
- byte_obj._ngram_counts_cache[n] = Counter(byte_obj.bytes[i:i+n] for i in range(0, byte_obj.size-n+1, step))
47
- return byte_obj._ngram_counts_cache[n]
42
+ if n not in (1, 2, 3):
43
+ raise ValueError("n must be 1, 2, or 3")
44
+ if step <= 0:
45
+ raise ValueError("step must be positive")
46
+ if isinstance(byte_obj, bytes) or hasattr(byte_obj, "bytes"):
47
+ a = np.frombuffer(data := byte_obj if isinstance(byte_obj, bytes) else byte_obj.bytes, dtype=np.uint8)
48
+ l = a.size
49
+ if l < n:
50
+ return {}
51
+ if n == 1:
52
+ counts = {b.to_bytes(1, "big"): int(c) for b, c in \
53
+ enumerate(np.bincount(np.frombuffer(data, dtype=np.uint8)))}
54
+ else:
55
+ end = (m := (l - n) // step + 1) * step
56
+ grams = np.stack((a[0:end:step], a[1:1+end:step]), axis=1) if n == 2 else \
57
+ np.stack((a[0:end:step], a[1:1+end:step], a[2:2+end:step]), axis=1)
58
+ counts = {bytes(row): int(c) for row, c in zip(*np.unique(grams, axis=0, return_counts=True))}
59
+ if isinstance(byte_obj, bytes):
60
+ return counts
61
+ elif hasattr(byte_obj, "bytes"):
62
+ if not hasattr(byte_obj, "_ngram_counts_cache"):
63
+ byte_obj._ngram_counts_cache = {}
64
+ if n not in byte_obj._ngram_counts_cache.keys():
65
+ byte_obj._ngram_counts_cache[n] = counts
66
+ return byte_obj._ngram_counts_cache[n]
48
67
  raise TypeError("Bad input type ; should be a byte sequence or object")
49
68
 
50
69
 
51
- def ngrams_distribution(byte_obj, n=1, step=1, n_most_common=None, n_exclude_top=0, exclude=None):
70
+ def ngrams_distribution(byte_obj: bytes | object, n: int = 1, step: int = 1, n_most_common: Optional[int] = None,
71
+ n_exclude_top: int = 0, exclude: Optional[list] = None) -> list[tuple[bytes, int]]:
52
72
  """ Compute the n-grams distribution of an input byte sequence or byte object given exclusions.
53
73
 
54
74
  :param byte_obj: byte sequence ('bytes') or byte object with "bytes" and "size" attributes (i.e. pathlib2.Path)
@@ -60,7 +80,8 @@ def ngrams_distribution(byte_obj, n=1, step=1, n_most_common=None, n_exclude_top
60
80
  :return: list of n_most_common (n-gram, count) pairs
61
81
  """
62
82
  c = ngrams_counts(byte_obj, n, step)
63
- r = c.most_common(len(c) if n_most_common is None else n_most_common + n_exclude_top + len(exclude or []))
83
+ n = len(c) if n_most_common is None else n_most_common + n_exclude_top + len(exclude or [])
84
+ r = sorted(c.items(), key=lambda p: p[1], reverse=True)[:n]
64
85
  if exclude is not None:
65
86
  r = [(ngram, count) for ngram, count in r if ngram not in exclude]
66
87
  return r[n_exclude_top:n_exclude_top+(n_most_common or len(c))]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exeplot
3
- Version: 0.4.2
3
+ Version: 0.5.2
4
4
  Summary: Library for plotting executable samples supporting multiple formats
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -689,7 +689,7 @@ Classifier: Intended Audience :: Developers
689
689
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
690
690
  Classifier: Programming Language :: Python :: 3
691
691
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
692
- Requires-Python: <4,>=3.9
692
+ Requires-Python: <4,>=3.10
693
693
  Description-Content-Type: text/markdown
694
694
  License-File: LICENSE
695
695
  Requires-Dist: lief>=0.16.1
@@ -730,6 +730,14 @@ $ exeplot byte calc_packed.exe
730
730
 
731
731
  ![Byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte.png?raw=true)
732
732
 
733
+ Draw a simplified byte plot of `calc_packed.exe`:
734
+
735
+ ```sh
736
+ $ exeplot byte calc_packed.exe --no-title --no-legend
737
+ ```
738
+
739
+ ![Simplified byte plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_byte2.png?raw=true)
740
+
733
741
  Draw a pie plot of `calc_packed.exe`:
734
742
 
735
743
  ```sh
@@ -754,4 +762,12 @@ $ exeplot entropy calc_orig.exe calc_packed.exe
754
762
 
755
763
  ![Entropy plot of `calc_orig.exe` and `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_orig_entropy.png?raw=true)
756
764
 
765
+ Draw a simplified entropy plot of `calc_packed.exe`:
766
+
767
+ ```sh
768
+ $ exeplot entropy calc_packed.exe --no-title --no-legend --no-label --no-entrypoint
769
+ ```
770
+
771
+ ![Simplified entropy plot of `calc_packed.exe`](https://github.com/packing-box/python-exeplot/blob/main/docs/pages/img/calc_packed_entropy.png?raw=true)
772
+
757
773
 
@@ -15,10 +15,14 @@ docs/pages/index.md
15
15
  docs/pages/css/extra.css
16
16
  docs/pages/img/calc_orig_entropy.png
17
17
  docs/pages/img/calc_packed_byte.png
18
+ docs/pages/img/calc_packed_byte2.png
19
+ docs/pages/img/calc_packed_entropy.png
18
20
  docs/pages/img/calc_packed_nested_pie.png
19
21
  docs/pages/img/calc_packed_pie.png
20
22
  docs/pages/img/icon.png
21
23
  docs/pages/img/logo.png
24
+ docs/pages/img/upx_calc_byte.png
25
+ docs/pages/img/upx_calc_entropy.png
22
26
  src/exeplot/VERSION.txt
23
27
  src/exeplot/__conf__.py
24
28
  src/exeplot/__info__.py
@@ -2,7 +2,6 @@
2
2
  # -*- coding: UTF-8 -*-
3
3
  import matplotlib.pyplot as plt
4
4
  import os
5
- from collections import Counter
6
5
  from exeplot.plots.__common__ import Binary
7
6
  from exeplot.utils import *
8
7
  from unittest import TestCase
@@ -21,10 +20,14 @@ class TestOthers(TestCase):
21
20
  class TestUtils(TestCase):
22
21
  def test_ngrams_functions(self):
23
22
  self.assertRaises(TypeError, ngrams_counts, 123)
24
- self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4), Counter))
23
+ for n in [0, 4]:
24
+ self.assertRaises(ValueError, ngrams_counts, b"abc", n=n)
25
+ self.assertRaises(ValueError, ngrams_counts, b"abc", step=-1)
26
+ self.assertEqual(ngrams_counts(b"a", n=2), {})
27
+ self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4), dict))
28
+ self.assertTrue(isinstance(ngrams_counts(seq := b"\x00" * 4 + os.urandom(120) + b"\xff" * 4, n=2), dict))
25
29
  class Test:
26
30
  bytes = seq
27
- size = len(seq)
28
31
  histogram = ngrams_distribution(t := Test(), exclude=(b"\x00", b"\xff"))
29
32
  self.assertTrue(isinstance(histogram, list))
30
33
  self.assertNotIn(b"\x00", [b for b, c in histogram])
@@ -6,6 +6,7 @@ from exeplot import *
6
6
  from exeplot import __all__ as _plots
7
7
  from exeplot.__conf__ import configure
8
8
  from exeplot.__main__ import _parser
9
+ from itertools import cycle
9
10
  from unittest import TestCase
10
11
 
11
12
 
@@ -69,6 +70,13 @@ class TestPlotOptions(TestCase):
69
70
  scale=True):
70
71
  os.remove(img)
71
72
  plt.clf()
73
+ g = cycle(iter_files())
74
+ for k in ["title", "legend", "label", "entrypoint"]:
75
+ path, kw = next(g), {(k := f'no_{k}'): True}
76
+ print(f"plotting entropy of {path} ({k}=True)...")
77
+ for img in entropy(path, **kw):
78
+ os.remove(img)
79
+ plt.clf()
72
80
 
73
81
  def test_pie_plot_function(self):
74
82
  for path in iter_files():
@@ -76,4 +84,11 @@ class TestPlotOptions(TestCase):
76
84
  for img in pie(path, donut=True):
77
85
  os.remove(img)
78
86
  plt.clf()
87
+ g = cycle(iter_files())
88
+ for k in ["title", "legend", "label"]:
89
+ path, kw = next(g), {(k := f'no_{k}'): True}
90
+ print(f"plotting pie of {path} ({k}=True)...")
91
+ for img in pie(path, **kw):
92
+ os.remove(img)
93
+ plt.clf()
79
94
 
@@ -1 +0,0 @@
1
- 0.4.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes