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,441 @@
1
+ """Standalone DOT and GraphML export for CODEVANTA's plain-dict graph model.
2
+
3
+ The exporters here are deliberately dependency free: they import only the
4
+ standard library and know nothing about CODEVANTA's analysis classes. Any
5
+ producer that can describe a graph as
6
+
7
+ .. code-block:: python
8
+
9
+ {
10
+ "nodes": [{"id": "0x401000", "label": "main", "kind": "entry_point"}],
11
+ "edges": [{"source": "0x401000", "target": "0x401040", "label": "call"}],
12
+ }
13
+
14
+ can therefore render it through :func:`to_dot` or :func:`to_graphml` — the call
15
+ graph, the control-flow graph, and future viewers all share one renderer.
16
+ :func:`validate_graph` exposes the same checks directly and returns the
17
+ normalized, deterministically ordered copy that both exporters render, so other
18
+ views (such as :mod:`codevanta.analysis.html_view`) can reuse it instead of
19
+ duplicating the contract.
20
+
21
+ Contract
22
+ --------
23
+ * ``nodes`` items: ``id`` (required, non-blank ``str``, unique), ``label``
24
+ (required ``str``), ``kind`` (optional ``str``).
25
+ * ``edges`` items: ``source`` and ``target`` (required, must be existing node
26
+ ids), ``label`` (optional ``str``, defaults to ``"call"``).
27
+ * Size is bounded by ``max_nodes`` / ``max_edges``, both ``>= 1``.
28
+ * Anything that violates the contract raises :class:`GraphExportError`; a
29
+ partially valid graph is never rendered.
30
+
31
+ Both exporters are deterministic: nodes are emitted sorted by ``id`` and edges
32
+ sorted by ``(source, target, label)``, so two graphs with the same content but a
33
+ different insertion order produce identical output.
34
+
35
+ Graph content is only ever *rendered* — nothing is executed or written to disk;
36
+ callers decide whether to persist the returned string.
37
+
38
+ Limitations
39
+ -----------
40
+ * DOT output escapes ``"``, ``\\``, newline, carriage return and tab, and
41
+ replaces other C0 control characters with ``U+FFFD``. It does not implement
42
+ the full DOT grammar for record/HTML-like labels.
43
+ * GraphML stores node ids in ``id`` attributes; XML attribute-value
44
+ normalization turns tab/newline characters inside an id into spaces, so such
45
+ ids do not round-trip byte-exactly (labels still do).
46
+ * No layout is emitted (no coordinates): renderers run their own layout engine.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import re
52
+ from dataclasses import dataclass
53
+ from typing import Any
54
+ from xml.etree import ElementTree
55
+
56
+ __all__ = [
57
+ "DEFAULT_EDGE_LABEL",
58
+ "DEFAULT_MAX_EDGES",
59
+ "DEFAULT_MAX_NODES",
60
+ "DEFAULT_TITLE",
61
+ "GraphExportError",
62
+ "sanitize_text",
63
+ "to_dot",
64
+ "to_graphml",
65
+ "validate_graph",
66
+ ]
67
+
68
+ #: Title used when the caller does not pass one.
69
+ DEFAULT_TITLE = "CODEVANTA graph"
70
+
71
+ #: Node budget per export; keeps output bounded for very large graphs.
72
+ DEFAULT_MAX_NODES = 5000
73
+
74
+ #: Edge budget per export.
75
+ DEFAULT_MAX_EDGES = 20000
76
+
77
+ #: Label rendered for an edge that carries no explicit label.
78
+ DEFAULT_EDGE_LABEL = "call"
79
+
80
+ #: Node ``kind`` values drawn as rectangles; everything else is an ellipse.
81
+ _BOX_KINDS = frozenset({"function", "entry_point"})
82
+
83
+ #: GraphML namespace and schema location declared on the root element.
84
+ _GRAPHML_NS = "http://graphml.graphdrawing.org/xmlns"
85
+ _XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
86
+ _GRAPHML_SCHEMA = "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"
87
+
88
+ #: ``key`` ids used by the GraphML ``data`` elements.
89
+ _KEY_NODE_LABEL = "d0"
90
+ _KEY_NODE_KIND = "d1"
91
+ _KEY_EDGE_LABEL = "d2"
92
+
93
+ #: Characters escaped inside DOT double-quoted strings.
94
+ _DOT_SPECIAL_RE = re.compile(r'[\\"\n\r\t]')
95
+ _DOT_ESCAPES = {"\\": "\\\\", '"': '\\"', "\n": "\\n", "\r": "\\r", "\t": "\\t"}
96
+
97
+ #: Characters that are not representable in XML 1.0 (or in a lone surrogate).
98
+ _ILLEGAL_TEXT_RE = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\ud800-\udfff\ufffe\uffff]")
99
+
100
+ #: Replacement for characters that cannot be represented at all.
101
+ _REPLACEMENT_CHAR = "\ufffd"
102
+
103
+
104
+ class GraphExportError(ValueError):
105
+ """Raised when a graph cannot be rendered in the requested format."""
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class _Node:
110
+ """A validated graph node."""
111
+
112
+ id: str
113
+ label: str
114
+ kind: str
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class _Edge:
119
+ """A validated graph edge; ``label`` always holds a non-blank label."""
120
+
121
+ source: str
122
+ target: str
123
+ label: str
124
+
125
+
126
+ def _check_limit(value: Any, name: str) -> int:
127
+ """Return ``value`` as an export limit, or raise :class:`GraphExportError`."""
128
+ if isinstance(value, bool) or not isinstance(value, int):
129
+ raise GraphExportError(f"{name} must be an int, got {type(value).__name__}")
130
+ if value < 1:
131
+ raise GraphExportError(f"{name} must be >= 1, got {value}")
132
+ return value
133
+
134
+
135
+ def _check_title(title: Any) -> str:
136
+ """Return a usable title, falling back to :data:`DEFAULT_TITLE` when blank."""
137
+ if not isinstance(title, str):
138
+ raise GraphExportError(f"title must be a str, got {type(title).__name__}")
139
+ return title if title.strip() else DEFAULT_TITLE
140
+
141
+
142
+ def _check_sequence(graph: dict[str, Any], key: str) -> list[Any]:
143
+ """Return ``graph[key]`` as a list, or raise :class:`GraphExportError`."""
144
+ if key not in graph:
145
+ raise GraphExportError(f"graph is missing required key {key!r}")
146
+ value = graph[key]
147
+ if not isinstance(value, list):
148
+ raise GraphExportError(f"graph[{key!r}] must be a list, got {type(value).__name__}")
149
+ return value
150
+
151
+
152
+ def _require_text(item: dict[str, Any], key: str, context: str) -> str:
153
+ """Return ``item[key]`` as a string, or raise :class:`GraphExportError`."""
154
+ if key not in item:
155
+ raise GraphExportError(f"{context}: missing required key {key!r}")
156
+ value = item[key]
157
+ if not isinstance(value, str):
158
+ raise GraphExportError(f"{context}: {key!r} must be a str, got {type(value).__name__}")
159
+ return value
160
+
161
+
162
+ def _require_id(item: dict[str, Any], key: str, context: str) -> str:
163
+ """Return a non-blank identifier, or raise :class:`GraphExportError`."""
164
+ value = _require_text(item, key, context)
165
+ if not value.strip():
166
+ raise GraphExportError(f"{context}: {key!r} must not be blank")
167
+ return value
168
+
169
+
170
+ def _optional_text(item: dict[str, Any], key: str, context: str, default: str) -> str:
171
+ """Return an optional string field, using ``default`` when absent or ``None``."""
172
+ value = item.get(key)
173
+ if value is None:
174
+ return default
175
+ if not isinstance(value, str):
176
+ raise GraphExportError(f"{context}: {key!r} must be a str, got {type(value).__name__}")
177
+ return value
178
+
179
+
180
+ def _check_mapping(value: Any, context: str) -> dict[str, Any]:
181
+ """Return ``value`` as a mapping, or raise :class:`GraphExportError`."""
182
+ if not isinstance(value, dict):
183
+ raise GraphExportError(f"{context} must be a dict, got {type(value).__name__}")
184
+ return value
185
+
186
+
187
+ def _parse_node(raw: Any, index: int) -> _Node:
188
+ """Validate one raw node entry."""
189
+ context = f"node {index}"
190
+ item = _check_mapping(raw, context)
191
+ node_id = _require_id(item, "id", context)
192
+ label = _require_text(item, "label", context)
193
+ kind = _optional_text(item, "kind", context, "")
194
+ return _Node(id=node_id, label=label, kind=kind)
195
+
196
+
197
+ def _parse_edge(raw: Any, index: int, node_ids: dict[str, _Node]) -> _Edge:
198
+ """Validate one raw edge entry and resolve both of its endpoints."""
199
+ context = f"edge {index}"
200
+ item = _check_mapping(raw, context)
201
+ source = _require_id(item, "source", context)
202
+ target = _require_id(item, "target", context)
203
+ if source not in node_ids:
204
+ raise GraphExportError(f"{context}: source {source!r} is not a known node id")
205
+ if target not in node_ids:
206
+ raise GraphExportError(f"{context}: target {target!r} is not a known node id")
207
+ label = _optional_text(item, "label", context, DEFAULT_EDGE_LABEL)
208
+ if not label.strip():
209
+ label = DEFAULT_EDGE_LABEL
210
+ return _Edge(source=source, target=target, label=label)
211
+
212
+
213
+ def _parse_graph(
214
+ graph: Any,
215
+ max_nodes: Any,
216
+ max_edges: Any,
217
+ ) -> tuple[list[_Node], list[_Edge]]:
218
+ """Validate the whole graph and return deterministically ordered nodes/edges."""
219
+ if not isinstance(graph, dict):
220
+ raise GraphExportError(f"graph must be a dict, got {type(graph).__name__}")
221
+ node_limit = _check_limit(max_nodes, "max_nodes")
222
+ edge_limit = _check_limit(max_edges, "max_edges")
223
+
224
+ raw_nodes = _check_sequence(graph, "nodes")
225
+ raw_edges = _check_sequence(graph, "edges")
226
+
227
+ if len(raw_nodes) > node_limit:
228
+ raise GraphExportError(f"graph has {len(raw_nodes)} nodes, max_nodes is {node_limit}")
229
+ if len(raw_edges) > edge_limit:
230
+ raise GraphExportError(f"graph has {len(raw_edges)} edges, max_edges is {edge_limit}")
231
+
232
+ nodes: list[_Node] = []
233
+ node_ids: dict[str, _Node] = {}
234
+ for index, raw in enumerate(raw_nodes):
235
+ node = _parse_node(raw, index)
236
+ if node.id in node_ids:
237
+ raise GraphExportError(f"node {index}: duplicate node id {node.id!r}")
238
+ node_ids[node.id] = node
239
+ nodes.append(node)
240
+
241
+ edges = [_parse_edge(raw, index, node_ids) for index, raw in enumerate(raw_edges)]
242
+
243
+ nodes.sort(key=lambda node: node.id)
244
+ edges.sort(key=lambda edge: (edge.source, edge.target, edge.label))
245
+ return nodes, edges
246
+
247
+
248
+ def validate_graph(
249
+ graph: Any,
250
+ max_nodes: int = DEFAULT_MAX_NODES,
251
+ max_edges: int = DEFAULT_MAX_EDGES,
252
+ ) -> dict[str, Any]:
253
+ """Validate ``graph`` and return a normalized, deterministically ordered copy.
254
+
255
+ The checks are exactly the ones :func:`to_dot` and :func:`to_graphml` apply,
256
+ so a graph accepted here renders without further validation. The returned
257
+ mapping never aliases the input: fresh node and edge dicts are built, blank
258
+ edge labels are replaced by ``"call"``, missing ``kind`` values become
259
+ ``""``, and both lists are sorted (nodes by ``id``, edges by
260
+ ``(source, target, label)``).
261
+
262
+ Args:
263
+ graph: Mapping with ``"nodes"`` and ``"edges"`` lists (see module docs).
264
+ max_nodes: Maximum number of nodes to accept (``>= 1``).
265
+ max_edges: Maximum number of edges to accept (``>= 1``).
266
+
267
+ Returns:
268
+ ``{"nodes": [{"id", "label", "kind"}...], "edges": [{"source",
269
+ "target", "label"}...]}`` with every value a ``str``.
270
+
271
+ Raises:
272
+ GraphExportError: If the graph or any limit violates the contract.
273
+
274
+ Examples:
275
+ >>> validate_graph(
276
+ ... {
277
+ ... "nodes": [{"id": "b", "label": "parse"}, {"id": "a", "label": "main"}],
278
+ ... "edges": [{"source": "a", "target": "b"}],
279
+ ... }
280
+ ... )["edges"]
281
+ [{'source': 'a', 'target': 'b', 'label': 'call'}]
282
+ """
283
+ nodes, edges = _parse_graph(graph, max_nodes, max_edges)
284
+ return {
285
+ "nodes": [{"id": node.id, "label": node.label, "kind": node.kind} for node in nodes],
286
+ "edges": [
287
+ {"source": edge.source, "target": edge.target, "label": edge.label} for edge in edges
288
+ ],
289
+ }
290
+
291
+
292
+ def _sanitize(value: str) -> str:
293
+ """Replace characters that cannot be represented in output with ``U+FFFD``."""
294
+ return _ILLEGAL_TEXT_RE.sub(_REPLACEMENT_CHAR, value)
295
+
296
+
297
+ def sanitize_text(value: str) -> str:
298
+ """Replace characters that cannot be represented in output with ``U+FFFD``.
299
+
300
+ Shared by the DOT, GraphML, and HTML exporters so every text surface treats
301
+ illegal characters (control characters, lone surrogates) the same way.
302
+ """
303
+ return _sanitize(value)
304
+
305
+
306
+ def _display_label(label: str, node_id: str) -> str:
307
+ """Return the label to render, falling back to the node id when blank."""
308
+ return label if label.strip() else node_id
309
+
310
+
311
+ def _dot_quote(value: str) -> str:
312
+ """Return ``value`` as a double-quoted, escaped DOT string."""
313
+ escaped = _DOT_SPECIAL_RE.sub(lambda match: _DOT_ESCAPES[match.group(0)], _sanitize(value))
314
+ return f'"{escaped}"'
315
+
316
+
317
+ def _dot_shape(kind: str) -> str:
318
+ """Return the DOT shape for a node ``kind``."""
319
+ return "box" if kind.strip().lower() in _BOX_KINDS else "ellipse"
320
+
321
+
322
+ def to_dot(
323
+ graph: dict[str, Any],
324
+ title: str = DEFAULT_TITLE,
325
+ max_nodes: int = DEFAULT_MAX_NODES,
326
+ max_edges: int = DEFAULT_MAX_EDGES,
327
+ ) -> str:
328
+ """Render ``graph`` as a Graphviz DOT ``digraph``.
329
+
330
+ Node ids and labels are emitted as double-quoted, escaped strings; nodes are
331
+ drawn as ``box`` when their ``kind`` is ``"function"`` or ``"entry_point"``
332
+ and as ``ellipse`` otherwise. Edges always carry a label, defaulting to
333
+ ``"call"``. Validation and deterministic ordering are delegated to
334
+ :func:`validate_graph`.
335
+
336
+ Args:
337
+ graph: Mapping with ``"nodes"`` and ``"edges"`` lists (see module docs).
338
+ title: Graph title; blank titles fall back to :data:`DEFAULT_TITLE`.
339
+ max_nodes: Maximum number of nodes to accept (``>= 1``).
340
+ max_edges: Maximum number of edges to accept (``>= 1``).
341
+
342
+ Returns:
343
+ A newline-terminated DOT document.
344
+
345
+ Raises:
346
+ GraphExportError: If the graph or any limit violates the contract.
347
+ """
348
+ validated = validate_graph(graph, max_nodes, max_edges)
349
+ heading = _dot_quote(_check_title(title))
350
+
351
+ lines = [f"digraph {heading} {{", f" graph [label={heading}];"]
352
+ for node in validated["nodes"]:
353
+ label = _dot_quote(_display_label(node["label"], node["id"]))
354
+ lines.append(f" {_dot_quote(node['id'])} [label={label}, shape={_dot_shape(node['kind'])}];")
355
+
356
+ for edge in validated["edges"]:
357
+ lines.append(
358
+ f" {_dot_quote(edge['source'])} -> {_dot_quote(edge['target'])}"
359
+ f" [label={_dot_quote(edge['label'])}];"
360
+ )
361
+
362
+ lines.append("}")
363
+ return "\n".join(lines) + "\n"
364
+
365
+
366
+ def _append_graphml_key(parent: ElementTree.Element, key_id: str, target: str, name: str) -> None:
367
+ """Append one GraphML ``key`` declaration for a string attribute."""
368
+ key = ElementTree.SubElement(parent, "key")
369
+ key.set("id", key_id)
370
+ key.set("for", target)
371
+ key.set("attr.name", name)
372
+ key.set("attr.type", "string")
373
+
374
+
375
+ def _append_graphml_data(parent: ElementTree.Element, key_id: str, text: str) -> None:
376
+ """Append one GraphML ``data`` element carrying ``text``."""
377
+ data = ElementTree.SubElement(parent, "data")
378
+ data.set("key", key_id)
379
+ data.text = _sanitize(text)
380
+
381
+
382
+ def to_graphml(
383
+ graph: dict[str, Any],
384
+ title: str = DEFAULT_TITLE,
385
+ max_nodes: int = DEFAULT_MAX_NODES,
386
+ max_edges: int = DEFAULT_MAX_EDGES,
387
+ ) -> str:
388
+ """Render ``graph`` as a valid GraphML 1.0 document.
389
+
390
+ The document declares ``key`` elements for the node ``label``, the node
391
+ ``kind`` and the edge ``label`` attributes, a directed ``graph`` element,
392
+ and a ``desc`` element holding the title. Validation and deterministic
393
+ ordering are delegated to :func:`validate_graph`.
394
+
395
+ Args:
396
+ graph: Mapping with ``"nodes"`` and ``"edges"`` lists (see module docs).
397
+ title: Graph title; blank titles fall back to :data:`DEFAULT_TITLE`.
398
+ max_nodes: Maximum number of nodes to accept (``>= 1``).
399
+ max_edges: Maximum number of edges to accept (``>= 1``).
400
+
401
+ Returns:
402
+ An XML declaration followed by the serialized GraphML document.
403
+
404
+ Raises:
405
+ GraphExportError: If the graph or any limit violates the contract.
406
+ """
407
+ validated = validate_graph(graph, max_nodes, max_edges)
408
+ heading = _check_title(title)
409
+
410
+ root = ElementTree.Element("graphml")
411
+ root.set("xmlns", _GRAPHML_NS)
412
+ root.set("xmlns:xsi", _XSI_NS)
413
+ root.set("xsi:schemaLocation", f"{_GRAPHML_NS} {_GRAPHML_SCHEMA}")
414
+
415
+ _append_graphml_key(root, _KEY_NODE_LABEL, "node", "label")
416
+ _append_graphml_key(root, _KEY_NODE_KIND, "node", "kind")
417
+ _append_graphml_key(root, _KEY_EDGE_LABEL, "edge", "label")
418
+
419
+ graph_element = ElementTree.SubElement(root, "graph")
420
+ graph_element.set("id", "G")
421
+ graph_element.set("edgedefault", "directed")
422
+
423
+ description = ElementTree.SubElement(graph_element, "desc")
424
+ description.text = _sanitize(heading)
425
+
426
+ for node in validated["nodes"]:
427
+ element = ElementTree.SubElement(graph_element, "node")
428
+ element.set("id", _sanitize(node["id"]))
429
+ _append_graphml_data(element, _KEY_NODE_LABEL, _display_label(node["label"], node["id"]))
430
+ if node["kind"].strip():
431
+ _append_graphml_data(element, _KEY_NODE_KIND, node["kind"])
432
+
433
+ for edge in validated["edges"]:
434
+ element = ElementTree.SubElement(graph_element, "edge")
435
+ element.set("source", _sanitize(edge["source"]))
436
+ element.set("target", _sanitize(edge["target"]))
437
+ _append_graphml_data(element, _KEY_EDGE_LABEL, edge["label"])
438
+
439
+ ElementTree.indent(root, space=" ")
440
+ body = ElementTree.tostring(root, encoding="unicode")
441
+ return f'<?xml version="1.0" encoding="UTF-8"?>\n{body}\n'