crypttrace 0.31.0__tar.gz → 0.32.0__tar.gz

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 (55) hide show
  1. {crypttrace-0.31.0/src/crypttrace.egg-info → crypttrace-0.32.0}/PKG-INFO +6 -1
  2. {crypttrace-0.31.0 → crypttrace-0.32.0}/README.md +5 -0
  3. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/__init__.py +1 -1
  4. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/cli.py +19 -0
  5. crypttrace-0.32.0/src/crypttrace/export.py +94 -0
  6. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/web/index.html +20 -0
  7. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/webapp.py +8 -0
  8. {crypttrace-0.31.0 → crypttrace-0.32.0/src/crypttrace.egg-info}/PKG-INFO +6 -1
  9. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace.egg-info/SOURCES.txt +1 -0
  10. {crypttrace-0.31.0 → crypttrace-0.32.0}/LICENSE +0 -0
  11. {crypttrace-0.31.0 → crypttrace-0.32.0}/pyproject.toml +0 -0
  12. {crypttrace-0.31.0 → crypttrace-0.32.0}/setup.cfg +0 -0
  13. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/addresses.py +0 -0
  14. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/analysis.py +0 -0
  15. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/assess.py +0 -0
  16. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/assets.py +0 -0
  17. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/bridges.py +0 -0
  18. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/chains.py +0 -0
  19. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/config.py +0 -0
  20. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/drain.py +0 -0
  21. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/evidence.py +0 -0
  22. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/__init__.py +0 -0
  23. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
  24. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/etherscan.py +0 -0
  25. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/http.py +0 -0
  26. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/solana.py +0 -0
  27. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/ton.py +0 -0
  28. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/fetchers/tron.py +0 -0
  29. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/freeze.py +0 -0
  30. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/funder.py +0 -0
  31. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/gascluster.py +0 -0
  32. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/investigate.py +0 -0
  33. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/__init__.py +0 -0
  34. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/audit.py +0 -0
  35. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/bulk.py +0 -0
  36. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/known.json +0 -0
  37. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/labels.py +0 -0
  38. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/names.py +0 -0
  39. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/partial.json +0 -0
  40. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/tonapi.py +0 -0
  41. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/labels/tronscan.py +0 -0
  42. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/offramp.py +0 -0
  43. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/poisoning.py +0 -0
  44. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/prices.py +0 -0
  45. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/render.py +0 -0
  46. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/report.py +0 -0
  47. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/spam.py +0 -0
  48. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/store.py +0 -0
  49. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/trace.py +0 -0
  50. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/verify.py +0 -0
  51. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace/watch.py +0 -0
  52. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
  53. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
  54. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace.egg-info/requires.txt +0 -0
  55. {crypttrace-0.31.0 → crypttrace-0.32.0}/src/crypttrace.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypttrace
3
- Version: 0.31.0
3
+ Version: 0.32.0
4
4
  Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron, Solana and TON
5
5
  Author: bobslayerX
6
6
  License-Expression: MIT
@@ -255,6 +255,10 @@ crypttrace trace 0xADDRESS --direction in
255
255
  # Trace a token rather than the native coin (most thefts are stablecoins)
256
256
  crypttrace trace 0xADDRESS --asset usdt --depth 4
257
257
 
258
+ # Save the traced graph for Gephi / yEd / Cytoscape, or for Maltego / Neo4j
259
+ crypttrace trace 0xADDRESS --depth 4 --export case.graphml
260
+ crypttrace trace 0xADDRESS --depth 4 --export case.csv
261
+
258
262
  # Token holdings, valued in USD
259
263
  crypttrace tokens 0xADDRESS
260
264
 
@@ -724,6 +728,7 @@ src/crypttrace/
724
728
  bridges.py # cross-chain matching
725
729
  watch.py # watchlist + alerts
726
730
  report.py # Markdown + JSON reports
731
+ export.py # traced graph as GraphML (Gephi) and edge-list CSV (Maltego)
727
732
  prices.py # USD valuation
728
733
  render.py # rich terminal rendering
729
734
  webapp.py # Flask backend for the web UI
@@ -221,6 +221,10 @@ crypttrace trace 0xADDRESS --direction in
221
221
  # Trace a token rather than the native coin (most thefts are stablecoins)
222
222
  crypttrace trace 0xADDRESS --asset usdt --depth 4
223
223
 
224
+ # Save the traced graph for Gephi / yEd / Cytoscape, or for Maltego / Neo4j
225
+ crypttrace trace 0xADDRESS --depth 4 --export case.graphml
226
+ crypttrace trace 0xADDRESS --depth 4 --export case.csv
227
+
224
228
  # Token holdings, valued in USD
225
229
  crypttrace tokens 0xADDRESS
226
230
 
