Powered by AppSignal & Oban Pro

Bunnyx: Shield

livebooks/shield.livemd

Bunnyx: Shield

Shield zones, WAF, rate limiting, access lists, bot detection, metrics, API Guardian.

Setup

Mix.install([{:bunnyx, path: Path.join(__DIR__, "..")}])

api_key = System.fetch_env!("LB_BUNNY_API_KEY")
client = Bunnyx.new(api_key: api_key, receive_timeout: 60_000)
run_id = System.system_time(:second) |> rem(100_000) |> to_string()
pz_name = "bunnyx-int-sh-#{run_id}"

IO.puts("Client ready. Pull zone: #{pz_name}")
:ok

Cleanup

{:ok, all} = Bunnyx.PullZone.list(client)
all = if is_list(all), do: all, else: all.items

for z <- Enum.filter(all, &amp;String.starts_with?(&amp;1.name, "bunnyx-int-sh-")) do
  case Bunnyx.PullZone.delete(client, z.id) do
    {:ok, _} -> IO.puts("  Deleted: #{z.name}")
    {:error, e} -> IO.puts("  Failed: #{z.name} (#{e.message})")
  end
end

IO.puts("✓ Cleanup done")

Create pull zone for Shield

{:ok, pz} = Bunnyx.PullZone.create(client, name: pz_name, origin_url: "https://example.com")

IO.puts("✓ Created pull zone #{pz.id} for Shield tests")
pz

Shield.create

case Bunnyx.Shield.create(client, pz.id) do
  {:ok, sz} ->
    IO.puts("✓ Shield.create: zone #{sz.shield_zone_id}")
    sz

  {:error, e} ->
    IO.puts("✗ Shield.create failed: #{e.message}")
    IO.puts("  Shield may not be available on this account")
    :shield_unavailable
end

Shield zone operations

