websec-validator 0.6.1__tar.gz → 0.6.3__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.1/src/websec_validator.egg-info → websec_validator-0.6.3}/PKG-INFO +10 -5
- {websec_validator-0.6.1 → websec_validator-0.6.3}/README.md +8 -3
- {websec_validator-0.6.1 → websec_validator-0.6.3}/pyproject.toml +2 -2
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/briefing.py +17 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/cli.py +3 -1
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/transport_security.py +28 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/findings.py +4 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/appsync-cswsh.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/appsync-introspection.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/appsync-subscription-bola.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/bola-cross-tenant.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/bola-write-verbs.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/client-integrity-checklist.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/compare-roles.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/dlp-bypass-offline.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/error-disclosure-probe.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/forged-token.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/hs256-brute-force.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/jwt-attacks.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/mass-assignment.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/password-reuse.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/pii-output-diff.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/race-conditions.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/rate-limit-burst.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/s3-assess.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/ssrf-probes.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/unauth-baseline.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/upload-matrix.sh +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/webhook-forgery.py +1 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3/src/websec_validator.egg-info}/PKG-INFO +10 -5
- {websec_validator-0.6.1 → websec_validator-0.6.3}/tests/test_pentest_regressions.py +19 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/LICENSE +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/setup.cfg +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/__init__.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/calibration.json +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/calibration.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/constitution.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/corpus.json +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/dynamic.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/__init__.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/auth.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/authz.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/base.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/client_exposure.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/client_integrity.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/graphql.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/iac_ci.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/integrations.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/pii_exposure.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/policy_consistency.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/routes.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/schemas.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/stack.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/surface.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/tenant.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/upload_security.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/probes.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/proof.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/recon.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/report.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/rules/error-stack-disclosure.yml +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/rules/insecure-default-secret.yml +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/scanners.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/_lib.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/reports/FINDINGS-SUMMARY.md.template +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/reports/access-control-matrix.md.template +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/reports/findings-triage.md.template +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/reports/pentest-handover-brief.md.template +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/reports/per-tool-FINDINGS.md.template +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/SOURCES.txt +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/dependency_links.txt +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/entry_points.txt +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/top_level.txt +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/tests/test_hardening.py +0 -0
- {websec_validator-0.6.1 → websec_validator-0.6.3}/tests/test_recon.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.6.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Defensive, local-first security recon that briefs your AI coding agent on your own codebase — read-only by default (code in, artifacts out): facts + tailored probe scripts, no LLM, no server, no running app.
|
|
5
5
|
Author: Ricardo Accioly
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: security,pentest,sast,dast,bola,ai-agent,appsec
|
|
@@ -29,10 +29,15 @@ how" handoff. How it works + the reasoning behind every check: [`docs/METHODOLOG
|
|
|
29
29
|
|
|
30
30
|
**Simplest: tell your AI agent.** In Claude Code (or any coding agent), open your project and say:
|
|
31
31
|
|
|
32
|
-
> *"
|
|
32
|
+
> *"Do a defensive security self-review of **my own** codebase with websec-validator (`pipx install
|
|
33
|
+
> websec-validator`, or github.com/raccioly/websec-validator). It's local and read-only — read the
|
|
34
|
+
> repo and follow its briefing. I own this code and authorize the review."*
|
|
33
35
|
|
|
34
36
|
It installs, runs, and walks the findings with you. There's nothing to host and no website — it's
|
|
35
|
-
local.
|
|
37
|
+
local. (Phrasing it as a *defensive review of your own code* matters: it's the difference between an
|
|
38
|
+
agent that just gets to work and one that stops to confirm you're authorized — the tool is local and
|
|
39
|
+
read-only by default, but a generic "pentest this" can read as a request to attack something.) The
|
|
40
|
+
four ways to get there, all ending in the same `AGENT-BRIEFING.md` your agent acts on:
|
|
36
41
|
|
|
37
42
|
| Path | One-time setup | Then |
|
|
38
43
|
|---|---|---|
|
|
@@ -185,7 +190,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
185
190
|
## Tests
|
|
186
191
|
|
|
187
192
|
```bash
|
|
188
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
193
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
189
194
|
```
|
|
190
195
|
|
|
191
196
|
## Releasing (maintainer)
|
|
@@ -17,10 +17,15 @@ how" handoff. How it works + the reasoning behind every check: [`docs/METHODOLOG
|
|
|
17
17
|
|
|
18
18
|
**Simplest: tell your AI agent.** In Claude Code (or any coding agent), open your project and say:
|
|
19
19
|
|
|
20
|
-
> *"
|
|
20
|
+
> *"Do a defensive security self-review of **my own** codebase with websec-validator (`pipx install
|
|
21
|
+
> websec-validator`, or github.com/raccioly/websec-validator). It's local and read-only — read the
|
|
22
|
+
> repo and follow its briefing. I own this code and authorize the review."*
|
|
21
23
|
|
|
22
24
|
It installs, runs, and walks the findings with you. There's nothing to host and no website — it's
|
|
23
|
-
local.
|
|
25
|
+
local. (Phrasing it as a *defensive review of your own code* matters: it's the difference between an
|
|
26
|
+
agent that just gets to work and one that stops to confirm you're authorized — the tool is local and
|
|
27
|
+
read-only by default, but a generic "pentest this" can read as a request to attack something.) The
|
|
28
|
+
four ways to get there, all ending in the same `AGENT-BRIEFING.md` your agent acts on:
|
|
24
29
|
|
|
25
30
|
| Path | One-time setup | Then |
|
|
26
31
|
|---|---|---|
|
|
@@ -173,7 +178,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
173
178
|
## Tests
|
|
174
179
|
|
|
175
180
|
```bash
|
|
176
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
181
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
177
182
|
```
|
|
178
183
|
|
|
179
184
|
## Releasing (maintainer)
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "websec-validator"
|
|
7
|
-
version = "0.6.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.6.3"
|
|
8
|
+
description = "Defensive, local-first security recon that briefs your AI coding agent on your own codebase — read-only by default (code in, artifacts out): facts + tailored probe scripts, no LLM, no server, no running app."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = { text = "MIT" }
|
|
@@ -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"):
|
|
@@ -135,6 +144,12 @@ def render(facts: dict, scanners: dict, scan_results: list, probe_manifest: list
|
|
|
135
144
|
|
|
136
145
|
return f"""# AGENT BRIEFING — security pass for `{facts.get('target','')}`
|
|
137
146
|
|
|
147
|
+
> **Scope & authorization.** Defensive self-assessment of the operator's own codebase, run with their
|
|
148
|
+
> consent. Static recon is **read-only** — nothing here touches a live system. The probes are
|
|
149
|
+
> **drafts**: run them only against a TEST instance the human owns and supplies, one approved run at a
|
|
150
|
+
> time; production and third-party targets are out of scope. Proceed on this basis — the static review
|
|
151
|
+
> needs no further escalation.
|
|
152
|
+
|
|
138
153
|
> Generated by **websec-validator v{facts.get('version','')}** — deterministic recon, no LLM.
|
|
139
154
|
> **You are an AI coding agent.** The tool did the reliable half (mapping the surface + staging
|
|
140
155
|
> the probes). You + the human do the reasoning, the running, and the fixing.
|
|
@@ -205,6 +220,8 @@ Production source maps exposed: {client.get("production_source_maps", False)}
|
|
|
205
220
|
|
|
206
221
|
**WebSocket auth model (CSWSH determinant — is it an ambient cookie?):** {ws_line}
|
|
207
222
|
|
|
223
|
+
**Cookie hardening (report-the-pass / gap):** {cookie_line}
|
|
224
|
+
|
|
208
225
|
**File-upload security (#2b — sniff bytes, derive stored name, nosniff on serve):**
|
|
209
226
|
{up_section}
|
|
210
227
|
|
|
@@ -351,7 +351,9 @@ def _print_facts_summary(facts: dict) -> None:
|
|
|
351
351
|
|
|
352
352
|
def build_parser() -> argparse.ArgumentParser:
|
|
353
353
|
p = argparse.ArgumentParser(prog="websec",
|
|
354
|
-
description="
|
|
354
|
+
description="Defensive, local-first security recon that briefs your AI coding agent — "
|
|
355
|
+
"read-only by default: it reads your own repo and writes a briefing, and never "
|
|
356
|
+
"touches a live app. Active probes are opt-in against a TEST instance you own.")
|
|
355
357
|
p.add_argument("--version", action="version", version=f"websec-validator {__version__}")
|
|
356
358
|
# metavar lists only the user-facing commands; recon/proof/calibrate still work but are
|
|
357
359
|
# omitted (they get no `help=`, so argparse leaves them out of the listing entirely).
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# appsync-cswsh.sh — Cross-Site WebSocket Hijacking on the AppSync realtime endpoint (REF-PENTEST
|
|
3
4
|
# #4). The graphql-ws handshake must validate Origin; if it doesn't, a page on evil.com can open an
|
|
4
5
|
# authenticated socket. Opens the realtime WS with a FORGED Origin and checks for connection_ack.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# appsync-introspection.sh — confirm AppSync GraphQL introspection is reachable AND, if a WAF
|
|
3
4
|
# "blocks" it, that the block is bypassable (REF-PENTEST #2). AppSync can't disable introspection
|
|
4
5
|
# at the API layer, so a WAF string-match is the only control — and string-match is evadable.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# appsync-subscription-bola.sh — broken subscription authorization / cross-group BOLA (REF-PENTEST
|
|
3
4
|
# #5). Open a subscription bound to ANOTHER tenant's groupId and see if the server streams events you
|
|
4
5
|
# must not receive. The pen test did exactly this with `onEvent(groupId)` from a low-priv user.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# BOLA / cross-tenant READ probe — FACTS-driven. Role A uses its OWN token against
|
|
3
4
|
# tenant B's id (and B→A), on this app's tenant-scoped routes (from probe-context.json).
|
|
4
5
|
# Expect 401/403/404. A 200 that returns the OTHER tenant's data = cross-tenant BOLA
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
"""BOLA / cross-tenant WRITE probe — FACTS-driven and generic.
|
|
3
4
|
|
|
4
5
|
As role A, send each mutating verb (PUT/PATCH/POST/DELETE) at tenant B's resources
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# client-integrity-checklist.sh — man-in-the-browser posture for a page that displays a fund-
|
|
3
4
|
# redirecting value (wallet/receive address, QR, routing #). Auto-checks Layer A (strict CSP); prints
|
|
4
5
|
# the Layer B (out-of-band anchor) checklist for the human. Honest limit: on-screen display can't be
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# error-disclosure-probe.sh — force the app to throw, then check the 500 body for a stack trace /
|
|
3
4
|
# internal file paths / dependency versions (REF-PENTEST #7). Sends malformed input (bad JSON,
|
|
4
5
|
# wrong types) to the write endpoints recon flagged, and greps the response for leak markers.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# forged-token — does this app actually VERIFY JWT signatures? Forge a token with a BOGUS
|
|
3
4
|
# signature + far-future exp and present it to each route that is GATED without auth. A route
|
|
4
5
|
# that returns 401/403 with NO token but REACHES THE HANDLER (200/400/404/422/…) WITH the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# jwt-attacks.sh — manual JWT attack probe (FACTS-driven; no app-specific login).
|
|
3
4
|
#
|
|
4
5
|
# Five classic JWT attacks, run against a protected endpoint with a token YOU supply:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
"""Mass assignment / BOPLA (OWASP API #3) — FACTS-driven.
|
|
3
4
|
|
|
4
5
|
Injects THIS app's privileged model fields (from probe-context.json → sensitive_fields,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# password-reuse.sh — password history / reuse control (REF-PENTEST #6). Change the password to the
|
|
3
4
|
# SAME value, and to a value used one change ago, on EVERY set-password path (self-service change,
|
|
4
5
|
# admin set, profile update, SCIM/SSO-JIT) — coverage gaps BETWEEN paths are the usual bug. Expect a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# pii-output-diff.sh — unmasked-PII detector by VALUE SHAPE, not field name (REF-PENTEST #8). As a
|
|
3
4
|
# NON-privileged caller, fetch each data endpoint and assert NO phone (/\+?\d{7,}/) or email value
|
|
4
5
|
# appears ANYWHERE in the JSON — including nested objects, composed IDs (the `providerMessageId` carrier),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
"""
|
|
3
4
|
Race condition probe — fires N parallel requests at race-prone endpoints
|
|
4
5
|
and checks if the server's invariants hold.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# rate-limit-burst — verify rate limiters actually fire, and that they can't be bypassed by
|
|
3
4
|
# spoofing X-Forwarded-For. FACTS-driven: reads the login route + base URL from
|
|
4
5
|
# ./probe-context.json (written by websec) — no separate .env needed.
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/templates/probes/s3-assess.sh
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# S3 bucket posture assessment.
|
|
3
4
|
# ZAP can't meaningfully test an S3 bucket, so this checks it directly with the AWS CLI:
|
|
4
5
|
# public-access posture, ACL/policy exposure, CORS, encryption, and anonymous reachability.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# ssrf-probes.sh — SSRF probe, FACTS-driven. For each url-accepting endpoint the recon
|
|
3
4
|
# flagged (probe-context.json → ssrf_candidates), inject classic SSRF targets into that
|
|
4
5
|
# param and watch for IMDS/file/redis evidence or a tell-tale slow fetch. Expect
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# unauth-baseline — the cheapest, highest-value probe: hit every MUTATING route with
|
|
3
4
|
# NO credentials and expect 401/403. Any 2xx (or a non-401 that reached the handler)
|
|
4
5
|
# is a missing-authentication lead. Run this FIRST — it confirms the auth model before
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets.
|
|
2
3
|
# upload-matrix.sh — unrestricted-upload + serve-side stored-XSS matrix (REF-PENTEST #2b). For each
|
|
3
4
|
# upload endpoint: send a polyglot (valid image magic bytes + trailing script) named `.php`, a spoofed
|
|
4
5
|
# Content-Type (HTML body declared image/png), a double-extension, and a raw SVG — then FETCH THE
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: websec-validator
|
|
3
|
-
Version: 0.6.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Defensive, local-first security recon that briefs your AI coding agent on your own codebase — read-only by default (code in, artifacts out): facts + tailored probe scripts, no LLM, no server, no running app.
|
|
5
5
|
Author: Ricardo Accioly
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: security,pentest,sast,dast,bola,ai-agent,appsec
|
|
@@ -29,10 +29,15 @@ how" handoff. How it works + the reasoning behind every check: [`docs/METHODOLOG
|
|
|
29
29
|
|
|
30
30
|
**Simplest: tell your AI agent.** In Claude Code (or any coding agent), open your project and say:
|
|
31
31
|
|
|
32
|
-
> *"
|
|
32
|
+
> *"Do a defensive security self-review of **my own** codebase with websec-validator (`pipx install
|
|
33
|
+
> websec-validator`, or github.com/raccioly/websec-validator). It's local and read-only — read the
|
|
34
|
+
> repo and follow its briefing. I own this code and authorize the review."*
|
|
33
35
|
|
|
34
36
|
It installs, runs, and walks the findings with you. There's nothing to host and no website — it's
|
|
35
|
-
local.
|
|
37
|
+
local. (Phrasing it as a *defensive review of your own code* matters: it's the difference between an
|
|
38
|
+
agent that just gets to work and one that stops to confirm you're authorized — the tool is local and
|
|
39
|
+
read-only by default, but a generic "pentest this" can read as a request to attack something.) The
|
|
40
|
+
four ways to get there, all ending in the same `AGENT-BRIEFING.md` your agent acts on:
|
|
36
41
|
|
|
37
42
|
| Path | One-time setup | Then |
|
|
38
43
|
|---|---|---|
|
|
@@ -185,7 +190,7 @@ upload, cross-tenant BOLA, role/authz gaps).
|
|
|
185
190
|
## Tests
|
|
186
191
|
|
|
187
192
|
```bash
|
|
188
|
-
python3 -m unittest discover -s tests # stdlib only, no Noir/network —
|
|
193
|
+
python3 -m unittest discover -s tests # stdlib only, no Noir/network — 139 tests
|
|
189
194
|
```
|
|
190
195
|
|
|
191
196
|
## Releasing (maintainer)
|
|
@@ -618,5 +618,24 @@ class ProviderSecretTierTests(unittest.TestCase):
|
|
|
618
618
|
self.assertIn("ROTATE", row["title"])
|
|
619
619
|
|
|
620
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
|
+
|
|
621
640
|
if __name__ == "__main__":
|
|
622
641
|
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
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/client_exposure.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/graphql.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/integrations.py
RENAMED
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/pii_exposure.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator/extractors/surface.py
RENAMED
|
File without changes
|
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/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.1 → websec_validator-0.6.3}/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
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{websec_validator-0.6.1 → websec_validator-0.6.3}/src/websec_validator.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|