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.
@@ -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
+ [![CI Pipeline](https://github.com/ajmax76/leakradar/actions/workflows/ci.yml/badge.svg)](https://github.com/ajmax76/leakradar/actions)
13
+ [![PyPI version](https://img.shields.io/pypi/v/leakradar.svg)](https://pypi.org/project/leakradar/)
14
+ [![Python Versions](https://img.shields.io/pypi/pyversions/leakradar.svg)](https://pypi.org/project/leakradar/)
15
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue.svg)](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
+ [![CI Pipeline](https://github.com/ajmax76/leakradar/actions/workflows/ci.yml/badge.svg)](https://github.com/ajmax76/leakradar/actions)
4
+ [![PyPI version](https://img.shields.io/pypi/v/leakradar.svg)](https://pypi.org/project/leakradar/)
5
+ [![Python Versions](https://img.shields.io/pypi/pyversions/leakradar.svg)](https://pypi.org/project/leakradar/)
6
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """
2
+ LeakRadar - Open-core API security CLI for BOLA/IDOR vulnerability scanning.
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -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