jame 1.0.dev0__tar.gz → 1.0.dev3__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.
- {jame-1.0.dev0 → jame-1.0.dev3}/PKG-INFO +40 -26
- {jame-1.0.dev0 → jame-1.0.dev3}/README.md +37 -25
- jame-1.0.dev3/jame/cli/jame_cli.py +210 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/PKG-INFO +40 -26
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/requires.txt +2 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/pyproject.toml +3 -1
- jame-1.0.dev3/tests/test_cli.py +116 -0
- jame-1.0.dev0/jame/cli/jame_cli.py +0 -151
- jame-1.0.dev0/tests/test_cli.py +0 -57
- {jame-1.0.dev0 → jame-1.0.dev3}/LICENSE +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/__init__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/__main__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/cli/__init__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/core/__init__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/core/filetype.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/core/jame.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/core/patterns.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/core/utils.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/__init__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/archive.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/binary.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/crypto.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/forensics.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/modules/stego.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/__init__.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/binary_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/crypto_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/forensics_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/scan_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/solve_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame/results/stego_result.py +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/SOURCES.txt +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/dependency_links.txt +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/entry_points.txt +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/jame.egg-info/top_level.txt +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/setup.cfg +0 -0
- {jame-1.0.dev0 → jame-1.0.dev3}/tests/test_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jame
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.dev3
|
|
4
4
|
Summary: Just Another Method of Exploitation
|
|
5
5
|
Author: JAMe Contributors
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,8 @@ Classifier: Development Status :: 4 - Beta
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: c2e
|
|
23
|
+
Requires-Dist: mapres
|
|
22
24
|
Requires-Dist: PyYAML>=6.0
|
|
23
25
|
Dynamic: license-file
|
|
24
26
|
|
|
@@ -40,31 +42,36 @@ JAMe is a lightweight forensic and triage toolkit for scanning files, extracting
|
|
|
40
42
|
|
|
41
43
|
## Installation
|
|
42
44
|
|
|
43
|
-
JAMe requires Python 3.10 or newer.
|
|
44
|
-
|
|
45
|
-
into the absolute path `$HOME/.local/bin`, and adds that directory to the
|
|
46
|
-
current shell and future Bash login/interactive shells:
|
|
45
|
+
JAMe requires Python 3.10 or newer. Install the latest published package and
|
|
46
|
+
register `jame` on your user PATH with:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
49
|
+
curl -fsSL https://raw.githubusercontent.com/HungerNet/JAMe/main/install.sh | bash
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
The installer creates a dedicated environment under `$HOME/.local/share/jame`,
|
|
53
|
+
installs the MapRes pre-release API, `jame`, and C2E, and links the command to
|
|
54
|
+
`$HOME/.local/bin/jame`. It adds that directory to Bash or Zsh startup files;
|
|
55
|
+
open a new shell if it was not already on your current PATH.
|
|
56
|
+
|
|
57
|
+
Set `JAME_PYTHON` to select a specific Python 3.10+ executable. Set
|
|
58
|
+
`JAME_INSTALL_DIR` or `JAME_BIN_DIR` to change the absolute install or bin path.
|
|
59
|
+
|
|
52
60
|
## CLI
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
virtual environment, the checkout's `.venv`, or `python3.10`, in that order:
|
|
62
|
+
After installation, run the `jame` command from any directory:
|
|
56
63
|
|
|
57
64
|
```bash
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
jame --help
|
|
66
|
+
jame tests/files/solution.txt
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
The default command finds a flag and extracts supported archives. The
|
|
63
|
-
|
|
69
|
+
The default command finds a flag and extracts supported archives. The explicit
|
|
70
|
+
`solve` form is also available:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
jame tests/files/solution.txt
|
|
74
|
+
jame solve tests/files/solution.txt --format 'flag{*}'
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
Use `*` for any-length text and `*N` for exactly `N` characters. Repeating
|
|
@@ -74,21 +81,28 @@ to require five characters, not `hcr{*****}`.
|
|
|
74
81
|
Other available methods:
|
|
75
82
|
|
|
76
83
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
jame update
|
|
85
|
+
jame update --pre
|
|
86
|
+
jame find-flag tests/files/challenge.txt --format 'hcr{*4}'
|
|
87
|
+
jame scan tests/files/scan.txt
|
|
88
|
+
jame crypto tests/files/strings.txt --verbose
|
|
89
|
+
jame binary tests/files/strings.txt
|
|
90
|
+
jame stego tests/files/strings.txt
|
|
91
|
+
jame forensics tests/files/strings.txt
|
|
92
|
+
jame archive path/to/archive.zip --max-depth 2
|
|
93
|
+
jame file-type tests/files/strings.txt
|
|
94
|
+
jame strings tests/files/strings.txt
|
|
95
|
+
jame binwalk path/to/file.bin
|
|
96
|
+
jame caesar 'Khoor'
|
|
97
|
+
jame caesar 'Khoor' --verbose
|
|
98
|
+
jame caesar 'Khoor' --shift 3
|
|
88
99
|
```
|
|
89
100
|
|
|
90
101
|
`--verbose` prints the structured result, including analyzer details and raw
|
|
91
|
-
output where available.
|
|
102
|
+
output where available. Caesar brute force prints only its highest-ranked
|
|
103
|
+
decoded candidate by default; `--verbose` shows the ranked candidates.
|
|
104
|
+
`jame update` upgrades JAMe and C2E; `jame update --pre` also upgrades MapRes
|
|
105
|
+
using prerelease versions. `binwalk` requires the external `binwalk` utility.
|
|
92
106
|
|
|
93
107
|
## Quickstart
|
|
94
108
|
|
|
@@ -16,31 +16,36 @@ JAMe is a lightweight forensic and triage toolkit for scanning files, extracting
|
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
|
-
JAMe requires Python 3.10 or newer.
|
|
20
|
-
|
|
21
|
-
into the absolute path `$HOME/.local/bin`, and adds that directory to the
|
|
22
|
-
current shell and future Bash login/interactive shells:
|
|
19
|
+
JAMe requires Python 3.10 or newer. Install the latest published package and
|
|
20
|
+
register `jame` on your user PATH with:
|
|
23
21
|
|
|
24
22
|
```bash
|
|
25
|
-
|
|
23
|
+
curl -fsSL https://raw.githubusercontent.com/HungerNet/JAMe/main/install.sh | bash
|
|
26
24
|
```
|
|
27
25
|
|
|
26
|
+
The installer creates a dedicated environment under `$HOME/.local/share/jame`,
|
|
27
|
+
installs the MapRes pre-release API, `jame`, and C2E, and links the command to
|
|
28
|
+
`$HOME/.local/bin/jame`. It adds that directory to Bash or Zsh startup files;
|
|
29
|
+
open a new shell if it was not already on your current PATH.
|
|
30
|
+
|
|
31
|
+
Set `JAME_PYTHON` to select a specific Python 3.10+ executable. Set
|
|
32
|
+
`JAME_INSTALL_DIR` or `JAME_BIN_DIR` to change the absolute install or bin path.
|
|
33
|
+
|
|
28
34
|
## CLI
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
virtual environment, the checkout's `.venv`, or `python3.10`, in that order:
|
|
36
|
+
After installation, run the `jame` command from any directory:
|
|
32
37
|
|
|
33
38
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
jame --help
|
|
40
|
+
jame tests/files/solution.txt
|
|
36
41
|
```
|
|
37
42
|
|
|
38
|
-
The default command finds a flag and extracts supported archives. The
|
|
39
|
-
|
|
43
|
+
The default command finds a flag and extracts supported archives. The explicit
|
|
44
|
+
`solve` form is also available:
|
|
40
45
|
|
|
41
46
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
jame tests/files/solution.txt
|
|
48
|
+
jame solve tests/files/solution.txt --format 'flag{*}'
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
Use `*` for any-length text and `*N` for exactly `N` characters. Repeating
|
|
@@ -50,21 +55,28 @@ to require five characters, not `hcr{*****}`.
|
|
|
50
55
|
Other available methods:
|
|
51
56
|
|
|
52
57
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
jame update
|
|
59
|
+
jame update --pre
|
|
60
|
+
jame find-flag tests/files/challenge.txt --format 'hcr{*4}'
|
|
61
|
+
jame scan tests/files/scan.txt
|
|
62
|
+
jame crypto tests/files/strings.txt --verbose
|
|
63
|
+
jame binary tests/files/strings.txt
|
|
64
|
+
jame stego tests/files/strings.txt
|
|
65
|
+
jame forensics tests/files/strings.txt
|
|
66
|
+
jame archive path/to/archive.zip --max-depth 2
|
|
67
|
+
jame file-type tests/files/strings.txt
|
|
68
|
+
jame strings tests/files/strings.txt
|
|
69
|
+
jame binwalk path/to/file.bin
|
|
70
|
+
jame caesar 'Khoor'
|
|
71
|
+
jame caesar 'Khoor' --verbose
|
|
72
|
+
jame caesar 'Khoor' --shift 3
|
|
64
73
|
```
|
|
65
74
|
|
|
66
75
|
`--verbose` prints the structured result, including analyzer details and raw
|
|
67
|
-
output where available.
|
|
76
|
+
output where available. Caesar brute force prints only its highest-ranked
|
|
77
|
+
decoded candidate by default; `--verbose` shows the ranked candidates.
|
|
78
|
+
`jame update` upgrades JAMe and C2E; `jame update --pre` also upgrades MapRes
|
|
79
|
+
using prerelease versions. `binwalk` requires the external `binwalk` utility.
|
|
68
80
|
|
|
69
81
|
## Quickstart
|
|
70
82
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'''Command-line entry point for JAMe.'''
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from c2e import LiveCLI, command
|
|
9
|
+
from mapres import ascii_colors, rprint, res, setGlobalMaps
|
|
10
|
+
|
|
11
|
+
from jame import Jame
|
|
12
|
+
|
|
13
|
+
setGlobalMaps(ascii_colors)
|
|
14
|
+
_COLOR_ENABLED = sys.stdout.isatty() and 'NO_COLOR' not in os.environ
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _paint(text: object, color: str, *, bold: bool = False) -> str:
|
|
18
|
+
if not _COLOR_ENABLED:
|
|
19
|
+
return str(text)
|
|
20
|
+
style = res(f'<{color}>')
|
|
21
|
+
if bold:
|
|
22
|
+
style = res('<bold>') + style
|
|
23
|
+
return f'{style}{text}{res("<reset>")}'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_result(cli: LiveCLI, result: object, detailed: bool = False) -> None:
|
|
27
|
+
if detailed:
|
|
28
|
+
to_dict = getattr(result, 'to_dict', None)
|
|
29
|
+
output = to_dict() if callable(to_dict) else result
|
|
30
|
+
cli.safePrint(json.dumps(output, indent=2, default=str))
|
|
31
|
+
elif hasattr(result, 'summary'):
|
|
32
|
+
if hasattr(result, 'flag'):
|
|
33
|
+
color = 'green' if result.flag else 'yellow'
|
|
34
|
+
else:
|
|
35
|
+
color = 'green' if getattr(result, 'confidence', 0.0) >= 0.7 else 'aqua'
|
|
36
|
+
cli.safePrint(_paint(result.summary, color))
|
|
37
|
+
if getattr(result, 'flag', None):
|
|
38
|
+
cli.safePrint(_paint(result.flag, 'green', bold=True))
|
|
39
|
+
elif result is None:
|
|
40
|
+
cli.safePrint(_paint('No flag found.', 'yellow'))
|
|
41
|
+
elif isinstance(result, list):
|
|
42
|
+
output = '\n'.join(str(item) for item in result) if result else 'No results.'
|
|
43
|
+
cli.safePrint(_paint(output, 'blue'))
|
|
44
|
+
elif isinstance(result, (dict, tuple)):
|
|
45
|
+
cli.safePrint(_paint(json.dumps(result, indent=2, default=str), 'blue'))
|
|
46
|
+
else:
|
|
47
|
+
cli.safePrint(_paint(result, 'aqua'))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@command('solve')
|
|
51
|
+
def solve(cli: LiveCLI, path: str) -> None:
|
|
52
|
+
'''Find a flag and extract supported archives.'''
|
|
53
|
+
engine = Jame(flag_patterns=[format()], max_depth=max_depth(), verbose=verbose())
|
|
54
|
+
result = engine.autoSolve(
|
|
55
|
+
path,
|
|
56
|
+
format=format(),
|
|
57
|
+
maxDepth=max_depth(),
|
|
58
|
+
verbose=verbose(),
|
|
59
|
+
)
|
|
60
|
+
_print_result(cli, result, verbose())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@solve.param('format', type=str, default='flag{*}')
|
|
64
|
+
def _solve_format(value):
|
|
65
|
+
'''Flag pattern; '*' matches any length and '*N' matches exactly N characters.'''
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@solve.param('max-depth', type=int, default=3)
|
|
70
|
+
def _solve_max_depth(value):
|
|
71
|
+
'''Maximum recursive archive extraction depth.'''
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@solve.flag('verbose')
|
|
76
|
+
def _solve_verbose(value):
|
|
77
|
+
'''Print structured result details.'''
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@command('find-flag')
|
|
82
|
+
def find_flag(cli: LiveCLI, path: str) -> None:
|
|
83
|
+
'''Find a flag matching a wildcard pattern.'''
|
|
84
|
+
result = Jame().findFlag(path, format())
|
|
85
|
+
_print_result(cli, result, verbose())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@find_flag.param('format', type=str, default='flag{*}')
|
|
89
|
+
def _find_flag_format(value):
|
|
90
|
+
'''Flag pattern; '*' matches any length and '*N' matches exactly N characters.'''
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@find_flag.flag('verbose')
|
|
95
|
+
def _find_flag_verbose(value):
|
|
96
|
+
'''Print structured result details.'''
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _register_path_command(name: str, method_name: str, description: str) -> None:
|
|
101
|
+
@command(name)
|
|
102
|
+
def handler(cli: LiveCLI, path: str) -> None:
|
|
103
|
+
result = getattr(Jame(), method_name)(path)
|
|
104
|
+
_print_result(cli, result, verbose())
|
|
105
|
+
|
|
106
|
+
handler.func.__doc__ = description
|
|
107
|
+
handler.desc = description
|
|
108
|
+
def verbose_flag(value):
|
|
109
|
+
'''Print structured result details.'''
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
handler.flag('verbose')(verbose_flag)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_register_path_command('scan', 'scanFile', 'Run all available analyzers.')
|
|
116
|
+
_register_path_command('crypto', 'cryptoAnalyze', 'Analyze crypto indicators.')
|
|
117
|
+
_register_path_command('binary', 'binaryAnalyze', 'Analyze binary indicators.')
|
|
118
|
+
_register_path_command('stego', 'stegoAnalyze', 'Check for steganography indicators.')
|
|
119
|
+
_register_path_command('forensics', 'forensicsAnalyze', 'Run forensic checks.')
|
|
120
|
+
_register_path_command('file-type', 'getFileType', 'Detect the file type.')
|
|
121
|
+
_register_path_command('strings', 'runStrings', 'Extract printable strings.')
|
|
122
|
+
_register_path_command('binwalk', 'runBinwalk', 'Run binwalk when installed.')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@command('archive')
|
|
126
|
+
def archive(cli: LiveCLI, path: str) -> None:
|
|
127
|
+
'''Extract supported archives.'''
|
|
128
|
+
engine = Jame(max_depth=max_depth(), verbose=verbose())
|
|
129
|
+
_print_result(cli, engine.archiveExtract(path), verbose())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@archive.param('max-depth', type=int, default=3)
|
|
133
|
+
def _archive_max_depth(value):
|
|
134
|
+
'''Maximum recursive archive extraction depth.'''
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@archive.flag('verbose')
|
|
139
|
+
def _archive_verbose(value):
|
|
140
|
+
'''Print structured result details.'''
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@command('caesar')
|
|
145
|
+
def caesar(cli: LiveCLI, text: str) -> None:
|
|
146
|
+
'''Decode Caesar-shifted text.'''
|
|
147
|
+
shift_value = shift()
|
|
148
|
+
if shift_value != 'brute':
|
|
149
|
+
try:
|
|
150
|
+
shift_value = int(shift_value)
|
|
151
|
+
except ValueError:
|
|
152
|
+
cli.safePrint("Error: --shift must be an integer or 'brute'")
|
|
153
|
+
raise SystemExit(2) from None
|
|
154
|
+
result = Jame().decodeCaesar(text, shift_value)
|
|
155
|
+
if verbose():
|
|
156
|
+
_print_result(cli, result, detailed=True)
|
|
157
|
+
elif shift_value == 'brute':
|
|
158
|
+
best = result.get('best')
|
|
159
|
+
if best:
|
|
160
|
+
cli.safePrint(_paint(best['decoded'], 'green', bold=True))
|
|
161
|
+
else:
|
|
162
|
+
cli.safePrint(_paint('No Caesar candidate found.', 'yellow'))
|
|
163
|
+
else:
|
|
164
|
+
cli.safePrint(_paint(result.get('decoded', result.get('error', '')), 'green'))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@caesar.param('shift', type=str, default='brute')
|
|
168
|
+
def _caesar_shift(value):
|
|
169
|
+
'''Integer shift or 'brute' to check every Caesar shift.'''
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@caesar.flag('verbose')
|
|
174
|
+
def _caesar_verbose(value):
|
|
175
|
+
'''Print structured result details.'''
|
|
176
|
+
return value
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@command('update')
|
|
180
|
+
def update(cli: LiveCLI) -> None:
|
|
181
|
+
'''Upgrade JAMe and C2E, optionally including the MapRes prerelease.'''
|
|
182
|
+
rprint(_paint('Updating JAMe...', 'aqua'))
|
|
183
|
+
command = [sys.executable, '-m', 'pip', 'install', '--upgrade']
|
|
184
|
+
if pre():
|
|
185
|
+
command.append('--pre')
|
|
186
|
+
command.extend(['c2e', 'jame'])
|
|
187
|
+
if pre():
|
|
188
|
+
command.append('mapres')
|
|
189
|
+
result = subprocess.run(command, check=False)
|
|
190
|
+
if result.returncode:
|
|
191
|
+
raise SystemExit(result.returncode)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@update.flag('pre')
|
|
195
|
+
def _update_pre(value):
|
|
196
|
+
'''Include pre-release versions when upgrading.'''
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main() -> None:
|
|
201
|
+
'''Parse command-line options and dispatch them through C2E.'''
|
|
202
|
+
LiveCLI().run_argv(
|
|
203
|
+
prog='jame',
|
|
204
|
+
description='Just Another Method of Exploitation',
|
|
205
|
+
default_command='solve',
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == '__main__':
|
|
210
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jame
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.dev3
|
|
4
4
|
Summary: Just Another Method of Exploitation
|
|
5
5
|
Author: JAMe Contributors
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,8 @@ Classifier: Development Status :: 4 - Beta
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: c2e
|
|
23
|
+
Requires-Dist: mapres
|
|
22
24
|
Requires-Dist: PyYAML>=6.0
|
|
23
25
|
Dynamic: license-file
|
|
24
26
|
|
|
@@ -40,31 +42,36 @@ JAMe is a lightweight forensic and triage toolkit for scanning files, extracting
|
|
|
40
42
|
|
|
41
43
|
## Installation
|
|
42
44
|
|
|
43
|
-
JAMe requires Python 3.10 or newer.
|
|
44
|
-
|
|
45
|
-
into the absolute path `$HOME/.local/bin`, and adds that directory to the
|
|
46
|
-
current shell and future Bash login/interactive shells:
|
|
45
|
+
JAMe requires Python 3.10 or newer. Install the latest published package and
|
|
46
|
+
register `jame` on your user PATH with:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
49
|
+
curl -fsSL https://raw.githubusercontent.com/HungerNet/JAMe/main/install.sh | bash
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
The installer creates a dedicated environment under `$HOME/.local/share/jame`,
|
|
53
|
+
installs the MapRes pre-release API, `jame`, and C2E, and links the command to
|
|
54
|
+
`$HOME/.local/bin/jame`. It adds that directory to Bash or Zsh startup files;
|
|
55
|
+
open a new shell if it was not already on your current PATH.
|
|
56
|
+
|
|
57
|
+
Set `JAME_PYTHON` to select a specific Python 3.10+ executable. Set
|
|
58
|
+
`JAME_INSTALL_DIR` or `JAME_BIN_DIR` to change the absolute install or bin path.
|
|
59
|
+
|
|
52
60
|
## CLI
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
virtual environment, the checkout's `.venv`, or `python3.10`, in that order:
|
|
62
|
+
After installation, run the `jame` command from any directory:
|
|
56
63
|
|
|
57
64
|
```bash
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
jame --help
|
|
66
|
+
jame tests/files/solution.txt
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
The default command finds a flag and extracts supported archives. The
|
|
63
|
-
|
|
69
|
+
The default command finds a flag and extracts supported archives. The explicit
|
|
70
|
+
`solve` form is also available:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
jame tests/files/solution.txt
|
|
74
|
+
jame solve tests/files/solution.txt --format 'flag{*}'
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
Use `*` for any-length text and `*N` for exactly `N` characters. Repeating
|
|
@@ -74,21 +81,28 @@ to require five characters, not `hcr{*****}`.
|
|
|
74
81
|
Other available methods:
|
|
75
82
|
|
|
76
83
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
jame update
|
|
85
|
+
jame update --pre
|
|
86
|
+
jame find-flag tests/files/challenge.txt --format 'hcr{*4}'
|
|
87
|
+
jame scan tests/files/scan.txt
|
|
88
|
+
jame crypto tests/files/strings.txt --verbose
|
|
89
|
+
jame binary tests/files/strings.txt
|
|
90
|
+
jame stego tests/files/strings.txt
|
|
91
|
+
jame forensics tests/files/strings.txt
|
|
92
|
+
jame archive path/to/archive.zip --max-depth 2
|
|
93
|
+
jame file-type tests/files/strings.txt
|
|
94
|
+
jame strings tests/files/strings.txt
|
|
95
|
+
jame binwalk path/to/file.bin
|
|
96
|
+
jame caesar 'Khoor'
|
|
97
|
+
jame caesar 'Khoor' --verbose
|
|
98
|
+
jame caesar 'Khoor' --shift 3
|
|
88
99
|
```
|
|
89
100
|
|
|
90
101
|
`--verbose` prints the structured result, including analyzer details and raw
|
|
91
|
-
output where available.
|
|
102
|
+
output where available. Caesar brute force prints only its highest-ranked
|
|
103
|
+
decoded candidate by default; `--verbose` shows the ranked candidates.
|
|
104
|
+
`jame update` upgrades JAMe and C2E; `jame update --pre` also upgrades MapRes
|
|
105
|
+
using prerelease versions. `binwalk` requires the external `binwalk` utility.
|
|
92
106
|
|
|
93
107
|
## Quickstart
|
|
94
108
|
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "jame"
|
|
10
|
-
version = "1.0-dev"
|
|
10
|
+
version = "1.0-dev.3"
|
|
11
11
|
description = "Just Another Method of Exploitation"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.10"
|
|
@@ -24,6 +24,8 @@ keywords = [
|
|
|
24
24
|
"jame",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
+
"c2e",
|
|
28
|
+
"mapres",
|
|
27
29
|
"PyYAML>=6.0",
|
|
28
30
|
]
|
|
29
31
|
classifiers = [
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'''Integration tests for the JAMe command-line interface.'''
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
from mapres import ascii_colors
|
|
8
|
+
|
|
9
|
+
from jame.cli import jame_cli
|
|
10
|
+
from jame.cli.jame_cli import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_cli(monkeypatch, capsys, *arguments: str) -> str:
|
|
14
|
+
monkeypatch.setattr(sys, 'argv', ['jame', *arguments])
|
|
15
|
+
monkeypatch.setattr(jame_cli, '_COLOR_ENABLED', False)
|
|
16
|
+
main()
|
|
17
|
+
return capsys.readouterr().out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_cli_uses_mapres_color_tokens(monkeypatch):
|
|
21
|
+
'''Render color tokens from MapRes's registered ASCII map.'''
|
|
22
|
+
monkeypatch.setattr(jame_cli, '_COLOR_ENABLED', True)
|
|
23
|
+
|
|
24
|
+
assert jame_cli._paint('candidate', 'orange') == f'{ascii_colors.orange}candidate{ascii_colors.reset}'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_cli_uses_numbered_wildcard_for_exact_length(monkeypatch, capsys):
|
|
28
|
+
'''Treat numbered wildcard length as exact and unnumbered stars as open-ended.'''
|
|
29
|
+
unbounded = run_cli(
|
|
30
|
+
monkeypatch,
|
|
31
|
+
capsys,
|
|
32
|
+
'tests/files/challenge.txt',
|
|
33
|
+
'--format',
|
|
34
|
+
'hcr{*****}',
|
|
35
|
+
)
|
|
36
|
+
assert 'hcr{abcd}' in unbounded
|
|
37
|
+
|
|
38
|
+
exact = run_cli(
|
|
39
|
+
monkeypatch,
|
|
40
|
+
capsys,
|
|
41
|
+
'tests/files/challenge.txt',
|
|
42
|
+
'--format',
|
|
43
|
+
'hcr{*5}',
|
|
44
|
+
)
|
|
45
|
+
assert 'No flag extracted.' in exact
|
|
46
|
+
assert 'hcr{abcd}' not in exact
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_cli_verbose_prints_structured_analyzer_details(monkeypatch, capsys):
|
|
50
|
+
'''Expose structured result details with the verbose flag.'''
|
|
51
|
+
output = run_cli(
|
|
52
|
+
monkeypatch,
|
|
53
|
+
capsys,
|
|
54
|
+
'crypto',
|
|
55
|
+
'tests/files/strings.txt',
|
|
56
|
+
'--verbose',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
result = json.loads(output)
|
|
60
|
+
assert result['file_path'] == 'tests/files/strings.txt'
|
|
61
|
+
assert 'findings' in result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_cli_supports_caesar_method(monkeypatch, capsys):
|
|
65
|
+
'''Return only the best Caesar decode by default.'''
|
|
66
|
+
output = run_cli(monkeypatch, capsys, 'caesar', 'Khoor')
|
|
67
|
+
|
|
68
|
+
assert output.strip() == 'Hello'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_cli_caesar_verbose_shows_ranked_candidates(monkeypatch, capsys):
|
|
72
|
+
'''Expose ranked Caesar candidates through verbose output.'''
|
|
73
|
+
output = run_cli(monkeypatch, capsys, 'caesar', 'Khoor', '--verbose')
|
|
74
|
+
|
|
75
|
+
result = json.loads(output)
|
|
76
|
+
assert result['best']['decoded'] == 'Hello'
|
|
77
|
+
assert len(result['results']) == 25
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cli_caesar_explicit_shift(monkeypatch, capsys):
|
|
81
|
+
'''Decode one Caesar shift without verbose metadata.'''
|
|
82
|
+
output = run_cli(monkeypatch, capsys, 'caesar', 'Khoor', '--shift', '3')
|
|
83
|
+
|
|
84
|
+
assert output.strip() == 'Hello'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_cli_update_pre_requests_prereleases(monkeypatch, capsys):
|
|
88
|
+
'''Pass --pre through to pip when updating JAMe and C2E.'''
|
|
89
|
+
calls = []
|
|
90
|
+
|
|
91
|
+
def fake_run(arguments, check):
|
|
92
|
+
calls.append((arguments, check))
|
|
93
|
+
return SimpleNamespace(returncode=0)
|
|
94
|
+
|
|
95
|
+
monkeypatch.setattr(jame_cli.subprocess, 'run', fake_run)
|
|
96
|
+
run_cli(monkeypatch, capsys, 'update', '--pre')
|
|
97
|
+
|
|
98
|
+
arguments, check = calls[0]
|
|
99
|
+
assert arguments[-5:] == ['--upgrade', '--pre', 'c2e', 'jame', 'mapres']
|
|
100
|
+
assert check is False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_cli_stable_update_leaves_mapres_prerelease_installed(monkeypatch, capsys):
|
|
104
|
+
'''Avoid downgrading the required MapRes API during a stable update.'''
|
|
105
|
+
calls = []
|
|
106
|
+
|
|
107
|
+
def fake_run(arguments, check):
|
|
108
|
+
calls.append(arguments)
|
|
109
|
+
return SimpleNamespace(returncode=0)
|
|
110
|
+
|
|
111
|
+
monkeypatch.setattr(jame_cli.subprocess, 'run', fake_run)
|
|
112
|
+
run_cli(monkeypatch, capsys, 'update')
|
|
113
|
+
|
|
114
|
+
assert calls[0][-2:] == ['c2e', 'jame']
|
|
115
|
+
assert '--pre' not in calls[0]
|
|
116
|
+
assert 'mapres' not in calls[0]
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
'''Command-line entry point for JAMe.'''
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
from jame import Jame
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_COMMANDS = {
|
|
11
|
-
'archive',
|
|
12
|
-
'binary',
|
|
13
|
-
'binwalk',
|
|
14
|
-
'caesar',
|
|
15
|
-
'crypto',
|
|
16
|
-
'file-type',
|
|
17
|
-
'find-flag',
|
|
18
|
-
'forensics',
|
|
19
|
-
'scan',
|
|
20
|
-
'solve',
|
|
21
|
-
'stego',
|
|
22
|
-
'strings',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _add_path_arguments(command: argparse.ArgumentParser) -> None:
|
|
27
|
-
command.add_argument('path', help='Path to the file to analyze')
|
|
28
|
-
command.add_argument('--verbose', action='store_true', help='Print structured result details')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _build_parser() -> argparse.ArgumentParser:
|
|
32
|
-
parser = argparse.ArgumentParser(
|
|
33
|
-
prog='jame',
|
|
34
|
-
description='Just Another Method of Exploitation',
|
|
35
|
-
)
|
|
36
|
-
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
37
|
-
|
|
38
|
-
solve = subparsers.add_parser('solve', help='Find a flag and extract supported archives')
|
|
39
|
-
_add_path_arguments(solve)
|
|
40
|
-
solve.add_argument(
|
|
41
|
-
'--format',
|
|
42
|
-
default='flag{*}',
|
|
43
|
-
help="Flag pattern; '*' matches any length and '*N' matches exactly N characters",
|
|
44
|
-
)
|
|
45
|
-
solve.add_argument('--max-depth', type=int, default=3, help='Archive extraction depth')
|
|
46
|
-
|
|
47
|
-
find_flag = subparsers.add_parser('find-flag', help='Find a flag matching a pattern')
|
|
48
|
-
_add_path_arguments(find_flag)
|
|
49
|
-
find_flag.add_argument(
|
|
50
|
-
'--format',
|
|
51
|
-
default='flag{*}',
|
|
52
|
-
help="Flag pattern; '*' matches any length and '*N' matches exactly N characters",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
for name, description in (
|
|
56
|
-
('scan', 'Run all available analyzers'),
|
|
57
|
-
('crypto', 'Analyze crypto indicators'),
|
|
58
|
-
('binary', 'Analyze binary indicators'),
|
|
59
|
-
('stego', 'Check for steganography indicators'),
|
|
60
|
-
('forensics', 'Run forensic checks'),
|
|
61
|
-
('file-type', 'Detect the file type'),
|
|
62
|
-
('strings', 'Extract printable strings'),
|
|
63
|
-
('binwalk', 'Run binwalk when installed'),
|
|
64
|
-
):
|
|
65
|
-
command = subparsers.add_parser(name, help=description)
|
|
66
|
-
_add_path_arguments(command)
|
|
67
|
-
|
|
68
|
-
archive = subparsers.add_parser('archive', help='Extract supported archives')
|
|
69
|
-
_add_path_arguments(archive)
|
|
70
|
-
archive.add_argument('--max-depth', type=int, default=3, help='Archive extraction depth')
|
|
71
|
-
|
|
72
|
-
caesar = subparsers.add_parser('caesar', help='Decode Caesar-shifted text')
|
|
73
|
-
caesar.add_argument('text', help='Text to decode')
|
|
74
|
-
caesar.add_argument('--shift', default='brute', help="Integer shift or 'brute'")
|
|
75
|
-
caesar.add_argument('--verbose', action='store_true', help='Print structured result details')
|
|
76
|
-
|
|
77
|
-
return parser
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _print_result(result: object, verbose: bool) -> None:
|
|
81
|
-
if verbose:
|
|
82
|
-
to_dict = getattr(result, 'to_dict', None)
|
|
83
|
-
output = to_dict() if callable(to_dict) else result
|
|
84
|
-
print(json.dumps(output, indent=2, default=str))
|
|
85
|
-
elif hasattr(result, 'summary'):
|
|
86
|
-
print(result.summary)
|
|
87
|
-
elif result is None:
|
|
88
|
-
print('No flag found.')
|
|
89
|
-
elif isinstance(result, list):
|
|
90
|
-
print('\n'.join(str(item) for item in result) if result else 'No results.')
|
|
91
|
-
elif isinstance(result, (dict, tuple)):
|
|
92
|
-
print(json.dumps(result, indent=2, default=str))
|
|
93
|
-
else:
|
|
94
|
-
print(result)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def main() -> None:
|
|
98
|
-
'''Parse command-line options and run the requested JAMe method.'''
|
|
99
|
-
argv = sys.argv[1:]
|
|
100
|
-
if argv and argv[0] not in _COMMANDS and argv[0] not in {'-h', '--help'}:
|
|
101
|
-
argv.insert(0, 'solve')
|
|
102
|
-
|
|
103
|
-
parser = _build_parser()
|
|
104
|
-
args = parser.parse_args(argv)
|
|
105
|
-
max_depth = getattr(args, 'max_depth', 3)
|
|
106
|
-
engine = Jame(
|
|
107
|
-
flag_patterns=[getattr(args, 'format', 'flag{*}')],
|
|
108
|
-
max_depth=max_depth,
|
|
109
|
-
verbose=args.verbose,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if args.command == 'solve':
|
|
113
|
-
result = engine.autoSolve(
|
|
114
|
-
args.path,
|
|
115
|
-
format=args.format,
|
|
116
|
-
maxDepth=args.max_depth,
|
|
117
|
-
verbose=args.verbose,
|
|
118
|
-
)
|
|
119
|
-
elif args.command == 'find-flag':
|
|
120
|
-
result = engine.findFlag(args.path, args.format)
|
|
121
|
-
elif args.command == 'scan':
|
|
122
|
-
result = engine.scanFile(args.path)
|
|
123
|
-
elif args.command == 'crypto':
|
|
124
|
-
result = engine.cryptoAnalyze(args.path)
|
|
125
|
-
elif args.command == 'binary':
|
|
126
|
-
result = engine.binaryAnalyze(args.path)
|
|
127
|
-
elif args.command == 'stego':
|
|
128
|
-
result = engine.stegoAnalyze(args.path)
|
|
129
|
-
elif args.command == 'forensics':
|
|
130
|
-
result = engine.forensicsAnalyze(args.path)
|
|
131
|
-
elif args.command == 'archive':
|
|
132
|
-
engine.setMaxDepth(args.max_depth)
|
|
133
|
-
result = engine.archiveExtract(args.path)
|
|
134
|
-
elif args.command == 'file-type':
|
|
135
|
-
result = engine.getFileType(args.path)
|
|
136
|
-
elif args.command == 'strings':
|
|
137
|
-
result = engine.runStrings(args.path)
|
|
138
|
-
elif args.command == 'binwalk':
|
|
139
|
-
result = engine.runBinwalk(args.path)
|
|
140
|
-
else:
|
|
141
|
-
try:
|
|
142
|
-
shift = 'brute' if args.shift == 'brute' else int(args.shift)
|
|
143
|
-
except ValueError:
|
|
144
|
-
parser.error("--shift must be an integer or 'brute'")
|
|
145
|
-
result = engine.decodeCaesar(args.text, shift)
|
|
146
|
-
|
|
147
|
-
_print_result(result, args.verbose)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if __name__ == '__main__':
|
|
151
|
-
main()
|
jame-1.0.dev0/tests/test_cli.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'''Integration tests for the JAMe command-line interface.'''
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from jame.cli.jame_cli import main
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def run_cli(monkeypatch, capsys, *arguments: str) -> str:
|
|
10
|
-
monkeypatch.setattr(sys, 'argv', ['jame', *arguments])
|
|
11
|
-
main()
|
|
12
|
-
return capsys.readouterr().out
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_cli_uses_numbered_wildcard_for_exact_length(monkeypatch, capsys):
|
|
16
|
-
'''Treat numbered wildcard length as exact and unnumbered stars as open-ended.'''
|
|
17
|
-
unbounded = run_cli(
|
|
18
|
-
monkeypatch,
|
|
19
|
-
capsys,
|
|
20
|
-
'tests/files/challenge.txt',
|
|
21
|
-
'--format',
|
|
22
|
-
'hcr{*****}',
|
|
23
|
-
)
|
|
24
|
-
assert 'hcr{abcd}' in unbounded
|
|
25
|
-
|
|
26
|
-
exact = run_cli(
|
|
27
|
-
monkeypatch,
|
|
28
|
-
capsys,
|
|
29
|
-
'tests/files/challenge.txt',
|
|
30
|
-
'--format',
|
|
31
|
-
'hcr{*5}',
|
|
32
|
-
)
|
|
33
|
-
assert 'No flag extracted.' in exact
|
|
34
|
-
assert 'hcr{abcd}' not in exact
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_cli_verbose_prints_structured_analyzer_details(monkeypatch, capsys):
|
|
38
|
-
'''Expose structured result details with the verbose flag.'''
|
|
39
|
-
output = run_cli(
|
|
40
|
-
monkeypatch,
|
|
41
|
-
capsys,
|
|
42
|
-
'crypto',
|
|
43
|
-
'tests/files/strings.txt',
|
|
44
|
-
'--verbose',
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
result = json.loads(output)
|
|
48
|
-
assert result['file_path'] == 'tests/files/strings.txt'
|
|
49
|
-
assert 'findings' in result
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_cli_supports_caesar_method(monkeypatch, capsys):
|
|
53
|
-
'''Expose the Caesar decoder as a CLI command.'''
|
|
54
|
-
output = run_cli(monkeypatch, capsys, 'caesar', 'Khoor', '--shift', '3')
|
|
55
|
-
|
|
56
|
-
result = json.loads(output)
|
|
57
|
-
assert result == {'shift': 3, 'decoded': 'Hello'}
|
|
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
|
|
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
|