tg-flowmap 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 (54) hide show
  1. tg_flowmap/__init__.py +0 -0
  2. tg_flowmap/__main__.py +6 -0
  3. tg_flowmap/analyzer.py +5 -0
  4. tg_flowmap/ast_evaluator.py +13 -0
  5. tg_flowmap/cli.py +470 -0
  6. tg_flowmap/core/__init__.py +16 -0
  7. tg_flowmap/core/comparator.py +168 -0
  8. tg_flowmap/core/llm_exporter.py +106 -0
  9. tg_flowmap/core/models.py +142 -0
  10. tg_flowmap/dast/__init__.py +11 -0
  11. tg_flowmap/dast/auth.py +33 -0
  12. tg_flowmap/dast/capture.py +235 -0
  13. tg_flowmap/dast/comparison.py +289 -0
  14. tg_flowmap/dast/config.py +252 -0
  15. tg_flowmap/dast/crawler.py +1044 -0
  16. tg_flowmap/dast/fingerprint.py +54 -0
  17. tg_flowmap/dast/html_render.py +140 -0
  18. tg_flowmap/dast/logging_setup.py +47 -0
  19. tg_flowmap/dast/map_render.py +506 -0
  20. tg_flowmap/dast/media.py +93 -0
  21. tg_flowmap/dast/models.py +303 -0
  22. tg_flowmap/dast/navigation_executor.py +135 -0
  23. tg_flowmap/dast/navigation_graph.py +265 -0
  24. tg_flowmap/dast/navigator.py +287 -0
  25. tg_flowmap/dast/ratelimit.py +76 -0
  26. tg_flowmap/dast/reports.py +475 -0
  27. tg_flowmap/dast/route_planner.py +141 -0
  28. tg_flowmap/dast/safety.py +267 -0
  29. tg_flowmap/dast/scheduler.py +220 -0
  30. tg_flowmap/dast/screen_identity.py +259 -0
  31. tg_flowmap/dast/state.py +1015 -0
  32. tg_flowmap/dast/timeline_log.py +102 -0
  33. tg_flowmap/dast/tl_serial.py +66 -0
  34. tg_flowmap/dast/utf16.py +36 -0
  35. tg_flowmap/emoji_fetcher.py +13 -0
  36. tg_flowmap/models.py +19 -0
  37. tg_flowmap/parser.py +9 -0
  38. tg_flowmap/sast/__init__.py +17 -0
  39. tg_flowmap/sast/analyzer.py +472 -0
  40. tg_flowmap/sast/emoji.py +168 -0
  41. tg_flowmap/sast/evaluator.py +241 -0
  42. tg_flowmap/sast/parser.py +1302 -0
  43. tg_flowmap/visualizer/__init__.py +5 -0
  44. tg_flowmap/visualizer/html_builder.py +33 -0
  45. tg_flowmap/visualizer/templates/viewer.css +750 -0
  46. tg_flowmap/visualizer/templates/viewer.html +82 -0
  47. tg_flowmap/visualizer/templates/viewer.js +1659 -0
  48. tg_flowmap/visualizer.py +5 -0
  49. tg_flowmap-0.1.0.dist-info/METADATA +162 -0
  50. tg_flowmap-0.1.0.dist-info/RECORD +54 -0
  51. tg_flowmap-0.1.0.dist-info/WHEEL +5 -0
  52. tg_flowmap-0.1.0.dist-info/entry_points.txt +2 -0
  53. tg_flowmap-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. tg_flowmap-0.1.0.dist-info/top_level.txt +1 -0
