ricotrevisan/bubble_ex

GitHub: ricotrevisan/bubble_ex

BubbleEx 是一个 Elixir 库,用于分析 Bubble.io 应用,支持数据库结构逆向导出、机密信息扫描和应用日志查询。

Stars: 0 | Forks: 0

# BubbleEx BubbleEx 是一组用于扫描 bubble.io 应用的实用工具。它可以: - 检查 URL 是否为 bubble.io 应用 - 检查 bubble 应用是否处于专用实例中 - 构建 db 结构并将其导出为 DBML、SQL (Postgres/SQLite/T-SQL)、Ecto、Zod、Xano 或 Convex - 检查暴露的 endpoint - 扫描暴露的机密信息(内置 Native 扫描器或 Trufflehog) - 查询应用日志以进行监控和调试 - 深入搜索嵌套数据结构以查找特定值 ## 错误处理 每个公共函数都返回 `{:ok, result}` 或 `{:error, %BubbleEx.Error{}}`。 `BubbleEx.Error` 是一个单一结构体,包含一个 `:kind`(一个封闭的 atom 集合,例如 `:not_a_bubble_app`、`:unauthorized`、`:invalid_input`、`:parse_failed`、 `:cli_missing`、`:request_failed`),一个人类可读的 `:message`,以及一个 `:context` 映射。可以通过 Pattern-match `kind` 来统一处理失败情况: ``` case BubbleEx.fetch_app("some-app") do {:ok, app} -> app {:error, %BubbleEx.Error{kind: :not_a_bubble_app}} -> :not_bubble {:error, %BubbleEx.Error{} = error} -> Logger.warning(Exception.message(error)) end ``` ## 安装 将 `bubble_ex` 添加到 `mix.exs` 的依赖列表中: ``` def deps do [{:bubble_ex, "~> 0.3"}] end ``` 文档可在 获取。 如果与 Phoenix 一起使用,您可能会遇到 `floki` 的错误。您必须移除 `only: test` 限制才能使其正常工作。 ## 配置 可以在您的应用配置中配置 BubbleEx: ``` config :bubble_ex, logs: [ default_endpoint: "https://bubble.io/appeditor/get_jetstream_logs", default_timeout: 30_000, default_app_version: "live", pool_max_connections: 10, pool_timeout: 30_000 ], apps: [ default_timeout: 10_000, max_body_length: 100_000_000 ] ``` ## 数据库结构与 Schema 导出 BubbleEx 会重构 Bubble 应用的数据模型 —— 数据类型(表)、选项集、字段、类型和关系 —— 并将其渲染为多种 schema 格式。向 `fetch_app/2` 传递 `:format`;渲染后的 schema 将在 `:schema` 键中返回。 ``` {:ok, app} = BubbleEx.fetch_app("my-app", format: :postgres) IO.puts(app.schema) # CREATE SCHEMA IF NOT EXISTS "custom"; # # CREATE TABLE "custom"."Survey Response" ( # "answer" text, # ... # "_id" text, # PRIMARY KEY ("_id") # ); # # ALTER TABLE "custom"."Survey Response" # ADD FOREIGN KEY ("status") REFERENCES "option"."Status Type" ("Display"); ``` ### 可用格式 | `:format` | 输出 | |-------------|--------| | `:dbml` | DBML ([dbdiagram.io](https://dbdiagram.io) / [dbml.org](https://dbml.org)) | | `:postgres` | PostgreSQL DDL | | `:sqlite` | SQLite DDL | | `:tsql` | SQL Server / Azure SQL T-SQL DDL | | `:ecto` | Ecto schema 模块 + 迁移 | | `:zod` | Zod (TypeScript) 验证 schema | | `:xano` | Xano 表 schema 导入 JSON | | `:convex` | Convex `schema.ts` | ### 命名 默认情况下,输出使用应用人类可读的显示名称(`naming: :proper`)。 传入 `naming: :id` 以改用 Bubble 的内部标识符: ``` {:ok, app} = BubbleEx.fetch_app("my-app", format: :ecto, naming: :id) ``` ### 保留与不保留的内容 每个编码器都会在目标允许的范围内尽可能忠实地映射 Bubble 的模型。标量 引用会变成真正的外键(SQL/Ecto)或 id 字段;Bubble 的 *list* 字段 在支持的地方会变为原生数组(如 Postgres 中的 `text[]`),否则会变成 JSON/text 列。选项集(enums)会作为查找表或字符串字段输出 —— Bubble 的 payload 不包含选项的 *成员值*,因此它们无法成为 原生数据库枚举。外部(`:api`)数据类型会被省略。每种格式的 输出都会内联记录其自身的降级处理。 ### DBML / 数据库图表(旧版选项) 原有的 DBML 路径保持不变,仍可通过其自身的选项使用: ``` {:ok, app} = BubbleEx.fetch_app("my-app", dbml: true) app.dbml # DBML text app.dbdiagram # same content ``` ### 自定义格式 格式是可插拔的。每种格式都是一个实现了 `BubbleEx.Db.Encoder` behaviour 的模块 —— `encode(db_map, opts) :: {:ok, String.t()} | {:error, %BubbleEx.Error{}}`, 作用于由 `BubbleEx.Db.Reader.parse/1` 生成的通用 map —— 注册在 `BubbleEx.Db.Encoder` 中。要添加新目标,请实现该 behaviour 并注册其 `:format` atom。 ## 扫描机密信息 机密扫描可通过 `BubbleEx.Secrets` behaviour 进行插拔。默认的 适配器 `BubbleEx.Secrets.Trufflehog` 会通过 shell 调用可选的 `trufflehog` CLI。您可以按每次调用(`adapter:` 选项)或全局替换为自己的扫描器: ``` config :bubble_ex, :secrets_adapter, MyApp.CustomScanner ``` ### BubbleEx.Secrets.Native `BubbleEx.Secrets.Native` 是一个纯 Elixir 编写、零依赖的离线扫描器 —— 无需 CLI。对于 `trufflehog` 不可用的环境,或者当您需要 快速、无依赖的初步扫描时,它是一个很好的基准。它能检测到的 机密类型少于 Trufflehog,并且 **不会进行实时验证** —— 每个 发现结果的 `verified: false` 均为 false,应被视为潜在的机密,有待 进一步审查。 **全局选择它:** ``` config :bubble_ex, :secrets_adapter, BubbleEx.Secrets.Native ``` **或按调用选择:** ``` {:ok, findings} = BubbleEx.Secrets.scan(payload, adapter: BubbleEx.Secrets.Native) ``` **检测器(默认运行):** AWS 凭证、GitHub 个人访问 token、 Stripe 密钥、Slack token、Google API 密钥、JWT 以及 PEM 私钥头部。 base64 解析阶段会重新扫描解码后的值。还有一个额外的熵级别,但它是 **可选的**(默认关闭): ``` {:ok, findings} = BubbleEx.Secrets.scan(payload, adapter: BubbleEx.Secrets.Native, entropy: true ) ``` **发现结果结构**(atom 键): ``` %{ detector: "github_pat", # string identifying the detector raw: "ghp_...", # the matched string redacted: "ghp_…abcd", # Path elements are strings (map keys) OR integers (list indices), # e.g. ["plugins", 0, "token"] for a secret inside the first list item. path: ["plugins", 0, "token"], decoder: :plain, # :plain | :base64 verified: false, # always false — no live check is performed confidence: :high # :high (regex/base64) | :low (entropy) } ``` ### 前置条件 默认适配器(`BubbleEx.Secrets.Trufflehog`)要求在您的 `PATH` 中包含 `trufflehog` CLI([安装说明](https://github.com/trufflesecurity/trufflehog))。当未安装时,扫描将返回 `{:error, %BubbleEx.Error{kind: :cli_missing}}` 而不会引发异常。如果您需要无 CLI 的 替代方案,请使用 `BubbleEx.Secrets.Native`。 ### 同步扫描 对于简单的同步扫描: ``` # 使用 Elixir map payload = %{"_id" => "app_123", "data" => "content to scan"} {:ok, results} = BubbleEx.scan_payload_for_secrets(payload) # 处理结果 Enum.each(results, fn item -> IO.puts("Found secret: #{item["DetectorType"]}") end) ``` ### 使用 Server 进行异步扫描 对于长时间运行的扫描,您可以使用 `Server` 异步运行扫描: #### 启动 Server 首先,将 Server 添加到您的 application.ex 文件中的监控树(supervision tree)中: ``` def start(_type, _args) do children = [ # ...other children {BubbleEx.Server, []} ] opts = [strategy: :one_for_one, name: YourApp.Supervisor] Supervisor.start_link(children, opts) end ``` 或者,手动启动它: ``` {:ok, _pid} = BubbleEx.Server.start_link() ``` #### 使用 Server ``` # 启动扫描并获取 ref payload = %{"_id" => "app_123", "data" => "content to scan"} {:ok, ref} = BubbleEx.start_scan_for_secrets(payload) # 调用进程将收到进度消息: receive do {:scan_started, ^ref} -> IO.puts("Scan started") {:scan_output, ^ref, output} -> IO.puts("Scan progress: #{output}") {:scan_completed, ^ref, results} -> IO.puts("Scan completed with #{length(results)} findings") {:scan_error, ^ref, error} -> IO.puts("Scan error: #{inspect(error)}") {:scan_cancelled, ^ref} -> IO.puts("Scan was cancelled") end # 检查状态 {:ok, status} = BubbleEx.scan_status(ref) # 如有需要取消扫描 :ok = BubbleEx.cancel_scan(ref) ``` ## 查询应用日志 BubbleEx 提供了查询 Bubble.io 应用日志的功能,用于监控、调试和分析。 ### 前置条件 您需要一个有效的 Bubble 会话 cookie 来向 Bubble API 验证身份。可以通过登录您的 Bubble 账户并从浏览器中提取该 session cookie 来获取。 ### 基本用法 ``` # 获取最近一小时的日志 {:ok, logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123..." ) # 访问日志条目 IO.inspect(logs.logs) ``` ### 时间范围过滤 ``` # 获取最近 30 分钟的日志 {after_time, before_time} = BubbleEx.Logs.time_range({:minutes, 30}) {:ok, logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", after: after_time, before: before_time ) # 获取最近一天的日志 {after_time, before_time} = BubbleEx.Logs.time_range(:last_day) {:ok, logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", after: after_time, before: before_time ) ``` ### 按日志类型过滤 ``` # 仅获取错误日志 {:ok, error_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", tags: BubbleEx.Logs.preset_filter(:errors, "my-app") ) # 仅获取与 workflow 相关的日志 {:ok, workflow_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", tags: BubbleEx.Logs.preset_filter(:workflows, "my-app") ) # 获取与 API 相关的日志 {:ok, api_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", tags: BubbleEx.Logs.preset_filter(:api, "my-app") ) ``` ### 可用的预设过滤器 - `:errors` - 错误和失败消息 - `:workflows` - Workflow 执行日志 - `:api` - HTTP 请求和 API workflow - `:database` - 数据库操作 - `:plugins` - 插件控制台输出和错误 - `:scheduled` - 计划任务执行 - `:all` - 所有日志类型(默认) ### 自定义过滤 ``` # 自定义 tag 过滤 {:ok, custom_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", tags: %{ message: ["running event", "event completed"], appname: "my-app", app_version: "live" } ) ``` ### 查询不同的应用版本 ``` # 从测试版本获取日志 {:ok, test_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", app_version: "test" ) # 从开发版本获取日志 {:ok, dev_logs} = BubbleEx.fetch_logs("my-app", cookie: "bubble_session=abc123...", app_version: "development" ) ``` ### 异步用法 对于需要异步查询日志或处理大量日志请求的应用,以下是一些模式: #### 使用 Task 获取异步日志 ``` # 异步获取日志 task = Task.async(fn -> BubbleEx.fetch_logs("my-app", cookie: System.get_env("BUBBLE_COOKIE"), tags: BubbleEx.Logs.preset_filter(:errors, "my-app") ) end) # 执行其他操作... # 获取结果 case Task.await(task, 30_000) do {:ok, logs} -> IO.puts("Found #{length(logs.logs)} error logs") {:error, reason} -> IO.puts("Error fetching logs: #{inspect(reason)}") end ``` #### 使用 GenServer 进行定期日志监控 ``` defmodule MyApp.LogMonitor do use GenServer def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def init(opts) do app_id = Keyword.fetch!(opts, :app_id) cookie = Keyword.fetch!(opts, :cookie) interval = Keyword.get(opts, :interval, 60_000) # 1 minute schedule_check(interval) {:ok, %{app_id: app_id, cookie: cookie, interval: interval}} end def handle_info(:check_logs, state) do {after_time, before_time} = BubbleEx.Logs.time_range({:minutes, 5}) case BubbleEx.fetch_logs(state.app_id, cookie: state.cookie, after: after_time, before: before_time, tags: BubbleEx.Logs.preset_filter(:errors, state.app_id)) do {:ok, %{logs: logs}} when length(logs) > 0 -> # Handle error logs - send alerts, store in database, etc. handle_error_logs(logs) {:ok, _} -> # No errors found :ok {:error, reason} -> # Log monitoring error Logger.error("Failed to fetch logs: #{inspect(reason)}") end schedule_check(state.interval) {:noreply, state} end defp schedule_check(interval) do Process.send_after(self(), :check_logs, interval) end defp handle_error_logs(logs) do # Process error logs... Enum.each(logs, fn log -> Logger.warning("App error detected: #{inspect(log)}") end) end end # 启动 monitor {:ok, _pid} = MyApp.LogMonitor.start_link( app_id: "my-app", cookie: System.get_env("BUBBLE_COOKIE") ) ``` #### 批量处理多个应用 ``` defmodule MyApp.LogAggregator do def fetch_logs_for_apps(app_configs) do app_configs |> Task.async_stream(fn %{app_id: app_id, cookie: cookie} -> {after_time, before_time} = BubbleEx.Logs.time_range(:last_hour) case BubbleEx.fetch_logs(app_id, cookie: cookie, after: after_time, before: before_time) do {:ok, logs} -> {:ok, app_id, logs} {:error, reason} -> {:error, app_id, reason} end end, max_concurrency: 5, timeout: 30_000) |> Enum.to_list() end end # 用法 apps = [ %{app_id: "app1", cookie: "cookie1"}, %{app_id: "app2", cookie: "cookie2"}, %{app_id: "app3", cookie: "cookie3"} ] results = MyApp.LogAggregator.fetch_logs_for_apps(apps) results |> Enum.each(fn {:ok, {:ok, app_id, logs}} -> IO.puts("#{app_id}: #{length(logs.logs)} logs") {:ok, {:error, app_id, reason}} -> IO.puts("#{app_id}: Error - #{inspect(reason)}") end) ``` ### 性能优化 BubbleEx 使用 HTTP 连接池来提升发起多次日志请求时的性能: ``` # 配置 connection pooling(可选) config :bubble_ex, logs: [ pool_max_connections: 20, # Maximum concurrent connections pool_timeout: 30_000 # Pool timeout in milliseconds ] ``` 连接池会被自动管理,并在请求间重用连接,这显著提升了频繁进行日志查询的应用的性能。 ### 安全注意事项 - 切勿在您的应用日志中记录或暴露 session cookie - 将 cookie 作为环境变量或安全地存储在配置中 - 使用正确的错误处理以避免泄露身份验证详细信息 - 考虑为长时间运行的应用实现 cookie 轮换 - 使用异步模式时,请确保进行适当的超时处理,以避免进程挂起 ## 深度搜索 BubbleEx 提供了强大的深度搜索功能,用于遍历和搜索复杂的嵌套数据结构,这在分析 Bubble.io 应用数据时特别有用。 ### 基本用法 ``` # 在嵌套数据中搜索特定值 data = %{ "user" => %{ "name" => "John Doe", "email" => "john@example.com", "settings" => %{ "theme" => "dark", "notifications" => ["email", "push"] } }, "posts" => [ %{"title" => "First Post", "content" => "Hello world"}, %{"title" => "Second Post", "content" => "Another post"} ] } # 查找所有包含 "email" 的路径 paths = BubbleEx.DeepSearch.find_all_paths(data, "email") # 返回:[["user", "settings", "notifications", 0], ["user", "email"]] # 查找所有包含 "Post" 的路径 paths = BubbleEx.DeepSearch.find_all_paths(data, "Post") # 返回:[["posts", 1, "title"], ["posts", 0, "title"]] ``` ### 理解路径结果 返回的路径是列表,其中: - String 元素代表 map 的键 - Integer 元素代表 list 的索引 **关于 `get_in/2` 用法的注意事项:** 虽然仅包含 map 键的路径可以在 `get_in/2` 中正常使用,但由于 Elixir 的 Access 限制,包含 list 索引的路径无法使用。对于 list 访问,请使用手动遍历或 `Enum.at/2`。 ``` # 仅包含 map 的路径可与 get_in 一起使用 data = %{"users" => %{"admin" => %{"name" => "Alice"}}} [path] = BubbleEx.DeepSearch.find_all_paths(data, "Alice") value = get_in(data, path) # 返回:"Alice" # 带有列表索引的路径需要手动遍历 data = %{"items" => ["first", "second", "third"]} [["items", 1]] = BubbleEx.DeepSearch.find_all_paths(data, "second") value = data["items"] |> Enum.at(1) # 返回:"second" ``` ### Bubble.io 数据的实际示例 ``` # 在 Bubble app 数据中搜索特定 field ID {:ok, app_data} = BubbleEx.fetch_bubble_app("myapp") field_paths = BubbleEx.DeepSearch.find_all_paths(app_data, "_id_1234567890") # 查找对特定用户的所有引用 user_refs = BubbleEx.DeepSearch.find_all_paths(app_data, "user_abc123") # 定位 API endpoints api_paths = BubbleEx.DeepSearch.find_all_paths(app_data, "api/1.1/") # 查找 database table 引用 table_paths = BubbleEx.DeepSearch.find_all_paths(app_data, "data_type_") ``` ### 处理搜索结果 ``` # 提取并处理所有匹配的值 data = fetch_complex_data() paths = BubbleEx.DeepSearch.find_all_paths(data, "secret_") # 获取所有实际值 values = Enum.map(paths, fn path -> {path, get_in(data, path)} end) # 按深度对路径分组 grouped = Enum.group_by(paths, &length/1) # 仅查找顶级出现位置 top_level = Enum.filter(paths, fn path -> length(path) == 1 end) ``` ### 性能注意事项 - 该函数会对数据结构进行完整的遍历 - 对于非常大的数据集,请考虑实现分页或流式传输 - 结果以发现的倒序返回(最深层的优先) - 出于性能考虑,字符串匹配区分大小写
标签:Bubble.io, Elixir, StruQ, 敏感信息扫描, 数据提取, 逆向分析