websec-validator 0.2.5__tar.gz → 0.2.6__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.
Files changed (59) hide show
  1. {websec_validator-0.2.5/src/websec_validator.egg-info → websec_validator-0.2.6}/PKG-INFO +1 -1
  2. {websec_validator-0.2.5 → websec_validator-0.2.6}/pyproject.toml +1 -1
  3. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/findings.py +3 -1
  4. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/scanners.py +25 -3
  5. {websec_validator-0.2.5 → websec_validator-0.2.6/src/websec_validator.egg-info}/PKG-INFO +1 -1
  6. {websec_validator-0.2.5 → websec_validator-0.2.6}/tests/test_hardening.py +36 -0
  7. {websec_validator-0.2.5 → websec_validator-0.2.6}/LICENSE +0 -0
  8. {websec_validator-0.2.5 → websec_validator-0.2.6}/README.md +0 -0
  9. {websec_validator-0.2.5 → websec_validator-0.2.6}/setup.cfg +0 -0
  10. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/__init__.py +0 -0
  11. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/briefing.py +0 -0
  12. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/calibration.json +0 -0
  13. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/calibration.py +0 -0
  14. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/cli.py +0 -0
  15. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/constitution.py +0 -0
  16. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/corpus.json +0 -0
  17. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/dynamic.py +0 -0
  18. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/__init__.py +0 -0
  19. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/auth.py +0 -0
  20. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/authz.py +0 -0
  21. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/base.py +0 -0
  22. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/client_exposure.py +0 -0
  23. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/graphql.py +0 -0
  24. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/iac_ci.py +0 -0
  25. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/integrations.py +0 -0
  26. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/routes.py +0 -0
  27. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/schemas.py +0 -0
  28. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/stack.py +0 -0
  29. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/surface.py +0 -0
  30. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/extractors/tenant.py +0 -0
  31. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/probes.py +0 -0
  32. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/proof.py +0 -0
  33. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/recon.py +0 -0
  34. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/report.py +0 -0
  35. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/_lib.py +0 -0
  36. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/bola-cross-tenant.sh +0 -0
  37. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/bola-write-verbs.py +0 -0
  38. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/compare-roles.sh +0 -0
  39. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/dlp-bypass-offline.py +0 -0
  40. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/forged-token.sh +0 -0
  41. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/hs256-brute-force.py +0 -0
  42. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/jwt-attacks.sh +0 -0
  43. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/mass-assignment.py +0 -0
  44. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/race-conditions.py +0 -0
  45. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/rate-limit-burst.sh +0 -0
  46. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/s3-assess.sh +0 -0
  47. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/ssrf-probes.sh +0 -0
  48. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/unauth-baseline.sh +0 -0
  49. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/probes/webhook-forgery.py +0 -0
  50. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/reports/FINDINGS-SUMMARY.md.template +0 -0
  51. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/reports/access-control-matrix.md.template +0 -0
  52. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/reports/findings-triage.md.template +0 -0
  53. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/reports/pentest-handover-brief.md.template +0 -0
  54. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator/templates/reports/per-tool-FINDINGS.md.template +0 -0
  55. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator.egg-info/SOURCES.txt +0 -0
  56. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator.egg-info/dependency_links.txt +0 -0
  57. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator.egg-info/entry_points.txt +0 -0
  58. {websec_validator-0.2.5 → websec_validator-0.2.6}/src/websec_validator.egg-info/top_level.txt +0 -0
  59. {websec_validator-0.2.5 → websec_validator-0.2.6}/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.5
