websec-validator 0.6.0__tar.gz → 0.6.2__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.6.0/src/websec_validator.egg-info → websec_validator-0.6.2}/PKG-INFO +2 -2
- {websec_validator-0.6.0 → websec_validator-0.6.2}/README.md +1 -1
- {websec_validator-0.6.0 → websec_validator-0.6.2}/pyproject.toml +1 -1
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/briefing.py +11 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/base.py +1 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/surface.py +7 -4
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/transport_security.py +28 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/findings.py +4 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2/src/websec_validator.egg-info}/PKG-INFO +2 -2
- {websec_validator-0.6.0 → websec_validator-0.6.2}/tests/test_pentest_regressions.py +26 -3
- {websec_validator-0.6.0 → websec_validator-0.6.2}/tests/test_recon.py +11 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/LICENSE +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/setup.cfg +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/__init__.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/calibration.json +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/calibration.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/cli.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/constitution.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/corpus.json +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/dynamic.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/__init__.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/auth.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/authz.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/client_exposure.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/client_integrity.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/graphql.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/iac_ci.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/integrations.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/pii_exposure.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/policy_consistency.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/routes.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/schemas.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/stack.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/tenant.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/upload_security.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/probes.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/proof.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/recon.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/report.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/rules/error-stack-disclosure.yml +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/rules/insecure-default-secret.yml +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/scanners.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/_lib.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/appsync-cswsh.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/appsync-introspection.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/appsync-subscription-bola.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/bola-cross-tenant.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/bola-write-verbs.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/client-integrity-checklist.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/compare-roles.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/dlp-bypass-offline.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/error-disclosure-probe.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/forged-token.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/hs256-brute-force.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/jwt-attacks.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/mass-assignment.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/password-reuse.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/pii-output-diff.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/race-conditions.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/rate-limit-burst.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/s3-assess.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/ssrf-probes.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/unauth-baseline.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/upload-matrix.sh +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/probes/webhook-forgery.py +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/reports/FINDINGS-SUMMARY.md.template +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/reports/access-control-matrix.md.template +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/reports/findings-triage.md.template +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/reports/pentest-handover-brief.md.template +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/templates/reports/per-tool-FINDINGS.md.template +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/SOURCES.txt +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/dependency_links.txt +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/entry_points.txt +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/top_level.txt +0 -0
- {websec_validator-0.6.0 → websec_validator-0.6.2}/tests/test_hardening.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -185,7 +185,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
185
185
|
## Tests
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
188
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
## Releasing (maintainer)
|
|
@@ -173,7 +173,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
173
173
|
## Tests
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
176
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
## Releasing (maintainer)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "websec-validator"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
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"
|
|
@@ -83,6 +83,15 @@ def render(facts: dict, scanners: dict, scan_results: list, probe_manifest: list
|
|
|
83
83
|
pii_section = ("\n".join(f"- **{f.get('severity')}** {f.get('kind')} — `{f.get('file')}`" for f in pii_findings[:20])
|
|
84
84
|
if pii_findings else "_no obvious raw-PII responses / dead masking controls_")
|
|
85
85
|
ws_line = (facts.get("client_integrity", {}) or {}).get("websocket_auth", "no websocket detected")
|
|
86
|
+
_cs = (facts.get("transport_security", {}) or {}).get("cookie_security")
|
|
87
|
+
if _cs:
|
|
88
|
+
if _cs.get("httponly") and _cs.get("secure") and _cs.get("samesite"):
|
|
89
|
+
cookie_line = "✓ HttpOnly + Secure + SameSite present (checked — verify against the live Set-Cookie)"
|
|
90
|
+
else:
|
|
91
|
+
_miss = [n for n, k in (("HttpOnly", "httponly"), ("Secure", "secure"), ("SameSite", "samesite")) if not _cs.get(k)]
|
|
92
|
+
cookie_line = f"⚠ cookie set WITHOUT {', '.join(_miss)} — an auth/session cookie should be HttpOnly + Secure + SameSite"
|
|
93
|
+
else:
|
|
94
|
+
cookie_line = "_no Set-Cookie detected_"
|
|
86
95
|
|
|
87
96
|
gql = facts.get("graphql", {})
|
|
88
97
|
if gql.get("present"):
|
|
@@ -205,6 +214,8 @@ Production source maps exposed: {client.get("production_source_maps", False)}
|
|
|
205
214
|
|
|
206
215
|
**WebSocket auth model (CSWSH determinant — is it an ambient cookie?):** {ws_line}
|
|
207
216
|
|
|
217
|
+
**Cookie hardening (report-the-pass / gap):** {cookie_line}
|
|
218
|
+
|
|
208
219
|
**File-upload security (#2b — sniff bytes, derive stored name, nosniff on serve):**
|
|
209
220
|
{up_section}
|
|
210
221
|
|
|
@@ -17,6 +17,7 @@ SKIP_DIRS = {".git", "node_modules", "dist", "build", ".next", ".nuxt", "venv",
|
|
|
17
17
|
".turbo", "out", "target", ".gradle", "vendor", "site-packages",
|
|
18
18
|
".terraform", "security", ".websec-out", "websec-out", ".cache",
|
|
19
19
|
".svelte-kit", "storybook-static", ".serverless", ".aws-sam", "cdk.out", ".sst", ".amplify",
|
|
20
|
+
".wrangler", ".vercel", # Cloudflare / Vercel dev-build caches (bundled output → phantom routes)
|
|
20
21
|
# agent tooling + editor dirs + worktree copies — not the target app
|
|
21
22
|
".wolf", ".claude", ".worktrees", ".idea", ".vscode", ".agent", ".agents"}
|
|
22
23
|
CODE_EXT = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go", ".rb",
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/surface.py
RENAMED
|
@@ -44,13 +44,16 @@ SINKS = {
|
|
|
44
44
|
r"libxmljs\.parseXml\s*\(|lxml\.etree\.(?:parse|fromstring|XML)\s*\(|xml\.etree\.ElementTree\.(?:parse|fromstring)\s*\(|new\s+DOMParser")),
|
|
45
45
|
"prototype-pollution": ("mass-assignment", None, re.compile(
|
|
46
46
|
r"(?:_\.merge|_\.mergeWith|_\.defaultsDeep|Object\.assign)\s*\([^)]*(?:req\.|request\.)|\.update\s*\([^)]*request\.(?:json|get_json|form)")),
|
|
47
|
-
# Mass-assignment via object SPREAD — `{...record, ...req.body}`
|
|
48
|
-
#
|
|
49
|
-
# class). User-input marker is inline
|
|
47
|
+
# Mass-assignment via object SPREAD — `{...record, ...req.body}` (client overwrites ANY field) or
|
|
48
|
+
# the shorthand `{...record, tier}` (a PRIVILEGED field pulled from a same-named var — the
|
|
49
|
+
# tier-downgrade / role-escalation class). User-input marker is inline; the privileged-field list
|
|
50
|
+
# gates the shorthand form so a benign `{...state, theme}` doesn't fire.
|
|
50
51
|
"mass-assignment": ("mass-assignment", None, re.compile(
|
|
51
52
|
r"\{\s*\.\.\.[\w.$]+\s*,\s*\.\.\.(?:(?:req\w*|request|ctx|c)\.(?:body|json|payload|data|params|query)"
|
|
52
53
|
r"|body|reqBody|requestBody|userInput|updates|patch)\b"
|
|
53
|
-
r"|Object\.assign\s*\(\s*[\w.$]+\s*,\s*(?:req|request|ctx|c)\.(?:body|json|payload|data)\b"
|
|
54
|
+
r"|Object\.assign\s*\(\s*[\w.$]+\s*,\s*(?:req|request|ctx|c)\.(?:body|json|payload|data)\b"
|
|
55
|
+
r"|\{\s*\.\.\.[\w.$]+[^{}]{0,80}?,\s*(?:role|roles|tier|plan|isAdmin|is_admin|admin|permissions?"
|
|
56
|
+
r"|scopes?|balance|credits|owner|ownerId|isOwner|isSuperuser|superuser)\s*[,}]")),
|
|
54
57
|
"redos": ("ssrf-probes", None, re.compile(
|
|
55
58
|
r"new\s+RegExp\s*\([^)]*(?:req\.|request\.|\+)|re\.(?:compile|match|search|fullmatch)\s*\([^,)]*(?:request\.|f['\"])")),
|
|
56
59
|
"eval-injection": ("bola-write-verbs", None, re.compile(
|
|
@@ -41,6 +41,12 @@ HTML_SURFACE = re.compile(r"\.(?:html|tsx|jsx|vue|svelte|astro)$|_document|app/l
|
|
|
41
41
|
# applies even with no frontend framework. This is the gap that missed a Cloudflare Worker's CSP.
|
|
42
42
|
HTML_CONTENT = re.compile(r"<!DOCTYPE\s+html|<html[\s>]|text/html|res\.send\(\s*[`'\"]\s*<|c\.html\(", re.I)
|
|
43
43
|
FRONTEND_FW = {"react", "next", "nextjs", "vue", "nuxt", "svelte", "sveltekit", "angular", "astro", "remix", "solid"}
|
|
44
|
+
# Cookie hardening — "report the PASS" (HttpOnly+Secure+SameSite ✓ builds trust + is a regression
|
|
45
|
+
# assertion) and flag the gap. Flags are matched per cookie-setting file (lenient — a positive lead).
|
|
46
|
+
SET_COOKIE = re.compile(r"set-?cookie|res\.cookie\(|cookies\.set\(|\.setCookie\(|c\.cookie\(", re.I)
|
|
47
|
+
CK_HTTPONLY = re.compile(r"httponly", re.I)
|
|
48
|
+
CK_SECURE = re.compile(r";\s*secure\b|\bsecure\s*[:=]\s*true|\bsecure\s*:\s*!", re.I)
|
|
49
|
+
CK_SAMESITE = re.compile(r"samesite", re.I)
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
class TransportSecurityExtractor(Extractor):
|
|
@@ -55,6 +61,7 @@ class TransportSecurityExtractor(Extractor):
|
|
|
55
61
|
hsts_present = hsts_sub = hsts_preload = False
|
|
56
62
|
html_surface = bool(frameworks & FRONTEND_FW)
|
|
57
63
|
inline_handlers = []
|
|
64
|
+
sets_cookie = ck_httponly = ck_secure = ck_samesite = False
|
|
58
65
|
hsts_files, hsts_api_only, hsts_html = [], True, False
|
|
59
66
|
|
|
60
67
|
# config manifests carry headers too (next.config, vercel.json, netlify.toml, _headers)
|
|
@@ -87,6 +94,11 @@ class TransportSecurityExtractor(Extractor):
|
|
|
87
94
|
hsts_html = True
|
|
88
95
|
elif not API_SCOPED.search(rel):
|
|
89
96
|
hsts_api_only = False # a non-API, non-HTML place (e.g. global edge middleware)
|
|
97
|
+
if SET_COOKIE.search(blob):
|
|
98
|
+
sets_cookie = True
|
|
99
|
+
ck_httponly = ck_httponly or bool(CK_HTTPONLY.search(blob))
|
|
100
|
+
ck_secure = ck_secure or bool(CK_SECURE.search(blob))
|
|
101
|
+
ck_samesite = ck_samesite or bool(CK_SAMESITE.search(blob))
|
|
90
102
|
|
|
91
103
|
if CSP_ANY.search(manifests):
|
|
92
104
|
csp_present = True
|
|
@@ -155,12 +167,28 @@ class TransportSecurityExtractor(Extractor):
|
|
|
155
167
|
"detail": f"HSTS is present but missing {', '.join(gaps)} — add where the domain "
|
|
156
168
|
"model allows (don't preload a domain whose subdomains aren't all HTTPS)."})
|
|
157
169
|
|
|
170
|
+
# 0.6.2: report the cookie-hardening PASS (✓ builds trust + is a regression assertion), or flag the gap.
|
|
171
|
+
passes, cookie_security = [], None
|
|
172
|
+
if sets_cookie:
|
|
173
|
+
cookie_security = {"httponly": ck_httponly, "secure": ck_secure, "samesite": ck_samesite}
|
|
174
|
+
if ck_httponly and ck_secure and ck_samesite:
|
|
175
|
+
passes.append("cookies set HttpOnly + Secure + SameSite (checked ✓)")
|
|
176
|
+
else:
|
|
177
|
+
miss = [n for n, ok in (("HttpOnly", ck_httponly), ("Secure", ck_secure),
|
|
178
|
+
("SameSite", ck_samesite)) if not ok]
|
|
179
|
+
findings.append({"severity": "LOW", "kind": "cookie-flags", "attack_class": "insecure-cookie",
|
|
180
|
+
"detail": f"A cookie is set without {', '.join(miss)} — an auth/session cookie should be "
|
|
181
|
+
"HttpOnly (no JS read), Secure (HTTPS-only), and SameSite=Lax/Strict (CSRF). "
|
|
182
|
+
"Verify against the live Set-Cookie."})
|
|
183
|
+
|
|
158
184
|
return {
|
|
159
185
|
"web_surface": web_surface, "html_surface": html_surface,
|
|
160
186
|
"csp_present": csp_present, "strict_csp": strict_csp, "csp_has_unsafe": csp_unsafe,
|
|
161
187
|
"hsts_present": hsts_present, "hsts_includes_subdomains": hsts_sub, "hsts_preload": hsts_preload,
|
|
162
188
|
"hsts_files": sorted(set(hsts_files))[:20],
|
|
163
189
|
"inline_event_handlers": sorted(set(inline_handlers)),
|
|
190
|
+
"cookie_security": cookie_security,
|
|
191
|
+
"passes": passes,
|
|
164
192
|
"findings": findings,
|
|
165
193
|
"note": ("CSP/HSTS baseline audit — these are the enabling controls for the client trust boundary. "
|
|
166
194
|
"LOW/architectural: verify against the LIVE response headers (a static scan can't see the edge/CDN "
|
|
@@ -88,6 +88,8 @@ STANDARDS = {
|
|
|
88
88
|
"ASVS V11.1.4", ["API4:2023 Unrestricted Resource Consumption",
|
|
89
89
|
"API6:2023 Unrestricted Access to Sensitive Business Flows"]),
|
|
90
90
|
"redundant-secret-fetch": (["CWE-200 Information Exposure"], "ASVS V2.10", ["API8:2023 Misconfiguration"]),
|
|
91
|
+
"insecure-cookie": (["CWE-1004 Sensitive Cookie Without HttpOnly", "CWE-614 Sensitive Cookie Without Secure"],
|
|
92
|
+
"ASVS V3.4.1", ["API8:2023 Misconfiguration"]),
|
|
91
93
|
}
|
|
92
94
|
REMEDIATION = {
|
|
93
95
|
"missing-auth": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a "
|
|
@@ -152,6 +154,8 @@ REMEDIATION = {
|
|
|
152
154
|
"redundant-secret-fetch": "Fetch each secret-manager key ONCE per request and reuse it; use the project's existing "
|
|
153
155
|
"secret-provider abstraction instead of a bespoke loader (smaller exposure window + "
|
|
154
156
|
"consistency).",
|
|
157
|
+
"insecure-cookie": "Set auth/session cookies `HttpOnly` (blocks JS/XSS theft) + `Secure` (HTTPS-only) + "
|
|
158
|
+
"`SameSite=Lax`/`Strict` (CSRF). Verify against the live Set-Cookie header.",
|
|
155
159
|
}
|
|
156
160
|
_DEFAULT_REM = "Review and remediate per the cited standard."
|
|
157
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -185,7 +185,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
185
185
|
## Tests
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
188
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
## Releasing (maintainer)
|
|
@@ -591,9 +591,13 @@ class StaticCheckP3Tests(unittest.TestCase):
|
|
|
591
591
|
out = SurfaceExtractor().extract(repo({"src/h.ts":
|
|
592
592
|
"const saved = {...record, ...req.body};\nawait db.put(saved);\n"}), {})
|
|
593
593
|
self.assertIn("mass-assignment", out["sinks"])
|
|
594
|
-
#
|
|
595
|
-
|
|
596
|
-
self.
|
|
594
|
+
# shorthand privileged-field form (the tier-downgrade case)
|
|
595
|
+
sh = SurfaceExtractor().extract(repo({"src/h.ts": "const o = {...record, tier};\n"}), {})
|
|
596
|
+
self.assertIn("mass-assignment", sh["sinks"])
|
|
597
|
+
# benign spreads / non-privileged shorthand / literal assignment don't fire
|
|
598
|
+
for benign in ("const s = {...defaults, ...config};\n", "const s = {...state, theme};\n",
|
|
599
|
+
"const s = {...record, role: 'user'};\n"):
|
|
600
|
+
self.assertNotIn("mass-assignment", SurfaceExtractor().extract(repo({"src/h.ts": benign}), {})["sinks"])
|
|
597
601
|
|
|
598
602
|
|
|
599
603
|
class ProviderSecretTierTests(unittest.TestCase):
|
|
@@ -614,5 +618,24 @@ class ProviderSecretTierTests(unittest.TestCase):
|
|
|
614
618
|
self.assertIn("ROTATE", row["title"])
|
|
615
619
|
|
|
616
620
|
|
|
621
|
+
class CookieReportPassTests(unittest.TestCase):
|
|
622
|
+
"""0.6.2 report-the-passes: cookie HttpOnly+Secure+SameSite → a PASS; a missing flag → a finding."""
|
|
623
|
+
|
|
624
|
+
def test_all_flags_is_a_pass_no_finding(self):
|
|
625
|
+
out = TransportSecurityExtractor().extract(repo({"a.ts":
|
|
626
|
+
"res.headers.set('Set-Cookie', 'sid=x; HttpOnly; Secure; SameSite=Lax');\n"}), {})
|
|
627
|
+
self.assertTrue(any("HttpOnly" in p for p in out["passes"]))
|
|
628
|
+
self.assertFalse(any(f["kind"] == "cookie-flags" for f in out["findings"]))
|
|
629
|
+
|
|
630
|
+
def test_missing_flag_is_a_finding(self):
|
|
631
|
+
out = TransportSecurityExtractor().extract(repo({"a.ts": "res.cookie('sid', v, {httpOnly:true});\n"}), {})
|
|
632
|
+
self.assertTrue(any(f["attack_class"] == "insecure-cookie" for f in out["findings"]))
|
|
633
|
+
|
|
634
|
+
def test_no_cookie_no_noise(self):
|
|
635
|
+
out = TransportSecurityExtractor().extract(repo({"a.ts": "const x = 1;\n"}), {})
|
|
636
|
+
self.assertEqual(out["passes"], [])
|
|
637
|
+
self.assertIsNone(out["cookie_security"])
|
|
638
|
+
|
|
639
|
+
|
|
617
640
|
if __name__ == "__main__":
|
|
618
641
|
unittest.main()
|
|
@@ -401,6 +401,17 @@ class RouteUnitTests(unittest.TestCase):
|
|
|
401
401
|
self.assertIsNotNone(out["coverage_warning"])
|
|
402
402
|
self.assertIn("INCOMPLETE", out["coverage_warning"])
|
|
403
403
|
|
|
404
|
+
def test_wrangler_build_dir_routes_excluded(self):
|
|
405
|
+
# 0.6.1: .wrangler / .vercel dev-build caches hold BUNDLED output → phantom duplicate routes.
|
|
406
|
+
d = Path(tempfile.mkdtemp())
|
|
407
|
+
(d / "src").mkdir()
|
|
408
|
+
(d / ".wrangler" / "tmp").mkdir(parents=True)
|
|
409
|
+
(d / "src" / "index.ts").write_text("router.get('/api/real', h);\n")
|
|
410
|
+
(d / ".wrangler" / "tmp" / "bundle.js").write_text("router.get('/api/phantom', h);\n")
|
|
411
|
+
paths = {e["path"] for e in routes.RoutesExtractor().extract(RepoContext(d), {})["endpoints"]}
|
|
412
|
+
self.assertIn("/api/real", paths)
|
|
413
|
+
self.assertNotIn("/api/phantom", paths)
|
|
414
|
+
|
|
404
415
|
|
|
405
416
|
class DedupTests(unittest.TestCase):
|
|
406
417
|
def test_within_tool_dedup_and_counts(self):
|
|
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.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/client_exposure.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/graphql.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/integrations.py
RENAMED
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/pii_exposure.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator/extractors/upload_security.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
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/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
|
|
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.6.0 → websec_validator-0.6.2}/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
|
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{websec_validator-0.6.0 → websec_validator-0.6.2}/src/websec_validator.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|