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,306 @@
1
+ """Shared analysis helpers used by the CLI and the MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from codevanta.evidence.confidence import Confidence
9
+ from codevanta.evidence.models import Evidence
10
+ from codevanta.evidence.provenance import Provenance
11
+
12
+ from .disassembler import Disassembler, DisassemblyError, DisassemblyResult
13
+ from .functions import discover_functions
14
+ from .loader import BinaryInfo, BinaryLoader
15
+ from .strings import StringExtractor
16
+
17
+
18
+ class AnalysisError(Exception):
19
+ """Raised when an analysis request cannot be satisfied."""
20
+
21
+
22
+ MAX_DISASSEMBLY_COUNT = 100_000
23
+ MAX_STRING_LIMIT = 200_000
24
+ #: Hard ceiling for ``xref_limit`` (mirrors ``MAX_STRING_LIMIT``).
25
+ MAX_XREF_LIMIT = 200_000
26
+ #: Cross references kept in the report by default (the scan budget stays at
27
+ #: :data:`codevanta.analysis.xrefs.DEFAULT_MAX_BYTES`).
28
+ DEFAULT_XREF_LIMIT = 2_000
29
+
30
+ #: Ceiling on call-graph nodes embedded in a report (bounded work).
31
+ REPORT_MAX_CALLGRAPH_FUNCTIONS = 1000
32
+
33
+ #: Ceiling on call-graph edges embedded in a report (bounded work).
34
+ REPORT_MAX_CALLGRAPH_EDGES = 5000
35
+
36
+ #: Instruction budget for the entry-point CFG embedded in a report (bounded
37
+ #: work; the :func:`codevanta.analysis.cfg.build_cfg` default, made explicit so
38
+ #: the report cap is visible next to the other section caps).
39
+ REPORT_MAX_CFG_INSTRUCTIONS = 2000
40
+
41
+ #: Block budget for the entry-point CFG embedded in a report (bounded work;
42
+ #: likewise the analyzer default).
43
+ REPORT_MAX_CFG_BLOCKS = 500
44
+
45
+
46
+ def load_binary(path: str | Path) -> BinaryInfo:
47
+ """Load a binary and return normalized metadata."""
48
+ return BinaryLoader().load(path)
49
+
50
+
51
+ def read_code(path: str | Path, info: BinaryInfo, address: int, want_bytes: int) -> bytes:
52
+ """Read bytes for ``address``; raise when the address is unmapped."""
53
+ offset = info.va_to_offset(address)
54
+ section = info.section_for_va(address)
55
+ if offset is None or section is None:
56
+ raise AnalysisError(f"address 0x{address:x} is not mapped to file bytes")
57
+ remaining = (section.offset + (section.raw_size or section.size)) - offset
58
+ if remaining <= 0:
59
+ raise AnalysisError(f"address 0x{address:x} has no file bytes")
60
+ with open(path, "rb") as handle:
61
+ handle.seek(offset)
62
+ return handle.read(min(want_bytes, remaining))
63
+
64
+
65
+ def resolve_target(info: BinaryInfo, address: int | None, function: str | None) -> int:
66
+ """Resolve the disassembly start from an address, a function name, or the entry point."""
67
+ if address is not None:
68
+ return address
69
+ if function:
70
+ match = next((c for c in discover_functions(info) if c.name == function), None)
71
+ if match is None:
72
+ raise AnalysisError(f"function {function!r} not found")
73
+ return match.address
74
+ if info.entry_point is not None:
75
+ return info.entry_point
76
+ raise AnalysisError("no address supplied and binary has no entry point")
77
+
78
+
79
+ def disassemble_region(
80
+ path: str | Path,
81
+ info: BinaryInfo,
82
+ start: int,
83
+ count: int = 50,
84
+ ) -> DisassemblyResult:
85
+ """Disassemble ``count`` instructions at ``start`` inside an executable section."""
86
+ if count < 1:
87
+ raise AnalysisError("count must be >= 1")
88
+ if count > MAX_DISASSEMBLY_COUNT:
89
+ raise AnalysisError(f"count too large (limit {MAX_DISASSEMBLY_COUNT})")
90
+ section = info.section_for_va(start)
91
+ if section is None or not section.is_executable:
92
+ raise AnalysisError(f"address 0x{start:x} is not inside an executable section")
93
+ code = read_code(path, info, start, count * 16)
94
+ disassembler = Disassembler(info.architecture, info.bitness, info.endianness)
95
+ return disassembler.disassemble(code, base_address=start, max_instructions=count)
96
+
97
+
98
+ def _empty_cfg(address: int | None, warnings: list[str]) -> dict[str, Any]:
99
+ """Build the degraded ``cfg`` payload used when no graph could be produced.
100
+
101
+ Mirrors :meth:`codevanta.analysis.cfg.FunctionCFG.to_dict` key for key so
102
+ consumers of the report can rely on one shape; ``warnings`` carries the
103
+ reason the sweep produced nothing.
104
+ """
105
+ evidence = Evidence(
106
+ source="cfg.linear_sweep",
107
+ method="linear_sweep",
108
+ confidence=Confidence.UNKNOWN,
109
+ address=address,
110
+ detail="no blocks; the control-flow graph was not built",
111
+ )
112
+ return {
113
+ "function_address": f"0x{address:x}" if address is not None else None,
114
+ "function_name": None,
115
+ "block_count": 0,
116
+ "instruction_count": 0,
117
+ "truncated": False,
118
+ "warnings": list(warnings),
119
+ "blocks": [],
120
+ "evidence": evidence.to_dict(),
121
+ }
122
+
123
+
124
+ def build_report(
125
+ path: str | Path,
126
+ string_limit: int = 5000,
127
+ xref_limit: int = DEFAULT_XREF_LIMIT,
128
+ include_callgraph: bool = False,
129
+ include_xrefs: bool = False,
130
+ include_cfg: bool = False,
131
+ ) -> dict[str, Any]:
132
+ """Build the phase 1 JSON report for ``path``.
133
+
134
+ The report always contains ``provenance``, ``binary``, ``functions``,
135
+ ``strings`` and ``disassembly``. Nothing more than the file bytes is read and
136
+ nothing is executed.
137
+
138
+ The phase 2 engines are additive and opt-in, one section each:
139
+ ``include_xrefs`` adds ``report["xrefs"]``, ``include_cfg`` adds
140
+ ``report["cfg"]`` and ``include_callgraph`` adds ``report["callgraph"]``.
141
+ Without those arguments the payload is exactly the phase 1 report, so no
142
+ existing consumer sees a new key. A requested section whose engine has
143
+ nothing to decode still appears, as a warning-carrying empty result: the
144
+ phase 2 sections never fail the report.
145
+
146
+ Args:
147
+ path: Binary to analyse.
148
+ string_limit: Maximum strings kept under ``report["strings"]``
149
+ (``0 .. MAX_STRING_LIMIT``).
150
+ xref_limit: Maximum cross references kept under
151
+ ``report["xrefs"]["items"]`` (``0 .. MAX_XREF_LIMIT``); ``count``
152
+ still reports the true total found by the scan.
153
+ include_callgraph: When true, a ``"callgraph"`` section holding
154
+ :meth:`~codevanta.analysis.callgraph.CallGraph.to_dict` is added.
155
+ When false the key is omitted entirely, so the payload is unchanged
156
+ for callers that do not ask for a graph.
157
+ include_xrefs: When true, an ``"xrefs"`` section with the scan result and
158
+ its evidence is added.
159
+ include_cfg: When true, a ``"cfg"`` section with the entry-point
160
+ control-flow graph is added.
161
+
162
+ Returns:
163
+ The JSON-serializable report.
164
+
165
+ Raises:
166
+ AnalysisError: If ``string_limit`` or ``xref_limit`` is out of range, or
167
+ -- when ``include_callgraph`` is true -- if the graph cannot be
168
+ built. The phase 2 sections never raise: an engine failure degrades
169
+ to a warning inside that section instead of failing the report.
170
+
171
+ Note:
172
+ **Layering.** :mod:`codevanta.core.report` is a *core* module and must not
173
+ import :mod:`codevanta.analysis` at import time: that package imports
174
+ :class:`AnalysisError` from here, so a module-level import would create a
175
+ cycle and would also drag the analysis layer into every core consumer.
176
+ The dependency is therefore resolved with a **lazy import inside this
177
+ function**, executed only when the corresponding section is requested:
178
+ :func:`~codevanta.analysis.callgraph.build_call_graph` for
179
+ ``include_callgraph``, :func:`~codevanta.analysis.xrefs.find_xrefs` for
180
+ ``include_xrefs`` and :func:`~codevanta.analysis.cfg.build_cfg` for
181
+ ``include_cfg``.
182
+
183
+ Note:
184
+ **Bounded work.** The embedded graph is capped at
185
+ ``REPORT_MAX_CALLGRAPH_FUNCTIONS`` (1000) nodes and
186
+ ``REPORT_MAX_CALLGRAPH_EDGES`` (5000) edges, which are well below the
187
+ call-graph defaults (:data:`~codevanta.analysis.callgraph.DEFAULT_MAX_FUNCTIONS`
188
+ / ``DEFAULT_MAX_EDGES``) so a report stays a bounded payload. When either
189
+ cap cuts the result short, ``report["callgraph"]["truncated"]`` is true.
190
+ The pre-computed function candidates are reused instead of rediscovering
191
+ them, so the report never scans the binary twice for candidates.
192
+
193
+ The requested phase 2 sections are bounded too, and say so in their own
194
+ payloads. The xref scan decodes at most
195
+ :data:`~codevanta.analysis.xrefs.DEFAULT_MAX_BYTES` (4 MB) of executable
196
+ bytes -- a budget cut sets ``report["xrefs"]["scan_truncated"]`` -- and
197
+ only ``xref_limit`` items are embedded, which sets
198
+ ``report["xrefs"]["truncated"]`` the same way the strings section
199
+ reports truncation. The CFG runs with
200
+ ``REPORT_MAX_CFG_INSTRUCTIONS`` (2000) instructions and
201
+ ``REPORT_MAX_CFG_BLOCKS`` (500) blocks, the analyzer's own defaults made
202
+ explicit, and marks ``report["cfg"]["truncated"]`` when either limit cut
203
+ the sweep.
204
+ """
205
+ if string_limit < 0:
206
+ raise AnalysisError("string_limit must be >= 0")
207
+ if string_limit > MAX_STRING_LIMIT:
208
+ raise AnalysisError(f"string_limit too large (limit {MAX_STRING_LIMIT})")
209
+ if xref_limit < 0:
210
+ raise AnalysisError("xref_limit must be >= 0")
211
+ if xref_limit > MAX_XREF_LIMIT:
212
+ raise AnalysisError(f"xref_limit too large (limit {MAX_XREF_LIMIT})")
213
+
214
+ info = load_binary(path)
215
+ provenance = (
216
+ Provenance()
217
+ .add_step("load_binary")
218
+ .add_step("discover_functions")
219
+ .add_step("extract_strings")
220
+ )
221
+ candidates = discover_functions(info)
222
+ all_strings = StringExtractor(min_length=4).extract(path, info=info)
223
+ kept = all_strings[:string_limit]
224
+ disassembly: dict[str, Any] | None = None
225
+ if info.entry_point is not None:
226
+ try:
227
+ disassembly = disassemble_region(path, info, info.entry_point, count=32).to_dict()
228
+ except (AnalysisError, DisassemblyError):
229
+ disassembly = None
230
+ xrefs_payload: dict[str, Any] | None = None
231
+ cfg_payload: dict[str, Any] | None = None
232
+ if include_xrefs:
233
+ # Lazy import on purpose: core must not import the analysis package at
234
+ # module import time (analysis imports AnalysisError from this module),
235
+ # and a caller that does not want the section pays nothing for it.
236
+ from codevanta.analysis.xrefs import DEFAULT_MAX_BYTES, find_xrefs
237
+
238
+ provenance.add_step("find_xrefs")
239
+ # find_xrefs never raises for undecodable input: it reports degraded
240
+ # coverage through warnings (no executable sections, unsupported
241
+ # architecture, unreadable sections) that are passed straight through.
242
+ xref_scan = find_xrefs(path, info, functions=candidates, max_bytes=DEFAULT_MAX_BYTES)
243
+ kept_xrefs = xref_scan.xrefs[:xref_limit]
244
+ xrefs_payload = {
245
+ "count": len(xref_scan.xrefs),
246
+ "returned": len(kept_xrefs),
247
+ "truncated": len(kept_xrefs) < len(xref_scan.xrefs),
248
+ "items": [xref.to_dict() for xref in kept_xrefs],
249
+ "scanned_sections": list(xref_scan.scanned_sections),
250
+ "scanned_bytes": xref_scan.scanned_bytes,
251
+ "scan_truncated": xref_scan.truncated,
252
+ "indirect_skipped": xref_scan.indirect_skipped,
253
+ "warnings": list(xref_scan.warnings),
254
+ "evidence": xref_scan.evidence.to_dict(),
255
+ }
256
+ if include_cfg:
257
+ # Lazy import, same reason as find_xrefs above.
258
+ from codevanta.analysis.cfg import build_cfg
259
+
260
+ provenance.add_step("build_cfg")
261
+ if info.entry_point is None:
262
+ cfg_payload = _empty_cfg(None, ["no entry point; control-flow graph not built"])
263
+ else:
264
+ try:
265
+ cfg_payload = build_cfg(
266
+ path,
267
+ info,
268
+ info.entry_point,
269
+ max_instructions=REPORT_MAX_CFG_INSTRUCTIONS,
270
+ max_blocks=REPORT_MAX_CFG_BLOCKS,
271
+ ).to_dict()
272
+ except (AnalysisError, DisassemblyError) as exc:
273
+ cfg_payload = _empty_cfg(
274
+ info.entry_point, [f"control-flow graph not built: {exc}"]
275
+ )
276
+
277
+ report: dict[str, Any] = {
278
+ "provenance": provenance.to_dict(),
279
+ "binary": info.to_dict(),
280
+ "functions": [c.to_dict() for c in candidates],
281
+ "strings": {
282
+ "count": len(all_strings),
283
+ "returned": len(kept),
284
+ "truncated": len(kept) < len(all_strings),
285
+ "items": [s.to_dict() for s in kept],
286
+ },
287
+ "disassembly": disassembly,
288
+ }
289
+ if include_callgraph:
290
+ # Lazy import on purpose: core must not import the analysis package at
291
+ # module import time (analysis imports AnalysisError from this module).
292
+ from codevanta.analysis.callgraph import build_call_graph
293
+
294
+ graph = build_call_graph(
295
+ path,
296
+ info,
297
+ functions=candidates,
298
+ max_functions=REPORT_MAX_CALLGRAPH_FUNCTIONS,
299
+ max_edges=REPORT_MAX_CALLGRAPH_EDGES,
300
+ )
301
+ report["callgraph"] = graph.to_dict()
302
+ if xrefs_payload is not None:
303
+ report["xrefs"] = xrefs_payload
304
+ if cfg_payload is not None:
305
+ report["cfg"] = cfg_payload
306
+ return report
@@ -0,0 +1,109 @@
1
+ """ASCII and UTF-16 string extraction with location mapping.
2
+
3
+ The UTF-16 byte order follows the binary's endianness when metadata is
4
+ available: little-endian targets are scanned as UTF-16LE, big-endian targets
5
+ as UTF-16BE. Without metadata the scan defaults to UTF-16LE and never guesses
6
+ a byte swap, so little-endian results cannot be fabricated from shifted
7
+ matches inside big-endian data (and vice versa).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from codevanta.evidence.confidence import Confidence
18
+ from codevanta.evidence.models import Evidence
19
+
20
+ from .loader import BinaryInfo
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class StringResult:
25
+ """A string found in the file with its offset and mapped address."""
26
+
27
+ offset: int
28
+ address: int | None
29
+ text: str
30
+ encoding: str
31
+ length: int
32
+ evidence: Evidence
33
+
34
+ def to_dict(self) -> dict[str, Any]:
35
+ data: dict[str, Any] = {
36
+ "offset": f"0x{self.offset:x}",
37
+ "text": self.text,
38
+ "encoding": self.encoding,
39
+ "length": self.length,
40
+ }
41
+ if self.address is not None:
42
+ data["address"] = f"0x{self.address:x}"
43
+ return data
44
+
45
+
46
+ class StringExtractor:
47
+ """Extracts printable ASCII and UTF-16 strings from a file.
48
+
49
+ UTF-16 output uses the byte order of ``BinaryInfo.endianness`` when
50
+ metadata is supplied; see the module docstring for the no-metadata rule.
51
+ """
52
+
53
+ def __init__(self, min_length: int = 4) -> None:
54
+ if min_length < 1:
55
+ raise ValueError("min_length must be >= 1")
56
+ self.min_length = min_length
57
+
58
+ def extract(
59
+ self,
60
+ path: str | Path,
61
+ info: BinaryInfo | None = None,
62
+ limit: int = 0,
63
+ ) -> list[StringResult]:
64
+ """Extract strings from ``path``; ``limit`` caps the result count.
65
+
66
+ ASCII strings are always scanned. UTF-16 strings are scanned in the
67
+ byte order of the binary when ``info`` is given (little-endian targets
68
+ as UTF-16LE, big-endian targets as UTF-16BE); without ``info`` the
69
+ scan stays at UTF-16LE rather than guessing a byte swap.
70
+ """
71
+ data = Path(path).read_bytes()
72
+ results: list[StringResult] = []
73
+ ascii_pattern = re.compile(rb"[\x20-\x7e]{%d,}" % self.min_length)
74
+ for match in ascii_pattern.finditer(data):
75
+ text = match.group().decode("ascii", errors="replace")
76
+ results.append(self._result(match.start(), text, "ascii", info))
77
+ if info is not None and info.endianness == "big":
78
+ utf16_pattern = re.compile(rb"(?:\x00[\x20-\x7e]){%d,}" % self.min_length)
79
+ codec, label = "utf-16-be", "utf-16be"
80
+ else:
81
+ utf16_pattern = re.compile(rb"(?:[\x20-\x7e]\x00){%d,}" % self.min_length)
82
+ codec, label = "utf-16-le", "utf-16le"
83
+ results.extend(
84
+ self._result(match.start(), match.group().decode(codec, errors="replace"), label, info)
85
+ for match in utf16_pattern.finditer(data)
86
+ )
87
+ results.sort(key=lambda r: (r.offset, r.encoding))
88
+ if limit and len(results) > limit:
89
+ return results[:limit]
90
+ return results
91
+
92
+ @staticmethod
93
+ def _result(offset: int, text: str, encoding: str, info: BinaryInfo | None) -> StringResult:
94
+ address = info.offset_to_va(offset) if info is not None else None
95
+ evidence = Evidence(
96
+ source="string_scanner",
97
+ method=f"{encoding}_scan",
98
+ confidence=Confidence.HIGH if address is not None else Confidence.MEDIUM,
99
+ address=address,
100
+ detail=f"{encoding} string, {len(text)} chars",
101
+ )
102
+ return StringResult(
103
+ offset=offset,
104
+ address=address,
105
+ text=text,
106
+ encoding=encoding,
107
+ length=len(text),
108
+ evidence=evidence,
109
+ )
@@ -0,0 +1,7 @@
1
+ """Evidence, provenance, and confidence primitives for CODEVANTA."""
2
+
3
+ from .confidence import Confidence
4
+ from .models import Evidence
5
+ from .provenance import Provenance, utc_now_iso
6
+
7
+ __all__ = ["Confidence", "Evidence", "Provenance", "utc_now_iso"]
@@ -0,0 +1,36 @@
1
+ """Confidence levels for analysis findings.
2
+
3
+ The engine never presents a guess as a certainty. Every finding carries one of
4
+ these levels and, where useful, the numeric ``score`` for ordering.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+
12
+ class Confidence(str, Enum):
13
+ """How strongly a finding is supported by evidence."""
14
+
15
+ CERTAIN = "certain" # direct, unambiguous data from the file format
16
+ HIGH = "high" # strong structural evidence (e.g. export table)
17
+ MEDIUM = "medium" # indirect evidence (e.g. entry point)
18
+ LOW = "low" # weak or inferred evidence
19
+ UNKNOWN = "unknown" # no confidence can be established
20
+
21
+ def __str__(self) -> str:
22
+ return self.value
23
+
24
+ @property
25
+ def score(self) -> float:
26
+ """Normalized weight in [0.0, 1.0] used for ordering and merging."""
27
+ return _SCORES[self]
28
+
29
+
30
+ _SCORES: dict[Confidence, float] = {
31
+ Confidence.CERTAIN: 1.0,
32
+ Confidence.HIGH: 0.8,
33
+ Confidence.MEDIUM: 0.5,
34
+ Confidence.LOW: 0.2,
35
+ Confidence.UNKNOWN: 0.0,
36
+ }
@@ -0,0 +1,43 @@
1
+ """Evidence records that justify every analysis finding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from .confidence import Confidence
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Evidence:
13
+ """A traceable justification for a single analysis finding.
14
+
15
+ Attributes:
16
+ source: Where the finding came from, e.g. ``"pe.export_table"``.
17
+ method: How it was obtained, e.g. ``"export_directory"``.
18
+ confidence: How reliable the finding is.
19
+ address: Optional virtual address the evidence refers to.
20
+ detail: Optional human-readable note.
21
+ extra: Optional structured metadata for the report.
22
+ """
23
+
24
+ source: str
25
+ method: str
26
+ confidence: Confidence = Confidence.UNKNOWN
27
+ address: int | None = None
28
+ detail: str | None = None
29
+ extra: dict[str, Any] = field(default_factory=dict)
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ data: dict[str, Any] = {
33
+ "source": self.source,
34
+ "method": self.method,
35
+ "confidence": self.confidence.value,
36
+ }
37
+ if self.address is not None:
38
+ data["address"] = f"0x{self.address:x}"
39
+ if self.detail:
40
+ data["detail"] = self.detail
41
+ if self.extra:
42
+ data["extra"] = dict(self.extra)
43
+ return data
@@ -0,0 +1,37 @@
1
+ """Provenance tracking for analysis runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+
10
+ def utc_now_iso() -> str:
11
+ """Return the current UTC time as an ISO-8601 string."""
12
+ return datetime.now(timezone.utc).isoformat()
13
+
14
+
15
+ @dataclass
16
+ class Provenance:
17
+ """Records how a report or finding was produced."""
18
+
19
+ tool: str = "codevanta"
20
+ tool_version: str = "0.1.0"
21
+ created_at: str = field(default_factory=utc_now_iso)
22
+ steps: list[str] = field(default_factory=list)
23
+
24
+ def add_step(self, step: str) -> Provenance:
25
+ """Append a processing step and return ``self`` for chaining."""
26
+ if not step:
27
+ raise ValueError("step must not be empty")
28
+ self.steps.append(step)
29
+ return self
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "tool": self.tool,
34
+ "tool_version": self.tool_version,
35
+ "created_at": self.created_at,
36
+ "steps": list(self.steps),
37
+ }
@@ -0,0 +1,46 @@
1
+ """Local, dependency-free web dashboard for CODEVANTA.
2
+
3
+ The GUI is a standard-library-only HTTP server bound to loopback. It exposes the
4
+ existing analysis engines over a small JSON API and serves one self-contained
5
+ HTML page, so a single command is enough to try CODEVANTA in a browser::
6
+
7
+ from codevanta.gui import serve
8
+
9
+ serve() # http://127.0.0.1:8765/
10
+
11
+ Nothing here reimplements analysis: every route delegates to the CLI/MCP engine
12
+ of the same name, keeps its bounds and returns its evidence payload. The server
13
+ is read-only (it parses bytes, never executes or writes them) and refuses to bind
14
+ anything but a loopback address.
15
+
16
+ Public API:
17
+
18
+ * :func:`~codevanta.gui.layout.render_dashboard_html` -- the dashboard page.
19
+ * :func:`~codevanta.gui.server.create_server` -- a bound, unstarted server.
20
+ * :func:`~codevanta.gui.server.create_handler` -- the request-handler class.
21
+ * :func:`~codevanta.gui.server.serve` -- bind, print the URL, serve until Ctrl+C.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from .layout import render_dashboard_html
27
+ from .server import (
28
+ MAX_RESPONSE_BYTES,
29
+ ROUTES,
30
+ VERSION,
31
+ create_handler,
32
+ create_server,
33
+ normalize_host,
34
+ serve,
35
+ )
36
+
37
+ __all__ = [
38
+ "MAX_RESPONSE_BYTES",
39
+ "ROUTES",
40
+ "VERSION",
41
+ "create_handler",
42
+ "create_server",
43
+ "normalize_host",
44
+ "render_dashboard_html",
45
+ "serve",
46
+ ]