jame 1.0.dev0__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.
jame/core/jame.py ADDED
@@ -0,0 +1,372 @@
1
+ '''Public API for the JAMe library.'''
2
+
3
+ import os
4
+ import shutil
5
+
6
+ from jame.core.filetype import get_file_type
7
+ from jame.core.patterns import find_pattern_tokens, match_pattern
8
+ from jame.core.utils import read_file_bytes, read_file_text, safe_run_command
9
+ from jame.modules.archive import extract_archive
10
+ from jame.modules.binary import (
11
+ detect_branches,
12
+ extract_symbols,
13
+ run_objdump,
14
+ run_readelf,
15
+ run_strings,
16
+ )
17
+ from jame.modules.crypto import (
18
+ caesar_detect,
19
+ detect_encoding,
20
+ entropy_score_for_text,
21
+ identify_hash,
22
+ xor_bruteforce,
23
+ )
24
+ from jame.modules.forensics import (
25
+ analyze_pdf,
26
+ detect_carved_files,
27
+ extract_metadata,
28
+ triage_pcap,
29
+ )
30
+ from jame.modules.stego import (
31
+ detect_appended_zip,
32
+ detect_exif_anomalies,
33
+ lsb_check,
34
+ zsteg_scan,
35
+ )
36
+ from jame.modules.stego import extract_steghide as _detect_steghide
37
+ from jame.results.binary_result import BinaryResult, BinwalkResult
38
+ from jame.results.crypto_result import CryptoResult
39
+ from jame.results.forensics_result import ForensicsResult
40
+ from jame.results.scan_result import ScanResult
41
+ from jame.results.solve_result import SolveResult
42
+ from jame.results.stego_result import StegoResult
43
+
44
+
45
+ class Jame:
46
+ '''Main public API for JAMe: Just Another Method of Exploitation.'''
47
+
48
+ def __init__(self, flag_patterns: list[str] | None = None, max_depth: int = 3, verbose: bool = False):
49
+ '''Initialize JAMe with optional flag patterns and archive settings.'''
50
+ self.flag_patterns = flag_patterns or ['flag{*}', 'hcr{*}', 'CTFx{*}']
51
+ self.max_depth = max_depth
52
+ self.verbose = verbose
53
+
54
+ def setFlagPatterns(self, patterns: list[str]) -> None:
55
+ '''Set the flag patterns used by automatic flag discovery.'''
56
+ self.flag_patterns = patterns
57
+
58
+ def setMaxDepth(self, depth: int) -> None:
59
+ '''Set the maximum recursive archive extraction depth.'''
60
+ self.max_depth = depth if depth > 0 else 1
61
+
62
+ def setVerbose(self, enabled: bool) -> None:
63
+ '''Enable or disable verbose-mode configuration.'''
64
+ self.verbose = enabled
65
+
66
+ def getFileType(self, path: str) -> str:
67
+ '''Return a best-effort file type label for a path.'''
68
+ return get_file_type(path)
69
+
70
+ def runStrings(self, path: str) -> list[str]:
71
+ '''Extract printable strings from a file.'''
72
+ return run_strings(path)
73
+
74
+ def runBinwalk(self, path: str) -> BinwalkResult:
75
+ '''Run binwalk when installed and return its structured result.'''
76
+ if shutil.which('binwalk') is None:
77
+ return BinwalkResult(
78
+ file_path=path,
79
+ available=False,
80
+ summary='binwalk is not installed',
81
+ raw_output={'stderr': 'binwalk not found'},
82
+ confidence=0.0,
83
+ )
84
+ code, stdout, stderr = safe_run_command(['binwalk', '-e', path], timeout=20)
85
+ extracted: list[str] = []
86
+ if stdout:
87
+ for line in stdout.splitlines():
88
+ if 'extracting' in line.lower() or 'created' in line.lower():
89
+ extracted.append(line.strip())
90
+ if code == 0:
91
+ message = 'binwalk completed successfully'
92
+ confidence = 0.9
93
+ else:
94
+ message = 'binwalk returned an error'
95
+ confidence = 0.2
96
+ return BinwalkResult(
97
+ file_path=path,
98
+ available=True,
99
+ summary=message,
100
+ extracted=extracted,
101
+ matches=[line.strip() for line in stdout.splitlines() if line.strip()],
102
+ raw_output={'stdout': stdout, 'stderr': stderr, 'returncode': code},
103
+ confidence=confidence,
104
+ )
105
+
106
+ def scanFile(self, path: str) -> ScanResult:
107
+ '''Run the available triage analyzers and summarize their findings.'''
108
+ if not os.path.exists(path):
109
+ return ScanResult(
110
+ file_path=path,
111
+ file_type='missing',
112
+ summary='Target file does not exist.',
113
+ details={'exists': False},
114
+ confidence=0.0,
115
+ raw_output={'error': 'missing file'},
116
+ )
117
+
118
+ file_type = self.getFileType(path)
119
+ crypto_result = self.cryptoAnalyze(path)
120
+ binary_result = self.binaryAnalyze(path)
121
+ stego_result = self.stegoAnalyze(path)
122
+ forensics_result = self.forensicsAnalyze(path)
123
+
124
+ summary = (
125
+ f'{crypto_result.summary} | {binary_result.summary} | '
126
+ f'{stego_result.summary} | {forensics_result.summary}'
127
+ )
128
+ confidence = round(
129
+ (crypto_result.confidence + binary_result.confidence + stego_result.confidence + forensics_result.confidence) / 4,
130
+ 3,
131
+ )
132
+ return ScanResult(
133
+ file_path=path,
134
+ file_type=file_type,
135
+ summary=summary,
136
+ details={
137
+ 'crypto': crypto_result.to_dict(),
138
+ 'binary': binary_result.to_dict(),
139
+ 'stego': stego_result.to_dict(),
140
+ 'forensics': forensics_result.to_dict(),
141
+ },
142
+ confidence=confidence,
143
+ raw_output={
144
+ 'crypto': crypto_result.raw_output,
145
+ 'binary': binary_result.raw_output,
146
+ 'stego': stego_result.raw_output,
147
+ 'forensics': forensics_result.raw_output,
148
+ },
149
+ )
150
+
151
+ def findFlag(self, path: str, format: str) -> str | None:
152
+ '''Find the first brace-wrapped candidate matching a wildcard format.'''
153
+ text = read_file_text(path, errors='replace')
154
+ candidates = find_pattern_tokens(text)
155
+ if not candidates:
156
+ return None
157
+ for candidate in candidates:
158
+ if match_pattern(candidate, format):
159
+ return candidate
160
+ for pattern in self.flag_patterns:
161
+ for candidate in candidates:
162
+ if match_pattern(candidate, pattern):
163
+ return candidate
164
+ return None
165
+
166
+ def autoSolve(self, path: str, **ctx) -> SolveResult:
167
+ '''Search a file for a flag and extract supported archives.'''
168
+ format_value = str(ctx.get('format', self.flag_patterns[0]))
169
+ max_depth = int(ctx.get('maxDepth', self.max_depth))
170
+ verbose = bool(ctx.get('verbose', self.verbose))
171
+ self.setVerbose(verbose)
172
+ self.setMaxDepth(max_depth)
173
+
174
+ flag = self.findFlag(path, format_value)
175
+ archive_files: list[str] = []
176
+ if os.path.exists(path):
177
+ archive_files = self.archiveExtract(path)
178
+
179
+ summary = 'No flag extracted.' if flag is None else f'Flag candidate found: {flag}'
180
+ if archive_files:
181
+ summary += f' | extracted {len(archive_files)} archive entries'
182
+ return SolveResult(
183
+ file_path=path,
184
+ summary=summary,
185
+ flag=flag,
186
+ confidence=0.8 if flag else 0.3,
187
+ details={
188
+ 'format': format_value,
189
+ 'max_depth': max_depth,
190
+ 'verbose': verbose,
191
+ 'archives': archive_files,
192
+ },
193
+ raw_output={'flag': flag, 'archives': archive_files},
194
+ )
195
+
196
+ def cryptoAnalyze(self, path: str) -> CryptoResult:
197
+ '''Analyze a file for common encodings, simple ciphers, and hashes.'''
198
+ data = read_file_bytes(path)
199
+ text = data.decode('utf-8', errors='replace')
200
+ encoding = detect_encoding(text)
201
+ xor = xor_bruteforce(data)
202
+ caesar = caesar_detect(text)
203
+ hash_result = identify_hash(text)
204
+ entropy = entropy_score_for_text(data)
205
+
206
+ summary = 'No obvious crypto patterns detected.'
207
+ confidence = 0.1
208
+ algorithm = 'unknown'
209
+ if encoding['encoding']:
210
+ summary = 'Likely encoded or obfuscated data detected.'
211
+ confidence = max(confidence, 0.8)
212
+ algorithm = 'encoded'
213
+ if xor:
214
+ summary = 'XOR-bruteforce candidates present.'
215
+ confidence = max(confidence, 0.75)
216
+ algorithm = 'xor'
217
+ if caesar['best'] and caesar['best']['score'] > 0:
218
+ summary = 'Caesar-style substitution detected.'
219
+ confidence = max(confidence, 0.7)
220
+ algorithm = 'caesar'
221
+ if hash_result['hash_type'] != 'unknown':
222
+ summary = 'Hash-like value detected.'
223
+ confidence = max(confidence, 0.9)
224
+ algorithm = 'hash'
225
+
226
+ return CryptoResult(
227
+ file_path=path,
228
+ summary=summary,
229
+ algorithm=algorithm,
230
+ confidence=round(confidence, 3),
231
+ findings={
232
+ 'encoding': encoding,
233
+ 'xor': xor[:10],
234
+ 'caesar': caesar,
235
+ 'hash': hash_result,
236
+ 'entropy': round(entropy, 3),
237
+ },
238
+ raw_output={
239
+ 'encoding': encoding,
240
+ 'xor': xor,
241
+ 'caesar': caesar,
242
+ 'hash': hash_result,
243
+ 'entropy': entropy,
244
+ },
245
+ )
246
+
247
+ def binaryAnalyze(self, path: str) -> BinaryResult:
248
+ '''Collect printable strings, symbols, and executable indicators.'''
249
+ strings = run_strings(path)
250
+ objdump = run_objdump(path)
251
+ readelf = run_readelf(path)
252
+ symbols = extract_symbols(path)
253
+ branch_hits = detect_branches(path)
254
+ summary = 'Binary-like file with no obvious anomalies.'
255
+ confidence = 0.3
256
+ architecture = 'unknown'
257
+ if strings:
258
+ summary = 'Printable strings suggest a binary or packed payload.'
259
+ confidence = 0.7
260
+ if readelf.get('available'):
261
+ architecture = 'ELF-like'
262
+ if branch_hits:
263
+ summary = 'Control-flow instructions suggest executable logic.'
264
+ confidence = 0.8
265
+ return BinaryResult(
266
+ file_path=path,
267
+ summary=summary,
268
+ architecture=architecture,
269
+ confidence=round(confidence, 3),
270
+ findings={
271
+ 'strings': strings[:50],
272
+ 'symbols': symbols[:50],
273
+ 'branches': branch_hits,
274
+ 'objdump': objdump,
275
+ 'readelf': readelf,
276
+ },
277
+ raw_output={
278
+ 'strings': strings,
279
+ 'objdump': objdump,
280
+ 'readelf': readelf,
281
+ 'symbols': symbols,
282
+ 'branches': branch_hits,
283
+ },
284
+ )
285
+
286
+ def stegoAnalyze(self, path: str) -> StegoResult:
287
+ '''Check a file for common steganography indicators.'''
288
+ zsteg = zsteg_scan(path)
289
+ lsb = lsb_check(path)
290
+ appended_zip = detect_appended_zip(path)
291
+ exif = detect_exif_anomalies(path)
292
+ steghide = _detect_steghide(path)
293
+ techniques: list[str] = []
294
+ if zsteg['findings']:
295
+ techniques.append('zsteg')
296
+ if lsb['likely_lsb']:
297
+ techniques.append('lsb')
298
+ if appended_zip['found']:
299
+ techniques.append('appended zip')
300
+ if exif['anomalies']:
301
+ techniques.append('exif')
302
+ summary = 'No steganography signals detected.' if not techniques else 'Steganography indicators detected.'
303
+ confidence = 0.8 if techniques else 0.1
304
+ return StegoResult(
305
+ file_path=path,
306
+ summary=summary,
307
+ confidence=round(confidence, 3),
308
+ techniques=techniques,
309
+ findings={
310
+ 'zsteg': zsteg,
311
+ 'lsb': lsb,
312
+ 'appended_zip': appended_zip,
313
+ 'exif': exif,
314
+ 'steghide': steghide,
315
+ },
316
+ raw_output={
317
+ 'zsteg': zsteg,
318
+ 'lsb': lsb,
319
+ 'appended_zip': appended_zip,
320
+ 'exif': exif,
321
+ 'steghide': steghide,
322
+ },
323
+ )
324
+
325
+ def forensicsAnalyze(self, path: str) -> ForensicsResult:
326
+ '''Collect file metadata and run PDF, PCAP, and carving checks.'''
327
+ metadata = extract_metadata(path)
328
+ pdf = analyze_pdf(path)
329
+ pcap = triage_pcap(path)
330
+ carved = detect_carved_files(path)
331
+ findings = {'pdf': pdf, 'pcap': pcap, 'carved': carved}
332
+ summary = 'No major forensic anomalies detected.'
333
+ confidence = 0.2
334
+ if carved:
335
+ summary = 'Carved file signatures detected.'
336
+ confidence = 0.8
337
+ if pdf.get('valid'):
338
+ summary = 'PDF structure looks valid.'
339
+ confidence = max(confidence, 0.7)
340
+ return ForensicsResult(
341
+ file_path=path,
342
+ summary=summary,
343
+ confidence=round(confidence, 3),
344
+ metadata=metadata,
345
+ findings=findings,
346
+ raw_output={'metadata': metadata, 'pdf': pdf, 'pcap': pcap, 'carved': carved},
347
+ )
348
+
349
+ def archiveExtract(self, path: str) -> list[str]:
350
+ '''Extract supported archive contents using the configured depth limit.'''
351
+ return extract_archive(path, max_depth=self.max_depth)
352
+
353
+ def decodeCaesar(self, text: str, shift: int | str = 'brute') -> dict:
354
+ '''Decode one Caesar shift or return ranked candidates for all shifts.'''
355
+ if shift == 'brute':
356
+ return caesar_detect(text)
357
+ if not isinstance(shift, int):
358
+ return {'error': "shift must be an integer or 'brute'"}
359
+ decoded = []
360
+ for char in text:
361
+ if 'a' <= char <= 'z':
362
+ decoded.append(chr((ord(char) - ord('a') - shift) % 26 + ord('a')))
363
+ elif 'A' <= char <= 'Z':
364
+ decoded.append(chr((ord(char) - ord('A') - shift) % 26 + ord('A')))
365
+ else:
366
+ decoded.append(char)
367
+ return {'shift': shift, 'decoded': ''.join(decoded)}
368
+
369
+
370
+ def create_jame() -> Jame:
371
+ '''Create a JAMe instance with its default configuration.'''
372
+ return Jame()
jame/core/patterns.py ADDED
@@ -0,0 +1,72 @@
1
+ '''Pattern matching helpers for JAMe.'''
2
+
3
+ import re
4
+
5
+
6
+ def _parse_pattern(pattern: str) -> list[str | tuple[str, int]]:
7
+ '''Convert a wildcard pattern into a simple token stream.'''
8
+ tokens: list[str | tuple[str, int]] = []
9
+ index = 0
10
+ while index < len(pattern):
11
+ char = pattern[index]
12
+ if char == '*':
13
+ end = index + 1
14
+ while end < len(pattern) and pattern[end].isdigit():
15
+ end += 1
16
+ if end > index + 1:
17
+ amount = int(pattern[index + 1:end])
18
+ else:
19
+ amount = 0
20
+ tokens.append(('*', amount))
21
+ index = end
22
+ continue
23
+ if char == '\\' and index + 1 < len(pattern):
24
+ tokens.append(pattern[index + 1])
25
+ index += 2
26
+ continue
27
+ tokens.append(char)
28
+ index += 1
29
+ return tokens
30
+
31
+
32
+ def match_pattern(text: str, pattern: str) -> bool:
33
+ '''Match text against a wildcard pattern using a custom matcher.
34
+
35
+ Supported patterns are literal text with '*' placeholders such as:
36
+ - hcr{*4}
37
+ - flag{*2-******\2}
38
+ - flag{*}
39
+ '''
40
+ if pattern == '':
41
+ return text == ''
42
+
43
+ tokens = _parse_pattern(pattern)
44
+
45
+ def _match(text_index: int, token_index: int) -> bool:
46
+ if token_index == len(tokens):
47
+ return text_index == len(text)
48
+
49
+ token = tokens[token_index]
50
+ if isinstance(token, tuple):
51
+ _, amount = token
52
+ if amount == 0:
53
+ for span in range(len(text) - text_index + 1):
54
+ if _match(text_index + span, token_index + 1):
55
+ return True
56
+ return False
57
+ if text_index + amount > len(text):
58
+ return False
59
+ return _match(text_index + amount, token_index + 1)
60
+
61
+ literal = str(token)
62
+ if text.startswith(literal, text_index):
63
+ return _match(text_index + len(literal), token_index + 1)
64
+ return False
65
+
66
+ return _match(0, 0)
67
+
68
+
69
+ def find_pattern_tokens(text: str) -> list[str]:
70
+ '''Extract braces-wrapped strings likely to contain flag-like values.'''
71
+ matches = re.findall(r'[A-Za-z0-9_]{2,}\{[^\n\r}]{1,120}\}', text)
72
+ return matches
jame/core/utils.py ADDED
@@ -0,0 +1,74 @@
1
+ '''Miscellaneous helpers used by JAMe.'''
2
+
3
+ import hashlib
4
+ import math
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+
11
+ def read_file_bytes(path: str) -> bytes:
12
+ '''Read bytes from a file.'''
13
+ with open(path, 'rb') as handle:
14
+ return handle.read()
15
+
16
+
17
+ def read_file_text(path: str, errors: str = 'replace') -> str:
18
+ '''Read a file as text.'''
19
+ with open(path, 'r', encoding='utf-8', errors=errors) as handle:
20
+ return handle.read()
21
+
22
+
23
+ def safe_run_command(command: list[str], timeout: int = 15) -> tuple[int, str, str]:
24
+ '''Run an external command safely and capture output.'''
25
+ try:
26
+ completed = subprocess.run(
27
+ command,
28
+ capture_output=True,
29
+ text=True,
30
+ timeout=timeout,
31
+ check=False,
32
+ )
33
+ return completed.returncode, completed.stdout, completed.stderr
34
+ except FileNotFoundError:
35
+ return 127, '', 'command not found'
36
+ except subprocess.TimeoutExpired:
37
+ return 124, '', 'command timed out'
38
+
39
+
40
+ def entropy_score(data: bytes) -> float:
41
+ '''Compute Shannon entropy for a byte string.'''
42
+ if not data:
43
+ return 0.0
44
+ counts = {}
45
+ for byte in data:
46
+ counts[byte] = counts.get(byte, 0) + 1
47
+ total = len(data)
48
+ entropy = 0.0
49
+ for count in counts.values():
50
+ probability = count / total
51
+ entropy -= probability * math.log2(probability)
52
+ return entropy
53
+
54
+
55
+ def sha256_hex(data: bytes) -> str:
56
+ '''Return the SHA256 hex digest of the supplied data.'''
57
+ return hashlib.sha256(data).hexdigest()
58
+
59
+
60
+ def strip_ansi(value: str) -> str:
61
+ '''Remove ANSI escape sequences from output.'''
62
+ return re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', value)
63
+
64
+
65
+ def file_exists(path: str) -> bool:
66
+ '''Return whether a path exists and is a file.'''
67
+ return os.path.isfile(path)
68
+
69
+
70
+ def ensure_directory(path: str) -> str:
71
+ '''Create a directory if needed and return the path.'''
72
+ directory = Path(path)
73
+ directory.mkdir(parents=True, exist_ok=True)
74
+ return str(directory)
@@ -0,0 +1,53 @@
1
+ '''Module analyzers for JAMe.'''
2
+
3
+ from jame.modules.archive import extract_archive
4
+ from jame.modules.binary import (
5
+ detect_branches,
6
+ extract_symbols,
7
+ run_objdump,
8
+ run_readelf,
9
+ run_strings,
10
+ )
11
+ from jame.modules.crypto import (
12
+ caesar_detect,
13
+ detect_encoding,
14
+ entropy_score,
15
+ identify_hash,
16
+ xor_bruteforce,
17
+ )
18
+ from jame.modules.forensics import (
19
+ analyze_pdf,
20
+ detect_carved_files,
21
+ extract_metadata,
22
+ triage_pcap,
23
+ )
24
+ from jame.modules.stego import (
25
+ detect_appended_zip,
26
+ detect_exif_anomalies,
27
+ extract_steghide,
28
+ lsb_check,
29
+ zsteg_scan,
30
+ )
31
+
32
+ __all__ = [
33
+ 'analyze_pdf',
34
+ 'caesar_detect',
35
+ 'detect_appended_zip',
36
+ 'detect_branches',
37
+ 'detect_carved_files',
38
+ 'detect_encoding',
39
+ 'detect_exif_anomalies',
40
+ 'entropy_score',
41
+ 'extract_archive',
42
+ 'extract_metadata',
43
+ 'extract_steghide',
44
+ 'extract_symbols',
45
+ 'identify_hash',
46
+ 'lsb_check',
47
+ 'run_objdump',
48
+ 'run_readelf',
49
+ 'run_strings',
50
+ 'triage_pcap',
51
+ 'xor_bruteforce',
52
+ 'zsteg_scan',
53
+ ]
@@ -0,0 +1,63 @@
1
+ '''Archive extraction helpers for JAMe.'''
2
+
3
+ import gzip
4
+ import os
5
+ import tarfile
6
+ import zipfile
7
+
8
+
9
+ def _extract_zip(path: str, output_dir: str) -> list[str]:
10
+ extracted: list[str] = []
11
+ with zipfile.ZipFile(path, 'r') as archive:
12
+ archive.extractall(output_dir)
13
+ extracted.extend([os.path.join(output_dir, item) for item in archive.namelist()])
14
+ return extracted
15
+
16
+
17
+ def _extract_tar(path: str, output_dir: str) -> list[str]:
18
+ extracted: list[str] = []
19
+ with tarfile.open(path, 'r:*') as archive:
20
+ archive.extractall(output_dir)
21
+ for member in archive.getmembers():
22
+ if member.isfile():
23
+ extracted.append(os.path.join(output_dir, member.name))
24
+ return extracted
25
+
26
+
27
+ def _extract_gzip(path: str, output_dir: str) -> list[str]:
28
+ target = os.path.join(output_dir, os.path.basename(path).replace('.gz', ''))
29
+ with gzip.open(path, 'rb') as source, open(target, 'wb') as destination:
30
+ destination.write(source.read())
31
+ return [target]
32
+
33
+
34
+ def extract_archive(path: str, max_depth: int = 3) -> list[str]:
35
+ '''Extract a zip/tar/gzip archive, recursively until max_depth is reached.'''
36
+ if not os.path.isfile(path):
37
+ return []
38
+
39
+ output_dir = os.path.join(os.path.dirname(path), '.jame_extract')
40
+ os.makedirs(output_dir, exist_ok=True)
41
+ extracted: list[str] = []
42
+ stack = [(path, 0)]
43
+ seen: set[str] = set()
44
+
45
+ while stack:
46
+ current_path, depth = stack.pop()
47
+ if current_path in seen or depth > max_depth:
48
+ continue
49
+ seen.add(current_path)
50
+ lower = current_path.lower()
51
+ if lower.endswith('.zip'):
52
+ files = _extract_zip(current_path, output_dir)
53
+ extracted.extend(files)
54
+ elif lower.endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz')):
55
+ files = _extract_tar(current_path, output_dir)
56
+ extracted.extend(files)
57
+ elif lower.endswith('.gz') and not lower.endswith('.tar.gz'):
58
+ files = _extract_gzip(current_path, output_dir)
59
+ extracted.extend(files)
60
+ for item in extracted:
61
+ if os.path.isfile(item) and item.lower().endswith(('.zip', '.tar', '.gz')):
62
+ stack.append((item, depth + 1))
63
+ return extracted
jame/modules/binary.py ADDED
@@ -0,0 +1,60 @@
1
+ '''Binary analysis helpers for JAMe.'''
2
+
3
+ import re
4
+
5
+ from jame.core.utils import read_file_bytes, safe_run_command
6
+
7
+
8
+ def run_strings(path: str) -> list[str]:
9
+ '''Return printable strings from a binary-like file.'''
10
+ data = read_file_bytes(path)
11
+ matches = re.findall(rb'[ -~]{4,}', data)
12
+ return [item.decode('utf-8', errors='ignore') for item in matches]
13
+
14
+
15
+ def run_objdump(path: str) -> dict:
16
+ '''Run objdump if present and return structured output.'''
17
+ code, stdout, stderr = safe_run_command(['objdump', '-d', path], timeout=10)
18
+ return {
19
+ 'available': code == 0,
20
+ 'output': stdout or stderr,
21
+ 'returncode': code,
22
+ }
23
+
24
+
25
+ def run_readelf(path: str) -> dict:
26
+ '''Run readelf if present and return structured output.'''
27
+ code, stdout, stderr = safe_run_command(['readelf', '-h', path], timeout=10)
28
+ return {
29
+ 'available': code == 0,
30
+ 'output': stdout or stderr,
31
+ 'returncode': code,
32
+ }
33
+
34
+
35
+ def extract_symbols(path: str) -> list[str]:
36
+ '''Extract symbol-like names from an ELF binary using readelf if available.'''
37
+ result = run_readelf(path)
38
+ output = result.get('output', '')
39
+ matches = re.findall(r'\b[A-Za-z_][A-Za-z0-9_]+\b', output)
40
+ return matches[:200]
41
+
42
+
43
+ def detect_branches(path: str) -> list[str]:
44
+ '''Identify common branch patterns in disassembly output.'''
45
+ result = run_objdump(path)
46
+ output = result.get('output', '')
47
+ patterns = [
48
+ 'call',
49
+ 'jmp',
50
+ 'jne',
51
+ 'je',
52
+ 'ret',
53
+ 'test',
54
+ 'cmp',
55
+ ]
56
+ hits: list[str] = []
57
+ for pattern in patterns:
58
+ if pattern in output:
59
+ hits.append(pattern)
60
+ return hits