From 17b7b0e5f7c680675798b8d9c7ac09e5ff2101f3 Mon Sep 17 00:00:00 2001 From: kappa Date: Mon, 13 Apr 2026 10:14:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Shield=20=EC=A0=84=EC=B2=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=E2=80=94=20Rate=20Limiting=20?= =?UTF-8?q?CRUD,=20WAF=20custom=20rules,=20Metrics,=20DDoS=20enums,=20WAF?= =?UTF-8?q?=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bunnycdn_mcp/tools/shield.py | 165 ++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/bunnycdn_mcp/tools/shield.py b/bunnycdn_mcp/tools/shield.py index cc63b8f..6144ad1 100644 --- a/bunnycdn_mcp/tools/shield.py +++ b/bunnycdn_mcp/tools/shield.py @@ -1,7 +1,15 @@ -"""Bunny Shield tools — Shield Zone API (/shield/shield-zone, /shield/shield-zones). +"""Bunny Shield tools - Shield Zone API. -NOTE: Shield는 Pull Zone 필드(ShieldDDosProtectionEnabled 등)가 아니라 -별도 Shield Zone API로 관리됨. Pull Zone 업데이트로는 Shield를 켤 수 없음. +API structure: +- Shield Zone: /shield/shield-zone, /shield/shield-zones +- WAF: /shield/waf/{shieldZoneId}, /shield/waf/custom-rules/{shieldZoneId} +- Rate Limiting: /shield/rate-limits/{shieldZoneId} +- Event Logs: /shield/event-logs/{shieldZoneId} +- Metrics: /shield/metrics/overview/{shieldZoneId} +- DDoS Enums: /shield/ddos/enums +- WAF Profiles: /shield/waf/profiles + +NOTE: Pull Zone fields (ShieldDDosProtectionEnabled) are read-only legacy. Do not use. """ import json @@ -15,6 +23,8 @@ from ..config import logger def register_shield_tools(mcp): + # -- Shield Zone CRUD -- + @mcp.tool() async def bunny_shield_list() -> str: """List all Shield Zones with WAF/DDoS configuration.""" @@ -86,11 +96,13 @@ def register_shield_tools(mcp): logger.error("bunny_shield_update failed: %s", e) return f"Error: {e}" + # -- WAF -- + @mcp.tool() async def bunny_waf_rules( shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], ) -> str: - """List WAF rules for a Shield Zone.""" + """List all managed WAF rules for a Shield Zone.""" try: data = await client.get(f"/shield/waf/{shield_zone_id}") return json.dumps(data, indent=2) @@ -98,6 +110,111 @@ def register_shield_tools(mcp): logger.error("bunny_waf_rules failed: %s", e) return f"Error: {e}" + @mcp.tool() + async def bunny_waf_custom_rules( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + ) -> str: + """List custom WAF rules for a Shield Zone.""" + try: + data = await client.get(f"/shield/waf/custom-rules/{shield_zone_id}") + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_waf_custom_rules failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_waf_custom_rule_create( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + rule: Annotated[str, Field(description="JSON string of custom WAF rule definition")], + ) -> str: + """Create a custom WAF rule for a Shield Zone.""" + try: + parsed = json.loads(rule) + parsed["shieldZoneId"] = shield_zone_id + except json.JSONDecodeError as e: + return f"Error: Invalid JSON: {e}" + try: + data = await client.post(f"/shield/waf/custom-rules/{shield_zone_id}", json=parsed) + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_waf_custom_rule_create failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_waf_profiles() -> str: + """List available WAF profiles.""" + try: + data = await client.get("/shield/waf/profiles") + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_waf_profiles failed: %s", e) + return f"Error: {e}" + + # -- Rate Limiting -- + + @mcp.tool() + async def bunny_rate_limits( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + ) -> str: + """List rate limit rules for a Shield Zone.""" + try: + data = await client.get(f"/shield/rate-limits/{shield_zone_id}") + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_rate_limits failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_rate_limit_create( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + rule: Annotated[str, Field(description="JSON string of rate limit rule")], + ) -> str: + """Create a rate limit rule for a Shield Zone.""" + try: + parsed = json.loads(rule) + parsed["shieldZoneId"] = shield_zone_id + except json.JSONDecodeError as e: + return f"Error: Invalid JSON: {e}" + try: + data = await client.post(f"/shield/rate-limits/{shield_zone_id}", json=parsed) + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_rate_limit_create failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_rate_limit_update( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + rule_id: Annotated[int, Field(description="Rate limit rule ID")], + rule: Annotated[str, Field(description="JSON string of updated rule")], + ) -> str: + """Update a rate limit rule.""" + try: + parsed = json.loads(rule) + except json.JSONDecodeError as e: + return f"Error: Invalid JSON: {e}" + try: + data = await client.put(f"/shield/rate-limits/{shield_zone_id}/{rule_id}", json=parsed) + return json.dumps(data, indent=2) if isinstance(data, dict) else f"Rate limit {rule_id} updated" + except Exception as e: + logger.error("bunny_rate_limit_update failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_rate_limit_delete( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + rule_id: Annotated[int, Field(description="Rate limit rule ID to delete")], + ) -> str: + """Delete a rate limit rule.""" + try: + await client.delete(f"/shield/rate-limits/{shield_zone_id}/{rule_id}") + return f"Rate limit rule {rule_id} deleted" + except Exception as e: + logger.error("bunny_rate_limit_delete failed: %s", e) + return f"Error: {e}" + + # -- Event Logs -- + @mcp.tool() async def bunny_waf_logs( shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], @@ -112,3 +229,43 @@ def register_shield_tools(mcp): except Exception as e: logger.error("bunny_waf_logs failed: %s", e) return f"Error: {e}" + + # -- Metrics -- + + @mcp.tool() + async def bunny_shield_metrics( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + ) -> str: + """Get Shield metrics overview: WAF, DDoS, Rate Limit, Bot Detection, Access List (28-day summary).""" + try: + data = await client.get(f"/shield/metrics/overview/{shield_zone_id}") + if isinstance(data, dict) and "data" in data: + d = data["data"] + summary = { + "overview": d.get("overview"), + "waf": d.get("waf"), + "dDoS": d.get("dDoS"), + "ratelimit": d.get("ratelimit"), + "botDetection": d.get("botDetection"), + "accessList": d.get("accessList"), + "uploadScanning": d.get("uploadScanning"), + "totalBillableRequests": d.get("totalBillableRequests"), + "totalCleanRequestsLimit": d.get("totalCleanRequestsLimit"), + } + return json.dumps(summary, indent=2) + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_shield_metrics failed: %s", e) + return f"Error: {e}" + + # -- DDoS Enums -- + + @mcp.tool() + async def bunny_ddos_enums() -> str: + """List DDoS sensitivity and execution mode enum values.""" + try: + data = await client.get("/shield/ddos/enums") + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_ddos_enums failed: %s", e) + return f"Error: {e}"