leakradar-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leakradar-cli-0.1.0/LICENSE +15 -0
- leakradar-cli-0.1.0/PKG-INFO +88 -0
- leakradar-cli-0.1.0/README.md +79 -0
- leakradar-cli-0.1.0/pyproject.toml +24 -0
- leakradar-cli-0.1.0/setup.cfg +4 -0
- leakradar-cli-0.1.0/src/leakradar/__init__.py +5 -0
- leakradar-cli-0.1.0/src/leakradar/auth.py +167 -0
- leakradar-cli-0.1.0/src/leakradar/bola_matrix.py +265 -0
- leakradar-cli-0.1.0/src/leakradar/canonicalizer.py +312 -0
- leakradar-cli-0.1.0/src/leakradar/cli.py +248 -0
- leakradar-cli-0.1.0/src/leakradar/markdown_poc.py +99 -0
- leakradar-cli-0.1.0/src/leakradar/pdf_report.py +281 -0
- leakradar-cli-0.1.0/src/leakradar/redactor.py +75 -0
- leakradar-cli-0.1.0/src/leakradar/secrets.py +110 -0
- leakradar-cli-0.1.0/src/leakradar/seeder.py +323 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/PKG-INFO +88 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/SOURCES.txt +19 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/dependency_links.txt +1 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/entry_points.txt +2 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/requires.txt +5 -0
- leakradar-cli-0.1.0/src/leakradar_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LeakRadar. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Commercial & Revenue Use Restriction: You may NOT use this software to generate revenue, perform paid client security audits, provide managed security services, or bundle this software into commercial products without purchasing a valid Pro Auditor or Enterprise commercial license key.
|
|
8
|
+
|
|
9
|
+
2. Anti-Circumvention & Civil Liability: Any attempt to tamper with, modify, reverse-engineer, or bypass the licensing authentication, key verification, or capability gating mechanisms of this software for commercial gain is strictly prohibited under international copyright laws, including 17 U.S.C. ยง 1201 (DMCA Anti-Circumvention) and the WIPO Copyright Treaty. Willful circumvention subjects violators to statutory civil damages up to $150,000 per infringement violation, immediate injunctive relief, and full recovery of legal fees.
|
|
10
|
+
|
|
11
|
+
3. Attribution: Notice of copyright and this license must be retained in all copies or substantial portions of the software.
|
|
12
|
+
|
|
13
|
+
4. Patent License: Grant of Patent License as specified under PolyForm Noncommercial 1.0.0 terms.
|
|
14
|
+
|
|
15
|
+
For commercial licensing requests, purchase Pro Auditor passes at https://ajmax76.github.io/leakradar/ or contact enterprise@leakradar.io.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: leakradar-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API Security Reconnaissance & BOLA Evidence Engine
|
|
5
|
+
Author: ajmax76
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
|
|
10
|
+
# โก LeakRadar
|
|
11
|
+
|
|
12
|
+
[](https://github.com/ajmax76/leakradar/actions)
|
|
13
|
+
[](https://pypi.org/project/leakradar/)
|
|
14
|
+
[](https://pypi.org/project/leakradar/)
|
|
15
|
+
[](https://polyformproject.org/)
|
|
16
|
+
|
|
17
|
+
**LeakRadar** is an automated API security reconnaissance engine designed to detect **Broken Object Level Authorization (BOLA / IDOR)** and exposed secrets across REST endpoints with near-zero false positives.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ๐ Key Architecture & Capabilities
|
|
22
|
+
|
|
23
|
+
* **3-Baseline Volatility Diffing:** Executes triple User A baselines to identify and prune volatile fields (timestamps, nonces, session tokens) before cross-token evaluation.
|
|
24
|
+
* **JWT Claim Harvesting:** Automatically parses bearer token claims (`sub`, `user_id`, `email`) to discover seed values for parameterized routes (`/api/users/{user_id}`).
|
|
25
|
+
* **Cross-Token Replay Matrix:** Replays candidate endpoints using User B's authentication identity and measures leaf-level scalar field overlap, ID echoing, and ownership matches.
|
|
26
|
+
* **Payload Secret Scanner:** Built-in Shannon entropy filter ($\ge 4.5$) and targeted regex rules for AWS keys, Stripe tokens, private keys, and API tokens.
|
|
27
|
+
* **Dual-Format Reporting:** Exports publication-ready HackerOne/Bugcrowd Markdown PoCs and executive ReportLab PDF deliverables with automatic credential redaction.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ๐ Quickstart
|
|
32
|
+
|
|
33
|
+
### 1. Installation
|
|
34
|
+
```bash
|
|
35
|
+
pip install leakradar
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Run a Reconnaissance Scan
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
leakradar scan \
|
|
42
|
+
--base-url "https://staging-api.example.com" \
|
|
43
|
+
--spec "https://staging-api.example.com/openapi.json" \
|
|
44
|
+
--token-a "JWT_TOKEN_VICTIM" \
|
|
45
|
+
--token-b "JWT_TOKEN_ATTACKER" \
|
|
46
|
+
--output "./findings" \
|
|
47
|
+
--format all \
|
|
48
|
+
--verbose
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Activate Pro License (Optional)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
leakradar auth --key "lr_live_..."
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ๐ Local Development & Testing
|
|
60
|
+
|
|
61
|
+
1. Clone repository:
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/ajmax76/leakradar.git
|
|
64
|
+
cd leakradar
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. Set up virtual environment:
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m venv venv
|
|
70
|
+
source venv/bin/activate
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. Run automated test suite against local VAmPI container:
|
|
75
|
+
```bash
|
|
76
|
+
docker compose up -d
|
|
77
|
+
pytest tests/test_vampi_e2e.py -v
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ๐ License & Distribution
|
|
83
|
+
|
|
84
|
+
LeakRadar is governed by the **PolyForm Noncommercial License 1.0.0**.
|
|
85
|
+
|
|
86
|
+
- **Non-Commercial Use**: Free for individual security researchers, academic research, and non-commercial open-source vulnerability testing.
|
|
87
|
+
- **Commercial & Revenue Use**: Using LeakRadar to offer paid client audits, managed security services, or commercial software products strictly requires a **Pro Auditor** or **Enterprise** commercial license key via [ajmax76.github.io/leakradar](https://ajmax76.github.io/leakradar/).
|
|
88
|
+
- **Anti-Circumvention**: Tampering with or cracking license verification mechanisms for commercial gain is strictly prohibited under 17 U.S.C. ยง 1201 (DMCA) and international copyright treaties (WIPO), subjecting violators to statutory civil damages up to $150,000 per violation plus legal fees.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# โก LeakRadar
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ajmax76/leakradar/actions)
|
|
4
|
+
[](https://pypi.org/project/leakradar/)
|
|
5
|
+
[](https://pypi.org/project/leakradar/)
|
|
6
|
+
[](https://polyformproject.org/)
|
|
7
|
+
|
|
8
|
+
**LeakRadar** is an automated API security reconnaissance engine designed to detect **Broken Object Level Authorization (BOLA / IDOR)** and exposed secrets across REST endpoints with near-zero false positives.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ๐ Key Architecture & Capabilities
|
|
13
|
+
|
|
14
|
+
* **3-Baseline Volatility Diffing:** Executes triple User A baselines to identify and prune volatile fields (timestamps, nonces, session tokens) before cross-token evaluation.
|
|
15
|
+
* **JWT Claim Harvesting:** Automatically parses bearer token claims (`sub`, `user_id`, `email`) to discover seed values for parameterized routes (`/api/users/{user_id}`).
|
|
16
|
+
* **Cross-Token Replay Matrix:** Replays candidate endpoints using User B's authentication identity and measures leaf-level scalar field overlap, ID echoing, and ownership matches.
|
|
17
|
+
* **Payload Secret Scanner:** Built-in Shannon entropy filter ($\ge 4.5$) and targeted regex rules for AWS keys, Stripe tokens, private keys, and API tokens.
|
|
18
|
+
* **Dual-Format Reporting:** Exports publication-ready HackerOne/Bugcrowd Markdown PoCs and executive ReportLab PDF deliverables with automatic credential redaction.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ๐ Quickstart
|
|
23
|
+
|
|
24
|
+
### 1. Installation
|
|
25
|
+
```bash
|
|
26
|
+
pip install leakradar
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Run a Reconnaissance Scan
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
leakradar scan \
|
|
33
|
+
--base-url "https://staging-api.example.com" \
|
|
34
|
+
--spec "https://staging-api.example.com/openapi.json" \
|
|
35
|
+
--token-a "JWT_TOKEN_VICTIM" \
|
|
36
|
+
--token-b "JWT_TOKEN_ATTACKER" \
|
|
37
|
+
--output "./findings" \
|
|
38
|
+
--format all \
|
|
39
|
+
--verbose
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Activate Pro License (Optional)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
leakradar auth --key "lr_live_..."
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## ๐ Local Development & Testing
|
|
51
|
+
|
|
52
|
+
1. Clone repository:
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/ajmax76/leakradar.git
|
|
55
|
+
cd leakradar
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Set up virtual environment:
|
|
59
|
+
```bash
|
|
60
|
+
python3 -m venv venv
|
|
61
|
+
source venv/bin/activate
|
|
62
|
+
pip install -e .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. Run automated test suite against local VAmPI container:
|
|
66
|
+
```bash
|
|
67
|
+
docker compose up -d
|
|
68
|
+
pytest tests/test_vampi_e2e.py -v
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ๐ License & Distribution
|
|
74
|
+
|
|
75
|
+
LeakRadar is governed by the **PolyForm Noncommercial License 1.0.0**.
|
|
76
|
+
|
|
77
|
+
- **Non-Commercial Use**: Free for individual security researchers, academic research, and non-commercial open-source vulnerability testing.
|
|
78
|
+
- **Commercial & Revenue Use**: Using LeakRadar to offer paid client audits, managed security services, or commercial software products strictly requires a **Pro Auditor** or **Enterprise** commercial license key via [ajmax76.github.io/leakradar](https://ajmax76.github.io/leakradar/).
|
|
79
|
+
- **Anti-Circumvention**: Tampering with or cracking license verification mechanisms for commercial gain is strictly prohibited under 17 U.S.C. ยง 1201 (DMCA) and international copyright treaties (WIPO), subjecting violators to statutory civil damages up to $150,000 per violation plus legal fees.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "leakradar-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "API Security Reconnaissance & BOLA Evidence Engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "ajmax76" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer>=0.9.0",
|
|
14
|
+
"httpx>=0.25.0",
|
|
15
|
+
"rich>=13.0.0",
|
|
16
|
+
"pyyaml>=6.0.0",
|
|
17
|
+
"reportlab>=3.6.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
leakradar = "leakradar.cli:app"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class LicenseContext:
|
|
15
|
+
active: bool = False
|
|
16
|
+
key: Optional[str] = None
|
|
17
|
+
tier: str = "free" # "free" | "paid" | "enterprise"
|
|
18
|
+
capabilities: Dict[str, bool] = field(default_factory=lambda: {
|
|
19
|
+
"pdf_export": False,
|
|
20
|
+
"cloud_rules": False,
|
|
21
|
+
"unlimited_scans": True,
|
|
22
|
+
})
|
|
23
|
+
expires_at: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LicenseManager:
|
|
27
|
+
"""
|
|
28
|
+
Manages Polar.sh license key activation, machine fingerprinting, and local cache.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
ACTIVATION_URL = "https://api.ajmax76.github.io/leakradar/v1/license/activate"
|
|
32
|
+
CACHE_DIR = Path.home() / ".leakradar"
|
|
33
|
+
CACHE_FILE = CACHE_DIR / "license.json"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_machine_fingerprint(cls) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Generate a unique hardware fingerprint using platform node, system, machine, and processor.
|
|
39
|
+
"""
|
|
40
|
+
raw_info = f"{platform.node()}-{platform.system()}-{platform.machine()}-{platform.processor()}"
|
|
41
|
+
return hashlib.sha256(raw_info.encode("utf-8")).hexdigest()
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def load_cached_license(cls) -> LicenseContext:
|
|
45
|
+
"""
|
|
46
|
+
Reads cached license from ~/.leakradar/license.json and checks expiration.
|
|
47
|
+
"""
|
|
48
|
+
if not cls.CACHE_FILE.exists():
|
|
49
|
+
return LicenseContext(active=False, tier="free")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(cls.CACHE_FILE, "r", encoding="utf-8") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
|
|
55
|
+
expires_at_str = data.get("expires_at")
|
|
56
|
+
if expires_at_str:
|
|
57
|
+
try:
|
|
58
|
+
exp_dt = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
|
|
59
|
+
if datetime.now(timezone.utc) > exp_dt:
|
|
60
|
+
return LicenseContext(active=False, tier="free")
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
tier = data.get("tier", "free")
|
|
65
|
+
is_paid = tier.lower() in ("paid", "pro", "enterprise")
|
|
66
|
+
return LicenseContext(
|
|
67
|
+
active=data.get("active", True),
|
|
68
|
+
key=data.get("key"),
|
|
69
|
+
tier=tier,
|
|
70
|
+
capabilities={
|
|
71
|
+
"pdf_export": is_paid or data.get("capabilities", {}).get("pdf_export", False),
|
|
72
|
+
"cloud_rules": is_paid or data.get("capabilities", {}).get("cloud_rules", False),
|
|
73
|
+
"unlimited_scans": True,
|
|
74
|
+
},
|
|
75
|
+
expires_at=expires_at_str,
|
|
76
|
+
)
|
|
77
|
+
except Exception:
|
|
78
|
+
return LicenseContext(active=False, tier="free")
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
async def activate_license(cls, license_key: str) -> LicenseContext:
|
|
82
|
+
"""
|
|
83
|
+
Activates a Dodo Payments / Polar.sh license key against the license server and caches response.
|
|
84
|
+
Enforces key format checks and device limit validation.
|
|
85
|
+
"""
|
|
86
|
+
clean_key = license_key.strip()
|
|
87
|
+
if not clean_key or len(clean_key) < 10:
|
|
88
|
+
return LicenseContext(active=False, tier="free")
|
|
89
|
+
|
|
90
|
+
fingerprint = cls.get_machine_fingerprint()
|
|
91
|
+
payload = {
|
|
92
|
+
"key": clean_key,
|
|
93
|
+
"fingerprint": fingerprint,
|
|
94
|
+
"system": platform.system(),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cls.CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
100
|
+
try:
|
|
101
|
+
resp = await client.post(cls.ACTIVATION_URL, json=payload)
|
|
102
|
+
if resp.status_code == 200:
|
|
103
|
+
data = resp.json()
|
|
104
|
+
tier = data.get("tier", "pro")
|
|
105
|
+
context_data = {
|
|
106
|
+
"active": True,
|
|
107
|
+
"key": clean_key,
|
|
108
|
+
"tier": tier,
|
|
109
|
+
"capabilities": {
|
|
110
|
+
"pdf_export": True,
|
|
111
|
+
"cloud_rules": True,
|
|
112
|
+
"unlimited_scans": True,
|
|
113
|
+
},
|
|
114
|
+
"expires_at": data.get("expires_at"),
|
|
115
|
+
"activated_at": datetime.now(timezone.utc).isoformat(),
|
|
116
|
+
}
|
|
117
|
+
with open(cls.CACHE_FILE, "w", encoding="utf-8") as f:
|
|
118
|
+
json.dump(context_data, f, indent=2)
|
|
119
|
+
|
|
120
|
+
return LicenseContext(
|
|
121
|
+
active=True,
|
|
122
|
+
key=clean_key,
|
|
123
|
+
tier=tier,
|
|
124
|
+
capabilities=context_data["capabilities"],
|
|
125
|
+
expires_at=data.get("expires_at"),
|
|
126
|
+
)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# Offline / Key Format Verification
|
|
131
|
+
# Valid keys issued by Dodo/Polar follow structured formats (e.g. pdt_..., LR-PRO-..., or 32+ char hex hashes)
|
|
132
|
+
is_valid_format = (
|
|
133
|
+
clean_key.startswith("pdt_") or
|
|
134
|
+
clean_key.startswith("LR-PRO-") or
|
|
135
|
+
clean_key.startswith("POLAR-") or
|
|
136
|
+
(len(clean_key) >= 24 and "-" in clean_key)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if is_valid_format:
|
|
140
|
+
context_data = {
|
|
141
|
+
"active": True,
|
|
142
|
+
"key": clean_key,
|
|
143
|
+
"tier": "pro",
|
|
144
|
+
"capabilities": {
|
|
145
|
+
"pdf_export": True,
|
|
146
|
+
"cloud_rules": True,
|
|
147
|
+
"unlimited_scans": True,
|
|
148
|
+
},
|
|
149
|
+
"expires_at": None,
|
|
150
|
+
"activated_at": datetime.now(timezone.utc).isoformat(),
|
|
151
|
+
}
|
|
152
|
+
with open(cls.CACHE_FILE, "w", encoding="utf-8") as f:
|
|
153
|
+
json.dump(context_data, f, indent=2)
|
|
154
|
+
|
|
155
|
+
return LicenseContext(
|
|
156
|
+
active=True,
|
|
157
|
+
key=clean_key,
|
|
158
|
+
tier="pro",
|
|
159
|
+
capabilities=context_data["capabilities"],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Invalid key fallback
|
|
163
|
+
return LicenseContext(active=False, tier="free")
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def get_active_context(cls) -> LicenseContext:
|
|
167
|
+
return cls.load_cached_license()
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from leakradar.canonicalizer import ErrorDetector, normalize_value, strip_paths
|
|
6
|
+
from leakradar.seeder import ResourceSeed, SeedResult
|
|
7
|
+
from leakradar.secrets import SecretDetector, SecretFinding
|
|
8
|
+
|
|
9
|
+
ID_FIELD_EXACT = {"id", "uuid", "guid", "key", "slug", "code", "identifier"}
|
|
10
|
+
ID_FIELD_SUFFIXES = ("_id", "-id", "Id")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_id_field(field_name: str) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Check if a field name is an ID field using exact matches or specific suffixes.
|
|
16
|
+
Prevents broad matches on words like 'valid' or 'solid'.
|
|
17
|
+
"""
|
|
18
|
+
fname = field_name.strip()
|
|
19
|
+
if fname in ID_FIELD_EXACT:
|
|
20
|
+
return True
|
|
21
|
+
for suff in ID_FIELD_SUFFIXES:
|
|
22
|
+
if fname.endswith(suff):
|
|
23
|
+
return True
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Finding:
|
|
29
|
+
seed: ResourceSeed
|
|
30
|
+
probe_url: str
|
|
31
|
+
probe_method: str
|
|
32
|
+
probe_status_code: int
|
|
33
|
+
confidence: str # "high" | "medium" | "low"
|
|
34
|
+
evidence_fields: List[Dict[str, Any]]
|
|
35
|
+
overlap_score: float
|
|
36
|
+
baseline_representative: Any
|
|
37
|
+
probe_response: Any
|
|
38
|
+
cvss_suggestion: str = "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (7.5 High)"
|
|
39
|
+
cwe_suggestion: str = "CWE-639: Authorization Bypass Through User-Controlled Key"
|
|
40
|
+
secret_findings: List[SecretFinding] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def to_curl(self) -> str:
|
|
43
|
+
headers_str = " \\\n ".join(
|
|
44
|
+
f'-H "{k}: <REDACTED_USER_B_TOKEN>"' if k.lower() == "authorization" else f'-H "{k}: {v}"'
|
|
45
|
+
for k, v in self.seed.headers.items()
|
|
46
|
+
)
|
|
47
|
+
if headers_str:
|
|
48
|
+
return f"curl -X {self.probe_method} \"{self.probe_url}\" \\\n {headers_str}"
|
|
49
|
+
return f"curl -X {self.probe_method} \"{self.probe_url}\""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class IdentityExtractor:
|
|
53
|
+
"""
|
|
54
|
+
Flattens payload structures into scalar leaf maps and extracts identity ownership values.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
OWNERSHIP_FIELDS = {
|
|
58
|
+
"user_id", "owner_id", "created_by", "customer_id", "account_id",
|
|
59
|
+
"tenant_id", "email", "username", "sub", "author_id", "creator_id"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_scalar_leaf_map(cls, data: Any, prefix: str = "$") -> Dict[str, Any]:
|
|
64
|
+
leaf_map: Dict[str, Any] = {}
|
|
65
|
+
|
|
66
|
+
if data is None:
|
|
67
|
+
return leaf_map
|
|
68
|
+
|
|
69
|
+
if isinstance(data, (str, int, float, bool)):
|
|
70
|
+
leaf_map[prefix] = data
|
|
71
|
+
return leaf_map
|
|
72
|
+
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
for k, v in data.items():
|
|
75
|
+
child_prefix = f"{prefix}.{k}"
|
|
76
|
+
leaf_map.update(cls.get_scalar_leaf_map(v, child_prefix))
|
|
77
|
+
return leaf_map
|
|
78
|
+
|
|
79
|
+
if isinstance(data, list):
|
|
80
|
+
for i, item in enumerate(data):
|
|
81
|
+
child_prefix = f"{prefix}[*]"
|
|
82
|
+
leaf_map.update(cls.get_scalar_leaf_map(item, child_prefix))
|
|
83
|
+
return leaf_map
|
|
84
|
+
|
|
85
|
+
return leaf_map
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def extract_identity_values(cls, baseline: Any, seed: ResourceSeed) -> Dict[str, Any]:
|
|
89
|
+
identity_map: Dict[str, Any] = {}
|
|
90
|
+
|
|
91
|
+
# Include seed path parameter values
|
|
92
|
+
for param_k, param_v in seed.param_values.items():
|
|
93
|
+
identity_map[f"$.param.{param_k}"] = param_v
|
|
94
|
+
|
|
95
|
+
# Traverse baseline payload for ownership fields
|
|
96
|
+
leaf_map = cls.get_scalar_leaf_map(baseline)
|
|
97
|
+
for path, val in leaf_map.items():
|
|
98
|
+
field_name = path.split(".")[-1].split("[")[0].lower()
|
|
99
|
+
if field_name in cls.OWNERSHIP_FIELDS or is_id_field(field_name):
|
|
100
|
+
identity_map[path] = val
|
|
101
|
+
|
|
102
|
+
return identity_map
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BolaMatrixRunner:
|
|
106
|
+
"""
|
|
107
|
+
Executes cross-token replay with User B credentials and classifies BOLA findings.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, user_b_headers: Dict[str, str], client: Optional[httpx.Client] = None):
|
|
111
|
+
self.user_b_headers = user_b_headers
|
|
112
|
+
self.client = client or httpx.Client(timeout=10.0, follow_redirects=True)
|
|
113
|
+
|
|
114
|
+
def _build_probe_url(self, seed: ResourceSeed) -> str:
|
|
115
|
+
url = seed.endpoint_template
|
|
116
|
+
for k, v in seed.param_values.items():
|
|
117
|
+
url = url.replace(f"{{{k}}}", str(v))
|
|
118
|
+
return f"{seed.base_url.rstrip('/')}{url}"
|
|
119
|
+
|
|
120
|
+
def _send_probe(self, method: str, url: str) -> Tuple[int, Any]:
|
|
121
|
+
try:
|
|
122
|
+
resp = self.client.request(method, url, headers=self.user_b_headers)
|
|
123
|
+
try:
|
|
124
|
+
data = resp.json()
|
|
125
|
+
except Exception:
|
|
126
|
+
data = resp.text
|
|
127
|
+
return resp.status_code, data
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return 500, str(e)
|
|
130
|
+
|
|
131
|
+
def _classify(
|
|
132
|
+
self,
|
|
133
|
+
baseline_stripped: Any,
|
|
134
|
+
probe_stripped: Any,
|
|
135
|
+
identity_map: Dict[str, Any],
|
|
136
|
+
seed: ResourceSeed,
|
|
137
|
+
status_code: int,
|
|
138
|
+
) -> Tuple[str, List[Dict[str, Any]], float]:
|
|
139
|
+
"""
|
|
140
|
+
Classifies confidence (high, medium, low) and returns (confidence, evidence_list, overlap_score).
|
|
141
|
+
"""
|
|
142
|
+
if status_code not in (200, 201, 204):
|
|
143
|
+
return "low", [], 0.0
|
|
144
|
+
|
|
145
|
+
is_err, _ = ErrorDetector.is_error_payload(status_code, probe_stripped)
|
|
146
|
+
if is_err:
|
|
147
|
+
return "low", [], 0.0
|
|
148
|
+
|
|
149
|
+
base_leaves = IdentityExtractor.get_scalar_leaf_map(baseline_stripped)
|
|
150
|
+
probe_leaves = IdentityExtractor.get_scalar_leaf_map(probe_stripped)
|
|
151
|
+
|
|
152
|
+
if not base_leaves or not probe_leaves:
|
|
153
|
+
return "low", [], 0.0
|
|
154
|
+
|
|
155
|
+
# Calculate Leaf Overlap
|
|
156
|
+
matching_leaves = 0
|
|
157
|
+
evidence_list = []
|
|
158
|
+
|
|
159
|
+
for path, val in base_leaves.items():
|
|
160
|
+
if path in probe_leaves and probe_leaves[path] == val:
|
|
161
|
+
matching_leaves += 1
|
|
162
|
+
|
|
163
|
+
overlap_score = matching_leaves / float(len(base_leaves))
|
|
164
|
+
|
|
165
|
+
# Signal 1: Resource ID Echoing
|
|
166
|
+
id_echo_signal = False
|
|
167
|
+
for param_k, param_v in seed.param_values.items():
|
|
168
|
+
param_v_str = str(param_v).lower()
|
|
169
|
+
for path, pval in probe_leaves.items():
|
|
170
|
+
field_name = path.split(".")[-1].split("[")[0]
|
|
171
|
+
if is_id_field(field_name) and str(pval).lower() == param_v_str:
|
|
172
|
+
id_echo_signal = True
|
|
173
|
+
evidence_list.append({
|
|
174
|
+
"type": "Resource ID Echo",
|
|
175
|
+
"field": path,
|
|
176
|
+
"value": pval,
|
|
177
|
+
"description": f"Target parameter value '{param_v}' for '{param_k}' echoed in User B response."
|
|
178
|
+
})
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
# Signal 2: Ownership Field Match
|
|
182
|
+
ownership_match_signal = False
|
|
183
|
+
for id_path, id_val in identity_map.items():
|
|
184
|
+
if id_path.startswith("$.param."):
|
|
185
|
+
continue
|
|
186
|
+
if id_path in probe_leaves and probe_leaves[id_path] == id_val:
|
|
187
|
+
ownership_match_signal = True
|
|
188
|
+
evidence_list.append({
|
|
189
|
+
"type": "Ownership Data Exposure",
|
|
190
|
+
"field": id_path,
|
|
191
|
+
"value": id_val,
|
|
192
|
+
"description": f"User A ownership field '{id_path}' returned to User B with identical value."
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
# General overlap evidence
|
|
196
|
+
if overlap_score >= 0.4:
|
|
197
|
+
evidence_list.append({
|
|
198
|
+
"type": "High Data Overlap",
|
|
199
|
+
"field": "$ (root)",
|
|
200
|
+
"value": f"{overlap_score * 100:.1f}%",
|
|
201
|
+
"description": f"User B response matches {overlap_score * 100:.1f}% of User A baseline fields."
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
# Classification Matrix Logic
|
|
205
|
+
if (id_echo_signal and ownership_match_signal) or \
|
|
206
|
+
(id_echo_signal and overlap_score >= 0.3) or \
|
|
207
|
+
(ownership_match_signal and overlap_score >= 0.6) or \
|
|
208
|
+
(overlap_score >= 0.85):
|
|
209
|
+
return "high", evidence_list, overlap_score
|
|
210
|
+
|
|
211
|
+
if (id_echo_signal and overlap_score >= 0.2) or \
|
|
212
|
+
ownership_match_signal or \
|
|
213
|
+
(overlap_score >= 0.4):
|
|
214
|
+
return "medium", evidence_list, overlap_score
|
|
215
|
+
|
|
216
|
+
return "low", evidence_list, overlap_score
|
|
217
|
+
|
|
218
|
+
def run(self, seed_result: SeedResult) -> List[Finding]:
|
|
219
|
+
findings: List[Finding] = []
|
|
220
|
+
|
|
221
|
+
for seed in seed_result.resources:
|
|
222
|
+
if not seed.param_values:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
probe_url = self._build_probe_url(seed)
|
|
226
|
+
status_code, probe_raw = self._send_probe(seed.method, probe_url)
|
|
227
|
+
|
|
228
|
+
# Error check
|
|
229
|
+
is_err, _ = ErrorDetector.is_error_payload(status_code, probe_raw)
|
|
230
|
+
if is_err:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
baseline_raw = seed.baseline_responses[0] if seed.baseline_responses else {}
|
|
234
|
+
baseline_norm = normalize_value(baseline_raw)
|
|
235
|
+
probe_norm = normalize_value(probe_raw)
|
|
236
|
+
|
|
237
|
+
# Scan probe response for exposed secrets and tokens
|
|
238
|
+
detected_secrets = SecretDetector.scan_payload(probe_raw)
|
|
239
|
+
|
|
240
|
+
# Strip volatile paths
|
|
241
|
+
baseline_stripped = strip_paths(baseline_norm, seed.volatile_paths)
|
|
242
|
+
probe_stripped = strip_paths(probe_norm, seed.volatile_paths)
|
|
243
|
+
|
|
244
|
+
# Extract identity values from User A baseline
|
|
245
|
+
identity_map = IdentityExtractor.extract_identity_values(baseline_norm, seed)
|
|
246
|
+
|
|
247
|
+
confidence, evidence, overlap = self._classify(
|
|
248
|
+
baseline_stripped, probe_stripped, identity_map, seed, status_code
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if confidence in ("high", "medium") or detected_secrets:
|
|
252
|
+
findings.append(Finding(
|
|
253
|
+
seed=seed,
|
|
254
|
+
probe_url=probe_url,
|
|
255
|
+
probe_method=seed.method,
|
|
256
|
+
probe_status_code=status_code,
|
|
257
|
+
confidence=confidence if confidence in ("high", "medium") else "medium",
|
|
258
|
+
evidence_fields=evidence,
|
|
259
|
+
overlap_score=overlap,
|
|
260
|
+
baseline_representative=baseline_stripped,
|
|
261
|
+
probe_response=probe_stripped,
|
|
262
|
+
secret_findings=detected_secrets,
|
|
263
|
+
))
|
|
264
|
+
|
|
265
|
+
return findings
|