websec-validator 0.2.6__tar.gz → 0.2.8__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.
- {websec_validator-0.2.6/src/websec_validator.egg-info → websec_validator-0.2.8}/PKG-INFO +1 -1
- {websec_validator-0.2.6 → websec_validator-0.2.8}/pyproject.toml +1 -1
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/dynamic.py +19 -12
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/auth.py +16 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/probes.py +1 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/scanners.py +25 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/forged-token.sh +15 -3
- {websec_validator-0.2.6 → websec_validator-0.2.8/src/websec_validator.egg-info}/PKG-INFO +1 -1
- {websec_validator-0.2.6 → websec_validator-0.2.8}/tests/test_hardening.py +62 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/LICENSE +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/README.md +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/setup.cfg +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/__init__.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/briefing.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/calibration.json +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/calibration.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/cli.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/constitution.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/corpus.json +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/__init__.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/authz.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/base.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/client_exposure.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/graphql.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/iac_ci.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/integrations.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/routes.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/schemas.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/stack.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/surface.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/tenant.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/findings.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/proof.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/recon.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/report.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/_lib.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/bola-cross-tenant.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/bola-write-verbs.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/compare-roles.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/dlp-bypass-offline.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/hs256-brute-force.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/jwt-attacks.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/mass-assignment.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/race-conditions.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/rate-limit-burst.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/s3-assess.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/ssrf-probes.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/unauth-baseline.sh +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/webhook-forgery.py +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/reports/FINDINGS-SUMMARY.md.template +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/reports/access-control-matrix.md.template +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/reports/findings-triage.md.template +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/reports/pentest-handover-brief.md.template +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/reports/per-tool-FINDINGS.md.template +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/SOURCES.txt +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/dependency_links.txt +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/entry_points.txt +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/top_level.txt +0 -0
- {websec_validator-0.2.6 → websec_validator-0.2.8}/tests/test_recon.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Local-first security recon that briefs your AI coding agent: facts + tailored probe scripts, code-in / artifacts-out. No LLM, no server, no running app.
|
|
5
5
|
Author: Ricardo Accioly
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "websec-validator"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "Local-first security recon that briefs your AI coding agent: facts + tailored probe scripts, code-in / artifacts-out. No LLM, no server, no running app."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -308,19 +308,24 @@ def forged_token_bypass(target: str, facts: dict, cookie_names=None,
|
|
|
308
308
|
base_code, _ = _request(method, url, token=None, data=body)
|
|
309
309
|
if base_code not in (401, 403):
|
|
310
310
|
continue # only routes that are gated WITHOUT auth tell us anything about forgery
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
hit =
|
|
315
|
-
if
|
|
316
|
-
|
|
317
|
-
row = {"method": method, "path": path, "baseline": base_code, "forged": fcode,
|
|
318
|
-
"via": via, "verdict": "BYPASS"}
|
|
319
|
-
bypassed.append(row)
|
|
311
|
+
# Bearer first (cheapest, most universal); only forge into each known auth cookie if
|
|
312
|
+
# Bearer didn't reach the handler — short-circuits to keep request volume (and
|
|
313
|
+
# rate-limiter pressure) down. cookie_names is what catches cookie-ONLY session apps.
|
|
314
|
+
hit, bearer_code = None, _request(method, url, token=forged, data=body)[0]
|
|
315
|
+
if bearer_code in _REACHED_HANDLER:
|
|
316
|
+
hit = ("Authorization: Bearer", bearer_code)
|
|
320
317
|
else:
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
for cn in (cookie_names or []):
|
|
319
|
+
cc = _request(method, url, token=None, data=body, cookie=f"{cn}={forged}")[0]
|
|
320
|
+
if cc in _REACHED_HANDLER:
|
|
321
|
+
hit = (f"cookie:{cn}", cc)
|
|
322
|
+
break
|
|
323
|
+
via, fcode = hit if hit else ("Authorization: Bearer", bearer_code)
|
|
324
|
+
row = {"method": method, "path": path, "baseline": base_code, "forged": fcode,
|
|
325
|
+
"via": via, "verdict": "BYPASS" if hit else "rejected"}
|
|
323
326
|
results.append(row)
|
|
327
|
+
if hit:
|
|
328
|
+
bypassed.append(row)
|
|
324
329
|
|
|
325
330
|
return {
|
|
326
331
|
"target": target,
|
|
@@ -338,8 +343,10 @@ def forged_token_bypass(target: str, facts: dict, cookie_names=None,
|
|
|
338
343
|
|
|
339
344
|
def run_unauth(target: str, facts_path: Path, outdir: Path, probe_writes: bool = False) -> dict:
|
|
340
345
|
facts = json.loads(Path(facts_path).read_text())
|
|
346
|
+
cookie_names = (facts.get("auth") or {}).get("cookie_names")
|
|
341
347
|
res = {"unauth_reachability": unauth_reachability(target, facts),
|
|
342
|
-
"forged_token_bypass": forged_token_bypass(target, facts,
|
|
348
|
+
"forged_token_bypass": forged_token_bypass(target, facts, cookie_names=cookie_names,
|
|
349
|
+
probe_writes=probe_writes)}
|
|
343
350
|
if probe_writes:
|
|
344
351
|
res["write_auth_enforcement"] = write_auth_enforcement(target, facts)
|
|
345
352
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
@@ -17,6 +17,15 @@ PASSPORT = re.compile(r"\bpassport\b|passport-jwt|passport-local")
|
|
|
17
17
|
SESSION = re.compile(r"express-session|cookie-session|iron-session|flask\.session|request\.session|getServerSession|getToken", re.I)
|
|
18
18
|
APIKEY = re.compile(r"x-api-key|api[_-]?key|apikey", re.I)
|
|
19
19
|
GUARDS = re.compile(r"requireAuth|requirePermission|requireRole|isAuthenticated|@login_required|@require|ensureAuth|withAuth|getServerSession|verifyToken|authMiddleware|@roles_required|can\(|ability\.", re.I)
|
|
20
|
+
# Cookie READ sites — the names the app pulls a token/session from. The forged-token probe
|
|
21
|
+
# forges into these (not just Authorization: Bearer) so a cookie-ONLY session app isn't a
|
|
22
|
+
# false negative. Covers cookies.get('X') / cookies['X'] / getCookie('X') / req.cookies.X.
|
|
23
|
+
COOKIE_READ = re.compile(
|
|
24
|
+
r"""cookies\s*(?:\.get\(|\[)\s*['"]([A-Za-z0-9_.\-]{2,64})['"]""" # cookies.get('X') / cookies['X']
|
|
25
|
+
r"""|getCookie\(\s*['"]([A-Za-z0-9_.\-]{2,64})['"]""" # getCookie('X')
|
|
26
|
+
r"""|\.cookies\.([A-Za-z_][A-Za-z0-9_]{1,63})\b(?!\s*\()""") # req.cookies.X (not a .get()/.set() call)
|
|
27
|
+
_COOKIE_RESERVED = {"get", "set", "getall", "has", "delete", "clear", "tostring",
|
|
28
|
+
"foreach", "entries", "keys", "values", "size", "name", "value", "length"}
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
class AuthExtractor(Extractor):
|
|
@@ -31,6 +40,7 @@ class AuthExtractor(Extractor):
|
|
|
31
40
|
# scheme: framework/route signals first, then grep
|
|
32
41
|
jwt = passport = session = apikey = 0
|
|
33
42
|
guard_files = []
|
|
43
|
+
cookie_names: list[str] = []
|
|
34
44
|
for _p, rel, text in ctx.iter_code():
|
|
35
45
|
if JWT_LIBS.search(text):
|
|
36
46
|
jwt += 1
|
|
@@ -42,6 +52,11 @@ class AuthExtractor(Extractor):
|
|
|
42
52
|
apikey += 1
|
|
43
53
|
if GUARDS.search(text) and len(guard_files) < 25:
|
|
44
54
|
guard_files.append(rel)
|
|
55
|
+
if len(cookie_names) < 20:
|
|
56
|
+
for m in COOKIE_READ.finditer(text):
|
|
57
|
+
name = m.group(1) or m.group(2) or m.group(3)
|
|
58
|
+
if name and name.lower() not in _COOKIE_RESERVED and name not in cookie_names:
|
|
59
|
+
cookie_names.append(name)
|
|
45
60
|
|
|
46
61
|
nextauth = "nextauth" in frameworks or any("nextauth" in e.lower() for e in auth_eps)
|
|
47
62
|
|
|
@@ -70,6 +85,7 @@ class AuthExtractor(Extractor):
|
|
|
70
85
|
"schemes_detected": detected,
|
|
71
86
|
"token_location": token_location,
|
|
72
87
|
"login_endpoints": auth_eps,
|
|
88
|
+
"cookie_names": cookie_names[:15],
|
|
73
89
|
"guard_files": guard_files,
|
|
74
90
|
"signal_counts": {"jwt": jwt, "passport": passport, "session": session, "api_key": apikey},
|
|
75
91
|
"note": "AGENT: confirm the PRIMARY auth flow + how a test token is minted before the JWT/auth "
|
|
@@ -111,6 +111,7 @@ def build_context(facts: dict) -> dict:
|
|
|
111
111
|
"auth": {
|
|
112
112
|
"scheme": auth.get("scheme"),
|
|
113
113
|
"token_location": auth.get("token_location"),
|
|
114
|
+
"cookie_names": auth.get("cookie_names", []),
|
|
114
115
|
"login_endpoints": tgt.get("auth_endpoints", [])[:10],
|
|
115
116
|
"how_to_authenticate": "cookie-session (e.g. NextAuth) → send the session cookie; "
|
|
116
117
|
"bearer → Authorization: Bearer <jwt>; api-key → the documented key header",
|
|
@@ -217,6 +217,27 @@ def _generic_secret(rule: str) -> bool:
|
|
|
217
217
|
return r in _GENERIC_SECRET_RULES or "generic" in r or "entropy" in r
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
# Secrets matched in DOCUMENTATION / EXAMPLE files are overwhelmingly placeholders, not live
|
|
221
|
+
# credentials — e.g. `curl -H "Authorization: Bearer <token>"` in a README/API doc, or a
|
|
222
|
+
# value in `.env.example`. Tier those to LOW + a verify note (still visible — a real key CAN be
|
|
223
|
+
# pasted into docs by mistake). Dogfooding flagged 4 HIGH curl-auth-header FPs across an API's
|
|
224
|
+
# README + docs/*.md (bug below).
|
|
225
|
+
_DOC_EXT = (".md", ".mdx", ".markdown", ".rst", ".txt", ".adoc")
|
|
226
|
+
_DOC_DIR_MARKERS = ("/docs/", "/doc/", "/examples/", "/example/", "/samples/", "/sample/", "/.github/")
|
|
227
|
+
_DOC_NAME_PREFIX = ("readme", "changelog", "contributing", "license", "authors", "history", "notice")
|
|
228
|
+
_EXAMPLE_SUFFIX = (".example", ".sample", ".dist", ".template", ".tmpl")
|
|
229
|
+
_DOC_NOTE = "in a documentation/example file — almost always a placeholder, verify before treating as real"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_doc_or_example(path: str) -> bool:
|
|
233
|
+
p = (path or "").replace("\\", "/").lower()
|
|
234
|
+
base = p.rsplit("/", 1)[-1]
|
|
235
|
+
return (p.endswith(_DOC_EXT)
|
|
236
|
+
or any(m in p for m in _DOC_DIR_MARKERS)
|
|
237
|
+
or any(base.startswith(m) for m in _DOC_NAME_PREFIX)
|
|
238
|
+
or any(s in base for s in _EXAMPLE_SUFFIX))
|
|
239
|
+
|
|
240
|
+
|
|
220
241
|
def _norm_trivy(data: dict) -> list:
|
|
221
242
|
out = []
|
|
222
243
|
for res in (data.get("Results") or []):
|
|
@@ -231,6 +252,8 @@ def _norm_trivy(data: dict) -> list:
|
|
|
231
252
|
sev, note = _aws_secret_tier(s.get("Match", ""), s.get("Code", "") or "")
|
|
232
253
|
if not sev and _generic_secret(rid):
|
|
233
254
|
sev, note = "MEDIUM", _GENERIC_NOTE
|
|
255
|
+
if _is_doc_or_example(tgt):
|
|
256
|
+
sev, note = "LOW", (note + "; " if note else "") + _DOC_NOTE
|
|
234
257
|
title = f"secret: {s.get('Title') or rid}" + (f" — {note}" if note else "")
|
|
235
258
|
out.append({"tool": "trivy", "category": "secret", "severity": sev or _sev(s.get("Severity") or "HIGH"),
|
|
236
259
|
"key": rid, "file": tgt, "line": s.get("StartLine", 0),
|
|
@@ -250,6 +273,8 @@ def _norm_gitleaks(data) -> list:
|
|
|
250
273
|
sev, note = _aws_secret_tier(x.get("Secret", ""), x.get("Match", ""))
|
|
251
274
|
if not sev and _generic_secret(rule):
|
|
252
275
|
sev, note = "MEDIUM", _GENERIC_NOTE
|
|
276
|
+
if _is_doc_or_example(f):
|
|
277
|
+
sev, note = "LOW", (note + "; " if note else "") + _DOC_NOTE
|
|
253
278
|
title = f"secret: {(x.get('Description') or rule)[:80]}" + (f" — {note}" if note else "")
|
|
254
279
|
out.append({"tool": "gitleaks", "category": "secret", "severity": sev or "HIGH",
|
|
255
280
|
"key": rule, "file": f, "line": x.get("StartLine", 0),
|
|
@@ -26,6 +26,16 @@ def b(o): return base64.urlsafe_b64encode(json.dumps(o).encode()).rstrip(b'=').d
|
|
|
26
26
|
print(b({'alg':'RS256','typ':'JWT','kid':'forged'})+'.'+b({'sub':'websec-forged','email':'websec-forged@example.com','role':'admin','roles':['admin'],'exp':9999999999})+'.d2Vic2VjLWZvcmdlZC1zaWc')
|
|
27
27
|
")
|
|
28
28
|
|
|
29
|
+
# Auth cookie names the app reads (from recon → probe-context.json) + an optional COOKIE_NAME
|
|
30
|
+
# override. We forge into these too, not just Authorization: Bearer, so a cookie-ONLY app isn't
|
|
31
|
+
# a false negative. (portable; macOS bash 3.2 lacks `mapfile`.)
|
|
32
|
+
COOKIES=()
|
|
33
|
+
[ -n "${COOKIE_NAME:-}" ] && COOKIES+=("$COOKIE_NAME")
|
|
34
|
+
while IFS= read -r cn; do [ -n "$cn" ] && COOKIES+=("$cn"); done < <(python3 -c "
|
|
35
|
+
import json
|
|
36
|
+
for c in json.load(open('$ctx')).get('auth',{}).get('cookie_names',[]): print(c)
|
|
37
|
+
" 2>/dev/null)
|
|
38
|
+
|
|
29
39
|
# Routes to test: GET reads + GET idor/ssrf candidates (always); writes when PROBE_WRITES=1.
|
|
30
40
|
# Skip any path with an unfilled {param}. (portable; macOS bash 3.2 lacks `mapfile`.)
|
|
31
41
|
ROUTES=()
|
|
@@ -64,9 +74,11 @@ for ep in "${ROUTES[@]}"; do
|
|
|
64
74
|
if [ "$na" != "401" ] && [ "$na" != "403" ]; then skip=$((skip+1)); continue; fi # not gated unauthenticated → N/A here
|
|
65
75
|
fg=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" -H "Authorization: Bearer $FORGED" ${data[@]+"${data[@]}"} --max-time 15)
|
|
66
76
|
via="Bearer"
|
|
67
|
-
if ! reached "$fg"
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
if ! reached "$fg"; then # Bearer didn't reach the handler → try forging into each known auth cookie
|
|
78
|
+
for cn in ${COOKIES[@]+"${COOKIES[@]}"}; do
|
|
79
|
+
cfg=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" -H "Cookie: $cn=$FORGED" ${data[@]+"${data[@]}"} --max-time 15)
|
|
80
|
+
if reached "$cfg"; then fg="$cfg"; via="cookie:$cn"; break; fi
|
|
81
|
+
done
|
|
70
82
|
fi
|
|
71
83
|
if reached "$fg"; then
|
|
72
84
|
printf ' BYPASS %s→%s %s %s (forged token accepted via %s)\n' "$na" "$fg" "$method" "$path" "$via"; bypass=$((bypass+1))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Local-first security recon that briefs your AI coding agent: facts + tailored probe scripts, code-in / artifacts-out. No LLM, no server, no running app.
|
|
5
5
|
Author: Ricardo Accioly
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
17
17
|
sys.path.insert(0, str(ROOT / "src"))
|
|
18
18
|
|
|
19
19
|
from websec_validator import dynamic, findings, probes, scanners # noqa: E402
|
|
20
|
+
from websec_validator.extractors.auth import AuthExtractor # noqa: E402
|
|
21
|
+
from websec_validator.extractors.base import RepoContext # noqa: E402
|
|
20
22
|
|
|
21
23
|
FACTS = {"routes": {"endpoints": [
|
|
22
24
|
{"method": "GET", "path": "/api/bypass"}, # gated; accepts forged token -> BYPASS
|
|
@@ -169,5 +171,65 @@ class SecretPrecisionTests(unittest.TestCase):
|
|
|
169
171
|
self.assertEqual(hit["confidence"], "MEDIUM")
|
|
170
172
|
|
|
171
173
|
|
|
174
|
+
class DocExampleSecretTests(unittest.TestCase):
|
|
175
|
+
"""0.2.8: secrets in documentation/example files (curl examples in a README, .env.example
|
|
176
|
+
placeholders) tier to LOW + a verify note. Real code files are untouched."""
|
|
177
|
+
|
|
178
|
+
def test_is_doc_or_example(self):
|
|
179
|
+
self.assertTrue(scanners._is_doc_or_example("README.md"))
|
|
180
|
+
self.assertTrue(scanners._is_doc_or_example("docs/API-REFERENCE.md"))
|
|
181
|
+
self.assertTrue(scanners._is_doc_or_example(".env.example"))
|
|
182
|
+
self.assertTrue(scanners._is_doc_or_example("config/settings.sample.json"))
|
|
183
|
+
self.assertFalse(scanners._is_doc_or_example("src/app/route.ts"))
|
|
184
|
+
|
|
185
|
+
def test_gitleaks_doc_secret_to_low_code_stays_high(self):
|
|
186
|
+
rows = [
|
|
187
|
+
{"File": "README.md", "RuleID": "curl-auth-header", "Secret": "x" * 30, "Match": "Authorization: Bearer x", "StartLine": 1},
|
|
188
|
+
{"File": "src/server.ts", "RuleID": "private-key", "Secret": "-----BEGIN", "Match": "-----BEGIN", "StartLine": 1},
|
|
189
|
+
]
|
|
190
|
+
by = {r["file"]: r for r in scanners._norm_gitleaks(rows)}
|
|
191
|
+
self.assertEqual(by["README.md"]["severity"], "LOW")
|
|
192
|
+
self.assertIn("documentation/example", by["README.md"]["title"])
|
|
193
|
+
self.assertEqual(by["src/server.ts"]["severity"], "HIGH") # real code file untouched
|
|
194
|
+
|
|
195
|
+
def test_trivy_doc_secret_to_low(self):
|
|
196
|
+
data = {"Results": [{"Target": "docs/SECURITY.md", "Secrets": [
|
|
197
|
+
{"RuleID": "curl-auth-header", "Title": "Auth header", "Match": "Bearer x", "StartLine": 1}]}]}
|
|
198
|
+
secs = [f for f in scanners._norm_trivy(data) if f["category"] == "secret"]
|
|
199
|
+
self.assertEqual(secs[0]["severity"], "LOW")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CookieCoverageTests(unittest.TestCase):
|
|
203
|
+
"""0.2.7: extract auth cookie names so the forged-token engine covers cookie-ONLY apps."""
|
|
204
|
+
|
|
205
|
+
def test_extracts_cookie_names(self):
|
|
206
|
+
with tempfile.TemporaryDirectory() as d:
|
|
207
|
+
d = Path(d)
|
|
208
|
+
(d / "auth.ts").write_text(
|
|
209
|
+
"const s = request.cookies.get('agent_wallet_session');\n"
|
|
210
|
+
"const p = req.cookies['ping_id_token'];\n"
|
|
211
|
+
"const x = getCookie('dynamic_authentication_token');\n")
|
|
212
|
+
out = AuthExtractor().extract(RepoContext(d), {"stack": {"frameworks": []}, "routes": {}})
|
|
213
|
+
names = set(out["cookie_names"])
|
|
214
|
+
self.assertIn("agent_wallet_session", names)
|
|
215
|
+
self.assertIn("ping_id_token", names)
|
|
216
|
+
self.assertIn("dynamic_authentication_token", names)
|
|
217
|
+
self.assertNotIn("get", names) # reserved method name filtered
|
|
218
|
+
|
|
219
|
+
def test_forged_bypass_detected_via_cookie(self):
|
|
220
|
+
facts = {"routes": {"endpoints": [{"method": "GET", "path": "/api/cookieonly"}]}}
|
|
221
|
+
|
|
222
|
+
def fake(method, url, token=None, timeout=20, data=None, cookie=None):
|
|
223
|
+
if token:
|
|
224
|
+
return 401, "x" # Bearer rejected
|
|
225
|
+
if cookie and "sess=" in cookie:
|
|
226
|
+
return 200, "x" # forged cookie accepted (cookie-only app)
|
|
227
|
+
return 401, "x" # no-auth baseline (gated)
|
|
228
|
+
with mock.patch.object(dynamic, "_request", fake):
|
|
229
|
+
r = dynamic.forged_token_bypass("http://t", facts, cookie_names=["sess"])
|
|
230
|
+
self.assertEqual([b["path"] for b in r["bypassed"]], ["/api/cookieonly"])
|
|
231
|
+
self.assertTrue(r["bypassed"][0]["via"].startswith("cookie:"))
|
|
232
|
+
|
|
233
|
+
|
|
172
234
|
if __name__ == "__main__":
|
|
173
235
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/client_exposure.py
RENAMED
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/graphql.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/extractors/surface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/_lib.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator/templates/probes/s3-assess.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{websec_validator-0.2.6 → websec_validator-0.2.8}/src/websec_validator.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|