Skip to content

Commit d7f85de

Browse files
Release v5.15.3 (#107)
* Stabilize WAF detection and fingerprint fallback * Tune runtime fingerprint detection * Harden report debug output * Harden debug and report safety * Stabilize WAF, fingerprint, and report safety * Pass Codecov token to reusable Python CI * Pass Codecov token to reusable Python CI * ci: add GitHub Actions E2E scan for OpenDoor CLI reports * Stabilize WAF challenge and cookie handling * (enhancement) prettify HTML reports make it more intelligible for humans * Improve debug diagnostics and fingerprint infrastructure * Document transport flow hardening * pre-relases 5.15.3 check * Add stacktrace response sniffer * (enhancement) expanded passive WAF recognition with block-response signatures for DDoS-GUARD, Tencent Cloud WAF, Google Cloud Armor, SafeLine, Vercel WAF, Wallarm and Wordfence, complementing existing infrastructure fingerprinting where applicable. * (fix) pause/ resume from keyboard * pre-release fixes * Configure CodeQL analysis paths * Address release security scanner findings * Fix tolerant HTML closing tag filtering * Progress shows processed requests, not only reportable findings. With include/exclude filters, auto-calibration or sniffers enabled, many responses can be ignored or shown only as transient debug progress lines. * Finalize release checks and scanner fixes * Disable Codecov patch gate * Fix runtime pause resume prompt normalization
1 parent 118b9de commit d7f85de

135 files changed

Lines changed: 11025 additions & 420 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codacy.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
exclude_paths:
23
- "benchmarks/results/**"
34
- "results.sarif"
@@ -7,4 +8,5 @@ exclude_paths:
78
- "**/*.pyc"
89
- ".coverage"
910
- ".DS_Store"
10-
- "**/.DS_Store"
11+
- "**/.DS_Store"
12+
- "tests/**"

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ data/openvpn-profiles/*.conf
6565
benchmarks
6666
docs
6767
tests
68+
e2e-server.log
69+
data/test.dat
6870

6971
# Local helper scripts are not needed in runtime image
7072
scripts

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ body:
122122
description: What did you expect to happen?
123123
placeholder: "The scan should..., report should..., output should..."
124124
validations:
125-
required: true
125+
required: false
126126

127127
- type: textarea
128128
id: reproduce
@@ -134,7 +134,7 @@ body:
134134
2. Run ...
135135
3. Observe ...
136136
validations:
137-
required: true
137+
required: false
138138

139139
- type: textarea
140140
id: logs

.github/codeql/codeql-config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: "OpenDoor CodeQL config"
2+
3+
paths-ignore:
4+
- "tests/**"
5+
6+
query-filters:
7+
- exclude:
8+
id: py/incomplete-url-substring-sanitization

.github/e2e/server.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
GitHub Actions E2E HTTP server for OpenDoor.
3+
4+
The server exposes deterministic routes so the workflow can verify
5+
OpenDoor CLI scan behavior, JSON reporting and SARIF reporting.
6+
"""
7+
8+
from http.server import BaseHTTPRequestHandler, HTTPServer
9+
10+
11+
ROUTES: dict[str, tuple[int, str]] = {
12+
"/admin": (200, "Admin panel"),
13+
"/backup": (200, "Backup index"),
14+
"/health": (200, "Health OK"),
15+
"/uploads": (200, "Uploads directory"),
16+
"/login": (200, "Login page"),
17+
"/forbidden": (403, "Forbidden"),
18+
"/auth-required": (401, "Unauthorized"),
19+
"/redirect": (301, "Moved"),
20+
}
21+
22+
23+
class Handler(BaseHTTPRequestHandler):
24+
"""Deterministic request handler for OpenDoor E2E."""
25+
26+
def do_HEAD(self) -> None:
27+
self._respond(with_body=False)
28+
29+
def do_GET(self) -> None:
30+
self._respond(with_body=True)
31+
32+
def _respond(self, with_body: bool = False) -> None:
33+
path = self.path.split("?")[0]
34+
status, body = ROUTES.get(path, (404, "Not Found"))
35+
36+
self.send_response(status)
37+
self.send_header("Content-Type", "text/plain")
38+
39+
if status == 301:
40+
self.send_header("Location", "https://example.com/")
41+
42+
self.send_header("Content-Length", str(len(body)))
43+
self.end_headers()
44+
45+
if with_body:
46+
self.wfile.write(body.encode("utf-8"))
47+
48+
def log_message(self, fmt: str, *args: object) -> None:
49+
return
50+
51+
52+
if __name__ == "__main__":
53+
server = HTTPServer(("127.0.0.1", 8088), Handler)
54+
print("E2E server listening on http://127.0.0.1:8088", flush=True)
55+
server.serve_forever()

.github/e2e/validate.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""
2+
Validate OpenDoor GitHub Actions E2E reports against v5.15.2 report shape.
3+
"""
4+
5+
import json
6+
import sys
7+
from pathlib import Path
8+
9+
10+
TARGET = "127.0.0.1"
11+
REPORTS_DIR = Path("./reports") / TARGET
12+
13+
EXPECTED_SUCCESS_PATHS = {
14+
"/admin",
15+
"/backup",
16+
"/health",
17+
"/uploads",
18+
"/login",
19+
}
20+
21+
EXPECTED_IGNORED_404_PATHS = {
22+
"/nonexistent",
23+
"/ghost",
24+
"/random-miss",
25+
"/doesnotexist",
26+
}
27+
28+
passed = 0
29+
failed = 0
30+
31+
32+
def assert_true(condition: bool, message: str) -> None:
33+
global passed, failed
34+
35+
if condition:
36+
print(f"✅ {message}")
37+
passed += 1
38+
return
39+
40+
print(f"❌ {message}", file=sys.stderr)
41+
failed += 1
42+
43+
44+
def load_json(path: Path) -> dict:
45+
with path.open("r", encoding="utf-8") as handler:
46+
return json.load(handler)
47+
48+
49+
def item_urls(report: dict, bucket: str) -> list[str]:
50+
details = report.get("report_items", {}).get(bucket)
51+
52+
if isinstance(details, list):
53+
return [
54+
str(item.get("url", ""))
55+
for item in details
56+
if isinstance(item, dict)
57+
]
58+
59+
return [
60+
str(item)
61+
for item in report.get("items", {}).get(bucket, [])
62+
]
63+
64+
65+
def item_details(report: dict, bucket: str) -> list[dict]:
66+
details = report.get("report_items", {}).get(bucket)
67+
68+
if isinstance(details, list):
69+
return [
70+
item
71+
for item in details
72+
if isinstance(item, dict)
73+
]
74+
75+
return [
76+
{"url": str(item), "code": "-"}
77+
for item in report.get("items", {}).get(bucket, [])
78+
]
79+
80+
81+
def has_path(urls: list[str], path: str) -> bool:
82+
return any(url.endswith(path) or path in url for url in urls)
83+
84+
85+
def validate_json_report() -> None:
86+
report = load_json(REPORTS_DIR / f"{TARGET}.json")
87+
total = report.get("total", {})
88+
89+
assert_true(total.get("success") == 5, "JSON: success bucket has exactly 5 hits")
90+
assert_true(total.get("forbidden") == 1, "JSON: forbidden bucket has exactly 1 hit")
91+
assert_true(total.get("auth") == 1, "JSON: auth bucket has exactly 1 hit")
92+
assert_true(total.get("redirect") == 1, "JSON: redirect bucket has exactly 1 hit")
93+
assert_true(total.get("ignored") == 4, "JSON: ignored bucket has exactly 4 filtered misses")
94+
95+
success_urls = item_urls(report, "success")
96+
97+
for path in sorted(EXPECTED_SUCCESS_PATHS):
98+
assert_true(has_path(success_urls, path), f"JSON: {path} is in success bucket")
99+
100+
assert_true(
101+
has_path(item_urls(report, "forbidden"), "/forbidden"),
102+
"JSON: /forbidden is in forbidden bucket",
103+
)
104+
105+
assert_true(
106+
has_path(item_urls(report, "auth"), "/auth-required"),
107+
"JSON: /auth-required is in auth bucket",
108+
)
109+
110+
ignored_items = item_details(report, "ignored")
111+
112+
for path in sorted(EXPECTED_IGNORED_404_PATHS):
113+
assert_true(
114+
any(
115+
has_path([str(item.get("url", ""))], path)
116+
and str(item.get("code")) == "404"
117+
for item in ignored_items
118+
),
119+
f"JSON: {path} is preserved as ignored 404",
120+
)
121+
122+
active_buckets = ("success", "forbidden", "auth", "redirect")
123+
active_urls = [
124+
url
125+
for bucket in active_buckets
126+
for url in item_urls(report, bucket)
127+
]
128+
129+
for path in sorted(EXPECTED_IGNORED_404_PATHS):
130+
assert_true(
131+
not has_path(active_urls, path),
132+
f"JSON: {path} is not in active finding buckets",
133+
)
134+
135+
136+
def validate_sarif_report() -> None:
137+
sarif = load_json(REPORTS_DIR / f"{TARGET}.sarif")
138+
139+
runs = sarif.get("runs", [])
140+
run = runs[0] if runs else {}
141+
results = run.get("results", [])
142+
143+
assert_true(sarif.get("version") == "2.1.0", "SARIF: version is 2.1.0")
144+
assert_true(
145+
run.get("tool", {}).get("driver", {}).get("name") == "OpenDoor",
146+
"SARIF: tool is OpenDoor",
147+
)
148+
149+
def result_matches(rule_id: str, path: str | None = None, code: int | None = None) -> bool:
150+
for result in results:
151+
if result.get("ruleId") != rule_id:
152+
continue
153+
154+
props = result.get("properties", {})
155+
uri = (
156+
result.get("locations", [{}])[0]
157+
.get("physicalLocation", {})
158+
.get("artifactLocation", {})
159+
.get("uri", "")
160+
)
161+
162+
if path is not None and path not in str(uri) and path not in str(props.get("url", "")):
163+
continue
164+
165+
if code is not None and props.get("statusCode") != code:
166+
continue
167+
168+
return True
169+
170+
return False
171+
172+
for path in sorted(EXPECTED_SUCCESS_PATHS):
173+
assert_true(
174+
result_matches("opendoor.finding.success", path, 200),
175+
f"SARIF: {path} is success/200",
176+
)
177+
178+
assert_true(
179+
result_matches("opendoor.finding.forbidden", "/forbidden", 403),
180+
"SARIF: /forbidden is forbidden/403",
181+
)
182+
183+
assert_true(
184+
result_matches("opendoor.finding.auth", "/auth-required", 401),
185+
"SARIF: /auth-required is auth/401",
186+
)
187+
188+
assert_true(
189+
result_matches("opendoor.finding.redirect", None, 301),
190+
"SARIF: redirect bucket has status 301",
191+
)
192+
193+
for path in sorted(EXPECTED_IGNORED_404_PATHS):
194+
assert_true(
195+
result_matches("opendoor.finding.ignored", path, 404),
196+
f"SARIF: {path} is ignored/404",
197+
)
198+
199+
200+
def main() -> int:
201+
try:
202+
validate_json_report()
203+
validate_sarif_report()
204+
except Exception as error:
205+
print(f"💥 Validation error: {error}", file=sys.stderr)
206+
return 1
207+
208+
print(f"\nResults: {passed} passed, {failed} failed")
209+
return 1 if failed else 0
210+
211+
212+
if __name__ == "__main__":
213+
raise SystemExit(main())

.github/e2e/wordlist.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
admin
2+
backup
3+
health
4+
uploads
5+
login
6+
forbidden
7+
auth-required
8+
redirect
9+
nonexistent
10+
ghost
11+
random-miss
12+
doesnotexist

.github/pull_request_template.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,10 @@
6060
- [ ] CHANGELOG updated
6161
- [ ] No documentation change required
6262

63-
## Packaging / runtime assets
64-
65-
<!-- Required for packaging, wheel/sdist, entrypoint, Docker, Homebrew, Debian/Kali, or runtime data changes. -->
66-
67-
- [ ] This PR does not affect packaging or runtime assets
68-
- [ ] Source distribution was checked
69-
- [ ] Wheel installation was checked in a clean environment
70-
- [ ] Runtime assets were verified after installation
71-
- [ ] Docker image was checked
72-
- [ ] Linux distribution packaging impact was considered
73-
7463
## Security and responsible use
7564

76-
- [ ] This PR does not add offensive behavior outside authorized testing workflows
7765
- [ ] This PR does not commit real tokens, cookies, VPN profiles, credentials, private targets, or scan reports
7866
- [ ] Security-sensitive details are not exposed in public logs, docs, examples, tests, or screenshots
79-
- [ ] If this fixes a vulnerability, disclosure details were handled according to `SECURITY.md`
8067

8168
## Backward compatibility
8269

.github/workflows/ci-linux-py313.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ jobs:
1616
with:
1717
runner: ubuntu-latest
1818
python-version: "3.13"
19+
secrets: inherit

.github/workflows/ci-linux-py314.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ jobs:
1616
with:
1717
runner: ubuntu-latest
1818
python-version: "3.14"
19+
secrets: inherit

0 commit comments

Comments
 (0)