crypttrace 0.7.0__tar.gz → 0.8.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.
- {crypttrace-0.7.0/src/crypttrace.egg-info → crypttrace-0.8.0}/PKG-INFO +6 -2
- {crypttrace-0.7.0 → crypttrace-0.8.0}/README.md +5 -1
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/report.py +20 -8
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/web/index.html +67 -1
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/webapp.py +35 -1
- {crypttrace-0.7.0 → crypttrace-0.8.0/src/crypttrace.egg-info}/PKG-INFO +6 -2
- {crypttrace-0.7.0 → crypttrace-0.8.0}/LICENSE +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/pyproject.toml +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/setup.cfg +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/chains.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/cli.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/config.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/etherscan.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/tron.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/known.json +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/poisoning.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/store.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.7.0 → crypttrace-0.8.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.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron and Solana
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -131,7 +131,11 @@ crypttrace serve # then open http://127.0.0.1:8000
|
|
|
131
131
|
Paste an address, pick a chain and direction, and get an interactive fund-flow
|
|
132
132
|
graph: nodes coloured by what they are, arrows showing where value went, click a
|
|
133
133
|
node for its full profile, click a transfer line to open it on the block
|
|
134
|
-
explorer. A side panel shows the profile, first-funder chain
|
|
134
|
+
explorer. A side panel shows the profile, first-funder chain, off-ramp check
|
|
135
|
+
and whether USDT/USDC at the address is frozen. Tabs add the assessment, the
|
|
136
|
+
timeline, the list of who sent funds here, and address poisoning (look-alikes
|
|
137
|
+
planted in the wallet's history, and payments lured by this address). **Case
|
|
138
|
+
file** downloads the same one-page HTML report `crypttrace report` writes.
|
|
135
139
|
Everything runs on your machine — nothing is uploaded anywhere.
|
|
136
140
|
|
|
137
141
|
This exists so non-technical victims can use the tool at all: a form and a
|
|
@@ -97,7 +97,11 @@ crypttrace serve # then open http://127.0.0.1:8000
|
|
|
97
97
|
Paste an address, pick a chain and direction, and get an interactive fund-flow
|
|
98
98
|
graph: nodes coloured by what they are, arrows showing where value went, click a
|
|
99
99
|
node for its full profile, click a transfer line to open it on the block
|
|
100
|
-
explorer. A side panel shows the profile, first-funder chain
|
|
100
|
+
explorer. A side panel shows the profile, first-funder chain, off-ramp check
|
|
101
|
+
and whether USDT/USDC at the address is frozen. Tabs add the assessment, the
|
|
102
|
+
timeline, the list of who sent funds here, and address poisoning (look-alikes
|
|
103
|
+
planted in the wallet's history, and payments lured by this address). **Case
|
|
104
|
+
file** downloads the same one-page HTML report `crypttrace report` writes.
|
|
101
105
|
Everything runs on your machine — nothing is uploaded anywhere.
|
|
102
106
|
|
|
103
107
|
This exists so non-technical victims can use the tool at all: a form and a
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.8.0"
|
|
@@ -174,22 +174,32 @@ def collect(address: str, chain: str, depth: int, branching: int, asset=None) ->
|
|
|
174
174
|
def generate(address: str, chain: str, depth: int, branching: int,
|
|
175
175
|
out_dir: Path, asset=None, guidance: Optional[dict] = None) -> Dict[str, Path]:
|
|
176
176
|
"""Write <case>.html, <case>.md and <case>.json; returns their paths."""
|
|
177
|
-
data =
|
|
178
|
-
if guidance:
|
|
179
|
-
data["guidance"] = guidance
|
|
177
|
+
data, raw, digest = build(address, chain, depth, branching, asset, guidance)
|
|
180
178
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
-
base =
|
|
179
|
+
base = case_name(address, chain)
|
|
182
180
|
paths = {k: out_dir / f"{base}.{k}" for k in ("html", "md", "json")}
|
|
183
|
-
raw = json.dumps(data, indent=2, ensure_ascii=False, default=str)
|
|
184
181
|
# bytes, not text mode: on Windows text mode turns LF into CRLF, and the hash
|
|
185
182
|
# printed in the HTML would no longer match the file next to it
|
|
186
183
|
paths["json"].write_bytes(raw.encode("utf-8"))
|
|
187
|
-
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
188
184
|
paths["md"].write_text(_render_md(data, paths["json"].name), encoding="utf-8")
|
|
189
185
|
paths["html"].write_text(render_html(data, raw, digest, paths["json"].name), encoding="utf-8")
|
|
190
186
|
return paths
|
|
191
187
|
|
|
192
188
|
|
|
189
|
+
def case_name(address: str, chain: str) -> str:
|
|
190
|
+
return f"{chain}_{address[:10]}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build(address: str, chain: str, depth: int, branching: int, asset=None,
|
|
194
|
+
guidance: Optional[dict] = None):
|
|
195
|
+
"""(data, raw JSON, its SHA-256) — the web UI renders the page from this in memory."""
|
|
196
|
+
data = collect(address, chain, depth, branching, asset)
|
|
197
|
+
if guidance:
|
|
198
|
+
data["guidance"] = guidance
|
|
199
|
+
raw = json.dumps(data, indent=2, ensure_ascii=False, default=str)
|
|
200
|
+
return data, raw, hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
201
|
+
|
|
202
|
+
|
|
193
203
|
# ------------------------------------------------------------------ markdown
|
|
194
204
|
|
|
195
205
|
def _render_md(d: dict, json_name: str) -> str:
|
|
@@ -460,8 +470,10 @@ def render_html(d: dict, raw_json: str, digest: str, json_name: str) -> str:
|
|
|
460
470
|
P.append(f"<h2>Method and limits</h2><p>{_e(_METHOD)}</p>")
|
|
461
471
|
if d.get("errors"):
|
|
462
472
|
P.append("<p class='muted'>Parts that could not be read: " + _e("; ".join(d["errors"])) + "</p>")
|
|
463
|
-
|
|
464
|
-
|
|
473
|
+
where = (f"saved next to this file as <span class='mono'>{_e(json_name)}</span>"
|
|
474
|
+
if json_name else "embedded in this file")
|
|
475
|
+
P.append(f"<p class='muted'>The raw data is {where} (SHA-256 of the JSON: "
|
|
476
|
+
f"<span class='mono'>{digest}</span>) and embedded below for analysts.</p>")
|
|
465
477
|
# JSON inside <script type=application/json> is data, never executed. Every "<" is
|
|
466
478
|
# written as < (still valid JSON), so a crafted token or label name can neither
|
|
467
479
|
# close the element nor push the parser into its "<!--<script" escaping states.
|
|
@@ -228,6 +228,7 @@
|
|
|
228
228
|
<select id="depth" title="Depth"><option>2</option><option selected>3</option><option>4</option><option>5</option></select>
|
|
229
229
|
<button id="go">Investigate</button>
|
|
230
230
|
<button id="fit" class="ghost" title="Fit graph to view">Fit</button>
|
|
231
|
+
<button id="casefile" class="ghost" title="Download a one-page case file for an exchange or the police">Case file</button>
|
|
231
232
|
</div>
|
|
232
233
|
|
|
233
234
|
<div class="legend">
|
|
@@ -246,6 +247,7 @@
|
|
|
246
247
|
<button class="tab" data-view="assess">Assessment</button>
|
|
247
248
|
<button class="tab" data-view="timeline">Timeline</button>
|
|
248
249
|
<button class="tab" data-view="victims">Who sent funds here</button>
|
|
250
|
+
<button class="tab" data-view="poison">Poisoning</button>
|
|
249
251
|
</div>
|
|
250
252
|
<div class="stage">
|
|
251
253
|
<div class="loader" id="loader"></div>
|
|
@@ -255,6 +257,7 @@
|
|
|
255
257
|
<div class="view" id="v-assess"></div>
|
|
256
258
|
<div class="view" id="v-timeline"></div>
|
|
257
259
|
<div class="view" id="v-victims"></div>
|
|
260
|
+
<div class="view" id="v-poison"></div>
|
|
258
261
|
<div class="scaleview" id="scaleview" style="display:none">100%</div>
|
|
259
262
|
<div class="zoomctl" id="zoomctl">
|
|
260
263
|
<button id="zin" title="Zoom in">+</button>
|
|
@@ -474,7 +477,7 @@ async function run(){
|
|
|
474
477
|
setStatus('tracing the chain…',false,true); loader(35);
|
|
475
478
|
$('#hint').classList.add('hide');
|
|
476
479
|
loaded={}; // new address -> other views must reload
|
|
477
|
-
$('#v-timeline').innerHTML=''; $('#v-victims').innerHTML=''; $('#v-assess').innerHTML='';
|
|
480
|
+
$('#v-timeline').innerHTML=''; $('#v-victims').innerHTML=''; $('#v-assess').innerHTML=''; $('#v-poison').innerHTML='';
|
|
478
481
|
try{
|
|
479
482
|
const dir=$('#direction').value;
|
|
480
483
|
const g=await api(`/api/trace?address=${q(a)}&chain=${chain()}&depth=${$('#depth').value}&asset=${q($('#asset').value)}&direction=${dir}`);
|
|
@@ -524,12 +527,14 @@ function showView(v){
|
|
|
524
527
|
$('#v-assess').classList.toggle('on',v==='assess');
|
|
525
528
|
$('#v-timeline').classList.toggle('on',v==='timeline');
|
|
526
529
|
$('#v-victims').classList.toggle('on',v==='victims');
|
|
530
|
+
$('#v-poison').classList.toggle('on',v==='poison');
|
|
527
531
|
const a=$('#addr').value.trim();
|
|
528
532
|
if(a && !loaded[v+a]) {
|
|
529
533
|
loaded[v+a]=true;
|
|
530
534
|
if(v==='timeline') loadTimeline(a);
|
|
531
535
|
if(v==='victims') loadVictims(a);
|
|
532
536
|
if(v==='assess') loadAssessment(a);
|
|
537
|
+
if(v==='poison') loadPoisoning(a);
|
|
533
538
|
}
|
|
534
539
|
}
|
|
535
540
|
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>showView(t.dataset.view));
|
|
@@ -647,6 +652,67 @@ async function verifyInto(container, addr){
|
|
|
647
652
|
}catch(e){ slot.innerHTML=`<span class="vfy vfy-unavailable">verification unavailable</span>`; }
|
|
648
653
|
}
|
|
649
654
|
|
|
655
|
+
// Address poisoning: look-alikes planted in this wallet's history, and — seen from
|
|
656
|
+
// the other side — payments this address received right after luring the payer.
|
|
657
|
+
async function loadPoisoning(a){
|
|
658
|
+
const el=$('#v-poison');
|
|
659
|
+
el.innerHTML='<div class="muted"><span class="spin"></span>Comparing every address in this history…</div>';
|
|
660
|
+
try{
|
|
661
|
+
const r=await api(`/api/poisoning?address=${q(a)}&chain=${chain()}`);
|
|
662
|
+
const amt=v=>Number(v).toLocaleString(undefined,{maximumFractionDigits:2});
|
|
663
|
+
const addr=x=>`<a class="mono" href="${safeUrl(addrUrl(x))}" target="_blank" rel="noopener">${esc(x)}</a>`;
|
|
664
|
+
let h='';
|
|
665
|
+
for(const x of r.lured||[]){
|
|
666
|
+
h+=`<div class="verdict">Lured payment: ${addr(x.payer)} paid <b>${amt(x.paid)} ${esc(x.symbol)}</b>
|
|
667
|
+
on ${dstamp(x.paid_ts)} UTC, right after this address lured it (${esc(x.lure)}).
|
|
668
|
+
${x.imitates?`It imitates ${addr(x.imitates)}, an address the payer had really used.`
|
|
669
|
+
:'<span class="muted">The imitated address was not found in the payer\'s recent history.</span>'}</div>`;
|
|
670
|
+
}
|
|
671
|
+
const paid=(r.lookalikes||[]).filter(p=>p.sent_to_lookalike>0);
|
|
672
|
+
for(const p of paid){
|
|
673
|
+
h+=`<div class="verdict">Money went to a look-alike: <b>${amt(p.sent_to_lookalike)} ${esc(p.symbols.join('/'))}</b>
|
|
674
|
+
<div class="kv"><span class="k">real address</span><span class="v">${addr(p.genuine)}</span></div>
|
|
675
|
+
<div class="kv"><span class="k">look-alike</span><span class="v">${addr(p.lookalike)}</span></div>
|
|
676
|
+
<div class="muted">Same first ${p.matching.prefix} and last ${p.matching.suffix} characters;
|
|
677
|
+
the look-alike first appeared ${dstamp(p.lookalike_first_seen)} UTC.</div></div>`;
|
|
678
|
+
}
|
|
679
|
+
if(r.campaign){
|
|
680
|
+
h+=`<div class="verdict">Poisoning campaign: this address sent ${r.campaign.bait_transfers} zero, dust or
|
|
681
|
+
counterfeit transfers to ${r.campaign.targets} different wallets.</div>`;
|
|
682
|
+
}
|
|
683
|
+
const planted=(r.lookalikes||[]).filter(p=>!(p.sent_to_lookalike>0));
|
|
684
|
+
if(planted.length){
|
|
685
|
+
h+=`<h3 style="font-size:11px;letter-spacing:.09em;color:var(--dim);margin:16px 0 8px">
|
|
686
|
+
${planted.length} LOOK-ALIKE ADDRESS(ES) PLANTED IN THIS HISTORY — NEVER COPY AN ADDRESS FROM HERE</h3>
|
|
687
|
+
<table class="grid"><thead><tr><th>Look-alike</th><th>Imitates</th><th>Match</th>
|
|
688
|
+
<th style="text-align:right">Bait</th><th>First seen (UTC)</th></tr></thead><tbody>`+
|
|
689
|
+
planted.map(p=>`<tr><td>${addr(p.lookalike)}</td><td>${addr(p.genuine)}</td>
|
|
690
|
+
<td>${esc(p.resemblance)} (${p.matching.prefix}+${p.matching.suffix})</td>
|
|
691
|
+
<td class="num">${p.bait_transfers}</td><td>${dstamp(p.lookalike_first_seen)}</td></tr>`).join('')+
|
|
692
|
+
'</tbody></table>';
|
|
693
|
+
}
|
|
694
|
+
el.innerHTML=h||'<div class="muted">No look-alike addresses or poisoning pattern in this history.</div>';
|
|
695
|
+
}catch(e){ el.innerHTML=`<div class="err">${esc(e.message)}</div>`; }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The case file: the same one-page HTML `crypttrace report` writes, built by the
|
|
699
|
+
// backend and handed to the browser as a download.
|
|
700
|
+
async function downloadCaseFile(){
|
|
701
|
+
const a=$('#addr').value.trim(), b=$('#casefile');
|
|
702
|
+
if(!a){ setStatus('enter an address',true); return; }
|
|
703
|
+
b.disabled=true; setStatus('building the case file…',false,true);
|
|
704
|
+
try{
|
|
705
|
+
const r=await fetch(`/api/report?address=${q(a)}&chain=${chain()}&asset=${q($('#asset').value)}&depth=${q($('#depth').value)}`);
|
|
706
|
+
if(!r.ok){ const j=await r.json().catch(()=>({})); throw new Error(j.error||`HTTP ${r.status}`); }
|
|
707
|
+
const name=((r.headers.get('Content-Disposition')||'').match(/filename="([^"]+)"/)||[])[1]||'case-file.html';
|
|
708
|
+
const u=URL.createObjectURL(await r.blob()), l=document.createElement('a');
|
|
709
|
+
l.href=u; l.download=name; l.click(); URL.revokeObjectURL(u);
|
|
710
|
+
setStatus(`case file saved: ${name}`);
|
|
711
|
+
}catch(e){ setStatus(e.message,true); }
|
|
712
|
+
finally{ b.disabled=false; }
|
|
713
|
+
}
|
|
714
|
+
$('#casefile').addEventListener('click',downloadCaseFile);
|
|
715
|
+
|
|
650
716
|
function downloadCsv(){
|
|
651
717
|
const head=['address','hop',`amount_sent_${victimSym}`,'transactions','first_seen_utc','last_seen_utc','label','explorer'];
|
|
652
718
|
const body=victimRows.map(r=>[r.address,r.hop,r.value,r.txs,dstamp(r.first_seen),dstamp(r.last_seen),
|
|
@@ -10,7 +10,7 @@ Launch with: crypttrace serve (then open http://127.0.0.1:8000)
|
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
from flask import Flask, jsonify, request, send_from_directory
|
|
13
|
+
from flask import Flask, Response, jsonify, request, send_from_directory
|
|
14
14
|
|
|
15
15
|
from crypttrace.fetchers import etherscan
|
|
16
16
|
from crypttrace.labels import labels
|
|
@@ -255,6 +255,40 @@ def create_app() -> Flask:
|
|
|
255
255
|
return jsonify({"error": str(e)}), 400
|
|
256
256
|
return jsonify({"hops": hops})
|
|
257
257
|
|
|
258
|
+
@app.route("/api/poisoning")
|
|
259
|
+
def api_poisoning():
|
|
260
|
+
addr = request.args.get("address", "")
|
|
261
|
+
chain = request.args.get("chain", "eth")
|
|
262
|
+
bad = validate(addr, chain)
|
|
263
|
+
if bad:
|
|
264
|
+
return jsonify({"error": bad}), 400
|
|
265
|
+
from crypttrace import poisoning
|
|
266
|
+
try:
|
|
267
|
+
return jsonify({"lookalikes": poisoning.lookalikes(addr, chain),
|
|
268
|
+
"lured": poisoning.baited_payments(addr, chain),
|
|
269
|
+
"campaign": poisoning.campaign(addr, chain)})
|
|
270
|
+
except _KNOWN_ERRORS as e:
|
|
271
|
+
return jsonify({"error": str(e)}), 400
|
|
272
|
+
|
|
273
|
+
@app.route("/api/report")
|
|
274
|
+
def api_report():
|
|
275
|
+
"""The HTML case file, built in memory and handed over as a download."""
|
|
276
|
+
addr = request.args.get("address", "")
|
|
277
|
+
chain = request.args.get("chain", "eth")
|
|
278
|
+
bad = validate(addr, chain)
|
|
279
|
+
if bad:
|
|
280
|
+
return jsonify({"error": bad}), 400
|
|
281
|
+
from crypttrace import report as report_mod
|
|
282
|
+
try:
|
|
283
|
+
asset = assets.resolve_asset(request.args.get("asset") or None, chain)
|
|
284
|
+
depth = max(1, min(5, int(request.args.get("depth", 3))))
|
|
285
|
+
data, raw, digest = report_mod.build(addr, chain, depth, 3, asset)
|
|
286
|
+
except _KNOWN_ERRORS as e:
|
|
287
|
+
return jsonify({"error": str(e)}), 400
|
|
288
|
+
name = report_mod.case_name(addr, chain) + ".html"
|
|
289
|
+
return Response(report_mod.render_html(data, raw, digest, None), mimetype="text/html",
|
|
290
|
+
headers={"Content-Disposition": f'attachment; filename="{name}"'})
|
|
291
|
+
|
|
258
292
|
@app.route("/api/freeze")
|
|
259
293
|
def api_freeze():
|
|
260
294
|
addr = request.args.get("address", "")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron and Solana
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -131,7 +131,11 @@ crypttrace serve # then open http://127.0.0.1:8000
|
|
|
131
131
|
Paste an address, pick a chain and direction, and get an interactive fund-flow
|
|
132
132
|
graph: nodes coloured by what they are, arrows showing where value went, click a
|
|
133
133
|
node for its full profile, click a transfer line to open it on the block
|
|
134
|
-
explorer. A side panel shows the profile, first-funder chain
|
|
134
|
+
explorer. A side panel shows the profile, first-funder chain, off-ramp check
|
|
135
|
+
and whether USDT/USDC at the address is frozen. Tabs add the assessment, the
|
|
136
|
+
timeline, the list of who sent funds here, and address poisoning (look-alikes
|
|
137
|
+
planted in the wallet's history, and payments lured by this address). **Case
|
|
138
|
+
file** downloads the same one-page HTML report `crypttrace report` writes.
|
|
135
139
|
Everything runs on your machine — nothing is uploaded anywhere.
|
|
136
140
|
|
|
137
141
|
This exists so non-technical victims can use the tool at all: a form and a
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|