codevanta 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. codevanta/__init__.py +1 -0
  2. codevanta/analysis/__init__.py +1 -0
  3. codevanta/analysis/binary_diff.py +1168 -0
  4. codevanta/analysis/callgraph.py +542 -0
  5. codevanta/analysis/cfg.py +1201 -0
  6. codevanta/analysis/dataflow.py +762 -0
  7. codevanta/analysis/debugger.py +289 -0
  8. codevanta/analysis/debuginfo.py +304 -0
  9. codevanta/analysis/decompile.py +1094 -0
  10. codevanta/analysis/decompilers/__init__.py +250 -0
  11. codevanta/analysis/decompilers/angr_backend.py +251 -0
  12. codevanta/analysis/decompilers/base.py +278 -0
  13. codevanta/analysis/decompilers/ghidra_backend.py +316 -0
  14. codevanta/analysis/dynamic.py +756 -0
  15. codevanta/analysis/emulate.py +1349 -0
  16. codevanta/analysis/evidence_bundle.py +736 -0
  17. codevanta/analysis/function_hash.py +562 -0
  18. codevanta/analysis/graph_export.py +441 -0
  19. codevanta/analysis/html_view.py +695 -0
  20. codevanta/analysis/jump_tables.py +546 -0
  21. codevanta/analysis/solver.py +715 -0
  22. codevanta/analysis/symbolic.py +1809 -0
  23. codevanta/analysis/trace.py +646 -0
  24. codevanta/analysis/type_propagation.py +1055 -0
  25. codevanta/analysis/type_recovery.py +1214 -0
  26. codevanta/analysis/vtables.py +572 -0
  27. codevanta/analysis/xrefs.py +434 -0
  28. codevanta/api/__init__.py +1 -0
  29. codevanta/cli/__init__.py +1 -0
  30. codevanta/cli/main.py +3038 -0
  31. codevanta/core/__init__.py +31 -0
  32. codevanta/core/disassembler.py +121 -0
  33. codevanta/core/entropy.py +237 -0
  34. codevanta/core/functions.py +176 -0
  35. codevanta/core/loader.py +1351 -0
  36. codevanta/core/report.py +306 -0
  37. codevanta/core/strings.py +109 -0
  38. codevanta/evidence/__init__.py +7 -0
  39. codevanta/evidence/confidence.py +36 -0
  40. codevanta/evidence/models.py +43 -0
  41. codevanta/evidence/provenance.py +37 -0
  42. codevanta/gui/__init__.py +46 -0
  43. codevanta/gui/layout.py +1384 -0
  44. codevanta/gui/server.py +1305 -0
  45. codevanta/ir/__init__.py +6 -0
  46. codevanta/ir/adapters/__init__.py +31 -0
  47. codevanta/ir/base.py +31 -0
  48. codevanta/ir/instruction.py +42 -0
  49. codevanta/mcp_server/__init__.py +5 -0
  50. codevanta/mcp_server/server.py +710 -0
  51. codevanta/mcp_server/tools.py +1364 -0
  52. codevanta/plugins/__init__.py +748 -0
  53. codevanta/setup/__init__.py +28 -0
  54. codevanta/setup/apply.py +22 -0
  55. codevanta/setup/clients.py +285 -0
  56. codevanta/setup/plan.py +79 -0
  57. codevanta/setup/verify.py +43 -0
  58. codevanta/storage/__init__.py +1 -0
  59. codevanta/storage/session.py +514 -0
  60. codevanta-0.1.0.dist-info/METADATA +688 -0
  61. codevanta-0.1.0.dist-info/RECORD +65 -0
  62. codevanta-0.1.0.dist-info/WHEEL +5 -0
  63. codevanta-0.1.0.dist-info/entry_points.txt +2 -0
  64. codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. codevanta-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,434 @@
