Files
Anthropic-Cybersecurity-Skills/skills/conducting-api-security-testing/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- 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
2026-03-19 13:26:49 +01:00

229 lines
8.2 KiB
Python

#!/usr/bin/env python3
# For authorized penetration testing and lab environments only
"""API Security Testing Agent - Tests REST/GraphQL APIs for OWASP API Top 10 vulnerabilities."""
import json
import logging
import argparse
from datetime import datetime
from urllib.parse import urljoin
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def test_bola(base_url, endpoint_template, id_field, valid_id, other_id, auth_token):
"""Test for Broken Object Level Authorization (BOLA/IDOR)."""
headers = {"Authorization": f"Bearer {auth_token}"}
own_resp = requests.get(
urljoin(base_url, endpoint_template.replace(f"{{{id_field}}}", str(valid_id))),
headers=headers, timeout=10,
)
other_resp = requests.get(
urljoin(base_url, endpoint_template.replace(f"{{{id_field}}}", str(other_id))),
headers=headers, timeout=10,
)
vulnerable = other_resp.status_code == 200 and len(other_resp.content) > 50
result = {
"test": "BOLA (API1:2023)",
"endpoint": endpoint_template,
"own_status": own_resp.status_code,
"other_status": other_resp.status_code,
"vulnerable": vulnerable,
}
if vulnerable:
logger.warning("BOLA vulnerability found: %s", endpoint_template)
return result
def test_bfla(base_url, admin_endpoints, low_priv_token):
"""Test for Broken Function Level Authorization (BFLA)."""
headers = {"Authorization": f"Bearer {low_priv_token}"}
results = []
for endpoint in admin_endpoints:
for method in ["GET", "POST", "DELETE"]:
try:
resp = requests.request(
method, urljoin(base_url, endpoint),
headers=headers, timeout=10,
)
vulnerable = resp.status_code in (200, 201, 204)
results.append({
"test": "BFLA (API5:2023)",
"endpoint": endpoint,
"method": method,
"status": resp.status_code,
"vulnerable": vulnerable,
})
if vulnerable:
logger.warning("BFLA: %s %s accessible with low-priv token", method, endpoint)
except requests.RequestException:
continue
return results
def test_mass_assignment(base_url, endpoint, auth_token, extra_fields):
"""Test for mass assignment vulnerability."""
headers = {"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"}
resp = requests.put(
urljoin(base_url, endpoint),
headers=headers, json=extra_fields, timeout=10,
)
verify = requests.get(urljoin(base_url, endpoint), headers=headers, timeout=10)
verify_data = verify.json() if verify.status_code == 200 else {}
vulnerable = False
for key, value in extra_fields.items():
if key in verify_data and verify_data[key] == value:
vulnerable = True
break
return {
"test": "Mass Assignment (API6:2023)",
"endpoint": endpoint,
"injected_fields": list(extra_fields.keys()),
"vulnerable": vulnerable,
"update_status": resp.status_code,
}
def test_rate_limiting(base_url, endpoint, num_requests=100):
"""Test rate limiting on sensitive endpoints."""
statuses = []
for i in range(num_requests):
try:
resp = requests.post(
urljoin(base_url, endpoint),
json={"username": f"test{i}", "password": "wrong"},
timeout=5,
)
statuses.append(resp.status_code)
if resp.status_code == 429:
logger.info("Rate limiting triggered after %d requests", i + 1)
return {
"test": "Rate Limiting (API4:2023)",
"endpoint": endpoint,
"requests_sent": i + 1,
"rate_limited": True,
"vulnerable": False,
}
except requests.RequestException:
break
return {
"test": "Rate Limiting (API4:2023)",
"endpoint": endpoint,
"requests_sent": len(statuses),
"rate_limited": False,
"vulnerable": True,
}
def test_jwt_none_algorithm(base_url, endpoint, jwt_token):
"""Test for JWT 'none' algorithm bypass."""
import base64
parts = jwt_token.split(".")
if len(parts) != 3:
return {"test": "JWT None Algorithm", "vulnerable": False, "error": "Invalid JWT"}
header = json.loads(base64.urlsafe_b64decode(parts[0] + "=="))
header["alg"] = "none"
new_header = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b"=").decode()
forged_token = f"{new_header}.{parts[1]}."
resp = requests.get(
urljoin(base_url, endpoint),
headers={"Authorization": f"Bearer {forged_token}"},
timeout=10,
)
vulnerable = resp.status_code == 200
return {
"test": "JWT None Algorithm",
"endpoint": endpoint,
"forged_status": resp.status_code,
"vulnerable": vulnerable,
}
def test_graphql_introspection(base_url, graphql_endpoint="/graphql"):
"""Test if GraphQL introspection is enabled."""
query = {"query": "{__schema{types{name,fields{name,args{name,type{name}}}}}}"}
resp = requests.post(
urljoin(base_url, graphql_endpoint),
json=query, timeout=10,
)
has_schema = "types" in resp.text if resp.status_code == 200 else False
return {
"test": "GraphQL Introspection Disclosure",
"endpoint": graphql_endpoint,
"status": resp.status_code,
"introspection_enabled": has_schema,
"vulnerable": has_schema,
}
def test_excessive_data_exposure(base_url, endpoint, auth_token, expected_fields):
"""Test for excessive data exposure in API responses."""
headers = {"Authorization": f"Bearer {auth_token}"}
resp = requests.get(urljoin(base_url, endpoint), headers=headers, timeout=10)
if resp.status_code != 200:
return {"test": "Excessive Data Exposure", "endpoint": endpoint, "vulnerable": False}
data = resp.json()
extra_fields = [k for k in data.keys() if k not in expected_fields] if isinstance(data, dict) else []
return {
"test": "Excessive Data Exposure (API3:2023)",
"endpoint": endpoint,
"expected_fields": expected_fields,
"extra_fields": extra_fields,
"vulnerable": len(extra_fields) > 0,
}
def generate_report(findings):
"""Generate API security testing report."""
critical = [f for f in findings if f.get("vulnerable")]
report = {
"timestamp": datetime.utcnow().isoformat(),
"total_tests": len(findings),
"vulnerabilities_found": len(critical),
"findings": findings,
}
logger.info("Report: %d tests, %d vulnerabilities", len(findings), len(critical))
return report
def main():
parser = argparse.ArgumentParser(description="API Security Testing Agent")
parser.add_argument("--base-url", required=True, help="API base URL")
parser.add_argument("--token", help="Auth bearer token")
parser.add_argument("--low-priv-token", help="Low-privilege bearer token for BFLA testing")
parser.add_argument("--login-endpoint", default="/api/auth/login", help="Login endpoint for rate limit test")
parser.add_argument("--graphql", action="store_true", help="Test GraphQL introspection")
parser.add_argument("--output", default="api_security_report.json")
args = parser.parse_args()
findings = []
findings.append(test_rate_limiting(args.base_url, args.login_endpoint, 50))
if args.graphql:
findings.append(test_graphql_introspection(args.base_url))
if args.low_priv_token:
admin_eps = ["/api/admin/users", "/api/admin/settings", "/api/admin/dashboard"]
findings.extend(test_bfla(args.base_url, admin_eps, args.low_priv_token))
if args.token:
findings.append(test_jwt_none_algorithm(args.base_url, "/api/profile", args.token))
report = generate_report(findings)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()