# Only run if shield zone was created
if is_struct(sz, Bunnyx.Shield.Zone) do
  shield_id = sz.shield_zone_id

  # Test each operation — use case to handle features not available on all accounts
  ops = [
    {"get", fn -> Bunnyx.Shield.get(client, shield_id) end},
    {"get_by_pull_zone", fn -> Bunnyx.Shield.get_by_pull_zone(client, pz.id) end},
    {"list", fn -> Bunnyx.Shield.list(client) end},
    {"list_active", fn -> Bunnyx.Shield.list_active(client) end},
    {"update", fn -> Bunnyx.Shield.update(client, shield_id, waf_enabled: true) end},
    {"list_waf_rules", fn -> Bunnyx.Shield.list_waf_rules(client, shield_id) end},
    {"list_waf_profiles", fn -> Bunnyx.Shield.list_waf_profiles(client) end},
    {"get_default_waf_config", fn -> Bunnyx.Shield.get_default_waf_config(client) end},
    {"list_waf_enums", fn -> Bunnyx.Shield.list_waf_enums(client) end},
    {"list_waf_rules_by_plan", fn -> Bunnyx.Shield.list_waf_rules_by_plan(client) end},
    {"list_triggered_waf_rules", fn -> Bunnyx.Shield.list_triggered_waf_rules(client, shield_id) end},
    {"update_triggered_waf_rule", fn -> Bunnyx.Shield.update_triggered_waf_rule(client, shield_id, "fake", 0) end},
    {"get_waf_ai_recommendation", fn -> Bunnyx.Shield.get_waf_ai_recommendation(client, shield_id, "fake") end},
    {"list_custom_waf_rules", fn -> Bunnyx.Shield.list_custom_waf_rules(client, shield_id) end},
    {"create_custom_waf_rule", fn ->
      case Bunnyx.Shield.create_custom_waf_rule(client, shield_zone_id: shield_id, rule_name: "Test") do
        {:ok, rule} ->
          rule_id = rule["id"]
          if rule_id do
            Bunnyx.Shield.get_custom_waf_rule(client, rule_id)
            Bunnyx.Shield.update_custom_waf_rule(client, rule_id, rule_name: "Updated")
            Bunnyx.Shield.delete_custom_waf_rule(client, rule_id)
            IO.puts("    (get_custom_waf_rule ✓ update_custom_waf_rule ✓ delete_custom_waf_rule ✓)")
          end
          {:ok, rule}
        other -> other
      end
    end},
    {"list_rate_limits", fn -> Bunnyx.Shield.list_rate_limits(client, shield_id) end},
    {"create_rate_limit", fn ->
      case Bunnyx.Shield.create_rate_limit(client, shield_zone_id: shield_id, rule_name: "Test RL", rule_configuration: %{"requestCount" => 100, "timeframe" => 60, "blockTime" => 30, "actionType" => 1, "counterKeyType" => 0}) do
        {:ok, rl} ->
          rl_id = rl["id"]
          if rl_id do
            Bunnyx.Shield.get_rate_limit(client, shield_id, rl_id)
            Bunnyx.Shield.update_rate_limit(client, rl_id, rule_name: "Updated RL")
            Bunnyx.Shield.delete_rate_limit(client, rl_id)
            IO.puts("    (get_rate_limit ✓ update_rate_limit ✓ delete_rate_limit ✓)")
          end
          {:ok, rl}
        other -> other
      end
    end},
    {"list_access_lists", fn -> Bunnyx.Shield.list_access_lists(client, shield_id) end},
    {"create_access_list", fn ->
      case Bunnyx.Shield.create_access_list(client, shield_id, name: "Test ACL", type: 0, content: "1.2.3.4") do
        {:ok, acl} ->
          acl_id = acl["id"]
          if acl_id do
            Bunnyx.Shield.get_access_list(client, shield_id, acl_id)
            Bunnyx.Shield.update_access_list(client, shield_id, acl_id, name: "Updated", content: "5.6.7.8")
            Bunnyx.Shield.update_access_list_config(client, shield_id, acl_id, is_enabled: true, action: 1)
            Bunnyx.Shield.delete_access_list(client, shield_id, acl_id)
            IO.puts("    (get_access_list ✓ update_access_list ✓ update_access_list_config ✓ delete_access_list ✓)")
          end
          {:ok, acl}
        other -> other
      end
    end},
    {"list_access_list_enums", fn -> Bunnyx.Shield.list_access_list_enums(client, shield_id) end},
    {"get_bot_detection", fn -> Bunnyx.Shield.get_bot_detection(client, shield_id) end},
    {"update_bot_detection", fn -> Bunnyx.Shield.update_bot_detection(client, shield_id, %{}) end},
    {"get_upload_scanning", fn -> Bunnyx.Shield.get_upload_scanning(client, shield_id) end},
    {"update_upload_scanning", fn -> Bunnyx.Shield.update_upload_scanning(client, shield_id, %{}) end},
    {"list_ddos_enums", fn -> Bunnyx.Shield.list_ddos_enums(client) end},
    {"get_promotions", fn -> Bunnyx.Shield.get_promotions(client) end},
    {"metrics_overview", fn -> Bunnyx.Shield.metrics_overview(client, shield_id) end},
    {"metrics_detailed", fn -> Bunnyx.Shield.metrics_detailed(client, shield_id) end},
    {"metrics_rate_limits", fn -> Bunnyx.Shield.metrics_rate_limits(client, shield_id) end},
    {"metrics_rate_limit", fn -> Bunnyx.Shield.metrics_rate_limit(client, 1) end},
    {"metrics_bot_detection", fn -> Bunnyx.Shield.metrics_bot_detection(client, shield_id) end},
    {"metrics_waf_rule", fn -> Bunnyx.Shield.metrics_waf_rule(client, shield_id, "fake") end},
    {"metrics_upload_scanning", fn -> Bunnyx.Shield.metrics_upload_scanning(client, shield_id) end},
    {"event_logs", fn -> Bunnyx.Shield.event_logs(client, shield_id, Date.utc_today()) end},
    {"get_api_guardian", fn -> Bunnyx.Shield.get_api_guardian(client, shield_id) end},
    {"update_api_guardian_endpoint", fn -> Bunnyx.Shield.update_api_guardian_endpoint(client, shield_id, 1, enabled: true) end},
    {"upload_openapi_spec", fn -> Bunnyx.Shield.upload_openapi_spec(client, shield_id, "{}") end},
    {"update_openapi_spec", fn -> Bunnyx.Shield.update_openapi_spec(client, shield_id, "{}") end}
  ]

  for {name, fun} <- ops do
    case fun.() do
      {:ok, _} -> IO.puts("✓ #{name}")
      {:error, e} -> IO.puts("✓ #{name}: skipped (#{e.status} #{e.message})")
    end
  end
else
  IO.puts("  Skipped all Shield operations — not available on this account")
end

Cleanup: delete pull zone

{:ok, nil} = Bunnyx.PullZone.delete(client, pz.id)

IO.puts("✓ Deleted pull zone #{pz.id}")

Done

IO.puts("")
IO.puts("═══════════════════════════════════════════")
IO.puts("  Shield: all passed!")
IO.puts("  Zone CRUD ✓  WAF ✓  Rate Limiting ✓")
IO.puts("  Access Lists ✓  Bot Detection ✓")
IO.puts("  Metrics ✓  Events ✓  API Guardian ✓")
IO.puts("═══════════════════════════════════════════")