codejury 0.16.1__tar.gz → 0.17.1__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 (88) hide show
  1. {codejury-0.16.1 → codejury-0.17.1}/PKG-INFO +4 -4
  2. {codejury-0.16.1 → codejury-0.17.1}/README.md +3 -3
  3. {codejury-0.16.1 → codejury-0.17.1}/codejury/cli.py +3 -2
  4. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/frameworks/django.md +4 -3
  5. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/languages/python.md +5 -5
  6. {codejury-0.16.1/codejury/data/agent → codejury-0.17.1/codejury/data/methodology}/repo-review.md +17 -16
  7. {codejury-0.16.1/codejury/data/agent → codejury-0.17.1/codejury/data/methodology}/security-review-memory.md +2 -2
  8. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/SKILL.md +1 -1
  9. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/code-injection.md +1 -1
  10. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/command-injection.md +1 -1
  11. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/cross-site-request-forgery.md +1 -1
  12. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/cross-site-scripting.md +1 -1
  13. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/hardcoded-secrets.md +1 -1
  14. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/http-response-splitting.md +1 -1
  15. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/improper-authentication.md +1 -1
  16. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/missing-authorization.md +1 -1
  17. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/server-side-template-injection.md +1 -1
  18. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/sql-injection.md +1 -1
  19. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/debate.py +13 -14
  20. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/debate_prompts.py +2 -2
  21. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/engine.py +19 -6
  22. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/prompts.py +1 -1
  23. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/rules.py +13 -26
  24. {codejury-0.16.1 → codejury-0.17.1}/codejury/domain/__init__.py +1 -1
  25. {codejury-0.16.1 → codejury-0.17.1}/codejury/domain/finding.py +2 -2
  26. {codejury-0.16.1 → codejury-0.17.1}/codejury/guides.py +15 -31
  27. {codejury-0.16.1 → codejury-0.17.1}/codejury/json_parse.py +1 -1
  28. codejury-0.17.1/codejury/mddoc.py +38 -0
  29. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/__init__.py +1 -1
  30. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/anthropic.py +3 -3
  31. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/factory.py +1 -1
  32. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/litellm.py +2 -2
  33. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/mock.py +1 -1
  34. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/openai.py +2 -2
  35. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/retry.py +1 -1
  36. codejury-0.17.1/codejury/repo/model.py +61 -0
  37. {codejury-0.16.1 → codejury-0.17.1}/codejury/repo/scaffold.py +26 -28
  38. {codejury-0.16.1 → codejury-0.17.1}/codejury/report.py +2 -2
  39. {codejury-0.16.1 → codejury-0.17.1}/codejury/resources.py +1 -2
  40. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/PKG-INFO +4 -4
  41. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/SOURCES.txt +4 -3
  42. {codejury-0.16.1 → codejury-0.17.1}/pyproject.toml +1 -1
  43. {codejury-0.16.1 → codejury-0.17.1}/tests/test_cli_audit.py +1 -1
  44. {codejury-0.16.1 → codejury-0.17.1}/tests/test_diff_debate.py +1 -1
  45. {codejury-0.16.1 → codejury-0.17.1}/tests/test_diff_engine.py +12 -3
  46. {codejury-0.16.1 → codejury-0.17.1}/tests/test_guides.py +2 -13
  47. codejury-0.17.1/tests/test_mddoc.py +28 -0
  48. codejury-0.17.1/tests/test_repo_model.py +38 -0
  49. {codejury-0.16.1 → codejury-0.17.1}/tests/test_repo_scaffold.py +23 -9
  50. codejury-0.16.1/codejury/data/entrypoints.yaml +0 -16
  51. codejury-0.16.1/codejury/repo/model.py +0 -262
  52. codejury-0.16.1/tests/test_repo_model.py +0 -227
  53. {codejury-0.16.1 → codejury-0.17.1}/LICENSE +0 -0
  54. {codejury-0.16.1 → codejury-0.17.1}/codejury/__init__.py +0 -0
  55. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/business-logic.md +0 -0
  56. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/information-exposure.md +0 -0
  57. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-cryptography.md +0 -0
  58. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-deserialization.md +0 -0
  59. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
  60. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-transport.md +0 -0
  61. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/jwt-validation.md +0 -0
  62. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/mass-assignment.md +0 -0
  63. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/open-redirect.md +0 -0
  64. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/path-traversal.md +0 -0
  65. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/race-condition.md +0 -0
  66. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/replay-attack.md +0 -0
  67. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/server-side-request-forgery.md +0 -0
  68. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/session-fixation.md +0 -0
  69. {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/xml-external-entity.md +0 -0
  70. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/__init__.py +0 -0
  71. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/findings_filter.py +0 -0
  72. {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/runner.py +0 -0
  73. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/base.py +0 -0
  74. {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/openai_format.py +0 -0
  75. {codejury-0.16.1 → codejury-0.17.1}/codejury/repo/__init__.py +0 -0
  76. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/dependency_links.txt +0 -0
  77. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/entry_points.txt +0 -0
  78. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/requires.txt +0 -0
  79. {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/top_level.txt +0 -0
  80. {codejury-0.16.1 → codejury-0.17.1}/setup.cfg +0 -0
  81. {codejury-0.16.1 → codejury-0.17.1}/tests/test_anthropic_provider.py +0 -0
  82. {codejury-0.16.1 → codejury-0.17.1}/tests/test_json_parse.py +0 -0
  83. {codejury-0.16.1 → codejury-0.17.1}/tests/test_litellm_provider.py +0 -0
  84. {codejury-0.16.1 → codejury-0.17.1}/tests/test_openai_format.py +0 -0
  85. {codejury-0.16.1 → codejury-0.17.1}/tests/test_openai_provider.py +0 -0
  86. {codejury-0.16.1 → codejury-0.17.1}/tests/test_report.py +0 -0
  87. {codejury-0.16.1 → codejury-0.17.1}/tests/test_retry_provider.py +0 -0
  88. {codejury-0.16.1 → codejury-0.17.1}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.16.1
3
+ Version: 0.17.1
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
@@ -104,7 +104,7 @@ Audit every pull request and surface findings in the code scanning tab. Copy
104
104
  2. write SARIF and upload it with `github/codeql-action/upload-sarif`,
105
105
  3. fail the check on a HIGH or CRITICAL finding (`--fail-on high`).
106
106
 
107
- The job makes one model call per PR (standard mode); the SARIF is uploaded even
107
+ The job makes one model call per PR (standard mode). The SARIF is uploaded even
108
108
  when the gate fails, so findings always show up on the PR.
109
109
 
110
110
  ## Whole-repo review
@@ -125,11 +125,11 @@ against production.
125
125
 
126
126
  Each finding carries a file and line, a severity and category, a concrete
127
127
  exploit scenario, a recommendation, and a confidence. A false-positive filter
128
- drops test/mock-path and low-confidence noise; the model is also told not to
128
+ drops test/mock-path and low-confidence noise. The model is also told not to
129
129
  report dependency CVEs, style notes, speculation, or config-leak-only risks.
130
130
 
131
131
  ## Extending
132
132
 
133
133
  Add a vulnerability class by dropping a new `codejury/data/rules/<class>.md` with
134
134
  the standard frontmatter (title, impact, tags, triggers) and vulnerable/secure
135
- examples. It is data; no code change needed.
135
+ examples. It is data, no code change needed.
@@ -73,7 +73,7 @@ Audit every pull request and surface findings in the code scanning tab. Copy
73
73
  2. write SARIF and upload it with `github/codeql-action/upload-sarif`,
74
74
  3. fail the check on a HIGH or CRITICAL finding (`--fail-on high`).
75
75
 
76
- The job makes one model call per PR (standard mode); the SARIF is uploaded even
76
+ The job makes one model call per PR (standard mode). The SARIF is uploaded even
77
77
  when the gate fails, so findings always show up on the PR.
78
78
 
79
79
  ## Whole-repo review
@@ -94,11 +94,11 @@ against production.
94
94
 
95
95
  Each finding carries a file and line, a severity and category, a concrete
96
96
  exploit scenario, a recommendation, and a confidence. A false-positive filter
97
- drops test/mock-path and low-confidence noise; the model is also told not to
97
+ drops test/mock-path and low-confidence noise. The model is also told not to
98
98
  report dependency CVEs, style notes, speculation, or config-leak-only risks.
99
99
 
100
100
  ## Extending
101
101
 
102
102
  Add a vulnerability class by dropping a new `codejury/data/rules/<class>.md` with
103
103
  the standard frontmatter (title, impact, tags, triggers) and vulnerable/secure
104
- examples. It is data; no code change needed.
104
+ examples. It is data, no code change needed.
@@ -132,9 +132,10 @@ def _dispatch(args, parser) -> int:
132
132
  if args.command == "review" and scope == "repo":
133
133
  res = scaffold(args.directory, args.workspace)
134
134
  print(f"Workspace: {res.workspace}", file=sys.stderr)
135
- print(f"Seeded {res.entrypoints} entrypoints into {res.workspace}/entrypoints/_entrypoints.md", file=sys.stderr)
136
135
  if res.guides:
137
- print(f"Detected stack ({', '.join(res.guides)}); notes in {res.workspace}/_stack.md", file=sys.stderr)
136
+ print(f"Detected stack: {', '.join(res.guides)}. Notes in {res.workspace}/_stack.md", file=sys.stderr)
137
+ print(f"Flagged {len(res.candidate_files)} candidate entrypoint files into "
138
+ f"{res.workspace}/entrypoints/_entrypoints.md", file=sys.stderr)
138
139
  print(f"Memory: {res.memory_path}", file=sys.stderr)
139
140
  print("\nRun this review with an interactive agent (Claude Code / Codex) using the methodology below.\n")
140
141
  print(res.methodology)
@@ -6,8 +6,9 @@ detect:
6
6
  files: ["*urls.py", "manage.py", "*settings.py"]
7
7
  manifest: ["django"]
8
8
  imports: ["from django", "import django"]
9
+ entrypoint_files: ["*urls.py"]
9
10
  ---
10
- # Django review notes
11
+ # Django review notes
11
12
 
12
13
  ## Entrypoints
13
14
  - Routes live in `urls.py`: `path()` / `re_path()` map a URL to a view.
@@ -25,6 +26,6 @@ detect:
25
26
 
26
27
  ## Common sinks / gotchas
27
28
  - SQL: `.raw()`, `.extra()`, `RawSQL`, or string-built SQL via `connection.cursor()`.
28
- - Templates: `mark_safe`, `|safe`, `format_html` on unescaped user input; autoescape off.
29
- - `pickle` / `yaml.load` on a cookie or upload; `DEBUG=True` leaking internals;
29
+ - Templates: `mark_safe`, `|safe`, `format_html` on unescaped user input, autoescape off.
30
+ - `pickle` / `yaml.load` on a cookie or upload, `DEBUG=True` leaking internals,
30
31
  a hardcoded `SECRET_KEY`.
@@ -7,16 +7,16 @@ detect:
7
7
  manifest: []
8
8
  imports: []
9
9
  ---
10
- # Python review notes
10
+ # Python review notes
11
11
 
12
12
  Where untrusted input enters (beyond web routes, which the framework guide covers):
13
13
  CLI (`argparse`/`click`), scheduled jobs, queue consumers, and any function fed an
14
14
  external value. Non-HTTP sources matter as much as routes:
15
15
 
16
- - deserialization: `pickle.loads`, `yaml.load` without `SafeLoader`, `marshal`;
17
- - code execution: `eval`, `exec`, `subprocess(..., shell=True)`, `os.system`;
18
- - XML/XXE: `lxml`/`xml.etree` parsing attacker XML;
19
- - filesystem: `open()` / `os.path.join` on a path built from user input;
16
+ - deserialization: `pickle.loads`, `yaml.load` without `SafeLoader`, `marshal`
17
+ - code execution: `eval`, `exec`, `subprocess(..., shell=True)`, `os.system`
18
+ - XML/XXE: `lxml`/`xml.etree` parsing attacker XML
19
+ - filesystem: `open()` / `os.path.join` on a path built from user input
20
20
  - network: `requests.get(user_url)` and friends (SSRF).
21
21
 
22
22
  Common sinks: string-built SQL handed to a DB cursor or ORM `.raw()`/`.extra()`,
@@ -1,10 +1,10 @@
1
- # Repo Security Review Agent Methodology
1
+ # Repo Security Review: Agent Methodology
2
2
 
3
3
  The `review repo` path: a whole-repository security audit, run by an interactive
4
- coding agent (Claude Code, Codex, etc.), not a one-shot LLM call. It maps the
4
+ coding agent such as Claude Code or Codex, not a one-shot LLM call. It maps the
5
5
  attack surface, traces inputs to sinks across files, verifies issues with a real
6
6
  PoC, and iterates over multiple rounds with a persistent memory. One round is
7
- roughly 30 minutes; run as many rounds as needed.
7
+ roughly 30 minutes. Run as many rounds as needed.
8
8
 
9
9
  Target repository: the directory you were given.
10
10
  Workspace: `<workspace>/<project>/` (created for you), holding `entrypoints/`,
@@ -12,15 +12,16 @@ Workspace: `<workspace>/<project>/` (created for you), holding `entrypoints/`,
12
12
 
13
13
  ---
14
14
 
15
- ## On start (do this first)
15
+ ## On start
16
16
 
17
17
  1. Read `security-review-memory.md` in the workspace if it exists:
18
- - skip every pattern under "Confirmed false positives";
19
- - do not re-report anything under "Fixed";
18
+ - skip every pattern under "Confirmed false positives".
19
+ - do not re-report anything under "Fixed".
20
20
  - weight the files under "High-risk areas" more heavily.
21
- 2. Read `entrypoints/_entrypoints.md` (seeded for you from a deterministic AST
22
- scan) as a *starting* map of the attack surface. It lists HTTP routes and CLI
23
- commands only, a subset, not the whole surface (see "Map the attack surface").
21
+ 2. Read `entrypoints/_entrypoints.md` (seeded): the files the detected stack
22
+ flags as likely to define entrypoints. Open them to find the actual
23
+ entrypoints. It is a *starting* subset, not the whole surface (see "Map the
24
+ attack surface").
24
25
  3. Read `_stack.md` (seeded): the detected languages and frameworks and review
25
26
  notes for them, so you know where this stack's entrypoints, sinks, and auth
26
27
  checks live. If it matched nothing, lean on your own knowledge of the stack.
@@ -33,12 +34,12 @@ The seeded inventory lists HTTP routes and CLI commands only. Before analysing,
33
34
  complete the surface: untrusted input enters at more than HTTP. Enumerate every
34
35
  source the attacker can influence and add it to `entrypoints/`:
35
36
 
36
- - HTTP routes, GraphQL resolvers, gRPC / RPC handlers, WebSocket handlers;
37
+ - HTTP routes, GraphQL resolvers, gRPC / RPC handlers, WebSocket handlers.
37
38
  - CLI commands, scheduled jobs / cron, queue and topic consumers, webhooks and
38
- third-party callbacks;
39
+ third-party callbacks.
39
40
  - deserialization points (pickle, yaml.load, marshal), file and document parsers
40
- (XML / XXE, YAML, CSV, zip, image / office), template rendering of user input;
41
- - file uploads, archive extraction, and any filesystem path built from user input;
41
+ (XML / XXE, YAML, CSV, zip, image / office), template rendering of user input.
42
+ - file uploads, archive extraction, and any filesystem path built from user input.
42
43
  - headers, cookies, environment, and config read as trusted, and inbound
43
44
  inter-service calls.
44
45
 
@@ -60,11 +61,11 @@ Read the implementation reachable from each source. For every one ask:
60
61
  - Mass assignment: is a user-controlled body bound wholesale into a model?
61
62
  - Signature: is a caller-supplied key trusted as the trust anchor?
62
63
 
63
- ## Trace attack paths (the core work)
64
+ ## Trace attack paths, the core work
64
65
 
65
66
  A whole-repo review earns its keep by reasoning *across files*: a flaw is usually
66
67
  a source in one file reaching a dangerous sink in another, past a control defined
67
- in a third (a route that trusts a helper which skips signature checks; an id that
68
+ in a third (a route that trusts a helper which skips signature checks, an id that
68
69
  reaches a query with no ownership check). For each promising source, trace the
69
70
  path and record it in `analysis/`:
70
71
 
@@ -128,7 +129,7 @@ destructive action without the operator's explicit go-ahead.
128
129
  ## Iteration
129
130
 
130
131
  Each round, read the workspace history first and do not repeat finished work.
131
- Process leftover TODOs; otherwise pick an unreviewed (❌) or to-deepen (⚠️)
132
+ Process leftover TODOs, otherwise pick an unreviewed (❌) or to-deepen (⚠️)
132
133
  source from the inventory. When every source is ✅ and there are no TODOs,
133
134
  report the review complete.
134
135
 
@@ -1,4 +1,4 @@
1
- # Security Review Memory <project>
1
+ # Security Review Memory: <project>
2
2
 
3
3
  > Maintained by the security-review agent. Read at the start of every round,
4
4
  > updated at the end. It skips confirmed false positives, avoids re-reporting
@@ -22,7 +22,7 @@
22
22
 
23
23
  ## High-risk areas (weight every round)
24
24
 
25
- <!-- - `<path>` <reason, e.g. past auth bypass> -->
25
+ <!-- - `<path>`: <reason, for example a past auth bypass> -->
26
26
 
27
27
  ## Audit history
28
28
 
@@ -8,7 +8,7 @@ description: "Application security rules for reviewing code for exploitable vuln
8
8
  Application-security rules, one file per weakness class under `rules/`, named by
9
9
  the specific weakness (CWE-style). Each rule states impact, the markers to hunt
10
10
  (`triggers`), and vulnerable-vs-secure examples. The diff-audit engine injects the
11
- rules relevant to a change into the prompt; the repo-review agent reads them for
11
+ rules relevant to a change into the prompt. The repo-review agent reads them for
12
12
  the target's stack. A finding's `category` is one of these ids.
13
13
 
14
14
  ## Rules by OWASP category
@@ -8,7 +8,7 @@ triggers: ["eval(", "exec(", "compile(", "pickle.loads", "new Function", "setTim
8
8
 
9
9
  ## Code Injection
10
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.
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
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["os.system", "os.popen", "subprocess", "shell=True", "Runtime.getRunt
8
8
 
9
9
  ## Command Injection
10
10
 
11
- Passing untrusted input to a shell lets an attacker run arbitrary commands. Never build a shell string from input; pass an argument list with the shell disabled.
11
+ Passing untrusted input to a shell lets an attacker run arbitrary commands. Never build a shell string from input. Pass an argument list with the shell disabled.
12
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -18,4 +18,4 @@ Vulnerable:
18
18
  def change_email():
19
19
  current_user.email = request.form["email"]; db.commit()
20
20
  ```
21
- Secure: keep CSRF protection on; validate the token, or set the session cookie `SameSite="Lax"` and check the Origin header.
21
+ Secure: keep CSRF protection on. Validate the token, or set the session cookie `SameSite="Lax"` and check the Origin header.
@@ -22,4 +22,4 @@ el.textContent = "Hello " + username;
22
22
 
23
23
  ### Python (templates)
24
24
  Vulnerable: `return render_template_string("<div>" + user_input + "</div>")`
25
- Secure: rely on Jinja auto-escaping; never apply `| safe` to user data.
25
+ Secure: rely on Jinja auto-escaping. Never apply `| safe` to user data.
@@ -8,7 +8,7 @@ triggers: ["api_key =", "API_KEY =", "password =", "secret =", "token =", "sk_li
8
8
 
9
9
  ## Hardcoded Secrets
10
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.
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
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["set_header", "add_header", "Location", "Set-Cookie", "response.heade
8
8
 
9
9
  ## HTTP Response Splitting / Header Injection
10
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.
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
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["login", "authenticate", "password ==", "== token", "auth", "bypass",
8
8
 
9
9
  ## Improper Authentication
10
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.
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
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["@app.route", "@router", "@login_required", "requires_", "permission"
8
8
 
9
9
  ## Missing Authorization
10
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.
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
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["render_template_string", "Template(", "from_string", "Jinja2", "{{",
8
8
 
9
9
  ## Server-Side Template Injection (SSTI)
10
10
 
11
- Building a template from untrusted input (rather than passing input as template data) lets an attacker inject template syntax that the engine evaluates, often reaching RCE. Pass untrusted values as data to a fixed template; never compile a template from user input.
11
+ Building a template from untrusted input (rather than passing input as template data) lets an attacker inject template syntax that the engine evaluates, often reaching RCE. Pass untrusted values as data to a fixed template. Never compile a template from user input.
12
12
 
13
13
  ### Python (Jinja2)
14
14
  Vulnerable:
@@ -8,7 +8,7 @@ triggers: ["execute(", "executemany", ".raw(", "cursor", "SELECT ", "INSERT ", "
8
8
 
9
9
  ## SQL Injection
10
10
 
11
- Untrusted input concatenated or interpolated into a SQL statement lets an attacker change the query. Use parameterized queries / bound parameters; never build SQL from input. Identifiers (table/column names) cannot be parameterized, so validate them against an allowlist.
11
+ Untrusted input concatenated or interpolated into a SQL statement lets an attacker change the query. Use parameterized queries / bound parameters. Never build SQL from input. Identifiers (table/column names) cannot be parameterized, so validate them against an allowlist.
12
12
 
13
13
  ### Python
14
14
  Vulnerable:
@@ -34,7 +34,7 @@ class AdversarialResult:
34
34
  investigate: list[dict] = field(default_factory=list)
35
35
  rounds: int = 0
36
36
  converged: bool = False
37
- degraded: bool = False # the judge response was unusable; findings are the unjudged fallback
37
+ degraded: bool = False # the judge response was unusable, findings are the unjudged fallback
38
38
 
39
39
 
40
40
  def _dicts(items: object) -> list[dict]:
@@ -91,16 +91,16 @@ class AdversarialAuditRunner:
91
91
  ) -> None:
92
92
  self._provider = provider
93
93
  self._max_tokens = max_tokens
94
- # each role can run on its own model (a strong finder, a skeptical
95
- # challenger, a careful judge); each defaults to the base model.
94
+ # each role can run on its own model, each defaults to the base model
96
95
  self._finder_model = finder_model or model
97
96
  self._challenger_model = challenger_model or model
98
97
  self._judge_model = judge_model or model
99
98
 
100
99
  def _ask(self, system: str, prompt: str, model: str) -> tuple[dict, bool]:
101
- """Returns (parsed object, ok). ok is False when the response could not be
102
- parsed into a JSON object (a provider error page, a blocked request, prose),
103
- so the caller can avoid treating an unusable reply as an empty result."""
100
+ """Return the parsed object and an ok flag. ok is False when the response
101
+ could not be parsed into a JSON object, for example a provider error page,
102
+ a blocked request, or prose, so the caller does not treat an unusable reply
103
+ as an empty result."""
104
104
  try:
105
105
  result = self._provider.complete(
106
106
  system=system,
@@ -109,15 +109,15 @@ class AdversarialAuditRunner:
109
109
  max_tokens=self._max_tokens,
110
110
  )
111
111
  except Exception:
112
- # a provider error (exhausted retries, blank body, transport failure)
113
- # is an unusable reply, not an empty result; degrade gracefully
112
+ # a provider error such as exhausted retries or a transport failure
113
+ # is an unusable reply, not an empty result, so degrade gracefully
114
114
  return {}, False
115
115
  obj = extract_json_object(result.text)
116
116
  return (obj or {}), bool(obj)
117
117
 
118
118
  def run(self, diff: str, *, rules: str = "", context: str = "", max_rounds: int = 3) -> AdversarialResult:
119
119
  if not rules:
120
- rules = rules_for_diff(diff) # inject the rules relevant to this diff
120
+ rules = rules_for_diff(diff)
121
121
  prior: list[dict] = []
122
122
  prev_keys: set | None = None
123
123
  judged = AdversarialResult(findings=[])
@@ -142,14 +142,13 @@ class AdversarialAuditRunner:
142
142
  verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
143
143
  if not judge_ok:
144
144
  # the judge is the filter that controls false positives, and an
145
- # unusable reply is often transient (a flaky proxy, a blocked
146
- # request); re-ask once before giving up on it
145
+ # unusable reply is usually transient, so re-ask once before degrading
147
146
  verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
148
147
  if not judge_ok:
149
148
  # judge still unusable: degrade, but apply the recall-safe
150
- # challenger's dismissals so a transient judge outage does not pass
151
- # through findings the challenger already showed are safe (this is
152
- # what otherwise inflates false positives in degraded runs)
149
+ # challenger dismissals so a transient judge outage does not pass
150
+ # through findings the challenger already showed are safe. without
151
+ # this the degraded fallback inflates false positives
153
152
  fallback = _dedup(_apply_dismissals(finder_findings, rebuttals) + new_findings)
154
153
  judged = AdversarialResult(findings=findings_from_list(fallback), rounds=rounds, degraded=True)
155
154
  degraded = True
@@ -26,7 +26,7 @@ _FINDING_FIELDS = (
26
26
 
27
27
  FINDER_SYSTEM = (
28
28
  "You are a red-team application security engineer. Enumerate every plausible "
29
- "exploitable vulnerability an attacker could reach; do not self-censor or pre-filter "
29
+ "exploitable vulnerability an attacker could reach. Do not self-censor or pre-filter "
30
30
  "for fear of false positives, the Challenger and Judge will do that. Respond with a "
31
31
  "single JSON object and nothing else."
32
32
  )
@@ -102,7 +102,7 @@ def judge_prompt(diff: str, finder_findings: list, rebuttals: list, new_findings
102
102
  "- UNRESOLVED: cannot decide from the code shown -> put it in `unresolved`.\n"
103
103
  "- INVESTIGATE: needs a dynamic/runtime check to confirm -> put it in `investigate`.\n"
104
104
  "Set `converged` to true when this round surfaced no new confirmed finding and nothing is left to "
105
- "investigate; false if another round could still change the ruling.\n\n"
105
+ "investigate. Set it to false if another round could still change the ruling.\n\n"
106
106
  f"Code change (unified diff):\n```diff\n{diff}\n```\n\n{context_block}"
107
107
  f"Finder findings:\n{json.dumps(finder_findings, ensure_ascii=False)}\n\n"
108
108
  f"Challenger rebuttals:\n{json.dumps(rebuttals, ensure_ascii=False)}\n\n"
@@ -7,12 +7,25 @@ coverage and lower false positives.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import re
11
+
10
12
  from codejury.domain.finding import Finding, findings_from_list
11
13
  from codejury.diff.prompts import SYSTEM, standard_audit_prompt
12
14
  from codejury.diff.rules import rules_for_diff
15
+ from codejury.guides import select_guides
13
16
  from codejury.json_parse import extract_json_object
14
17
  from codejury.providers.base import Message, Provider
15
- from codejury.guides import guides_for_diff
18
+
19
+ _DIFF_PATH = re.compile(r"^(?:\+\+\+ b/|diff --git a/\S+ b/)(\S+)", re.MULTILINE)
20
+
21
+
22
+ def guides_for_diff(diff: str) -> str:
23
+ """Concatenated bodies of the language/framework guides relevant to a diff,
24
+ selected by its changed paths and its content. Empty when nothing matches.
25
+ Lives here, not in the shared guides module, because parsing a diff is a
26
+ diff-path concern."""
27
+ paths = _DIFF_PATH.findall(diff)
28
+ return "\n\n---\n\n".join(g.body for g in select_guides(paths, text=diff))
16
29
 
17
30
 
18
31
  class AuditError(RuntimeError):
@@ -33,8 +46,8 @@ class AuditRunner:
33
46
 
34
47
  def run(self, diff: str, *, rules: str = "", context: str = "") -> list[Finding]:
35
48
  if not rules:
36
- rules = rules_for_diff(diff) # inject the rules relevant to this diff
37
- stack = guides_for_diff(diff) # inject the target's language/framework conventions
49
+ rules = rules_for_diff(diff)
50
+ stack = guides_for_diff(diff)
38
51
  result = self._provider.complete(
39
52
  system=SYSTEM,
40
53
  messages=[Message(role="user", content=standard_audit_prompt(diff, rules=rules, context=context, stack=stack))],
@@ -44,8 +57,8 @@ class AuditRunner:
44
57
  obj = extract_json_object(result.text)
45
58
  if obj is None or "findings" not in obj:
46
59
  raise AuditError(
47
- "the model reply was not a valid audit result (no JSON object, or a "
48
- "JSON object without a `findings` key); treating it as a failed "
49
- "audit rather than a clean pass"
60
+ "the model reply was not a valid audit result. it had no JSON object, "
61
+ "or a JSON object without a findings key, so it is a failed audit "
62
+ "rather than a clean pass"
50
63
  )
51
64
  return findings_from_list(obj.get("findings"))
@@ -33,7 +33,7 @@ Do NOT report, regardless of severity:
33
33
  - Risks that only matter if a production config is leaked (do not assume the code
34
34
  shown reflects production configuration).
35
35
  For input-driven issues, flag only when untrusted input can plausibly reach the
36
- sink; a constant, a stored field, trusted config, or an operator-supplied CLI
36
+ sink. A constant, a stored field, trusted config, or an operator-supplied CLI
37
37
  argument is not attacker-controlled.
38
38
  """
39
39
 
@@ -12,8 +12,7 @@ from __future__ import annotations
12
12
  from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
 
15
- import yaml
16
-
15
+ from codejury.mddoc import iter_md_docs
17
16
  from codejury.resources import RULES_DIR
18
17
 
19
18
  _IMPACT_RANK = {"CRITICAL": 3, "HIGH": 2, "MEDIUM": 1, "LOW": 0}
@@ -29,31 +28,19 @@ class Rule:
29
28
  body: str
30
29
 
31
30
 
32
- def _parse(path: Path) -> Rule | None:
33
- text = path.read_text(encoding="utf-8")
34
- meta: dict = {}
35
- body = text
36
- if text.startswith("---"):
37
- parts = text.split("---", 2)
38
- if len(parts) == 3:
39
- meta = yaml.safe_load(parts[1]) or {}
40
- body = parts[2].strip()
41
- return Rule(
42
- id=path.stem,
43
- title=str(meta.get("title", path.stem)),
44
- impact=str(meta.get("impact", "MEDIUM")).upper(),
45
- tags=tuple(meta.get("tags", [])),
46
- triggers=tuple(str(t) for t in meta.get("triggers", [])),
47
- body=body,
48
- )
49
-
50
-
51
31
  def load_rules(directory: str | Path = RULES_DIR) -> list[Rule]:
52
- root = Path(directory)
53
- if not root.is_dir():
54
- return []
55
- rules = [_parse(p) for p in root.glob("*.md") if p.name != "SKILL.md"]
56
- return sorted([r for r in rules if r], key=lambda r: r.id)
32
+ rules = [
33
+ Rule(
34
+ id=path.stem,
35
+ title=str(meta.get("title", path.stem)),
36
+ impact=str(meta.get("impact", "MEDIUM")).upper(),
37
+ tags=tuple(meta.get("tags", [])),
38
+ triggers=tuple(str(t) for t in meta.get("triggers", [])),
39
+ body=body,
40
+ )
41
+ for path, meta, body in iter_md_docs(directory)
42
+ ]
43
+ return sorted(rules, key=lambda r: r.id)
57
44
 
58
45
 
59
46
  def select_rules(diff: str, rules: list[Rule], *, limit: int = 6) -> list[Rule]:
@@ -1,5 +1,5 @@
1
1
  """codejury.domain: the framework's typed data model.
2
2
 
3
- Layers communicate only through these structures; this package depends on no
3
+ Layers communicate only through these structures. This package depends on no
4
4
  concrete implementation (Provider / Source / ...).
5
5
  """
@@ -30,10 +30,10 @@ class Finding:
30
30
 
31
31
 
32
32
  def _to_float(value: object, default: float) -> float:
33
- if isinstance(value, bool):
33
+ if isinstance(value, bool) or not isinstance(value, (int, float, str)):
34
34
  return default
35
35
  try:
36
- f = float(value) # type: ignore[arg-type]
36
+ f = float(value)
37
37
  except (TypeError, ValueError):
38
38
  return default
39
39
  return f if 0.0 <= f <= 1.0 else default
@@ -13,16 +13,11 @@ code change, which keeps the unbounded language/framework axis out of code.
13
13
  from __future__ import annotations
14
14
 
15
15
  import fnmatch
16
- import re
17
16
  from dataclasses import dataclass
18
- from pathlib import Path
19
-
20
- import yaml
21
17
 
18
+ from codejury.mddoc import iter_md_docs
22
19
  from codejury.resources import FRAMEWORKS_DIR, LANGUAGES_DIR
23
20
 
24
- _DIFF_PATH = re.compile(r"^(?:\+\+\+ b/|diff --git a/\S+ b/)(\S+)", re.MULTILINE)
25
-
26
21
 
27
22
  @dataclass(frozen=True, kw_only=True)
28
23
  class Guide:
@@ -32,18 +27,11 @@ class Guide:
32
27
  detect_files: tuple[str, ...]
33
28
  detect_manifest: tuple[str, ...]
34
29
  detect_imports: tuple[str, ...]
30
+ entrypoint_files: tuple[str, ...] # globs for files likely to define entrypoints
35
31
  body: str
36
32
 
37
33
 
38
- def _parse(path: Path, kind: str) -> Guide:
39
- text = path.read_text(encoding="utf-8")
40
- meta: dict = {}
41
- body = text
42
- if text.startswith("---"):
43
- parts = text.split("---", 2)
44
- if len(parts) == 3:
45
- meta = yaml.safe_load(parts[1]) or {}
46
- body = parts[2].strip()
34
+ def _guide(path, meta: dict, body: str, kind: str) -> Guide:
47
35
  detect = meta.get("detect", {}) or {}
48
36
  return Guide(
49
37
  id=str(meta.get("id", path.stem)),
@@ -52,16 +40,24 @@ def _parse(path: Path, kind: str) -> Guide:
52
40
  detect_files=tuple(str(f) for f in detect.get("files", [])),
53
41
  detect_manifest=tuple(str(m).lower() for m in detect.get("manifest", [])),
54
42
  detect_imports=tuple(str(i) for i in detect.get("imports", [])),
43
+ entrypoint_files=tuple(str(g) for g in meta.get("entrypoint_files", [])),
55
44
  body=body,
56
45
  )
57
46
 
58
47
 
48
+ def entrypoint_globs(guides: list[Guide]) -> tuple[str, ...]:
49
+ """The entrypoint-file globs declared by a set of guides, deduplicated."""
50
+ seen: dict[str, None] = {}
51
+ for g in guides:
52
+ for pat in g.entrypoint_files:
53
+ seen.setdefault(pat, None)
54
+ return tuple(seen)
55
+
56
+
59
57
  def load_guides(languages_dir=LANGUAGES_DIR, frameworks_dir=FRAMEWORKS_DIR) -> list[Guide]:
60
58
  out: list[Guide] = []
61
59
  for directory, kind in ((languages_dir, "language"), (frameworks_dir, "framework")):
62
- root = Path(directory)
63
- if root.is_dir():
64
- out += [_parse(p, kind) for p in sorted(root.glob("*.md")) if p.name != "SKILL.md"]
60
+ out += [_guide(path, meta, body, kind) for path, meta, body in iter_md_docs(directory)]
65
61
  return out
66
62
 
67
63
 
@@ -77,21 +73,9 @@ def _matches(guide: Guide, files: list[str], text: str) -> bool:
77
73
 
78
74
  def select_guides(files, *, text: str = "", guides: list[Guide] | None = None) -> list[Guide]:
79
75
  """The guides whose detect signals fire on the target, languages first then
80
- frameworks. `files` are the target's file paths; `text` is content to scan for
76
+ frameworks. `files` are the target's file paths. `text` is content to scan for
81
77
  manifest substrings and import markers (a repo's manifests, or a diff body)."""
82
78
  pool = load_guides() if guides is None else guides
83
79
  file_list = list(files)
84
80
  blob = text.lower()
85
81
  return [g for g in pool if _matches(g, file_list, blob)]
86
-
87
-
88
- def _changed_paths(diff: str) -> list[str]:
89
- """The file paths touched by a unified diff (from its `+++ b/` / `diff --git` headers)."""
90
- return _DIFF_PATH.findall(diff)
91
-
92
-
93
- def guides_for_diff(diff: str, *, guides: list[Guide] | None = None) -> str:
94
- """Concatenated bodies of the language/framework guides relevant to a diff, by
95
- its changed paths and its content. Empty when nothing matches."""
96
- selected = select_guides(_changed_paths(diff), text=diff, guides=guides)
97
- return "\n\n---\n\n".join(g.body for g in selected)