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,31 @@
1
+ """CODEVANTA analysis core: loader, disassembler, strings, functions."""
2
+
3
+ from .disassembler import Disassembler, DisassemblyError, DisassemblyResult
4
+ from .functions import FunctionCandidate, discover_functions
5
+ from .loader import (
6
+ BinaryInfo,
7
+ BinaryLoader,
8
+ BinaryLoadError,
9
+ ExportInfo,
10
+ ImportInfo,
11
+ SectionInfo,
12
+ SymbolInfo,
13
+ )
14
+ from .strings import StringExtractor, StringResult
15
+
16
+ __all__ = [
17
+ "BinaryInfo",
18
+ "BinaryLoader",
19
+ "BinaryLoadError",
20
+ "Disassembler",
21
+ "DisassemblyError",
22
+ "DisassemblyResult",
23
+ "ExportInfo",
24
+ "FunctionCandidate",
25
+ "ImportInfo",
26
+ "SectionInfo",
27
+ "StringExtractor",
28
+ "StringResult",
29
+ "SymbolInfo",
30
+ "discover_functions",
31
+ ]
@@ -0,0 +1,121 @@
1
+ """Capstone-backed disassembly for supported architectures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import capstone
9
+
10
+ from codevanta.evidence.confidence import Confidence
11
+ from codevanta.evidence.models import Evidence
12
+ from codevanta.ir.instruction import IRInstruction
13
+
14
+
15
+ class DisassemblyError(Exception):
16
+ """Raised when disassembly cannot be performed."""
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DisassemblyResult:
21
+ """Outcome of a single disassembly request."""
22
+
23
+ instructions: list[IRInstruction]
24
+ base_address: int
25
+ consumed_bytes: int
26
+ evidence: Evidence
27
+ truncated: bool = False
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "base_address": f"0x{self.base_address:x}",
32
+ "consumed_bytes": self.consumed_bytes,
33
+ "truncated": self.truncated,
34
+ "instruction_count": len(self.instructions),
35
+ "instructions": [i.to_dict() for i in self.instructions],
36
+ "evidence": self.evidence.to_dict(),
37
+ }
38
+
39
+
40
+ _ARCH_MAP: dict[tuple[str, int], tuple[int, int]] = {
41
+ ("x86", 32): (capstone.CS_ARCH_X86, capstone.CS_MODE_32),
42
+ ("x86", 64): (capstone.CS_ARCH_X86, capstone.CS_MODE_64),
43
+ ("x86_64", 64): (capstone.CS_ARCH_X86, capstone.CS_MODE_64),
44
+ ("arm", 32): (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
45
+ ("aarch64", 64): (capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM),
46
+ ("arm64", 64): (capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM),
47
+ ("mips", 32): (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS32),
48
+ ("mips", 64): (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS64),
49
+ }
50
+
51
+
52
+ class Disassembler:
53
+ """Disassembles machine code with Capstone."""
54
+
55
+ MAX_BYTES = 16 * 1024 * 1024
56
+ MAX_INSTRUCTIONS = 1_000_000
57
+
58
+ def __init__(self, architecture: str, bitness: int, endianness: str = "little") -> None:
59
+ self.architecture = architecture
60
+ self.bitness = bitness
61
+ self.endianness = endianness
62
+ arch_mode = _ARCH_MAP.get((architecture.lower(), bitness))
63
+ if arch_mode is None:
64
+ raise DisassemblyError(f"Unsupported architecture: {architecture} ({bitness}-bit)")
65
+ cs_arch, cs_mode = arch_mode
66
+ if endianness.lower() == "big":
67
+ if cs_arch in (capstone.CS_ARCH_ARM, capstone.CS_ARCH_MIPS):
68
+ cs_mode |= capstone.CS_MODE_BIG_ENDIAN
69
+ elif cs_arch == capstone.CS_ARCH_X86:
70
+ raise DisassemblyError("x86 is little-endian only")
71
+ elif cs_arch == capstone.CS_ARCH_ARM64:
72
+ raise DisassemblyError("big-endian AArch64 is not supported")
73
+ self._cs = capstone.Cs(cs_arch, cs_mode)
74
+ self._cs.detail = False
75
+
76
+ def disassemble(
77
+ self,
78
+ data: bytes,
79
+ base_address: int,
80
+ max_instructions: int = 0,
81
+ max_bytes: int = 0,
82
+ ) -> DisassemblyResult:
83
+ """Decode ``data`` starting at ``base_address``."""
84
+ if not data:
85
+ raise DisassemblyError("No bytes supplied for disassembly")
86
+ if base_address < 0:
87
+ raise DisassemblyError("base_address must be non-negative")
88
+ byte_limit = min(len(data), max_bytes if max_bytes > 0 else self.MAX_BYTES)
89
+ instruction_limit = max_instructions if max_instructions > 0 else self.MAX_INSTRUCTIONS
90
+ window = data[:byte_limit]
91
+ instructions: list[IRInstruction] = []
92
+ consumed = 0
93
+ truncated = byte_limit < len(data)
94
+ for insn in self._cs.disasm(window, base_address):
95
+ instructions.append(
96
+ IRInstruction(
97
+ address=insn.address,
98
+ size=insn.size,
99
+ mnemonic=insn.mnemonic,
100
+ operands=insn.op_str,
101
+ raw_bytes=bytes(insn.bytes),
102
+ )
103
+ )
104
+ consumed += insn.size
105
+ if len(instructions) >= instruction_limit:
106
+ truncated = truncated or consumed < len(window)
107
+ break
108
+ evidence = Evidence(
109
+ source="capstone",
110
+ method="linear_disassembly",
111
+ confidence=Confidence.CERTAIN,
112
+ address=base_address,
113
+ detail=f"{len(instructions)} instruction(s) decoded",
114
+ )
115
+ return DisassemblyResult(
116
+ instructions=instructions,
117
+ base_address=base_address,
118
+ consumed_bytes=consumed,
119
+ evidence=evidence,
120
+ truncated=truncated,
121
+ )
@@ -0,0 +1,237 @@
1
+ """Per-section Shannon entropy for packing triage.
2
+
3
+ Entropy itself is a plain measurement over file bytes (confidence ``certain``);
4
+ the *packed/compressed* interpretation is a heuristic and is labelled as such
5
+ (``medium`` confidence, threshold stated in the evidence detail). Sections
6
+ without file bytes (e.g. ``S_ZEROFILL``) are reported with ``entropy=None``
7
+ rather than a fabricated value. Sampling is capped per section because entropy
8
+ converges quickly; the sample size actually used is part of every result.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from codevanta.core.loader import BinaryInfo
19
+ from codevanta.core.report import AnalysisError, read_code
20
+ from codevanta.evidence.confidence import Confidence
21
+ from codevanta.evidence.models import Evidence
22
+
23
+ __all__ = [
24
+ "DEFAULT_MAX_SAMPLE_BYTES",
25
+ "DEFAULT_MIN_SAMPLE_BYTES",
26
+ "PACKED_HINT_THRESHOLD",
27
+ "EntropyReport",
28
+ "SectionEntropy",
29
+ "analyze_entropy",
30
+ ]
31
+
32
+ #: Entropy (bits/byte) at or above which a section is flagged as possibly
33
+ #: packed or compressed. 7.2 is a widely used rule of thumb, not a law.
34
+ PACKED_HINT_THRESHOLD = 7.2
35
+
36
+ #: Default cap on bytes read per section; entropy converges well before this.
37
+ DEFAULT_MAX_SAMPLE_BYTES = 65_536
38
+
39
+ #: Below this sample size the entropy is still reported but too noisy to mean
40
+ #: much, so the packed hint is suppressed and a caveat is attached instead.
41
+ DEFAULT_MIN_SAMPLE_BYTES = 256
42
+
43
+ #: Hard ceiling accepted for ``max_sample_bytes`` (bounded-resource rule).
44
+ MAX_SAMPLE_LIMIT = 4 * 1024 * 1024
45
+
46
+ _MAX_ENTROPY = 8.0
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class SectionEntropy:
51
+ """Entropy of one section, sampled from its file bytes."""
52
+
53
+ name: str
54
+ virtual_address: int
55
+ size: int
56
+ entropy: float | None
57
+ sampled_bytes: int
58
+ is_executable: bool
59
+ packed_hint: bool
60
+ evidence: Evidence
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ data: dict[str, Any] = {
64
+ "name": self.name,
65
+ "virtual_address": f"0x{self.virtual_address:x}",
66
+ "size": self.size,
67
+ "entropy": round(self.entropy, 4) if self.entropy is not None else None,
68
+ "sampled_bytes": self.sampled_bytes,
69
+ "executable": self.is_executable,
70
+ "packed_hint": self.packed_hint,
71
+ "evidence": self.evidence.to_dict(),
72
+ }
73
+ return data
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class EntropyReport:
78
+ """Entropy triage across every section of one binary."""
79
+
80
+ sections: list[SectionEntropy]
81
+ threshold: float
82
+ warnings: tuple[str, ...]
83
+ evidence: Evidence
84
+
85
+ @property
86
+ def packed_candidates(self) -> list[SectionEntropy]:
87
+ """Sections whose entropy reached the packed-compression threshold."""
88
+ return [section for section in self.sections if section.packed_hint]
89
+
90
+ def to_dict(self) -> dict[str, Any]:
91
+ packed = self.packed_candidates
92
+ return {
93
+ "section_count": len(self.sections),
94
+ "threshold": self.threshold,
95
+ "packed_candidate_count": len(packed),
96
+ "packed_candidates": [section.name for section in packed],
97
+ "sections": [section.to_dict() for section in self.sections],
98
+ "warnings": list(self.warnings),
99
+ "evidence": self.evidence.to_dict(),
100
+ }
101
+
102
+
103
+ def shannon_entropy(data: bytes) -> float:
104
+ """Return the Shannon entropy of ``data`` in bits per byte (0..8)."""
105
+ if not data:
106
+ return 0.0
107
+ counts = [0] * 256
108
+ for byte in data:
109
+ counts[byte] += 1
110
+ total = len(data)
111
+ entropy = 0.0
112
+ for count in counts:
113
+ if count:
114
+ probability = count / total
115
+ entropy -= probability * math.log2(probability)
116
+ return min(entropy, _MAX_ENTROPY)
117
+
118
+
119
+ def analyze_entropy(
120
+ path: str | Path,
121
+ info: BinaryInfo,
122
+ *,
123
+ threshold: float = PACKED_HINT_THRESHOLD,
124
+ max_sample_bytes: int = DEFAULT_MAX_SAMPLE_BYTES,
125
+ min_sample_bytes: int = DEFAULT_MIN_SAMPLE_BYTES,
126
+ ) -> EntropyReport:
127
+ """Compute per-section entropy and flag possible packing.
128
+
129
+ Args:
130
+ path: File the metadata in ``info`` was loaded from (read-only).
131
+ info: Normalized binary metadata.
132
+ threshold: Entropy at or above which ``packed_hint`` is set (0 < t <= 8).
133
+ max_sample_bytes: Bytes read per section at most (1 .. 4 MiB).
134
+ min_sample_bytes: Sample size below which the packed hint is suppressed.
135
+
136
+ Returns:
137
+ An :class:`EntropyReport` with one entry per section, sorted by
138
+ entropy (descending), sections without file bytes carrying ``None``.
139
+
140
+ Raises:
141
+ AnalysisError: If any limit is out of range.
142
+ """
143
+ if not 0.0 < threshold <= _MAX_ENTROPY:
144
+ raise AnalysisError(f"threshold must be in (0, {_MAX_ENTROPY}]")
145
+ if max_sample_bytes < 1:
146
+ raise AnalysisError("max_sample_bytes must be >= 1")
147
+ if max_sample_bytes > MAX_SAMPLE_LIMIT:
148
+ raise AnalysisError(f"max_sample_bytes too large (limit {MAX_SAMPLE_LIMIT})")
149
+ if min_sample_bytes < 1:
150
+ raise AnalysisError("min_sample_bytes must be >= 1")
151
+
152
+ warnings: list[str] = []
153
+ results: list[SectionEntropy] = []
154
+ for section in info.sections:
155
+ entropy: float | None = None
156
+ sampled = 0
157
+ # File-backed means: PE/Mach-O raw bytes, or an ELF section with a real
158
+ # file offset (ELF loader metadata carries raw_size=0 by design).
159
+ if section.raw_size > 0 or section.offset > 0:
160
+ try:
161
+ data = read_code(path, info, section.virtual_address, max_sample_bytes)
162
+ except (AnalysisError, OSError) as exc:
163
+ warnings.append(
164
+ f"section {section.name!r}: cannot read bytes ({exc}); entropy unknown"
165
+ )
166
+ else:
167
+ sampled = len(data)
168
+ if sampled:
169
+ entropy = shannon_entropy(data)
170
+ else:
171
+ warnings.append(
172
+ f"section {section.name!r}: no file bytes; entropy unknown"
173
+ )
174
+ else:
175
+ warnings.append(
176
+ f"section {section.name!r}: not file-backed (size {section.size}); "
177
+ "entropy unknown"
178
+ )
179
+
180
+ packed_hint = entropy is not None and entropy >= threshold
181
+ detail_suffix = ""
182
+ if entropy is None:
183
+ detail = "entropy not computable: no file bytes"
184
+ confidence = Confidence.LOW
185
+ else:
186
+ confidence = Confidence.CERTAIN
187
+ if sampled < min_sample_bytes:
188
+ packed_hint = False
189
+ detail_suffix = "; sample too small for a packing judgment"
190
+ detail = (
191
+ f"entropy {entropy:.3f} bits/byte from {sampled} sampled byte(s)"
192
+ + detail_suffix
193
+ )
194
+ else:
195
+ detail = (
196
+ f"entropy {entropy:.3f} bits/byte from {sampled} sampled byte(s); "
197
+ f"packed hint at >={threshold} is a heuristic, not proof"
198
+ if packed_hint
199
+ else f"entropy {entropy:.3f} bits/byte from {sampled} sampled byte(s)"
200
+ )
201
+ results.append(
202
+ SectionEntropy(
203
+ name=section.name,
204
+ virtual_address=section.virtual_address,
205
+ size=section.size,
206
+ entropy=entropy,
207
+ sampled_bytes=sampled,
208
+ is_executable=section.is_executable,
209
+ packed_hint=packed_hint,
210
+ evidence=Evidence(
211
+ source="entropy.section",
212
+ method="shannon_entropy",
213
+ confidence=confidence,
214
+ address=section.virtual_address,
215
+ detail=detail,
216
+ ),
217
+ )
218
+ )
219
+
220
+ results.sort(key=lambda item: (-1 if item.entropy is None else -item.entropy, item.name))
221
+ packed_names = [item.name for item in results if item.packed_hint]
222
+ report_evidence = Evidence(
223
+ source="entropy.triage",
224
+ method="per_section_shannon_entropy",
225
+ confidence=Confidence.CERTAIN,
226
+ detail=(
227
+ f"{len(results)} section(s) measured; threshold {threshold}; "
228
+ f"{len(packed_names)} packed candidate(s)"
229
+ + (f" ({', '.join(packed_names)})" if packed_names else "")
230
+ ),
231
+ )
232
+ return EntropyReport(
233
+ sections=results,
234
+ threshold=threshold,
235
+ warnings=tuple(warnings),
236
+ evidence=report_evidence,
237
+ )
@@ -0,0 +1,176 @@
1
+ """Function candidate discovery with explicit provenance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from codevanta.evidence.confidence import Confidence
9
+ from codevanta.evidence.models import Evidence
10
+
11
+ from .loader import BinaryInfo
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class FunctionCandidate:
16
+ """A function candidate and the evidence that identified it."""
17
+
18
+ address: int
19
+ name: str | None
20
+ size: int | None
21
+ source: str
22
+ confidence: Confidence
23
+ evidence: Evidence
24
+ aliases: tuple[str, ...] = field(default_factory=tuple)
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ data: dict[str, Any] = {
28
+ "address": f"0x{self.address:x}",
29
+ "source": self.source,
30
+ "confidence": self.confidence.value,
31
+ "evidence": self.evidence.to_dict(),
32
+ }
33
+ if self.name:
34
+ data["name"] = self.name
35
+ if self.size is not None:
36
+ data["size"] = self.size
37
+ if self.aliases:
38
+ data["aliases"] = list(self.aliases)
39
+ return data
40
+
41
+
42
+ @dataclass
43
+ class _Draft:
44
+ address: int
45
+ name: str | None
46
+ size: int | None
47
+ source: str
48
+ confidence: Confidence
49
+ evidence: Evidence
50
+ aliases: list[str] = field(default_factory=list)
51
+
52
+
53
+ #: Evidence source for exported functions, by binary format.
54
+ _EXPORT_SOURCES: dict[str, str] = {
55
+ "PE": "pe.export_table",
56
+ "ELF": "elf.dynsym",
57
+ "MachO": "macho.exported_symbols",
58
+ }
59
+
60
+ #: Evidence source for symbol-table functions, by binary format.
61
+ _SYMBOL_SOURCES: dict[str, str] = {
62
+ "PE": "pe.coff_symbols",
63
+ "ELF": "elf.symtab",
64
+ "MachO": "macho.symbol_table",
65
+ "DEX": "dex.method_ids",
66
+ }
67
+
68
+ #: Discovery confidence for symbol-table candidates, by binary format. Mach-O
69
+ #: symbol entries do not encode kind, so function-ness is inferred from
70
+ #: executable-section membership and deserves slightly lower confidence.
71
+ _SYMBOL_CONFIDENCE: dict[str, Confidence] = {
72
+ "PE": Confidence.HIGH,
73
+ "ELF": Confidence.HIGH,
74
+ "MachO": Confidence.MEDIUM,
75
+ }
76
+
77
+
78
+ def discover_functions(info: BinaryInfo) -> list[FunctionCandidate]:
79
+ """Discover function candidates from structural evidence only.
80
+
81
+ Phase 1 reports exported functions, function symbols, and the entry point.
82
+ Speculative heuristics are intentionally excluded; every candidate states
83
+ where it came from and how much confidence it deserves.
84
+ """
85
+ drafts: dict[int, _Draft] = {}
86
+
87
+ def add(
88
+ address: int | None,
89
+ name: str | None,
90
+ size: int | None,
91
+ source: str,
92
+ confidence: Confidence,
93
+ method: str,
94
+ detail: str,
95
+ ) -> None:
96
+ if not address or address <= 0:
97
+ return
98
+ clean_name = name or None
99
+ evidence = Evidence(
100
+ source=source,
101
+ method=method,
102
+ confidence=confidence,
103
+ address=address,
104
+ detail=detail,
105
+ )
106
+ existing = drafts.get(address)
107
+ if existing is None:
108
+ drafts[address] = _Draft(
109
+ address=address,
110
+ name=clean_name,
111
+ size=size,
112
+ source=source,
113
+ confidence=confidence,
114
+ evidence=evidence,
115
+ )
116
+ return
117
+ if confidence.score > existing.confidence.score:
118
+ if existing.name and existing.name != clean_name:
119
+ existing.aliases.append(existing.name)
120
+ existing.name = clean_name
121
+ existing.size = size if size is not None else existing.size
122
+ existing.source = source
123
+ existing.confidence = confidence
124
+ existing.evidence = evidence
125
+ elif clean_name and clean_name != existing.name and clean_name not in existing.aliases:
126
+ existing.aliases.append(clean_name)
127
+
128
+ for export in info.exports:
129
+ add(
130
+ export.address,
131
+ export.name,
132
+ None,
133
+ _EXPORT_SOURCES.get(info.format, "binary.exports"),
134
+ Confidence.HIGH,
135
+ "exported_function",
136
+ "exported function entry",
137
+ )
138
+
139
+ for symbol in info.symbols:
140
+ if not symbol.is_function:
141
+ continue
142
+ add(
143
+ symbol.address,
144
+ symbol.name,
145
+ symbol.size or None,
146
+ _SYMBOL_SOURCES.get(info.format, "binary.symbol_table"),
147
+ _SYMBOL_CONFIDENCE.get(info.format, Confidence.HIGH),
148
+ "function_symbol",
149
+ "function symbol from the symbol table",
150
+ )
151
+
152
+ if info.entry_point:
153
+ add(
154
+ info.entry_point,
155
+ None,
156
+ None,
157
+ "binary.entry_point",
158
+ Confidence.MEDIUM,
159
+ "entry_point",
160
+ "program entry point; a code candidate, not a proven function boundary",
161
+ )
162
+
163
+ candidates = [
164
+ FunctionCandidate(
165
+ address=draft.address,
166
+ name=draft.name,
167
+ size=draft.size,
168
+ source=draft.source,
169
+ confidence=draft.confidence,
170
+ evidence=draft.evidence,
171
+ aliases=tuple(dict.fromkeys(draft.aliases)),
172
+ )
173
+ for draft in drafts.values()
174
+ ]
175
+ candidates.sort(key=lambda c: c.address)
176
+ return candidates