@@ -690,6 +694,7 @@ src/crypttrace/
690
694
  bridges.py # cross-chain matching
691
695
  watch.py # watchlist + alerts
692
696
  report.py # Markdown + JSON reports
697
+ export.py # traced graph as GraphML (Gephi) and edge-list CSV (Maltego)
693
698
  prices.py # USD valuation
694
699
  render.py # rich terminal rendering
695
700
  webapp.py # Flask backend for the web UI
@@ -1,2 +1,2 @@
1
1
  """crypttrace — OSINT crypto investigation CLI."""
2
- __version__ = "0.31.0"
2
+ __version__ = "0.32.0"
@@ -222,6 +222,9 @@ def trace(
222
222
  help="Top-N outflows to follow per address"),
223
223
  direction: str = typer.Option("out", "--direction", "-D",
224
224
  help="'out' = where funds went, 'in' = where they came from"),
225
+ export: Optional[Path] = typer.Option(None, "--export", "-o",
226
+ help="Also save the graph: .graphml (Gephi, yEd, Cytoscape) "
227
+ "or .csv (Maltego, Neo4j, spreadsheets)"),
225
228
  ):
226
229
  """Trace where funds moved, hop by hop, as a coloured tree (ETH or a token)."""
227
230
  try:
@@ -232,6 +235,10 @@ def trace(
232
235
  if direction not in ("out", "in"):
233
236
  console.print("[red]Error:[/red] --direction must be 'out' or 'in'")
234
237
  raise typer.Exit(1)
238
+ fmt = export.suffix.lower().lstrip(".") if export else None
239
+ if export and fmt not in ("graphml", "csv"):
240
+ console.print("[red]Error:[/red] --export takes a .graphml or .csv file name")
241
+ raise typer.Exit(1)
235
242
  try:
236
243
  tree = trace_mod.build_tree(address, chain, depth, branching, asset_desc, direction)
237
244
  except (chains_mod.ChainError, etherscan.EtherscanError) as e:
@@ -242,6 +249,18 @@ def trace(
242
249
  "\n[dim]Legend: \U0001F7E2 exchange \U0001F7E3 mixer \U0001F534 sanctioned/scam"
243
250
  " \U0001F309 bridge \U0001F504 DEX \U0001F537 service ⚪ unknown[/dim]"
244
251
  )
252
+ if export:
253
+ from crypttrace import export as export_mod
254
+ try:
255
+ graph = trace_mod.build_graph(address, chain, depth, branching, asset_desc, direction)
256
+ except (chains_mod.ChainError, etherscan.EtherscanError) as e:
257
+ console.print(f"[red]Error:[/red] {e}")
258
+ raise typer.Exit(1)
259
+ export.write_text(export_mod.render(graph, chain, fmt), encoding="utf-8", newline="")
260
+ console.print(f"\n[green]Graph saved:[/green] {export} — {len(graph['nodes'])} addresses, "
261
+ f"{len(graph['edges'])} transfers"
262
+ + (" [dim](Maltego: Import Graph from Table, map from/to to an address entity)[/dim]"
263
+ if fmt == "csv" else " [dim](open in Gephi, yEd or Cytoscape)[/dim]"))
245
264
 
246
265
 
247
266
  @app.command()
@@ -0,0 +1,94 @@
1
+ """Export a traced fund-flow graph for the tools analysts already use.
2
+
3
+ GraphML — Gephi, yEd, Cytoscape: nodes keep their label, kind, hop and
4
+ explorer link; edges their amount, USD value, transfer count and
5
+ exchange memo, so a layout can be coloured and sized by them.
6
+ CSV — one row per edge with both ends' labels and kinds: Maltego's
7
+ "Import Graph from Table" (map `from` and `to` to a
8
+ cryptocurrency-address entity), Neo4j's LOAD CSV, a spreadsheet.
9
+
10
+ Both take the dict `trace.build_graph` returns, the same graph the web UI draws.
11
+ """
12
+ import csv
13
+ import io
14
+ from xml.sax.saxutils import escape, quoteattr
15
+
16
+ from crypttrace import chains
17
+
18
+ NODE_KEYS = [("label", "string"), ("kind", "string"), ("hop", "int"), ("root", "boolean"),
19
+ ("trail_ends", "boolean"), ("chain", "string"), ("explorer", "string")]
20
+ EDGE_KEYS = [("amount", "double"), ("symbol", "string"), ("usd", "double"), ("transfers", "int"),
21
+ ("memo", "string")]
22
+ CSV_HEAD = ["from", "from_label", "from_kind", "to", "to_label", "to_kind", "amount", "symbol",
23
+ "usd", "transfers", "memo", "chain", "from_explorer", "to_explorer"]
24
+
25
+
26
+ def _node_attrs(n: dict, chain: str) -> dict:
27
+ return {"label": n.get("label") or n.get("short") or n["id"], "kind": n.get("type") or "unknown",
28
+ "hop": n.get("level", 0), "root": bool(n.get("root")), "trail_ends": bool(n.get("terminal")),
29
+ "chain": chain, "explorer": chains.explorer_url(n["id"], chain)}
30
+
31
+
32
+ def _edge_attrs(e: dict, symbol: str) -> dict:
33
+ return {"amount": e.get("value") or 0, "symbol": symbol, "usd": e.get("usd"),
34
+ "transfers": e.get("tx") or 0, "memo": e.get("memo") or ""}
35
+
36
+
37
+ def _val(v, kind: str) -> str:
38
+ if v is None:
39
+ return ""
40
+ if kind == "boolean":
41
+ return "true" if v else "false"
42
+ return escape(str(v))
43
+
44
+
45
+ def to_graphml(graph: dict, chain: str) -> str:
46
+ """GraphML 1.0, directed: money flows along the edges."""
47
+ symbol = graph.get("symbol", "")
48
+ out = ['<?xml version="1.0" encoding="UTF-8"?>',
49
+ '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">']
50
+ for k, t in NODE_KEYS:
51
+ out.append(f' <key id="n_{k}" for="node" attr.name="{k}" attr.type="{t}"/>')
52
+ for k, t in EDGE_KEYS:
53
+ out.append(f' <key id="e_{k}" for="edge" attr.name="{k}" attr.type="{t}"/>')
54
+ out.append(f' <graph id={quoteattr("crypttrace " + chain)} edgedefault="directed">')
55
+ for n in graph.get("nodes", []):
56
+ out.append(f' <node id={quoteattr(n["id"])}>')
57
+ for (k, t), v in zip(NODE_KEYS, _node_attrs(n, chain).values()):
58
+ if v not in (None, ""):
59
+ out.append(f' <data key="n_{k}">{_val(v, t)}</data>')
60
+ out.append(" </node>")
61
+ for i, e in enumerate(graph.get("edges", [])):
62
+ out.append(f' <edge id="e{i}" source={quoteattr(e["from"])} target={quoteattr(e["to"])}>')
63
+ for (k, t), v in zip(EDGE_KEYS, _edge_attrs(e, symbol).values()):
64
+ if v not in (None, ""):
65
+ out.append(f' <data key="e_{k}">{_val(v, t)}</data>')
66
+ out.append(" </edge>")
67
+ out += [" </graph>", "</graphml>", ""]
68
+ return "\n".join(out)
69
+
70
+
71
+ def to_csv(graph: dict, chain: str) -> str:
72
+ """One row per edge, both ends described — what a link-analysis import wants."""
73
+ symbol = graph.get("symbol", "")
74
+ nodes = {n["id"]: n for n in graph.get("nodes", [])}
75
+ buf = io.StringIO()
76
+ w = csv.writer(buf, lineterminator="\n")
77
+ w.writerow(CSV_HEAD)
78
+ for e in graph.get("edges", []):
79
+ a, b = nodes.get(e["from"], {"id": e["from"]}), nodes.get(e["to"], {"id": e["to"]})
80
+ ea = _edge_attrs(e, symbol)
81
+ w.writerow([e["from"], a.get("label") or "", a.get("type") or "unknown",
82
+ e["to"], b.get("label") or "", b.get("type") or "unknown",
83
+ ea["amount"], symbol, "" if ea["usd"] is None else ea["usd"], ea["transfers"],
84
+ ea["memo"], chain, chains.explorer_url(e["from"], chain), chains.explorer_url(e["to"], chain)])
85
+ return buf.getvalue()
86
+
87
+
88
+ FORMATS = {"graphml": (to_graphml, "application/graphml+xml"), "csv": (to_csv, "text/csv")}
89
+
90
+
91
+ def render(graph: dict, chain: str, fmt: str) -> str:
92
+ if fmt not in FORMATS:
93
+ raise ValueError(f"unknown export format '{fmt}' — use one of: {', '.join(FORMATS)}")
94
+ return FORMATS[fmt][0](graph, chain)
@@ -230,6 +230,8 @@
230
230
  <button id="go">Investigate</button>
231
231
  <button id="fit" class="ghost" title="Fit graph to view">Fit</button>
232
232
  <button id="casefile" class="ghost" title="Download a one-page case file for an exchange or the police">Case file</button>
233
+ <button id="exp-graphml" class="ghost" title="Download the graph for Gephi, yEd or Cytoscape">GraphML</button>
234
+ <button id="exp-csv" class="ghost" title="Download the graph as an edge list for Maltego, Neo4j or a spreadsheet">CSV</button>
233
235
  </div>
234
236
 
235
237
  <div class="legend">
@@ -779,6 +781,24 @@ async function downloadCaseFile(){
779
781
  }
780
782
  $('#casefile').addEventListener('click',downloadCaseFile);
781
783
 
784
+ // The traced graph as a file: GraphML for Gephi/yEd/Cytoscape, CSV for Maltego/Neo4j.
785
+ async function downloadGraph(fmt){
786
+ const a=$('#addr').value.trim(), b=$('#exp-'+fmt);
787
+ if(!a){ setStatus('enter an address',true); return; }
788
+ b.disabled=true; setStatus(`building the ${fmt} file…`,false,true);
789
+ try{
790
+ const r=await fetch(`/api/trace?address=${q(a)}&chain=${chain()}&depth=${$('#depth').value}&asset=${q($('#asset').value)}&direction=${$('#direction').value}&format=${fmt}`);
791
+ if(!r.ok){ const j=await r.json().catch(()=>({})); throw new Error(j.error||`HTTP ${r.status}`); }
792
+ const name=((r.headers.get('Content-Disposition')||'').match(/filename="([^"]+)"/)||[])[1]||`graph.${fmt}`;
793
+ const u=URL.createObjectURL(await r.blob()), l=document.createElement('a');
794
+ l.href=u; l.download=name; l.click(); URL.revokeObjectURL(u);
795
+ setStatus(`graph saved: ${name}`);
796
+ }catch(e){ setStatus(e.message,true); }
797
+ finally{ b.disabled=false; }
798
+ }
799
+ $('#exp-graphml').addEventListener('click',()=>downloadGraph('graphml'));
800
+ $('#exp-csv').addEventListener('click',()=>downloadGraph('csv'));
801
+
782
802
  function downloadCsv(){
783
803
  const head=['address','hop',`amount_sent_${victimSym}`,'transactions','first_seen_utc','last_seen_utc','label','explorer'];
784
804
  const body=victimRows.map(r=>[r.address,r.hop,r.value,r.txs,dstamp(r.first_seen),dstamp(r.last_seen),
@@ -145,6 +145,14 @@ def create_app() -> Flask:
145
145
  graph = trace_mod.build_graph(addr, chain, depth, branching, asset, direction)
146
146
  except _KNOWN_ERRORS as e:
147
147
  return jsonify({"error": str(e)}), 400
148
+ fmt = request.args.get("format", "")
149
+ if fmt: # the same graph as a file for Gephi / Maltego
150
+ from crypttrace import export as export_mod
151
+ if fmt not in export_mod.FORMATS:
152
+ return jsonify({"error": f"unknown format '{fmt}'"}), 400
153
+ name = f"crypttrace_{chain}_{addr[:12]}_{direction}.{fmt}"
154
+ return Response(export_mod.render(graph, chain, fmt), mimetype=export_mod.FORMATS[fmt][1],
155
+ headers={"Content-Disposition": f'attachment; filename="{name}"'})
148
156
  return jsonify(graph)
149
157
 
150
158
  @app.route("/api/assess")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypttrace
3
- Version: 0.31.0
3
+ Version: 0.32.0
4
4
  Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron, Solana and TON
5
5
  Author: bobslayerX
6
6
  License-Expression: MIT
@@ -255,6 +255,10 @@ crypttrace trace 0xADDRESS --direction in
255
255
  # Trace a token rather than the native coin (most thefts are stablecoins)
256
256
  crypttrace trace 0xADDRESS --asset usdt --depth 4
257
257
 
258
+ # Save the traced graph for Gephi / yEd / Cytoscape, or for Maltego / Neo4j
259
+ crypttrace trace 0xADDRESS --depth 4 --export case.graphml
260
+ crypttrace trace 0xADDRESS --depth 4 --export case.csv
261
+
258
262
  # Token holdings, valued in USD
259
263
  crypttrace tokens 0xADDRESS
260
264
 
@@ -724,6 +728,7 @@ src/crypttrace/
724
728
  bridges.py # cross-chain matching
725
729
  watch.py # watchlist + alerts
726
730
  report.py # Markdown + JSON reports
731
+ export.py # traced graph as GraphML (Gephi) and edge-list CSV (Maltego)
727
732
  prices.py # USD valuation
728
733
  render.py # rich terminal rendering
729
734
  webapp.py # Flask backend for the web UI
@@ -12,6 +12,7 @@ src/crypttrace/cli.py
12
12
  src/crypttrace/config.py
13
13
  src/crypttrace/drain.py
14
14
  src/crypttrace/evidence.py
15
+ src/crypttrace/export.py
15
16
  src/crypttrace/freeze.py
16
17
  src/crypttrace/funder.py
17
18
  src/crypttrace/gascluster.py
File without changes
File without changes
File without changes