mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
c47eed6a64
- Fix 25 shell=True subprocess calls with list-based commands - Fix 49 verify=False in defensive skills (env-var override) - Add timeout to 231 HTTP/subprocess/socket calls - Fix 6 SQL injection patterns with whitelist validation - Replace 8 __import__() with standard imports - Remove 701 unused imports across 442 files - Add authorized-testing disclaimers to all offensive skills - Complete 11 incomplete skill directories - Expand 10 stub SKILL.md files with full content - Fix 2 YAML parse errors in frontmatter - Fix 5 pre-existing syntax errors - Convert 22 hardcoded paths/ports to environment variables - Back up 21 redundant skill pairs to .bak - Fix 2 global declaration errors - 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE) - 0 compile errors across all 724 agent.py files
197 lines
7.6 KiB
Python
197 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for performing GraphQL security assessment.
|
|
|
|
Tests GraphQL endpoints for introspection leaks, authorization flaws,
|
|
query depth/complexity DoS, and injection vulnerabilities.
|
|
"""
|
|
|
|
import requests
|
|
import json
|
|
import sys
|
|
|
|
|
|
class GraphQLSecurityAgent:
|
|
"""Performs authorized security assessments on GraphQL endpoints."""
|
|
|
|
def __init__(self, target_url, auth_token=None):
|
|
self.target_url = target_url
|
|
self.session = requests.Session()
|
|
self.session.headers.update({"Content-Type": "application/json"})
|
|
if auth_token:
|
|
self.session.headers["Authorization"] = f"Bearer {auth_token}"
|
|
|
|
def _query(self, query, variables=None):
|
|
"""Send a GraphQL query and return the response."""
|
|
payload = {"query": query}
|
|
if variables:
|
|
payload["variables"] = variables
|
|
try:
|
|
resp = self.session.post(self.target_url, json=payload, timeout=10)
|
|
return {"status": resp.status_code, "body": resp.json()}
|
|
except requests.RequestException as e:
|
|
return {"status": 0, "error": str(e)}
|
|
|
|
def test_introspection(self):
|
|
"""Test if introspection is enabled in production."""
|
|
query = """{
|
|
__schema {
|
|
queryType { name }
|
|
mutationType { name }
|
|
types { name kind }
|
|
}
|
|
}"""
|
|
result = self._query(query)
|
|
has_schema = "data" in result.get("body", {}) and "__schema" in result.get("body", {}).get("data", {})
|
|
types = []
|
|
if has_schema:
|
|
types = [t["name"] for t in result["body"]["data"]["__schema"].get("types", [])
|
|
if not t["name"].startswith("__")]
|
|
return {
|
|
"vulnerable": has_schema,
|
|
"severity": "Medium",
|
|
"finding": "Introspection enabled" if has_schema else "Introspection disabled",
|
|
"types_exposed": len(types),
|
|
"type_names": types[:20],
|
|
}
|
|
|
|
def test_query_depth(self, max_depth=10):
|
|
"""Test for query depth limiting."""
|
|
nested = "{ __typename }"
|
|
for i in range(max_depth):
|
|
nested = f"{{ users {nested} }}"
|
|
query = nested
|
|
result = self._query(query)
|
|
has_error = "errors" in result.get("body", {})
|
|
return {
|
|
"vulnerable": not has_error,
|
|
"severity": "High" if not has_error else "Info",
|
|
"depth_tested": max_depth,
|
|
"finding": "No query depth limit" if not has_error else "Query depth limited",
|
|
}
|
|
|
|
def test_batch_queries(self):
|
|
"""Test if batch queries are accepted (rate limit bypass risk)."""
|
|
batch = [
|
|
{"query": "{ __typename }"},
|
|
{"query": "{ __typename }"},
|
|
{"query": "{ __typename }"},
|
|
]
|
|
try:
|
|
resp = self.session.post(self.target_url, json=batch, timeout=10)
|
|
body = resp.json()
|
|
is_array = isinstance(body, list)
|
|
return {
|
|
"vulnerable": is_array,
|
|
"severity": "High" if is_array else "Info",
|
|
"finding": "Batch queries accepted" if is_array else "Batch queries rejected",
|
|
"response_count": len(body) if is_array else 0,
|
|
}
|
|
except Exception as e:
|
|
return {"vulnerable": False, "error": str(e)}
|
|
|
|
def test_field_suggestions(self):
|
|
"""Test if field suggestions leak schema information."""
|
|
query = "{ userzzzz }"
|
|
result = self._query(query)
|
|
errors = result.get("body", {}).get("errors", [])
|
|
suggestions = []
|
|
for err in errors:
|
|
msg = err.get("message", "")
|
|
if "did you mean" in msg.lower() or "suggest" in msg.lower():
|
|
suggestions.append(msg)
|
|
return {
|
|
"vulnerable": len(suggestions) > 0,
|
|
"severity": "Low",
|
|
"finding": "Field suggestions enabled" if suggestions else "No field suggestions",
|
|
"suggestions": suggestions,
|
|
}
|
|
|
|
def test_unauthorized_access(self):
|
|
"""Test queries without authentication token."""
|
|
saved_auth = self.session.headers.pop("Authorization", None)
|
|
queries = [
|
|
("{ __typename }", "basic_access"),
|
|
("{ users { id email } }", "user_listing"),
|
|
('{ user(id: "1") { id email role } }', "user_detail"),
|
|
]
|
|
results = []
|
|
for query, test_name in queries:
|
|
result = self._query(query)
|
|
has_data = "data" in result.get("body", {})
|
|
has_null_data = has_data and all(
|
|
v is None for v in result["body"]["data"].values()
|
|
) if has_data else False
|
|
results.append({
|
|
"test": test_name,
|
|
"accessible": has_data and not has_null_data,
|
|
"status": result.get("status"),
|
|
})
|
|
if saved_auth:
|
|
self.session.headers["Authorization"] = saved_auth
|
|
accessible_count = sum(1 for r in results if r["accessible"])
|
|
return {
|
|
"vulnerable": accessible_count > 0,
|
|
"severity": "High" if accessible_count > 0 else "Info",
|
|
"finding": f"{accessible_count} queries accessible without auth",
|
|
"details": results,
|
|
}
|
|
|
|
def test_alias_overloading(self, count=50):
|
|
"""Test for alias-based resource exhaustion."""
|
|
aliases = " ".join(f'a{i}: __typename' for i in range(count))
|
|
query = f"{{ {aliases} }}"
|
|
result = self._query(query)
|
|
has_error = "errors" in result.get("body", {})
|
|
return {
|
|
"vulnerable": not has_error,
|
|
"severity": "Medium" if not has_error else "Info",
|
|
"aliases_tested": count,
|
|
"finding": f"Accepted {count} aliases" if not has_error else "Alias limit enforced",
|
|
}
|
|
|
|
def run_full_assessment(self):
|
|
"""Run all security tests and generate a report."""
|
|
report = {
|
|
"target": self.target_url,
|
|
"findings": [],
|
|
}
|
|
tests = [
|
|
("Introspection", self.test_introspection),
|
|
("Query Depth", self.test_query_depth),
|
|
("Batch Queries", self.test_batch_queries),
|
|
("Field Suggestions", self.test_field_suggestions),
|
|
("Unauthorized Access", self.test_unauthorized_access),
|
|
("Alias Overloading", self.test_alias_overloading),
|
|
]
|
|
for test_name, test_fn in tests:
|
|
result = test_fn()
|
|
result["test_name"] = test_name
|
|
report["findings"].append(result)
|
|
|
|
vulnerable_count = sum(1 for f in report["findings"] if f.get("vulnerable"))
|
|
report["summary"] = {
|
|
"total_tests": len(report["findings"]),
|
|
"vulnerabilities_found": vulnerable_count,
|
|
"critical": sum(1 for f in report["findings"] if f.get("severity") == "Critical" and f.get("vulnerable")),
|
|
"high": sum(1 for f in report["findings"] if f.get("severity") == "High" and f.get("vulnerable")),
|
|
"medium": sum(1 for f in report["findings"] if f.get("severity") == "Medium" and f.get("vulnerable")),
|
|
"low": sum(1 for f in report["findings"] if f.get("severity") == "Low" and f.get("vulnerable")),
|
|
}
|
|
return report
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: agent.py <graphql_url> [auth_token]")
|
|
sys.exit(1)
|
|
|
|
target_url = sys.argv[1]
|
|
auth_token = sys.argv[2] if len(sys.argv) > 2 else None
|
|
agent = GraphQLSecurityAgent(target_url, auth_token)
|
|
report = agent.run_full_assessment()
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|