Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

160 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""BOPLA vulnerability scanner for OWASP API3:2023 Broken Object Property Level Authorization.
Tests APIs for excessive data exposure and mass assignment vulnerabilities
by comparing responses against expected field sets and injecting extra properties.
"""
import argparse
import json
import sys
from copy import deepcopy
try:
import requests
except ImportError:
print("Install requests: pip install requests")
sys.exit(1)
SENSITIVE_PATTERNS = {
"critical": ["password", "password_hash", "secret", "token", "api_key",
"private_key", "access_token", "refresh_token"],
"high": ["ssn", "social_security", "tax_id", "credit_card", "card_number",
"cvv", "bank_account", "routing_number"],
"medium": ["salary", "income", "internal_notes", "role", "permissions",
"is_admin", "is_superuser", "session_id", "ip_address"],
"low": ["phone", "address", "date_of_birth", "dob", "gender"]
}
MASS_ASSIGNMENT_PAYLOADS = [
("role", "admin"), ("is_admin", True), ("is_verified", True),
("email_verified", True), ("account_type", "premium"),
("discount_rate", 100), ("permissions", ["admin", "write", "delete"]),
("account_balance", 999999), ("subscription_tier", "enterprise"),
]
def classify_field(field_name):
lower = field_name.lower().split(".")[-1]
for severity, patterns in SENSITIVE_PATTERNS.items():
for p in patterns:
if p in lower:
return severity.upper()
return None
def flatten_keys(obj, prefix=""):
keys = []
for k, v in obj.items():
full = f"{prefix}.{k}" if prefix else k
keys.append(full)
if isinstance(v, dict):
keys.extend(flatten_keys(v, full))
return keys
def test_excessive_exposure(base_url, endpoint, expected_fields, headers):
findings = []
url = f"{base_url.rstrip('/')}{endpoint}"
try:
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code != 200:
return findings
data = resp.json()
objects = data if isinstance(data, list) else [data]
if isinstance(data, dict) and "data" in data:
inner = data["data"]
objects = inner if isinstance(inner, list) else [inner]
for obj in objects[:5]:
if not isinstance(obj, dict):
continue
response_fields = set(flatten_keys(obj))
unexpected = response_fields - set(expected_fields)
for field in unexpected:
sev = classify_field(field)
if sev:
findings.append({
"endpoint": endpoint, "method": "GET",
"type": "excessive_exposure", "severity": sev,
"property": field,
"detail": f"Unexpected sensitive field '{field}' in response"
})
except (requests.RequestException, json.JSONDecodeError) as e:
findings.append({"error": str(e)})
return findings
def test_mass_assignment(base_url, endpoint, headers, method="PUT"):
findings = []
url = f"{base_url.rstrip('/')}{endpoint}"
try:
original = requests.get(url, headers=headers, timeout=10)
original_data = original.json() if original.status_code == 200 else {}
except (requests.RequestException, json.JSONDecodeError):
original_data = {}
for field_name, injected_value in MASS_ASSIGNMENT_PAYLOADS:
if original_data.get(field_name) == injected_value:
continue
test_data = deepcopy(original_data)
test_data[field_name] = injected_value
try:
h = {**headers, "Content-Type": "application/json"}
if method == "PUT":
resp = requests.put(url, json=test_data, headers=h, timeout=10)
elif method == "PATCH":
resp = requests.patch(url, json={field_name: injected_value}, headers=h, timeout=10)
else:
resp = requests.post(url, json=test_data, headers=h, timeout=10)
if resp.status_code in (200, 201, 204):
verify = requests.get(url, headers=headers, timeout=10)
if verify.status_code == 200 and verify.json().get(field_name) == injected_value:
sev = "CRITICAL" if field_name in ("role", "is_admin", "permissions") else "HIGH"
findings.append({
"endpoint": endpoint, "method": method,
"type": "mass_assignment", "severity": sev,
"property": field_name,
"detail": f"Injected {field_name}={injected_value} accepted"
})
if field_name in original_data:
requests.patch(url, json={field_name: original_data[field_name]},
headers=h, timeout=10)
except requests.RequestException:
continue
return findings
def main():
parser = argparse.ArgumentParser(description="BOPLA Vulnerability Scanner (OWASP API3:2023)")
parser.add_argument("--base-url", required=True, help="API base URL")
parser.add_argument("--endpoint", required=True, help="Endpoint to test (e.g. /api/v1/users/1)")
parser.add_argument("--token", help="Bearer token for Authorization header")
parser.add_argument("--expected-fields", nargs="+", default=[], help="Expected response fields")
parser.add_argument("--test", choices=["exposure", "mass_assignment", "both"], default="both")
parser.add_argument("--method", choices=["PUT", "PATCH", "POST"], default="PUT")
args = parser.parse_args()
headers = {}
if args.token:
headers["Authorization"] = f"Bearer {args.token}"
results = {"endpoint": args.endpoint, "findings": []}
if args.test in ("exposure", "both"):
results["findings"].extend(
test_excessive_exposure(args.base_url, args.endpoint, args.expected_fields, headers))
if args.test in ("mass_assignment", "both"):
results["findings"].extend(
test_mass_assignment(args.base_url, args.endpoint, headers, args.method))
results["total_findings"] = len(results["findings"])
results["by_severity"] = {}
for f in results["findings"]:
sev = f.get("severity", "UNKNOWN")
results["by_severity"][sev] = results["by_severity"].get(sev, 0) + 1
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()