codejury 0.12.0__tar.gz → 0.13.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.
Files changed (91) hide show
  1. {codejury-0.12.0 → codejury-0.13.2}/PKG-INFO +2 -1
  2. {codejury-0.12.0 → codejury-0.13.2}/codejury/cli.py +62 -5
  3. codejury-0.13.2/codejury/data/rules/SKILL.md +58 -0
  4. codejury-0.13.2/codejury/data/rules/business-logic.md +23 -0
  5. codejury-0.13.2/codejury/data/rules/code-injection.md +23 -0
  6. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/command-injection.md +3 -2
  7. codejury-0.13.2/codejury/data/rules/cross-site-request-forgery.md +21 -0
  8. codejury-0.12.0/codejury/data/rules/xss.md → codejury-0.13.2/codejury/data/rules/cross-site-scripting.md +6 -5
  9. codejury-0.13.2/codejury/data/rules/hardcoded-secrets.md +21 -0
  10. codejury-0.13.2/codejury/data/rules/http-response-splitting.md +24 -0
  11. codejury-0.13.2/codejury/data/rules/improper-authentication.md +26 -0
  12. codejury-0.13.2/codejury/data/rules/information-exposure.md +34 -0
  13. codejury-0.13.2/codejury/data/rules/insecure-cryptography.md +23 -0
  14. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/insecure-deserialization.md +2 -1
  15. codejury-0.13.2/codejury/data/rules/insecure-direct-object-reference.md +25 -0
  16. codejury-0.13.2/codejury/data/rules/insecure-transport.md +22 -0
  17. codejury-0.13.2/codejury/data/rules/jwt-validation.md +22 -0
  18. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/mass-assignment.md +3 -2
  19. codejury-0.13.2/codejury/data/rules/missing-authorization.md +28 -0
  20. codejury-0.13.2/codejury/data/rules/open-redirect.md +24 -0
  21. codejury-0.13.2/codejury/data/rules/path-traversal.md +34 -0
  22. codejury-0.13.2/codejury/data/rules/race-condition.md +26 -0
  23. codejury-0.13.2/codejury/data/rules/replay-attack.md +23 -0
  24. codejury-0.12.0/codejury/data/rules/ssrf.md → codejury-0.13.2/codejury/data/rules/server-side-request-forgery.md +4 -3
  25. codejury-0.13.2/codejury/data/rules/server-side-template-injection.md +21 -0
  26. codejury-0.13.2/codejury/data/rules/session-fixation.md +23 -0
  27. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/rules/sql-injection.md +4 -3
  28. codejury-0.13.2/codejury/data/rules/xml-external-entity.md +23 -0
  29. codejury-0.13.2/codejury/diff/debate.py +162 -0
  30. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/debate_prompts.py +26 -12
  31. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/engine.py +15 -1
  32. codejury-0.13.2/codejury/diff/findings_filter.py +61 -0
  33. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/prompts.py +15 -1
  34. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/rules.py +16 -0
  35. codejury-0.13.2/codejury/infrastructure/json_parse.py +98 -0
  36. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/anthropic.py +1 -0
  37. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/factory.py +4 -0
  38. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/litellm.py +1 -0
  39. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/openai.py +1 -0
  40. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/retry.py +16 -2
  41. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/PKG-INFO +2 -1
  42. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/SOURCES.txt +21 -7
  43. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/requires.txt +1 -0
  44. {codejury-0.12.0 → codejury-0.13.2}/pyproject.toml +2 -1
  45. codejury-0.13.2/tests/test_cli_audit.py +45 -0
  46. {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_debate.py +76 -1
  47. {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_engine.py +33 -3
  48. {codejury-0.12.0 → codejury-0.13.2}/tests/test_json_parse.py +16 -0
  49. {codejury-0.12.0 → codejury-0.13.2}/tests/test_retry_provider.py +28 -1
  50. {codejury-0.12.0 → codejury-0.13.2}/tests/test_rules.py +36 -3
  51. codejury-0.12.0/codejury/data/rules/SKILL.md +0 -41
  52. codejury-0.12.0/codejury/data/rules/authentication-jwt.md +0 -21
  53. codejury-0.12.0/codejury/data/rules/broken-access-control.md +0 -27
  54. codejury-0.12.0/codejury/data/rules/idor.md +0 -24
  55. codejury-0.12.0/codejury/data/rules/insecure-crypto.md +0 -22
  56. codejury-0.12.0/codejury/data/rules/path-traversal.md +0 -22
  57. codejury-0.12.0/codejury/data/rules/secrets.md +0 -20
  58. codejury-0.12.0/codejury/diff/debate.py +0 -104
  59. codejury-0.12.0/codejury/diff/findings_filter.py +0 -39
  60. codejury-0.12.0/codejury/infrastructure/json_parse.py +0 -62
  61. {codejury-0.12.0 → codejury-0.13.2}/LICENSE +0 -0
  62. {codejury-0.12.0 → codejury-0.13.2}/README.md +0 -0
  63. {codejury-0.12.0 → codejury-0.13.2}/codejury/__init__.py +0 -0
  64. {codejury-0.12.0 → codejury-0.13.2}/codejury/analysis/__init__.py +0 -0
  65. {codejury-0.12.0 → codejury-0.13.2}/codejury/analysis/repo_model.py +0 -0
  66. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/agent/full-review.md +0 -0
  67. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/agent/security-review-memory.md +0 -0
  68. {codejury-0.12.0 → codejury-0.13.2}/codejury/data/entrypoints.yaml +0 -0
  69. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/__init__.py +0 -0
  70. {codejury-0.12.0 → codejury-0.13.2}/codejury/diff/report.py +0 -0
  71. {codejury-0.12.0 → codejury-0.13.2}/codejury/domain/__init__.py +0 -0
  72. {codejury-0.12.0 → codejury-0.13.2}/codejury/domain/finding.py +0 -0
  73. {codejury-0.12.0 → codejury-0.13.2}/codejury/fullreview/__init__.py +0 -0
  74. {codejury-0.12.0 → codejury-0.13.2}/codejury/fullreview/scaffold.py +0 -0
  75. {codejury-0.12.0 → codejury-0.13.2}/codejury/infrastructure/__init__.py +0 -0
  76. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/__init__.py +0 -0
  77. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/base.py +0 -0
  78. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/mock.py +0 -0
  79. {codejury-0.12.0 → codejury-0.13.2}/codejury/providers/openai_format.py +0 -0
  80. {codejury-0.12.0 → codejury-0.13.2}/codejury/resources.py +0 -0
  81. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/dependency_links.txt +0 -0
  82. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/entry_points.txt +0 -0
  83. {codejury-0.12.0 → codejury-0.13.2}/codejury.egg-info/top_level.txt +0 -0
  84. {codejury-0.12.0 → codejury-0.13.2}/setup.cfg +0 -0
  85. {codejury-0.12.0 → codejury-0.13.2}/tests/test_anthropic_provider.py +0 -0
  86. {codejury-0.12.0 → codejury-0.13.2}/tests/test_diff_report.py +0 -0
  87. {codejury-0.12.0 → codejury-0.13.2}/tests/test_full_review_scaffold.py +0 -0
  88. {codejury-0.12.0 → codejury-0.13.2}/tests/test_litellm_provider.py +0 -0
  89. {codejury-0.12.0 → codejury-0.13.2}/tests/test_openai_format.py +0 -0
  90. {codejury-0.12.0 → codejury-0.13.2}/tests/test_openai_provider.py +0 -0
  91. {codejury-0.12.0 → codejury-0.13.2}/tests/test_repo_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.12.0
3
+ Version: 0.13.2
4
4
  Summary: AI code security review: an adversarial diff-audit engine and an agent-driven whole-repo review methodology, with security knowledge as rich rules
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -17,6 +17,7 @@ Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: json-repair>=0.30
20
21
  Provides-Extra: anthropic
21
22
  Requires-Dist: anthropic>=0.40; extra == "anthropic"
22
23
  Provides-Extra: openai
@@ -17,15 +17,25 @@ import argparse
17
17
  import subprocess
18
18
  import sys
19
19
 
20
+ import dataclasses
21
+
20
22
  from codejury.diff.debate import AdversarialAuditRunner
21
23
  from codejury.diff.engine import AuditRunner
22
24
  from codejury.diff.findings_filter import FindingsFilter
23
25
  from codejury.diff.report import gate, render
26
+ from codejury.diff.rules import allowed_categories, normalize_category
24
27
  from codejury.domain.finding import Finding
28
+
29
+ # A diff larger than this is audited file-by-file so a big PR does not overflow
30
+ # the model's context and silently truncate the reply.
31
+ _MAX_DIFF_CHARS = 60_000
25
32
  from codejury.fullreview.scaffold import scaffold
26
33
  from codejury.providers.factory import (
27
34
  DEFAULT_API_BASE,
28
35
  DEFAULT_API_KEY,
36
+ DEFAULT_CHALLENGER_MODEL,
37
+ DEFAULT_FINDER_MODEL,
38
+ DEFAULT_JUDGE_MODEL,
29
39
  DEFAULT_MODEL,
30
40
  PROVIDERS,
31
41
  make_provider,
@@ -36,6 +46,31 @@ _FORMATS = ("text", "markdown", "json", "sarif")
36
46
  _FAIL_ON = ("critical", "high", "medium", "low")
37
47
 
38
48
 
49
+ def _split_diff_by_file(diff: str) -> list[str]:
50
+ """Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
51
+ chunks: list[str] = []
52
+ cur: list[str] = []
53
+ for line in diff.splitlines(keepends=True):
54
+ if line.startswith("diff --git ") and cur:
55
+ chunks.append("".join(cur))
56
+ cur = []
57
+ cur.append(line)
58
+ if cur:
59
+ chunks.append("".join(cur))
60
+ return chunks or ([diff] if diff.strip() else [])
61
+
62
+
63
+ def _dedup_findings(findings: list[Finding]) -> list[Finding]:
64
+ seen: set = set()
65
+ out: list[Finding] = []
66
+ for f in findings:
67
+ k = (f.file, f.line, f.category, f.description)
68
+ if k not in seen:
69
+ seen.add(k)
70
+ out.append(f)
71
+ return out
72
+
73
+
39
74
  def audit_diff(
40
75
  diff: str,
41
76
  *,
@@ -44,12 +79,30 @@ def audit_diff(
44
79
  mode: str = "standard",
45
80
  max_rounds: int = 3,
46
81
  filter_findings: bool = True,
82
+ finder_model: str | None = None,
83
+ challenger_model: str | None = None,
84
+ judge_model: str | None = None,
47
85
  ) -> tuple[list[Finding], list[tuple[Finding, str]]]:
48
- """Audit a diff and return (kept findings, dropped (finding, reason))."""
49
- if mode == "adversarial":
50
- findings = AdversarialAuditRunner(provider=provider, model=model).run(diff, max_rounds=max_rounds).findings
86
+ """Audit a diff and return (kept findings, dropped (finding, reason)).
87
+
88
+ A diff over the size budget is audited one file at a time so it does not
89
+ overflow the context. Finding categories are normalized to the rule-id set."""
90
+ def _run_one(d: str) -> list[Finding]:
91
+ if mode == "adversarial":
92
+ return AdversarialAuditRunner(
93
+ provider=provider, model=model,
94
+ finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
95
+ ).run(d, max_rounds=max_rounds).findings
96
+ return AuditRunner(provider=provider, model=model).run(d)
97
+
98
+ if len(diff) > _MAX_DIFF_CHARS:
99
+ chunks = _split_diff_by_file(diff)
100
+ findings = _dedup_findings([f for c in chunks for f in _run_one(c)])
51
101
  else:
52
- findings = AuditRunner(provider=provider, model=model).run(diff)
102
+ findings = _run_one(diff)
103
+
104
+ allowed = set(allowed_categories())
105
+ findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
53
106
  if filter_findings:
54
107
  return FindingsFilter().filter(findings)
55
108
  return findings, []
@@ -85,9 +138,12 @@ def main(argv: list[str] | None = None) -> int:
85
138
  a.add_argument("--rounds", type=int, default=3, help="adversarial debate rounds")
86
139
  a.add_argument("--provider", choices=PROVIDERS, default="anthropic")
87
140
  a.add_argument("--model", default=DEFAULT_MODEL)
141
+ a.add_argument("--finder-model", default=DEFAULT_FINDER_MODEL, help="adversarial: finder role model (default: --model)")
142
+ a.add_argument("--challenger-model", default=DEFAULT_CHALLENGER_MODEL, help="adversarial: challenger role model")
143
+ a.add_argument("--judge-model", default=DEFAULT_JUDGE_MODEL, help="adversarial: judge role model")
88
144
  a.add_argument("--api-base", default=DEFAULT_API_BASE)
89
145
  a.add_argument("--api-key", default=DEFAULT_API_KEY)
90
- a.add_argument("--retries", type=int, default=0)
146
+ a.add_argument("--retries", type=int, default=2, help="provider retry attempts on transient failure")
91
147
  a.add_argument("--format", choices=_FORMATS, default="text", dest="fmt")
92
148
  a.add_argument("--no-filter", action="store_true", help="skip the false-positive filter")
93
149
  a.add_argument("--fail-on", choices=_FAIL_ON, default=None, dest="fail_on")
@@ -110,6 +166,7 @@ def _dispatch(args, parser) -> int:
110
166
  kept, _ = audit_diff(
111
167
  _read_diff(args), provider=provider, model=args.model,
112
168
  mode=args.mode, max_rounds=args.rounds, filter_findings=not args.no_filter,
169
+ finder_model=args.finder_model, challenger_model=args.challenger_model, judge_model=args.judge_model,
113
170
  )
114
171
  print(render(args.fmt, kept))
115
172
  return 1 if gate(kept, args.fail_on) else 0
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: code-security-review
3
+ description: "Application security rules for reviewing code for exploitable vulnerabilities. Use when reviewing a diff or a codebase for security issues, or when writing code that handles untrusted input, authentication, authorization, file paths, database queries, network requests, deserialization, or cryptography. Read the matching rule before judging whether code is vulnerable."
4
+ ---
5
+
6
+ # Code Security Review Rules
7
+
8
+ Application-security rules, one file per weakness class under `rules/`, named by
9
+ the specific weakness (CWE-style). Each rule states impact, the markers to hunt
10
+ (`triggers`), and vulnerable-vs-secure examples. The diff-audit engine injects the
11
+ rules relevant to a change into the prompt; the full-review agent reads them for
12
+ the target's stack. A finding's `category` is one of these ids.
13
+
14
+ ## Rules by OWASP category
15
+
16
+ ### A01 Broken Access Control
17
+ - `missing-authorization` (CWE-862)
18
+ - `insecure-direct-object-reference` (CWE-639)
19
+ - `cross-site-request-forgery` (CWE-352)
20
+ - `path-traversal` (CWE-22)
21
+ - `open-redirect` (CWE-601)
22
+
23
+ ### A02 Cryptographic Failures
24
+ - `insecure-cryptography` (CWE-327)
25
+ - `insecure-transport` (CWE-319)
26
+ - `hardcoded-secrets` (CWE-798)
27
+ - `information-exposure` (CWE-200/532)
28
+
29
+ ### A03 Injection
30
+ - `sql-injection` (CWE-89)
31
+ - `command-injection` (CWE-78)
32
+ - `code-injection` (CWE-94)
33
+ - `cross-site-scripting` (CWE-79)
34
+ - `xml-external-entity` (CWE-611)
35
+ - `server-side-template-injection` (CWE-1336)
36
+ - `http-response-splitting` (CWE-113)
37
+
38
+ ### A04 Insecure Design / Business Logic
39
+ - `business-logic` (CWE-840)
40
+ - `replay-attack` (CWE-294)
41
+ - `race-condition` (CWE-362)
42
+ - `mass-assignment` (CWE-915)
43
+
44
+ ### A07 Identification and Authentication
45
+ - `improper-authentication` (CWE-287)
46
+ - `jwt-validation` (CWE-347)
47
+ - `session-fixation` (CWE-384)
48
+
49
+ ### A08 Software and Data Integrity
50
+ - `insecure-deserialization` (CWE-502)
51
+
52
+ ### A10 Server-Side Request Forgery
53
+ - `server-side-request-forgery` (CWE-918)
54
+
55
+ Report only real, exploitable, high-confidence issues with a concrete exploit
56
+ path. Do not report dependency CVEs, style, speculation, or config-leak-only
57
+ risks. The set is data: add a class by dropping a new `rules/<id>.md` with the
58
+ same frontmatter (id, title, impact, tags, triggers) and examples.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: business-logic
3
+ title: Business Logic Flaw
4
+ impact: HIGH
5
+ tags: [cwe-840, cwe-841, owasp-a04]
6
+ triggers: ["price", "amount", "quantity", "balance", "status", "approve", "discount", "total =", "request.json[\"price\"", "if status =="]
7
+ ---
8
+
9
+ ## Business Logic Flaw
10
+
11
+ A stateful workflow trusts client-supplied values or does not enforce its own rules: a client-set price/amount/quantity, a state-machine step reached without its prerequisites, an approval or limit bypassed. Validate amounts and state transitions server-side from trusted data, and enforce the workflow's invariants.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ total = request.json["price"] * request.json["qty"] # client sets the price
17
+ order.charge(total)
18
+ ```
19
+ Secure:
20
+ ```python
21
+ price = Product.objects.get(id=pid).price # price from the server
22
+ total = price * validate_qty(request.json["qty"])
23
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: code-injection
3
+ title: Code Injection
4
+ impact: CRITICAL
5
+ tags: [cwe-94, owasp-a03, injection, rce]
6
+ triggers: ["eval(", "exec(", "compile(", "pickle.loads", "new Function", "setTimeout(\"", "vm.runInContext"]
7
+ ---
8
+
9
+ ## Code Injection
10
+
11
+ Passing untrusted input to a language evaluation primitive (eval, exec, compile, dynamic import, JS Function) lets an attacker execute arbitrary code. Never evaluate untrusted input; parse it with a data-only parser or dispatch through an allowlist.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ result = eval(request.args["expr"])
17
+ exec(user_supplied_code)
18
+ ```
19
+ Secure:
20
+ ```python
21
+ import ast
22
+ result = ast.literal_eval(request.args["expr"]) # data only, no code
23
+ ```
@@ -1,8 +1,9 @@
1
1
  ---
2
+ id: command-injection
2
3
  title: Command Injection
3
4
  impact: CRITICAL
4
- tags: [injection, command, cwe-78, owasp-a03]
5
- triggers: ["os.system", "os.popen", "subprocess", "shell=True", "exec(", "Runtime.getRuntime", "child_process", "popen"]
5
+ tags: [cwe-78, owasp-a03, injection]
6
+ triggers: ["os.system", "os.popen", "subprocess", "shell=True", "Runtime.getRuntime", "child_process", "popen", "exec("]
6
7
  ---
7
8
 
8
9
  ## Command Injection
@@ -0,0 +1,21 @@
1
+ ---
2
+ id: cross-site-request-forgery
3
+ title: Cross-Site Request Forgery
4
+ impact: HIGH
5
+ tags: [cwe-352, owasp-a01]
6
+ triggers: ["@app.route", "methods=[\"POST\"", "csrf", "SameSite", "csrf_exempt", "@csrf", "form"]
7
+ ---
8
+
9
+ ## Cross-Site Request Forgery
10
+
11
+ A state-changing request is accepted using only ambient credentials (a session cookie) with no anti-CSRF token or SameSite protection, so a malicious site can make the victim's browser perform the action. Require a CSRF token (or SameSite=strict/lax cookies + origin check) on every state-changing endpoint.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ @app.route("/account/email", methods=["POST"])
17
+ @csrf.exempt # disables CSRF protection on a state-changing route
18
+ def change_email():
19
+ current_user.email = request.form["email"]; db.commit()
20
+ ```
21
+ Secure: keep CSRF protection on; validate the token, or set the session cookie `SameSite="Lax"` and check the Origin header.
@@ -1,13 +1,14 @@
1
1
  ---
2
- title: Cross-Site Scripting (XSS)
2
+ id: cross-site-scripting
3
+ title: Cross-Site Scripting
3
4
  impact: HIGH
4
- tags: [xss, output-encoding, cwe-79, owasp-a03]
5
+ tags: [cwe-79, owasp-a03, injection]
5
6
  triggers: ["innerHTML", "dangerouslySetInnerHTML", "|safe", "mark_safe", "render_template_string", "v-html", "document.write", "Markup("]
6
7
  ---
7
8
 
8
- ## Cross-Site Scripting (XSS)
9
+ ## Cross-Site Scripting
9
10
 
10
- Untrusted data rendered into HTML without encoding executes as script in the victim's browser. Render data as text (textContent), rely on framework auto-escaping, and never disable it for user data.
11
+ Untrusted data rendered into HTML without context-aware encoding executes as script in the victim's browser. Render data as text, rely on framework auto-escaping, and never disable it for user data.
11
12
 
12
13
  ### JavaScript
13
14
  Vulnerable:
@@ -21,4 +22,4 @@ el.textContent = "Hello " + username;
21
22
 
22
23
  ### Python (templates)
23
24
  Vulnerable: `return render_template_string("<div>" + user_input + "</div>")`
24
- Secure: rely on Jinja auto-escaping; never pass `| safe` to user data.
25
+ Secure: rely on Jinja auto-escaping; never apply `| safe` to user data.
@@ -0,0 +1,21 @@
1
+ ---
2
+ id: hardcoded-secrets
3
+ title: Hardcoded Secrets
4
+ impact: HIGH
5
+ tags: [cwe-798, cwe-259, owasp-a02]
6
+ triggers: ["api_key =", "API_KEY =", "password =", "secret =", "token =", "sk_live", "ghp_", "aws_secret"]
7
+ ---
8
+
9
+ ## Hardcoded Secrets
10
+
11
+ A literal credential, key, or token in source leaks with the code and cannot be rotated easily. Load secrets from environment variables or a secret manager. A variable that reads from the environment or is passed in as a parameter is fine; only an actual literal value is a finding.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ API_KEY = "sk_live_51HxQ...actual-secret"
17
+ ```
18
+ Secure:
19
+ ```python
20
+ api_key = os.environ["API_KEY"]
21
+ ```
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: http-response-splitting
3
+ title: HTTP Response Splitting
4
+ impact: MEDIUM
5
+ tags: [cwe-113, cwe-93, owasp-a03]
6
+ triggers: ["set_header", "add_header", "Location", "Set-Cookie", "response.headers", "resp.headers", "make_response", "setHeader"]
7
+ ---
8
+
9
+ ## HTTP Response Splitting / Header Injection
10
+
11
+ Putting untrusted input into a response header (or redirect Location, Set-Cookie) without stripping CR/LF lets an attacker inject headers or split the response. Strip or reject newline characters in any header value built from input; frameworks often do this, but manual header construction may not.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ resp.headers["X-Echo"] = request.args["v"] # v may contain \r\n
17
+ ```
18
+ Secure:
19
+ ```python
20
+ v = request.args["v"]
21
+ if "\n" in v or "\r" in v:
22
+ abort(400)
23
+ resp.headers["X-Echo"] = v
24
+ ```
@@ -0,0 +1,26 @@
1
+ ---
2
+ id: improper-authentication
3
+ title: Improper Authentication
4
+ impact: HIGH
5
+ tags: [cwe-287, cwe-306, owasp-a07]
6
+ triggers: ["login", "authenticate", "password ==", "== token", "auth", "bypass", "if not user", "verify_password"]
7
+ ---
8
+
9
+ ## Improper Authentication
10
+
11
+ Authentication is missing on a protected path, can be bypassed by a logic flaw, or compares credentials unsafely (a hardcoded bypass token, a non-constant-time compare, trusting a client-asserted identity). Verify identity server-side against a trusted store before granting access; compare secrets in constant time.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ if request.headers.get("X-Auth") == "debug-bypass": # hardcoded bypass
17
+ return admin_dashboard()
18
+ if user.token == request.args["token"]: # non-constant-time, and client-asserted
19
+ login(user)
20
+ ```
21
+ Secure:
22
+ ```python
23
+ import hmac
24
+ if user and hmac.compare_digest(user.token, request.args["token"]):
25
+ login(user)
26
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ id: information-exposure
3
+ title: Information Exposure
4
+ impact: MEDIUM
5
+ tags: [cwe-200, cwe-532, cwe-209, owasp-a02]
6
+ triggers: ["traceback.format_exc", "str(e)", "log.info(token", "logger.debug(secret", "print(password", "DEBUG = True", "jsonify(error="]
7
+ ---
8
+
9
+ ## Information Exposure
10
+
11
+ Sensitive data (secrets, tokens, PII) written to logs, or internal detail (stack traces, exception messages, debug output) returned to the client, helps an attacker and widens breach impact. Log only non-secret data, return a generic error to the caller, and keep detail server-side.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ logger.info("auth token: %s", token)
17
+ return jsonify(error=traceback.format_exc()), 500
18
+ ```
19
+ Secure:
20
+ ```python
21
+ logger.info("auth attempt for user %s", user_id)
22
+ app.logger.exception("auth failed"); return jsonify(error="internal error"), 500
23
+ ```
24
+
25
+ ### Not a finding (do not flag)
26
+
27
+ This is about leaking secrets/PII or internal detail. It is not a finding to:
28
+ - read or serve a file, make a request, or return ordinary application data,
29
+ - log non-sensitive identifiers (a user id, a request path, a status).
30
+
31
+ Reaching a file or returning a record is only information exposure when the data
32
+ returned is itself sensitive (a secret, another user's PII) or internal (a stack
33
+ trace, a query). A plain `open(...)` or response is not this weakness.
34
+
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: insecure-cryptography
3
+ title: Insecure Cryptography
4
+ impact: HIGH
5
+ tags: [cwe-327, cwe-916, owasp-a02]
6
+ triggers: ["hashlib.md5", "hashlib.sha1", "DES", "ECB", "MODE_ECB", "random.random", "random.randint", "md5(", "sha1("]
7
+ ---
8
+
9
+ ## Insecure Cryptography
10
+
11
+ Weak algorithms (MD5, SHA-1, DES, RC4), ECB mode, fast hashes for passwords, static or reused IVs, and non-cryptographic randomness for security values are exploitable. Use AES-GCM or ChaCha20-Poly1305, bcrypt/scrypt/argon2 for passwords, a fresh random nonce per message, and a CSPRNG (secrets / os.urandom).
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ hashlib.md5(password.encode()).hexdigest()
17
+ token = str(random.randint(0, 999999))
18
+ ```
19
+ Secure:
20
+ ```python
21
+ bcrypt.hashpw(password.encode(), bcrypt.gensalt())
22
+ token = secrets.token_urlsafe(32)
23
+ ```
@@ -1,7 +1,8 @@
1
1
  ---
2
+ id: insecure-deserialization
2
3
  title: Insecure Deserialization
3
4
  impact: CRITICAL
4
- tags: [deserialization, rce, cwe-502, owasp-a08]
5
+ tags: [cwe-502, owasp-a08, rce]
5
6
  triggers: ["pickle.loads", "pickle.load", "yaml.load", "marshal.loads", "jsonpickle", "ObjectInputStream", "torch.load"]
6
7
  ---
7
8
 
@@ -0,0 +1,25 @@
1
+ ---
2
+ id: insecure-direct-object-reference
3
+ title: Insecure Direct Object Reference
4
+ impact: HIGH
5
+ tags: [cwe-639, owasp-a01, access-control]
6
+ triggers: ["objects.get(", "findById", "get_object_or_404", "/<id>", "/:id", "request.args", "params[", "pk=", "where id ="]
7
+ ---
8
+
9
+ ## Insecure Direct Object Reference
10
+
11
+ A record is fetched or mutated by a user-supplied id without checking the caller owns or may access it, so an authenticated user reaches another user's, tenant's, or service's data by changing the id. Scope every object lookup to the caller's identity or tenant.
12
+
13
+ ### Python (Django)
14
+ Vulnerable:
15
+ ```python
16
+ account = Account.objects.get(id=request.GET["account_id"])
17
+ ```
18
+ Secure:
19
+ ```python
20
+ account = get_object_or_404(Account, id=request.GET["account_id"], owner=request.user)
21
+ ```
22
+
23
+ ### Node.js (Express)
24
+ Vulnerable: `const doc = await Document.findById(req.params.id)`
25
+ Secure: `const doc = await Document.findOne({ _id: req.params.id, userId: req.user.id })`
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: insecure-transport
3
+ title: Insecure Transport
4
+ impact: HIGH
5
+ tags: [cwe-319, cwe-295, owasp-a02]
6
+ triggers: ["http://", "verify=False", "CERT_NONE", "check_hostname", "_create_unverified", "rejectUnauthorized", "InsecureSkipVerify"]
7
+ ---
8
+
9
+ ## Insecure Transport
10
+
11
+ Sending sensitive data over cleartext HTTP, or disabling TLS certificate/hostname verification, exposes it to interception and man-in-the-middle. Use HTTPS and leave certificate verification on (the secure default).
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ requests.get("https://api.example.com/data", verify=False)
17
+ requests.post("http://api.example.com/login", data=creds)
18
+ ```
19
+ Secure:
20
+ ```python
21
+ requests.get("https://api.example.com/data") # verify defaults to True
22
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: jwt-validation
3
+ title: JWT Validation Flaw
4
+ impact: HIGH
5
+ tags: [cwe-347, cwe-345, owasp-a07]
6
+ triggers: ["jwt.decode", "verify=False", "verify_signature", "algorithms", "none", "decode("]
7
+ ---
8
+
9
+ ## JWT Validation Flaw
10
+
11
+ Accepting a JWT without verifying its signature, allowing the "none" algorithm, or reading claims before verification lets an attacker forge identity. Verify the signature with a fixed algorithm and validate iss/aud/exp before using any claim.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ claims = jwt.decode(token, options={"verify_signature": False})
17
+ jwt.decode(token, key, algorithms=["none"])
18
+ ```
19
+ Secure:
20
+ ```python
21
+ claims = jwt.decode(token, key, algorithms=["RS256"], audience=AUD, issuer=ISS)
22
+ ```
@@ -1,8 +1,9 @@
1
1
  ---
2
+ id: mass-assignment
2
3
  title: Mass Assignment
3
4
  impact: HIGH
4
- tags: [mass-assignment, overposting, cwe-915, owasp-a08]
5
- triggers: ["(**request", "update(**", "setattr(", "Object.assign", ".save()", "create(**", "request.get_json"]
5
+ tags: [cwe-915, owasp-a04, owasp-a08]
6
+ triggers: ["(**request", "update(**", "setattr(", "Object.assign", "create(**", "request.get_json", ".save()"]
6
7
  ---
7
8
 
8
9
  ## Mass Assignment
@@ -0,0 +1,28 @@
1
+ ---
2
+ id: missing-authorization
3
+ title: Missing Authorization
4
+ impact: HIGH
5
+ tags: [cwe-862, owasp-a01, access-control]
6
+ triggers: ["@app.route", "@router", "@login_required", "requires_", "permission", "is_admin", "role", "def delete", "def admin"]
7
+ ---
8
+
9
+ ## Missing Authorization
10
+
11
+ A privileged or state-changing endpoint performs its action without verifying the caller is allowed to, or derives the role/permission from a client-controlled value. Enforce authorization server-side, per request, from a trusted store; a route that mutates or exposes privileged data must check the caller's rights the same way its peers do.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ @app.route("/admin/users/<uid>", methods=["DELETE"])
17
+ def delete_user(uid): # no authorization check
18
+ User.objects.get(id=uid).delete()
19
+
20
+ is_admin = request.json["is_admin"] # privilege from the client
21
+ ```
22
+ Secure:
23
+ ```python
24
+ @app.route("/admin/users/<uid>", methods=["DELETE"])
25
+ @requires_admin
26
+ def delete_user(uid):
27
+ User.objects.get(id=uid).delete()
28
+ ```
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: open-redirect
3
+ title: Open Redirect
4
+ impact: MEDIUM
5
+ tags: [cwe-601, owasp-a01]
6
+ triggers: ["redirect(", "Location", "next=", "return_url", "redirect_uri", "sendRedirect", "res.redirect"]
7
+ ---
8
+
9
+ ## Open Redirect
10
+
11
+ A redirect target taken from untrusted input without validation lets an attacker send victims to an attacker-controlled site (phishing, OAuth token theft). Validate the target against an allowlist of paths/hosts, or only allow relative paths.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ return redirect(request.args["next"])
17
+ ```
18
+ Secure:
19
+ ```python
20
+ target = request.args["next"]
21
+ if not target.startswith("/") or target.startswith("//"):
22
+ target = "/"
23
+ return redirect(target)
24
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ id: path-traversal
3
+ title: Path Traversal
4
+ impact: HIGH
5
+ tags: [cwe-22, owasp-a01]
6
+ triggers: ["open(", "os.path.join", "send_file", "sendfile", "readFile", "filename", "../", "upload"]
7
+ ---
8
+
9
+ ## Path Traversal
10
+
11
+ A filesystem path built from untrusted input without containment lets `../` escape the intended directory. Resolve the path and confirm it stays within an allowed base, or use only the basename.
12
+
13
+ ### Python
14
+ Vulnerable:
15
+ ```python
16
+ open(os.path.join(UPLOAD_DIR, request.args["filename"]))
17
+ ```
18
+ Secure:
19
+ ```python
20
+ target = (UPLOAD_DIR / filename).resolve()
21
+ if not target.is_relative_to(UPLOAD_DIR):
22
+ raise ValueError("path escapes base dir")
23
+ ```
24
+
25
+ ### Not a finding (do not flag)
26
+
27
+ If the input is neutralized before the file operation there is no traversal:
28
+ - `os.path.basename(name)` is applied (strips `../` and any directory parts),
29
+ - the resolved path is confirmed within a base (`is_relative_to`, `realpath` under base),
30
+ - the value is from an allowlist, or is a constant / trusted-config path.
31
+
32
+ `open(os.path.join(BASE, os.path.basename(name)))` is safe: basename removes the
33
+ traversal. Do not report it.
34
+
@@ -0,0 +1,26 @@
1
+ ---
2
+ id: race-condition
3
+ title: Race Condition (TOCTOU)
4
+ impact: HIGH
5
+ tags: [cwe-362, cwe-367, owasp-a04]
6
+ triggers: ["if balance", "balance -=", "select_for_update", "get(...).save", "check", "transaction", "lock", "atomic"]
7
+ ---
8
+
9
+ ## Race Condition / TOCTOU
10
+
11
+ A check and the action it guards run on shared state without a lock or atomic update, so two concurrent requests both pass the check (double-spend, double-redeem, limit bypass). Use a row lock, an atomic conditional update, or a transaction.
12
+
13
+ ### Python (Django)
14
+ Vulnerable:
15
+ ```python
16
+ acct = Account.objects.get(pk=pk)
17
+ if acct.balance >= amount:
18
+ acct.balance -= amount; acct.save() # concurrent requests both pass
19
+ ```
20
+ Secure:
21
+ ```python
22
+ with transaction.atomic():
23
+ acct = Account.objects.select_for_update().get(pk=pk)
24
+ if acct.balance >= amount:
25
+ acct.balance -= amount; acct.save()
26
+ ```