3
+ Version: 0.2.6
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.5"
7
+ version = "0.2.6"
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"
@@ -184,7 +184,9 @@ def build_ledger(facts: dict, unified: dict | None, dynamic: dict | None = None,
184
184
  cat = t.get("category", "")
185
185
  cls = cat_to_class.get(cat, "sast")
186
186
  sev = t.get("severity", "MEDIUM")
187
- conf = "HIGH" if cat in ("secret",) or (cat == "sca" and sev in ("HIGH", "CRITICAL")) else "MEDIUM"
187
+ # Confidence follows severity for secrets/CVEs: a generic-api-key tiered down to MEDIUM
188
+ # (low-precision rule, bug-072) should NOT be stamped HIGH-confidence — keep P(real) honest.
189
+ conf = "HIGH" if (cat in ("secret", "sca") and sev in ("HIGH", "CRITICAL")) else "MEDIUM"
188
190
  out.append(_f(t.get("title", cat), f"static-{cat}", cls, sev, conf, t.get("file", ""),
189
191
  [{"layer": "static", "detail": f"{'+'.join(t.get('tools', []))}: {t.get('title','')}"}]))
190
192
 
@@ -200,6 +200,23 @@ def _aws_secret_tier(secret: str, match: str):
200
200
  return None, None
201
201
 
202
202
 
203
+ # gitleaks/trivy "generic" + entropy/keyword rules are high-recall, low-precision: they fire on
204
+ # public keys, wallet addresses, hashes, env-var refs and test fixtures about as often as real
205
+ # credentials. Tier those to MEDIUM + a verify note (NEVER hide them) so the HIGH secret tier
206
+ # stays trustworthy in a shareable report; specific-format rules (AKIA, private-key, GitHub/
207
+ # Stripe/Slack/JWT, etc.) keep HIGH. (bug-072 — dogfooding a wallet app surfaced ~20 HIGH
208
+ # generic-api-key FPs in committed source.)
209
+ _GENERIC_SECRET_RULES = {"generic-api-key", "generic-api-key-1", "generic", "api-key",
210
+ "secret-keyword", "high-entropy", "high-entropy-string", "entropy"}
211
+ _GENERIC_NOTE = ("generic/entropy match — verify it's a live credential "
212
+ "(often a public key, address, hash or env-ref, not a secret)")
213
+
214
+
215
+ def _generic_secret(rule: str) -> bool:
216
+ r = (rule or "").lower()
217
+ return r in _GENERIC_SECRET_RULES or "generic" in r or "entropy" in r
218
+
219
+
203
220
  def _norm_trivy(data: dict) -> list:
204
221
  out = []
205
222
  for res in (data.get("Results") or []):
@@ -210,11 +227,14 @@ def _norm_trivy(data: dict) -> list:
210
227
  "title": f"{v.get('PkgName')} {v.get('InstalledVersion')} → {v.get('FixedVersion', '(no fix)')}",
211
228
  "fingerprint": f"cve|{v.get('PkgName')}|{v.get('VulnerabilityID')}"})
212
229
  for s in (res.get("Secrets") or []):
230
+ rid = s.get("RuleID", "")
213
231
  sev, note = _aws_secret_tier(s.get("Match", ""), s.get("Code", "") or "")
214
- title = f"secret: {s.get('Title') or s.get('RuleID')}" + (f" — {note}" if note else "")
232
+ if not sev and _generic_secret(rid):
233
+ sev, note = "MEDIUM", _GENERIC_NOTE
234
+ title = f"secret: {s.get('Title') or rid}" + (f" — {note}" if note else "")
215
235
  out.append({"tool": "trivy", "category": "secret", "severity": sev or _sev(s.get("Severity") or "HIGH"),
216
- "key": s.get("RuleID", ""), "file": tgt, "line": s.get("StartLine", 0),
217
- "title": title, "fingerprint": f"secret|{tgt}|{s.get('RuleID')}"})
236
+ "key": rid, "file": tgt, "line": s.get("StartLine", 0),
237
+ "title": title, "fingerprint": f"secret|{tgt}|{rid}"})
218
238
  for m in (res.get("Misconfigurations") or []):