1
+ """Cross-reference extraction from direct and memory-indirect transfers.
2
+
3
+ The scan stays conservative. Two operand shapes are resolved:
4
+
5
+ 1. A direct literal target (``^0x[0-9a-fA-F]+$``) for ``call``/``jmp``/
6
+ conditional jumps.
7
+ 2. A ``call`` through an absolute or RIP-relative pointer slot, i.e.
8
+ ``(byte|word|dword|qword) ptr [0x...]`` or
9
+ ``(byte|word|dword|qword) ptr [rip +/- 0x...]``. The reported
10
+ ``to_address`` is the address of that slot, not of the callee: the pointer
11
+ is read from memory at run time, so the callee itself is unknown here. When
12
+ the slot address matches an ``info.imports`` entry the reference is tagged
13
+ ``call_import`` and named after the import; otherwise it is ``call_indirect``.
14
+
15
+ Everything else (``call rax``, ``call qword ptr [rax + 8]``, and any indirect
16
+ ``jmp``/conditional jump) is skipped rather than guessed. Skipped indirect
17
+ *calls* are counted in :attr:`XrefResult.indirect_skipped` so callers can see
18
+ how much of the call graph was left unresolved.
19
+
20
+ Every step is bounded (``max_bytes``) and the analysed file is only ever read
21
+ from disk; nothing is loaded or executed.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from codevanta.core.disassembler import Disassembler, DisassemblyError
32
+ from codevanta.core.functions import FunctionCandidate, discover_functions
33
+ from codevanta.core.loader import BinaryInfo
34
+ from codevanta.core.report import AnalysisError, read_code
35
+ from codevanta.evidence.confidence import Confidence
36
+ from codevanta.evidence.models import Evidence
37
+ from codevanta.ir.instruction import IRInstruction
38
+
39
+ __all__ = ["DEFAULT_MAX_BYTES", "Xref", "XrefResult", "find_xrefs"]
40
+
41
+ #: Default ceiling on how many executable bytes a single scan will decode.
42
+ DEFAULT_MAX_BYTES = 4_000_000
43
+
44
+ #: Operand text that denotes an absolute, statically known target.
45
+ _DIRECT_OPERAND = re.compile(r"^0x[0-9a-fA-F]+$")
46
+
47
+ #: Operand text of a pointer slot: absolute or RIP-relative, sized explicitly.
48
+ #:
49
+ #: Examples matched: ``qword ptr [0x401000]``, ``qword ptr [rip + 0xa]``,
50
+ #: ``dword ptr [rip - 0xa]``. Anything with a register base or index
51
+ #: (``qword ptr [rax]``, ``qword ptr [rax + rcx*8]``) deliberately misses,
52
+ #: because the slot address is not statically known.
53
+ _MEMORY_POINTER = re.compile(
54
+ r"^(?:byte|word|dword|qword)\s+ptr\s*\[\s*"
55
+ r"(?:(?P<absolute>0x[0-9a-fA-F]+)|rip\s*(?P<sign>[+-])\s*(?P<displacement>0x[0-9a-fA-F]+))"
56
+ r"\s*\]$",
57
+ re.IGNORECASE,
58
+ )
59
+
60
+ _BRANCH_MNEMONICS = frozenset(
61
+ {
62
+ "je",
63
+ "jne",
64
+ "jz",
65
+ "jnz",
66
+ "jb",
67
+ "jbe",
68
+ "ja",
69
+ "jae",
70
+ "jl",
71
+ "jle",
72
+ "jg",
73
+ "jge",
74
+ "js",
75
+ "jns",
76
+ "jo",
77
+ "jno",
78
+ "jp",
79
+ "jnp",
80
+ "jcxz",
81
+ "jecxz",
82
+ "jrcxz",
83
+ }
84
+ )
85
+
86
+ #: Maps a lowercase mnemonic to the cross-reference kind it produces.
87
+ _KIND_BY_MNEMONIC: dict[str, str] = {
88
+ "call": "call",
89
+ "jmp": "jump",
90
+ **{mnemonic: "branch" for mnemonic in _BRANCH_MNEMONICS},
91
+ }
92
+
93
+ #: Maps a cross-reference kind to the evidence source that justifies it.
94
+ _SOURCE_BY_KIND: dict[str, str] = {
95
+ "call": "capstone.direct_call",
96
+ "jump": "capstone.direct_jump",
97
+ "branch": "capstone.conditional_branch",
98
+ "call_import": "capstone.import_call",
99
+ "call_indirect": "capstone.indirect_call",
100
+ }
101
+
102
+ _EVIDENCE_METHOD = "linear_disassembly"
103
+ _RESULT_EVIDENCE_SOURCE = "capstone.xref_scan"
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class Xref:
108
+ """A single directed reference from one instruction to another address.
109
+
110
+ Attributes:
111
+ from_address: Virtual address of the referring instruction.
112
+ to_address: Virtual address the instruction transfers control to. For
113
+ ``call_import``/``call_indirect`` this is the *pointer slot*, since
114
+ the callee is only known at run time.
115
+ kind: ``"call"``, ``"jump"``, ``"branch"``, ``"call_import"`` or
116
+ ``"call_indirect"``.
117
+ instruction: Disassembled instruction text, e.g. ``"call 0x401010"``.
118
+ source: Evidence source label for this specific reference.
119
+ evidence: Full evidence record for the reference.
120
+ function: Name of the function candidate located at ``to_address``.
121
+ target_name: Import name when the call goes through a known import slot,
122
+ otherwise ``None``.
123
+ """
124
+
125
+ from_address: int
126
+ to_address: int
127
+ kind: str
128
+ instruction: str
129
+ source: str
130
+ evidence: Evidence
131
+ function: str | None = None
132
+ target_name: str | None = None
133
+
134
+ def to_dict(self) -> dict[str, Any]:
135
+ data: dict[str, Any] = {
136
+ "from_address": f"0x{self.from_address:x}",
137
+ "to_address": f"0x{self.to_address:x}",
138
+ "kind": self.kind,
139
+ "instruction": self.instruction,
140
+ "source": self.source,
141
+ "evidence": self.evidence.to_dict(),
142
+ }
143
+ if self.function:
144
+ data["function"] = self.function
145
+ if self.target_name:
146
+ data["target_name"] = self.target_name
147
+ return data
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class XrefResult:
152
+ """Outcome of one cross-reference scan.
153
+
154
+ Attributes:
155
+ xrefs: References sorted by ``from_address``.
156
+ scanned_sections: Names of the sections that were successfully decoded.
157
+ scanned_bytes: Number of bytes actually decoded into instructions.
158
+ truncated: ``True`` when ``max_bytes`` or a decoder limit cut the scan.
159
+ evidence: Evidence describing the scan as a whole.
160
+ warnings: Non-fatal problems encountered while scanning.
161
+ indirect_skipped: Number of indirect ``call`` instructions whose target
162
+ could not be resolved statically (register operands, indexed memory
163
+ operands, ...). Such calls emit no :class:`Xref`.
164
+ """
165
+
166
+ xrefs: list[Xref]
167
+ scanned_sections: list[str]
168
+ scanned_bytes: int
169
+ truncated: bool
170
+ evidence: Evidence
171
+ warnings: tuple[str, ...] = ()
172
+ indirect_skipped: int = 0
173
+
174
+ def to_dict(self) -> dict[str, Any]:
175
+ return {
176
+ "xref_count": len(self.xrefs),
177
+ "xrefs": [xref.to_dict() for xref in self.xrefs],
178
+ "scanned_sections": list(self.scanned_sections),
179
+ "scanned_bytes": self.scanned_bytes,
180
+ "truncated": self.truncated,
181
+ "indirect_skipped": self.indirect_skipped,
182
+ "warnings": list(self.warnings),
183
+ "evidence": self.evidence.to_dict(),
184
+ }
185
+
186
+
187
+ def _kind_for(mnemonic: str) -> str | None:
188
+ """Return the cross-reference kind for ``mnemonic`` if it is a transfer."""
189
+ return _KIND_BY_MNEMONIC.get(mnemonic.strip().lower())
190
+
191
+
192
+ def _direct_target(operands: str) -> int | None:
193
+ """Parse an absolute target from ``operands``; ignore registers/memory."""
194
+ text = operands.strip()
195
+ if not _DIRECT_OPERAND.match(text):
196
+ return None
197
+ try:
198
+ return int(text, 16)
199
+ except ValueError: # pragma: no cover - the regex guarantees hex digits
200
+ return None
201
+
202
+
203
+ def _memory_pointer_target(operands: str, address: int, size: int) -> int | None:
204
+ """Resolve the pointer-slot address of an indirect memory operand.
205
+
206
+ Args:
207
+ operands: Capstone operand text, e.g. ``"qword ptr [rip + 0xa]"``.
208
+ address: Virtual address of the instruction itself.
209
+ size: Length of the instruction in bytes, used as the RIP bias.
210
+
211
+ Returns:
212
+ The absolute address of the pointer slot, or ``None`` when the operand
213
+ is not an absolute/RIP-relative sized memory pointer. Note that the
214
+ value stored in the slot (the real callee) is unknown here.
215
+ """
216
+ match = _MEMORY_POINTER.match(operands.strip())
217
+ if match is None:
218
+ return None
219
+ absolute = match.group("absolute")
220
+ try:
221
+ if absolute is not None:
222
+ return int(absolute, 16)
223
+ displacement = int(match.group("displacement"), 16)
224
+ except ValueError: # pragma: no cover - the regex guarantees hex digits
225
+ return None
226
+ # RIP-relative operands are encoded relative to the next instruction.
227
+ base = address + size
228
+ return base + displacement if match.group("sign") == "+" else base - displacement
229
+
230
+
231
+ def _build_xref(
232
+ instruction: IRInstruction,
233
+ to_address: int,
234
+ kind: str,
235
+ names_by_address: dict[int, str],
236
+ target_name: str | None = None,
237
+ confidence: Confidence = Confidence.CERTAIN,
238
+ ) -> Xref:
239
+ """Assemble one :class:`Xref` together with its evidence record."""
240
+ text = instruction.text
241
+ source = _SOURCE_BY_KIND[kind]
242
+ return Xref(
243
+ from_address=instruction.address,
244
+ to_address=to_address,
245
+ kind=kind,
246
+ instruction=text,
247
+ source=source,
248
+ evidence=Evidence(
249
+ source=source,
250
+ method=_EVIDENCE_METHOD,
251
+ confidence=confidence,
252
+ address=instruction.address,
253
+ detail=text,
254
+ ),
255
+ function=names_by_address.get(to_address),
256
+ target_name=target_name,
257
+ )
258
+
259
+
260
+ def _scan_evidence(xref_count: int) -> Evidence:
261
+ """Build the evidence record for the scan as a whole.
262
+
263
+ The confidence is ``CERTAIN`` because every emitted reference is a read of
264
+ the decoded operand stream; individual indirect references may lower their
265
+ own confidence. Incomplete coverage is reported through
266
+ ``XrefResult.warnings``, ``XrefResult.truncated`` and
267
+ ``XrefResult.indirect_skipped`` instead.
268
+ """
269
+ return Evidence(
270
+ source=_RESULT_EVIDENCE_SOURCE,
271
+ method=_EVIDENCE_METHOD,
272
+ confidence=Confidence.CERTAIN,
273
+ detail=f"{xref_count} xrefs",
274
+ )
275
+
276
+
277
+ def find_xrefs(
278
+ path: str | Path,
279
+ info: BinaryInfo,
280
+ functions: list[FunctionCandidate] | None = None,
281
+ max_bytes: int = DEFAULT_MAX_BYTES,
282
+ ) -> XrefResult:
283
+ """Collect cross references from executable sections.
284
+
285
+ Direct transfers (``call``/``jmp``/conditional jumps to a literal address)
286
+ and ``call`` instructions through an absolute or RIP-relative pointer slot
287
+ are recorded. Indirect calls that cannot be resolved statically are skipped
288
+ and counted in :attr:`XrefResult.indirect_skipped`.
289
+
290
+ Args:
291
+ path: File the metadata in ``info`` was loaded from.
292
+ info: Normalized binary metadata. Its ``imports`` list is used to name
293
+ indirect calls whose pointer slot matches an import address.
294
+ functions: Pre-computed candidates used to name targets;
295
+ :func:`~codevanta.core.functions.discover_functions` runs when omitted.
296
+ max_bytes: Hard ceiling on executable bytes read and decoded.
297
+
298
+ Returns:
299
+ An :class:`XrefResult` with references sorted by ``from_address``.
300
+
301
+ Raises:
302
+ AnalysisError: If ``max_bytes`` is not positive.
303
+ """
304
+ if max_bytes < 1:
305
+ raise AnalysisError("max_bytes must be >= 1")
306
+
307
+ candidates = list(functions) if functions is not None else discover_functions(info)
308
+ names_by_address: dict[int, str] = {
309
+ candidate.address: candidate.name for candidate in candidates if candidate.name
310
+ }
311
+ # Built once: absolute pointer-slot address -> import name.
312
+ import_names_by_address: dict[int, str] = {
313
+ imported.address: imported.name
314
+ for imported in info.imports
315
+ if imported.address is not None and imported.name
316
+ }
317
+
318
+ warnings: list[str] = []
319
+ executable_sections = [
320
+ section
321
+ for section in info.sections
322
+ if section.is_executable and (section.raw_size or section.size) > 0
323
+ ]
324
+ if not executable_sections:
325
+ warnings.append("no executable sections with file bytes were found")
326
+ return XrefResult(
327
+ xrefs=[],
328
+ scanned_sections=[],
329
+ scanned_bytes=0,
330
+ truncated=False,
331
+ evidence=_scan_evidence(0),
332
+ warnings=tuple(warnings),
333
+ )
334
+
335
+ try:
336
+ disassembler = Disassembler(info.architecture, info.bitness, info.endianness)
337
+ except DisassemblyError as exc:
338
+ warnings.append(f"disassembly unavailable: {exc}")
339
+ return XrefResult(
340
+ xrefs=[],
341
+ scanned_sections=[],
342
+ scanned_bytes=0,
343
+ truncated=False,
344
+ evidence=_scan_evidence(0),
345
+ warnings=tuple(warnings),
346
+ )
347
+
348
+ total_bytes = sum(section.raw_size or section.size for section in executable_sections)
349
+ remaining = max_bytes
350
+ truncated = total_bytes > max_bytes
351
+ scanned_sections: list[str] = []
352
+ scanned_bytes = 0
353
+ indirect_skipped = 0
354
+ xrefs: list[Xref] = []
355
+
356
+ for section in executable_sections:
357
+ span = section.raw_size or section.size
358
+ if remaining <= 0:
359
+ truncated = True
360
+ break
361
+ budget = min(remaining, span)
362
+ if budget < span:
363
+ truncated = True
364
+ try:
365
+ data = read_code(path, info, section.virtual_address, budget)
366
+ except (AnalysisError, OSError) as exc:
367
+ warnings.append(f"section {section.name!r}: cannot read bytes ({exc})")
368
+ continue
369
+ if not data:
370
+ warnings.append(f"section {section.name!r}: no file bytes available")
371
+ continue
372
+ remaining -= len(data)
373
+ try:
374
+ disassembly = disassembler.disassemble(data, base_address=section.virtual_address)
375
+ except DisassemblyError as exc:
376
+ warnings.append(f"section {section.name!r}: disassembly failed ({exc})")
377
+ continue
378
+
379
+ scanned_sections.append(section.name)
380
+ scanned_bytes += disassembly.consumed_bytes
381
+ truncated = truncated or disassembly.truncated
382
+
383
+ for instruction in disassembly.instructions:
384
+ kind = _kind_for(instruction.mnemonic)
385
+ if kind is None:
386
+ continue
387
+ to_address = _direct_target(instruction.operands)
388
+ if to_address is not None:
389
+ xrefs.append(_build_xref(instruction, to_address, kind, names_by_address))
390
+ continue
391
+ # From here on only indirect calls are interesting; indirect jumps
392
+ # and conditional branches stay unresolved without being counted.
393
+ if kind != "call":
394
+ continue
395
+ slot_address = _memory_pointer_target(
396
+ instruction.operands, instruction.address, instruction.size
397
+ )
398
+ if slot_address is None:
399
+ indirect_skipped += 1
400
+ continue
401
+ import_name = import_names_by_address.get(slot_address)
402
+ if import_name is not None:
403
+ xrefs.append(
404
+ _build_xref(
405
+ instruction,
406
+ slot_address,
407
+ "call_import",
408
+ names_by_address,
409
+ target_name=import_name,
410
+ )
411
+ )
412
+ else:
413
+ # The slot address is certain, the callee it holds is not.
414
+ xrefs.append(
415
+ _build_xref(
416
+ instruction,
417
+ slot_address,
418
+ "call_indirect",
419
+ names_by_address,
420
+ confidence=Confidence.HIGH,
421
+ )
422
+ )
423
+
424
+ xrefs.sort(key=lambda xref: (xref.from_address, xref.to_address))
425
+ return XrefResult(
426
+ xrefs=xrefs,
427
+ scanned_sections=scanned_sections,
428
+ scanned_bytes=scanned_bytes,
429
+ truncated=truncated,
430
+ evidence=_scan_evidence(len(xrefs)),
431
+ warnings=tuple(warnings),
432
+ indirect_skipped=indirect_skipped,
433
+ )
434
+
@@ -0,0 +1 @@
1
+ """API layer (FastAPI service) — phase 2+."""
@@ -0,0 +1 @@
1
+ """CODEVANTA command line package."""