tg_flowmap/__init__.py ADDED
File without changes
tg_flowmap/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
tg_flowmap/analyzer.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.sast.analyzer import GraphAnalyzer
4
+
5
+ __all__ = ["GraphAnalyzer"]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.sast.evaluator import (
4
+ _ast_to_value,
5
+ evaluate_ast_node,
6
+ extract_module_constants,
7
+ )
8
+
9
+ __all__ = [
10
+ "_ast_to_value",
11
+ "evaluate_ast_node",
12
+ "extract_module_constants",
13
+ ]
tg_flowmap/cli.py ADDED
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from tg_flowmap.core.comparator import BotComparator
11
+ from tg_flowmap.core.models import BotMap
12
+ from tg_flowmap.sast.analyzer import GraphAnalyzer
13
+ from tg_flowmap.sast.parser import AstParser
14
+ from tg_flowmap.visualizer.html_builder import HtmlVisualizer
15
+
16
+
17
+ def load_env_token(target_path: Path) -> Optional[str]:
18
+ """Tries to find BOT_TOKEN or TELEGRAM_BOT_TOKEN in env or .env files."""
19
+ for var in ("TELEGRAM_BOT_TOKEN", "BOT_TOKEN"):
20
+ val = os.environ.get(var)
21
+ if val and ":" in val:
22
+ return val.strip()
23
+
24
+ candidate_envs = [
25
+ Path.cwd() / ".env",
26
+ target_path / ".env" if target_path.is_dir() else target_path.parent / ".env",
27
+ target_path.parent / ".env",
28
+ Path(__file__).resolve().parent.parent / ".env",
29
+ ]
30
+ for env_path in candidate_envs:
31
+ if env_path.exists():
32
+ try:
33
+ for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
34
+ line = line.strip()
35
+ if line.startswith("#") or "=" not in line:
36
+ continue
37
+ k, v = line.split("=", 1)
38
+ k = k.strip()
39
+ v = v.strip().strip("'\"")
40
+ if k in ("TELEGRAM_BOT_TOKEN", "BOT_TOKEN") and ":" in v:
41
+ return v
42
+ except Exception:
43
+ pass
44
+ return None
45
+
46
+
47
+ def build_sast_markdown_report(bot_map: BotMap, target_path: Path) -> str:
48
+ stats = bot_map.stats
49
+ name = target_path.name
50
+ lines = [
51
+ f"# 🧭 TG-FLOWMAP SAST Report: {name}",
52
+ "",
53
+ f"- **Путь:** `{target_path.resolve()}`",
54
+ f"- **Всего экранов:** {stats.get('total_nodes', 0)}",
55
+ f"- **Всего переходов / связей:** {stats.get('total_edges', 0)}",
56
+ f"- **Тупиков (Dead-end):** {stats.get('dead_ends', 0)}",
57
+ f"- **Ошибок эмодзи:** {stats.get('broken_emojis', 0)}",
58
+ f"- **Экранов без кнопки «Назад»:** {stats.get('missing_backs', 0)}",
59
+ f"- **Скрытых команд (Orphan Commands):** {stats.get('orphan_commands', 0)}",
60
+ "",
61
+ "---",
62
+ "",
63
+ "## 🔍 Найденные дефекты и предупреждения",
64
+ "",
65
+ ]
66
+ if bot_map.issues:
67
+ for idx, issue in enumerate(bot_map.issues, 1):
68
+ icon = "❌" if issue.severity == "error" else "⚠️" if issue.severity == "warning" else "ℹ️"
69
+ src_info = ""
70
+ if issue.source_file:
71
+ p = Path(issue.source_file).name
72
+ line = f":{issue.line_number}" if issue.line_number else ""
73
+ src_info = f" *({p}{line})*"
74
+ lines.append(f"{idx}. {icon} **[{issue.issue_type.upper()}]** {issue.message}{src_info}")
75
+ else:
76
+ lines.append("✅ *Дефектов и тупиков в навигации не обнаружено.*")
77
+
78
+ lines.extend([
79
+ "",
80
+ "---",
81
+ "",
82
+ "## 🗺️ Зарегистрированные экраны и клавиатуры",
83
+ "",
84
+ ])
85
+ for node_id, node in sorted(bot_map.nodes.items(), key=lambda x: x[0]):
86
+ btns = []
87
+ if node.keyboard:
88
+ btns = [b.text for b in node.keyboard.all_buttons()]
89
+ btn_str = ", ".join(f"`{b}`" for b in btns) if btns else "*(без кнопок)*"
90
+ lines.append(f"- **`{node_id}`** ({node.title or 'screen'}): кнопки -> {btn_str}")
91
+
92
+ return "\n".join(lines)
93
+
94
+
95
+ def run_analysis(
96
+ target_path: Path,
97
+ output_html: Path | None = None,
98
+ output_json: Path | None = None,
99
+ strict: bool = False,
100
+ bot_token: str | None = None,
101
+ parser: AstParser | None = None,
102
+ output_md: Path | None = None,
103
+ ) -> int:
104
+ if sys.platform == "win32":
105
+ try:
106
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
107
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
108
+ except Exception:
109
+ pass
110
+
111
+ if not target_path.exists():
112
+ print(f"[ERROR] Целевой путь не существует: {target_path}", file=sys.stderr)
113
+ return 1
114
+
115
+ if parser is None:
116
+ parser = AstParser()
117
+ if target_path.is_file():
118
+ parser.parse_file(target_path)
119
+ else:
120
+ parser.parse_dir(target_path)
121
+
122
+ if parser.skipped_files:
123
+ print(f"⚠️ Предупреждение: пропущено файлов из-за синтаксических ошибок: {len(parser.skipped_files)}", file=sys.stderr)
124
+ for sp, err in parser.skipped_files:
125
+ print(f" - {sp.name}: {err}", file=sys.stderr)
126
+
127
+ analyzer = GraphAnalyzer(
128
+ parser.nodes,
129
+ parser.keyboards,
130
+ screen_aliases=parser.screen_aliases,
131
+ hub_return_nodes=parser.hub_return_nodes,
132
+ registered_commands=parser.registered_menu_commands,
133
+ has_set_my_commands=parser.has_set_my_commands,
134
+ skipped_files=parser.skipped_files,
135
+ )
136
+ bot_map = analyzer.build_map()
137
+
138
+ # Fetch Telegram Premium custom emojis if present
139
+ token = bot_token or load_env_token(target_path)
140
+ if parser.custom_emoji_ids:
141
+ if token:
142
+ from tg_flowmap.sast.emoji import fetch_custom_emojis
143
+ emojis = fetch_custom_emojis(token, parser.custom_emoji_ids)
144
+ bot_map.custom_emojis = emojis
145
+ else:
146
+ from tg_flowmap.sast.emoji import get_default_cache_dir, load_cached_emoji
147
+ cache_dir = get_default_cache_dir()
148
+ cached = {}
149
+ for eid in parser.custom_emoji_ids:
150
+ data_url = load_cached_emoji(eid, cache_dir)
151
+ if data_url:
152
+ cached[eid] = data_url
153
+ if cached:
154
+ bot_map.custom_emojis = cached
155
+
156
+ stats = bot_map.stats
157
+ print("=" * 60)
158
+ print(" 🧭 TG-FLOWMAP — Telegram Bot Analyzer (SAST)")
159
+ print("=" * 60)
160
+ print(f"Цель: {target_path.resolve()}")
161
+ print(f"Всего экранов: {stats.get('total_nodes', 0)}")
162
+ print(f"Всего связей: {stats.get('total_edges', 0)}")
163
+ print(f"Клавиатур найдено: {len(parser.keyboards)}")
164
+ print(f"Дубликатов экранов:{stats.get('duplicate_screens', 0)}")
165
+ if parser.custom_emoji_ids:
166
+ print(f"Премиум эмодзи: {len(parser.custom_emoji_ids)} (в кэше: {len(bot_map.custom_emojis)})")
167
+ print(f"Тупиков (Dead-end):{stats.get('dead_ends', 0)}")
168
+ print(f"Ошибок эмодзи: {stats.get('broken_emojis', 0)}")
169
+ print(f"Без кнопки Назад: {stats.get('missing_backs', 0)}")
170
+ print(f"Скрытых команд: {stats.get('orphan_commands', 0)}")
171
+ print("-" * 60)
172
+
173
+ if bot_map.issues:
174
+ print("\n🔍 НАЙДЕННЫЕ ДЕФЕКТЫ И ПРЕДУПРЕЖДЕНИЯ:")
175
+ for idx, issue in enumerate(bot_map.issues, 1):
176
+ icon = "❌" if issue.severity == "error" else "⚠️" if issue.severity == "warning" else "ℹ️"
177
+ src_info = ""
178
+ if issue.source_file:
179
+ p = Path(issue.source_file).name
180
+ line = f":{issue.line_number}" if issue.line_number else ""
181
+ src_info = f" ({p}{line})"
182
+ print(f" {idx}. {icon} [{issue.issue_type.upper()}] {issue.message}{src_info}")
183
+ print()
184
+ else:
185
+ print("\n✅ Дефектов и тупиков не обнаружено!")
186
+
187
+ if output_md:
188
+ output_md.parent.mkdir(parents=True, exist_ok=True)
189
+ output_md.write_text(build_sast_markdown_report(bot_map, target_path), encoding="utf-8")
190
+ print(f"📄 Отчёт сохранён: {output_md.resolve()}")
191
+
192
+ if output_html:
193
+ output_html.parent.mkdir(parents=True, exist_ok=True)
194
+ visualizer = HtmlVisualizer(bot_map)
195
+ visualizer.render(output_html)
196
+ print(f"📊 Интерактивная карта сохранена: {output_html.resolve()}")
197
+
198
+ if output_json:
199
+ output_json.parent.mkdir(parents=True, exist_ok=True)
200
+ output_json.write_text(json.dumps(bot_map.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
201
+ print(f"📦 JSON-структура сохранена: {output_json.resolve()}")
202
+
203
+ dead_ends = stats.get("dead_ends", 0)
204
+ broken_emojis = stats.get("broken_emojis", 0)
205
+ has_errors = any(i.severity == "error" for i in bot_map.issues) or (dead_ends > 0) or (broken_emojis > 0)
206
+ if strict and has_errors:
207
+ print("\n[STRICT MODE] Обнаружены критические ошибки! Выход с кодом 1.")
208
+ return 1
209
+
210
+ return 0
211
+
212
+
213
+ def run_dast_scan(
214
+ bot_target: str,
215
+ output_dir: Path | None = None,
216
+ config_path: Path | None = None,
217
+ ) -> int:
218
+ try:
219
+ import asyncio
220
+ import telethon
221
+ except ImportError as e:
222
+ print(
223
+ f"[ERROR] Для DAST режима требуется Telethon.\n"
224
+ f"Установите зависимость: pip install \"tg-flowmap[dast]\"\n({e})",
225
+ file=sys.stderr,
226
+ )
227
+ return 1
228
+
229
+ try:
230
+ from tg_flowmap.dast.config import load_config, normalize_bot_username
231
+ from tg_flowmap.dast.reports import generate_reports
232
+ from tg_flowmap.dast.scheduler import run_scan
233
+ from tg_flowmap.dast.state import State
234
+ except Exception as e:
235
+ print(f"[ERROR] Ошибка инициализации DAST модулей: {e}", file=sys.stderr)
236
+ return 1
237
+
238
+ bot_name = normalize_bot_username(bot_target)
239
+ cfg_file = config_path or Path("config.json")
240
+ if not cfg_file.exists():
241
+ print(
242
+ f"[ERROR] Не найден конфигурационный файл: {cfg_file}\n"
243
+ f"Создайте config.json с параметрами api_id и api_hash (см. config.example.json)",
244
+ file=sys.stderr,
245
+ )
246
+ return 1
247
+
248
+ cfg = load_config(cfg_file)
249
+ if output_dir:
250
+ cfg.custom_output_dir = output_dir
251
+
252
+ print("=" * 60)
253
+ print(" 🤖 TG-FLOWMAP — Telegram Bot Crawler (DAST)")
254
+ print("=" * 60)
255
+ print(f"Целевой бот: {bot_name}")
256
+ print(f"Директория: {cfg.output_dir.resolve()}")
257
+ print("-" * 60)
258
+
259
+ try:
260
+ statuses = asyncio.run(run_scan(cfg, bots=[bot_name]))
261
+ bot_status = statuses.get(bot_name, "ok")
262
+ if "error" in bot_status.lower():
263
+ print(f"[WARN] Обход завершился со статусом: {bot_status}")
264
+ except KeyboardInterrupt:
265
+ print("\n[INFO] Обход прерван пользователем. Прогресс сохранён в БД.")
266
+ except Exception as e:
267
+ print(f"[ERROR] Ошибка выполнения обхода: {e}", file=sys.stderr)
268
+ return 1
269
+
270
+ state = State(cfg.db_path)
271
+ try:
272
+ reports = generate_reports(cfg, state, bot_name)
273
+ finally:
274
+ state.close()
275
+
276
+ print(f"\n✅ Обход завершён!")
277
+ if reports.get("report"):
278
+ print(f"📄 Отчёт сохранён: {reports.get('report')}")
279
+ if reports.get("map_html"):
280
+ print(f"📊 Интерактивная карта: {reports.get('map_html')}")
281
+ return 0
282
+
283
+
284
+ def _load_target_data(target: str, out_dir: Path) -> dict:
285
+ p = Path(target)
286
+ if p.exists():
287
+ parser = AstParser()
288
+ if p.is_file():
289
+ parser.parse_file(p)
290
+ else:
291
+ parser.parse_dir(p)
292
+ analyzer = GraphAnalyzer(
293
+ parser.nodes,
294
+ parser.keyboards,
295
+ screen_aliases=parser.screen_aliases,
296
+ hub_return_nodes=parser.hub_return_nodes,
297
+ registered_commands=parser.registered_menu_commands,
298
+ has_set_my_commands=parser.has_set_my_commands,
299
+ skipped_files=parser.skipped_files,
300
+ )
301
+ bot_map = analyzer.build_map()
302
+ return {
303
+ "metrics": BotComparator.extract_metrics_from_bot_map(bot_map),
304
+ "screens": [
305
+ {
306
+ "screen_id": nid,
307
+ "title": n.title,
308
+ "buttons": [b.text for b in n.keyboard.all_buttons()] if n.keyboard else [],
309
+ }
310
+ for nid, n in bot_map.nodes.items()
311
+ ],
312
+ }
313
+ clean = target.lstrip("@")
314
+ cand = out_dir / clean / "graph.json"
315
+ if cand.exists():
316
+ try:
317
+ graph_data = json.loads(cand.read_text(encoding="utf-8"))
318
+ nodes_data = graph_data.get("nodes") or graph_data.get("screens") or []
319
+ nodes = list(nodes_data.values()) if isinstance(nodes_data, dict) else (nodes_data if isinstance(nodes_data, list) else [])
320
+ edges = graph_data.get("edges") or []
321
+ total_buttons = sum(len(n.get("buttons", [])) for n in nodes)
322
+ screens_list = [
323
+ {
324
+ "screen_id": n.get("id") or n.get("screen_id"),
325
+ "title": (n.get("text_normalized") or n.get("text") or n.get("id") or "").splitlines()[0][:40]
326
+ if (n.get("text_normalized") or n.get("text"))
327
+ else (n.get("id") or n.get("screen_id")),
328
+ "buttons": n.get("buttons", []),
329
+ }
330
+ for n in nodes
331
+ ]
332
+ return {
333
+ "metrics": {
334
+ "screens_count": len(nodes),
335
+ "buttons_count": total_buttons if total_buttons > 0 else len(edges),
336
+ "dead_ends": 0,
337
+ "missing_back": 0,
338
+ },
339
+ "screens": screens_list,
340
+ }
341
+ except Exception:
342
+ pass
343
+ return {"metrics": {"screens_count": 0, "buttons_count": 0, "dead_ends": 0, "missing_back": 0}, "screens": []}
344
+
345
+
346
+ def run_compare(
347
+ target_a: str,
348
+ target_b: str,
349
+ output_dir: Path | None = None,
350
+ ) -> int:
351
+ out_dir = output_dir or Path("output")
352
+ out_dir.mkdir(parents=True, exist_ok=True)
353
+
354
+ print("=" * 60)
355
+ print(" ⚔️ TG-FLOWMAP — Telegram Bot Benchmark & Comparison")
356
+ print("=" * 60)
357
+ print(f"Бот A (Исходный): {target_a}")
358
+ print(f"Бот B (Конкурент): {target_b}")
359
+ print("-" * 60)
360
+
361
+ data_a = _load_target_data(target_a, out_dir)
362
+ data_b = _load_target_data(target_b, out_dir)
363
+
364
+ path_a = Path(target_a)
365
+ path_b = Path(target_b)
366
+
367
+ comparator = BotComparator(
368
+ bot_a_name=path_a.name if path_a.exists() else target_a,
369
+ bot_a_data=data_a,
370
+ bot_b_name=path_b.name if path_b.exists() else target_b,
371
+ bot_b_data=data_b,
372
+ )
373
+
374
+ exported = comparator.export(out_dir)
375
+ print(f"\n✅ Сравнение успешно завершено!")
376
+ print(f"📊 Отчёт сравнения сохранён: {exported['comparison'].resolve()}")
377
+ print(f"🤖 Промпт для нейросети (ChatGPT / Claude) готов: {exported['llm_prompt'].resolve()}")
378
+ return 0
379
+
380
+
381
+ def main() -> None:
382
+ if sys.platform == "win32":
383
+ try:
384
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
385
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
386
+ except Exception:
387
+ pass
388
+
389
+ # Top-level parser
390
+ parser = argparse.ArgumentParser(
391
+ prog="tg-flowmap",
392
+ description="Unified Telegram Bot Analyzer: Static AST (SAST) + Dynamic Userbot Crawler (DAST) + LLM Benchmarker",
393
+ )
394
+ parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0.0")
395
+ subparsers = parser.add_subparsers(dest="subcommand", help="Доступные режимы анализа")
396
+
397
+ # SAST command
398
+ sast_parser = subparsers.add_parser("sast", aliases=["code", "scan"], help="Статический анализ кода бота (Zero-Runtime AST)")
399
+ sast_parser.add_argument("target", type=str, help="Путь к файлу бота (.py) или папке проекта")
400
+ sast_parser.add_argument("--strict", action="store_true", help="Возвращать код 1 при критических дефектах (для CI/CD)")
401
+ sast_parser.add_argument("--html", action="store_true", help="Сгенерировать интерактивную HTML-карту")
402
+ sast_parser.add_argument("-t", "--token", type=str, default=None, help="Токен бота для подгрузки оригинальных Premium эмодзи")
403
+ sast_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
404
+
405
+ # DAST command
406
+ dast_parser = subparsers.add_parser("dast", aliases=["crawl", "bot"], help="Динамический реверс-инжиниринг через Telethon юзербота")
407
+ dast_parser.add_argument("bot", type=str, help="Имя или юзернейм целевого бота (например, @example_bot)")
408
+ dast_parser.add_argument("--config", type=str, default="config.json", help="Путь к config.json Telethon")
409
+ dast_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
410
+
411
+ # COMPARE command
412
+ compare_parser = subparsers.add_parser("compare", aliases=["diff", "bench"], help="Сравнение двух ботов и генерация промпта для ИИ")
413
+ compare_parser.add_argument("target_a", type=str, help="Бот A (путь к коду или @bot)")
414
+ compare_parser.add_argument("target_b", type=str, help="Бот B (путь к коду или @bot конкурента)")
415
+ compare_parser.add_argument("-o", "--output-dir", type=str, default="output", help="Директория для отчетов (по умолчанию: output/)")
416
+
417
+ # Fast-path backward compatibility:
418
+ # If user called `tg-flowmap path/to/bot` directly without subcommand:
419
+ args_list = sys.argv[1:]
420
+ if args_list and args_list[0] not in (
421
+ "sast", "code", "scan", "dast", "crawl", "bot", "compare", "diff", "bench",
422
+ "-h", "--help", "-v", "--version"
423
+ ):
424
+ # Auto-route to sast mode!
425
+ args_list = ["sast"] + args_list
426
+
427
+ args = parser.parse_args(args_list)
428
+
429
+ if not args.subcommand:
430
+ parser.print_help()
431
+ sys.exit(0)
432
+
433
+ if args.subcommand in ("sast", "code", "scan"):
434
+ target = Path(args.target)
435
+ out_dir = Path(getattr(args, "output_dir", "output"))
436
+ out_dir.mkdir(parents=True, exist_ok=True)
437
+ stem = (target.stem if target.is_file() else target.resolve().name) or "report"
438
+ out_md = out_dir / f"{stem}_report.md"
439
+ out_json = out_dir / f"{stem}_data.json"
440
+ out_html = out_dir / f"{stem}_map.html" if getattr(args, "html", False) else None
441
+
442
+ exit_code = run_analysis(
443
+ target_path=target,
444
+ output_md=out_md,
445
+ output_html=out_html,
446
+ output_json=out_json,
447
+ strict=args.strict,
448
+ bot_token=args.token,
449
+ )
450
+ sys.exit(exit_code)
451
+
452
+ elif args.subcommand in ("dast", "crawl", "bot"):
453
+ exit_code = run_dast_scan(
454
+ bot_target=args.bot,
455
+ output_dir=Path(args.output_dir),
456
+ config_path=Path(args.config) if args.config else None,
457
+ )
458
+ sys.exit(exit_code)
459
+
460
+ elif args.subcommand in ("compare", "diff", "bench"):
461
+ exit_code = run_compare(
462
+ target_a=args.target_a,
463
+ target_b=args.target_b,
464
+ output_dir=Path(args.output_dir),
465
+ )
466
+ sys.exit(exit_code)
467
+
468
+
469
+ if __name__ == "__main__":
470
+ main()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from tg_flowmap.core.comparator import BotComparator
4
+ from tg_flowmap.core.llm_exporter import generate_llm_benchmark_prompt
5
+ from tg_flowmap.core.models import BotMap, Button, Edge, Issue, Keyboard, MessageNode
6
+
7
+ __all__ = [
8
+ "Button",
9
+ "Keyboard",
10
+ "MessageNode",
11
+ "Edge",
12
+ "Issue",
13
+ "BotMap",
14
+ "BotComparator",
15
+ "generate_llm_benchmark_prompt",
16
+ ]