219
239
  out.append({"tool": "trivy", "category": "iac", "severity": _sev(m.get("Severity")),
220
240
  "key": m.get("ID", ""), "file": tgt, "line": 0, "title": (m.get("Title") or "")[:90],
@@ -228,6 +248,8 @@ def _norm_gitleaks(data) -> list:
228
248
  for x in rows:
229
249
  f, rule = x.get("File", ""), x.get("RuleID", "")
230
250
  sev, note = _aws_secret_tier(x.get("Secret", ""), x.get("Match", ""))
251
+ if not sev and _generic_secret(rule):
252
+ sev, note = "MEDIUM", _GENERIC_NOTE
231
253
  title = f"secret: {(x.get('Description') or rule)[:80]}" + (f" — {note}" if note else "")
232
254
  out.append({"tool": "gitleaks", "category": "secret", "severity": sev or "HIGH",
233
255
  "key": rule, "file": f, "line": x.get("StartLine", 0),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: websec-validator
3
- Version: 0.2.5
3
+ Version: 0.2.6
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
@@ -133,5 +133,41 @@ class ProbeRegistrationTests(unittest.TestCase):
133
133
  self.assertEqual(ctx["endpoints"]["reads"], ["GET /api/a"])
134
134
 
135
135
 
136
+ class SecretPrecisionTests(unittest.TestCase):
137
+ """bug-072: low-precision generic/entropy secret rules -> MEDIUM (+verify note); specific
138
+ rules (AKIA, private-key, …) keep HIGH. Nothing is hidden."""
139
+
140
+ def test_generic_rule_detection(self):
141
+ self.assertTrue(scanners._generic_secret("generic-api-key"))
142
+ self.assertTrue(scanners._generic_secret("high-entropy-string"))
143
+ self.assertFalse(scanners._generic_secret("aws-access-token"))
144
+ self.assertFalse(scanners._generic_secret("private-key"))
145
+
146
+ def test_gitleaks_generic_is_medium_specific_is_high(self):
147
+ rows = [
148
+ {"File": "src/lib/chains.ts", "RuleID": "generic-api-key", "Secret": "x" * 40, "Match": "x" * 40, "StartLine": 1},
149
+ {"File": "src/k.pem", "RuleID": "private-key", "Secret": "-----BEGIN", "Match": "-----BEGIN", "StartLine": 1},
150
+ {"File": "src/a.ts", "RuleID": "aws-access-token", "Secret": "AKIA" + "A" * 16, "Match": "AKIA" + "A" * 16, "StartLine": 1},
151
+ ]
152
+ by = {r["key"]: r for r in scanners._norm_gitleaks(rows)}
153
+ self.assertEqual(by["generic-api-key"]["severity"], "MEDIUM")
154
+ self.assertIn("generic/entropy", by["generic-api-key"]["title"])
155
+ self.assertEqual(by["private-key"]["severity"], "HIGH") # specific rule untouched
156
+ self.assertEqual(by["aws-access-token"]["severity"], "HIGH") # AKIA via _aws_secret_tier
157
+
158
+ def test_trivy_generic_secret_is_medium(self):
159
+ data = {"Results": [{"Target": "src/x.ts", "Secrets": [
160
+ {"RuleID": "generic-api-key", "Title": "Generic API Key", "Match": "y" * 40, "StartLine": 2}]}]}
161
+ secs = [f for f in scanners._norm_trivy(data) if f["category"] == "secret"]
162
+ self.assertEqual(secs[0]["severity"], "MEDIUM")
163
+
164
+ def test_medium_secret_gets_medium_confidence_in_ledger(self):
165
+ unified = {"top": [{"severity": "MEDIUM", "category": "secret",
166
+ "title": "secret: Generic API Key — generic/entropy match", "file": "src/x.ts", "tools": ["gitleaks"]}]}
167
+ led = findings.build_ledger({}, unified, None, [])
168
+ hit = [f for f in led["findings"] if f["category"] == "static-secret"][0]
169
+ self.assertEqual(hit["confidence"], "MEDIUM")
170
+
171
+
136
172
  if __name__ == "__main__":
137
173
  unittest.main()