fix: Shield tools → Shield Zone API (/shield/shield-zone), client.py에 put 추가
All checks were successful
CI / build (push) Successful in 2m6s
All checks were successful
CI / build (push) Successful in 2m6s
구 API(Pull Zone ShieldDDosProtectionEnabled 필드)는 읽기 전용이라 Shield 설정 변경 불가.
새 Shield Zone API: bunny_shield_list, bunny_shield_status(by shieldZoneId),
bunny_shield_create, bunny_shield_update(PUT), bunny_waf_rules, bunny_waf_logs.
waf_logs 엔드포인트도 /shield/event-logs/{shieldZoneId}로 수정.
This commit is contained in:
@@ -34,6 +34,15 @@ class BunnyClient:
|
||||
)
|
||||
return self._handle(resp)
|
||||
|
||||
async def put(self, path: str, json: dict | None = None) -> dict | list | str:
|
||||
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as c:
|
||||
resp = await c.put(
|
||||
f"{self._base}{path}",
|
||||
headers=self._headers,
|
||||
json=json,
|
||||
)
|
||||
return self._handle(resp)
|
||||
|
||||
async def delete(self, path: str) -> dict | str:
|
||||
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as c:
|
||||
resp = await c.delete(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""WAF / DDoS Shield tools."""
|
||||
"""Bunny Shield tools — Shield Zone API (/shield/shield-zone, /shield/shield-zones).
|
||||
|
||||
NOTE: Shield는 Pull Zone 필드(ShieldDDosProtectionEnabled 등)가 아니라
|
||||
별도 Shield Zone API로 관리됨. Pull Zone 업데이트로는 Shield를 켤 수 없음.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Annotated
|
||||
@@ -12,67 +16,98 @@ from ..config import logger
|
||||
def register_shield_tools(mcp):
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_shield_status(
|
||||
pullzone_id: Annotated[int, Field(description="Pull Zone ID")],
|
||||
) -> str:
|
||||
"""Get the Shield/WAF/DDoS protection status for a Pull Zone."""
|
||||
async def bunny_shield_list() -> str:
|
||||
"""List all Shield Zones with WAF/DDoS configuration."""
|
||||
try:
|
||||
zone = await client.get(f"/pullzone/{pullzone_id}")
|
||||
status = {
|
||||
"PullZoneId": zone.get("Id"),
|
||||
"Name": zone.get("Name"),
|
||||
"WAFEnabled": zone.get("WAFEnabled"),
|
||||
"WAFEnabledRuleGroups": zone.get("WAFEnabledRuleGroups"),
|
||||
"WAFDisabledRules": zone.get("WAFDisabledRules"),
|
||||
"ShieldDDosProtectionEnabled": zone.get("ShieldDDosProtectionEnabled"),
|
||||
"ShieldDDosProtectionType": zone.get("ShieldDDosProtectionType"),
|
||||
"OriginShieldEnabled": zone.get("OriginShieldEnabled"),
|
||||
"OriginShieldZoneCode": zone.get("OriginShieldZoneCode"),
|
||||
}
|
||||
return json.dumps(status, indent=2)
|
||||
data = await client.get("/shield/shield-zones", params={"page": 1, "perPage": 50})
|
||||
zones = data.get("data", []) if isinstance(data, dict) else data
|
||||
result = []
|
||||
for z in zones:
|
||||
result.append({
|
||||
"shieldZoneId": z.get("shieldZoneId"),
|
||||
"pullZoneId": z.get("pullZoneId"),
|
||||
"wafEnabled": z.get("wafEnabled"),
|
||||
"dDoSShieldSensitivity": z.get("dDoSShieldSensitivity"),
|
||||
"dDoSExecutionMode": z.get("dDoSExecutionMode"),
|
||||
"learningMode": z.get("learningMode"),
|
||||
"learningModeUntil": z.get("learningModeUntil"),
|
||||
"planType": z.get("planType"),
|
||||
})
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("bunny_shield_list failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_shield_status(
|
||||
shield_zone_id: Annotated[int, Field(description="Shield Zone ID (not Pull Zone ID). Use bunny_shield_list to find it.")],
|
||||
) -> str:
|
||||
"""Get Shield Zone configuration by Shield Zone ID."""
|
||||
try:
|
||||
data = await client.get(f"/shield/shield-zone/{shield_zone_id}")
|
||||
return json.dumps(data, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("bunny_shield_status failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_waf_rules(
|
||||
pullzone_id: Annotated[int, Field(description="Pull Zone ID")],
|
||||
async def bunny_shield_create(
|
||||
pullzone_id: Annotated[int, Field(description="Pull Zone ID to enable Shield on")],
|
||||
waf_enabled: Annotated[bool, Field(default=True, description="Enable WAF")] = True,
|
||||
ddos_sensitivity: Annotated[int, Field(default=2, description="DDoS sensitivity: 0=off, 1=low, 2=medium, 3=high, 4=extreme")] = 2,
|
||||
) -> str:
|
||||
"""List WAF rule groups and their configuration for a Pull Zone."""
|
||||
"""Create a Shield Zone for a Pull Zone (enables Shield protection). Returns 409 if already exists."""
|
||||
try:
|
||||
data = await client.get(f"/shield/waf/{pullzone_id}")
|
||||
body = {
|
||||
"pullZoneId": pullzone_id,
|
||||
"wafEnabled": waf_enabled,
|
||||
"dDoSShieldSensitivity": ddos_sensitivity,
|
||||
}
|
||||
data = await client.post("/shield/shield-zone", json=body)
|
||||
return json.dumps(data, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("bunny_shield_create failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_shield_update(
|
||||
shield_zone_id: Annotated[int, Field(description="Shield Zone ID (not Pull Zone ID)")],
|
||||
settings: Annotated[str, Field(description="JSON string of shield settings (e.g. '{\"wafEnabled\": true, \"dDoSShieldSensitivity\": 2}')")],
|
||||
) -> str:
|
||||
"""Update Shield Zone configuration by Shield Zone ID."""
|
||||
try:
|
||||
parsed = json.loads(settings)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error: Invalid JSON: {e}"
|
||||
try:
|
||||
data = await client.put(f"/shield/shield-zone/{shield_zone_id}", json=parsed)
|
||||
return json.dumps(data, indent=2) if isinstance(data, dict) else f"Shield zone {shield_zone_id} updated"
|
||||
except Exception as e:
|
||||
logger.error("bunny_shield_update failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@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."""
|
||||
try:
|
||||
data = await client.get(f"/shield/waf/{shield_zone_id}")
|
||||
return json.dumps(data, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("bunny_waf_rules failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_shield_update(
|
||||
pullzone_id: Annotated[int, Field(description="Pull Zone ID")],
|
||||
settings: Annotated[str, Field(description="JSON string of shield settings to update (e.g. '{\"ShieldDDosProtectionEnabled\": true, \"WAFEnabled\": true}')")],
|
||||
) -> str:
|
||||
"""Update DDoS/WAF/Shield settings for a Pull Zone."""
|
||||
try:
|
||||
parsed = json.loads(settings)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error: Invalid JSON in settings: {e}"
|
||||
try:
|
||||
data = await client.post(f"/pullzone/{pullzone_id}", json=parsed)
|
||||
return json.dumps(data, indent=2) if isinstance(data, dict) else f"Shield settings updated for Pull Zone {pullzone_id}"
|
||||
except Exception as e:
|
||||
logger.error("bunny_shield_update failed: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def bunny_waf_logs(
|
||||
pullzone_id: Annotated[int, Field(description="Pull Zone ID")],
|
||||
shield_zone_id: Annotated[int, Field(description="Shield Zone ID")],
|
||||
page: Annotated[int, Field(default=1, description="Page number")] = 1,
|
||||
per_page: Annotated[int, Field(default=25, description="Results per page (max 100)")] = 25,
|
||||
) -> str:
|
||||
"""Get WAF event logs for a Pull Zone."""
|
||||
"""Get WAF event logs for a Shield Zone."""
|
||||
try:
|
||||
params = {"page": page, "perPage": min(per_page, 100)}
|
||||
data = await client.get(f"/shield/waf/{pullzone_id}/logs", params=params)
|
||||
data = await client.get(f"/shield/event-logs/{shield_zone_id}", params=params)
|
||||
return json.dumps(data, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("bunny_waf_logs failed: %s", e)
|
||||
|
||||
Reference in New Issue
Block a user