codejury 0.14.5__tar.gz → 0.14.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 (80) hide show
  1. {codejury-0.14.5 → codejury-0.14.6}/PKG-INFO +1 -1
  2. {codejury-0.14.5 → codejury-0.14.6}/codejury/repo/model.py +98 -27
  3. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/PKG-INFO +1 -1
  4. {codejury-0.14.5 → codejury-0.14.6}/pyproject.toml +1 -1
  5. {codejury-0.14.5 → codejury-0.14.6}/tests/test_repo_model.py +48 -0
  6. {codejury-0.14.5 → codejury-0.14.6}/LICENSE +0 -0
  7. {codejury-0.14.5 → codejury-0.14.6}/README.md +0 -0
  8. {codejury-0.14.5 → codejury-0.14.6}/codejury/__init__.py +0 -0
  9. {codejury-0.14.5 → codejury-0.14.6}/codejury/cli.py +0 -0
  10. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/agent/full-review.md +0 -0
  11. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/agent/security-review-memory.md +0 -0
  12. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/entrypoints.yaml +0 -0
  13. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/SKILL.md +0 -0
  14. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/business-logic.md +0 -0
  15. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/code-injection.md +0 -0
  16. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/command-injection.md +0 -0
  17. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/cross-site-request-forgery.md +0 -0
  18. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/cross-site-scripting.md +0 -0
  19. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/hardcoded-secrets.md +0 -0
  20. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/http-response-splitting.md +0 -0
  21. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/improper-authentication.md +0 -0
  22. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/information-exposure.md +0 -0
  23. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/insecure-cryptography.md +0 -0
  24. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/insecure-deserialization.md +0 -0
  25. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
  26. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/insecure-transport.md +0 -0
  27. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/jwt-validation.md +0 -0
  28. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/mass-assignment.md +0 -0
  29. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/missing-authorization.md +0 -0
  30. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/open-redirect.md +0 -0
  31. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/path-traversal.md +0 -0
  32. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/race-condition.md +0 -0
  33. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/replay-attack.md +0 -0
  34. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/server-side-request-forgery.md +0 -0
  35. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/server-side-template-injection.md +0 -0
  36. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/session-fixation.md +0 -0
  37. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/sql-injection.md +0 -0
  38. {codejury-0.14.5 → codejury-0.14.6}/codejury/data/rules/xml-external-entity.md +0 -0
  39. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/__init__.py +0 -0
  40. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/debate.py +0 -0
  41. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/debate_prompts.py +0 -0
  42. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/engine.py +0 -0
  43. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/findings_filter.py +0 -0
  44. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/prompts.py +0 -0
  45. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/rules.py +0 -0
  46. {codejury-0.14.5 → codejury-0.14.6}/codejury/diff/runner.py +0 -0
  47. {codejury-0.14.5 → codejury-0.14.6}/codejury/domain/__init__.py +0 -0
  48. {codejury-0.14.5 → codejury-0.14.6}/codejury/domain/finding.py +0 -0
  49. {codejury-0.14.5 → codejury-0.14.6}/codejury/json_parse.py +0 -0
  50. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/__init__.py +0 -0
  51. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/anthropic.py +0 -0
  52. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/base.py +0 -0
  53. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/factory.py +0 -0
  54. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/litellm.py +0 -0
  55. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/mock.py +0 -0
  56. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/openai.py +0 -0
  57. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/openai_format.py +0 -0
  58. {codejury-0.14.5 → codejury-0.14.6}/codejury/providers/retry.py +0 -0
  59. {codejury-0.14.5 → codejury-0.14.6}/codejury/repo/__init__.py +0 -0
  60. {codejury-0.14.5 → codejury-0.14.6}/codejury/repo/scaffold.py +0 -0
  61. {codejury-0.14.5 → codejury-0.14.6}/codejury/report.py +0 -0
  62. {codejury-0.14.5 → codejury-0.14.6}/codejury/resources.py +0 -0
  63. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/SOURCES.txt +0 -0
  64. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/dependency_links.txt +0 -0
  65. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/entry_points.txt +0 -0
  66. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/requires.txt +0 -0
  67. {codejury-0.14.5 → codejury-0.14.6}/codejury.egg-info/top_level.txt +0 -0
  68. {codejury-0.14.5 → codejury-0.14.6}/setup.cfg +0 -0
  69. {codejury-0.14.5 → codejury-0.14.6}/tests/test_anthropic_provider.py +0 -0
  70. {codejury-0.14.5 → codejury-0.14.6}/tests/test_cli_audit.py +0 -0
  71. {codejury-0.14.5 → codejury-0.14.6}/tests/test_diff_debate.py +0 -0
  72. {codejury-0.14.5 → codejury-0.14.6}/tests/test_diff_engine.py +0 -0
  73. {codejury-0.14.5 → codejury-0.14.6}/tests/test_json_parse.py +0 -0
  74. {codejury-0.14.5 → codejury-0.14.6}/tests/test_litellm_provider.py +0 -0
  75. {codejury-0.14.5 → codejury-0.14.6}/tests/test_openai_format.py +0 -0
  76. {codejury-0.14.5 → codejury-0.14.6}/tests/test_openai_provider.py +0 -0
  77. {codejury-0.14.5 → codejury-0.14.6}/tests/test_repo_scaffold.py +0 -0
  78. {codejury-0.14.5 → codejury-0.14.6}/tests/test_report.py +0 -0
  79. {codejury-0.14.5 → codejury-0.14.6}/tests/test_retry_provider.py +0 -0
  80. {codejury-0.14.5 → codejury-0.14.6}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.14.5
3
+ Version: 0.14.6
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
@@ -80,31 +80,32 @@ def build_repo_model_from_dir(root: str | Path, *, signatures: _Signatures | Non
80
80
  def build_repo_model(root: str | Path, files: dict[str, str], *, signatures: _Signatures | None = None) -> RepoModel:
81
81
  """Build a RepoModel from {path: content}. Files that do not parse are skipped."""
82
82
  sigs = signatures or load_entrypoint_signatures()
83
- entrypoints: list[Entrypoint] = []
83
+ asts: dict[str, ast.Module] = {}
84
84
  for path, content in sorted(files.items()):
85
85
  try:
86
- tree = ast.parse(content)
86
+ asts[path] = ast.parse(content)
87
87
  except SyntaxError:
88
- continue
89
- # an entrypoint must name a reviewable handler function; drop entries
90
- # with no resolvable function (a Django include() mount, or a view the
91
- # AST scan cannot resolve) so the seeded inventory stays clean
92
- entrypoints.extend(e for e in _entrypoints_in(path, tree, sigs) if e.function)
88
+ continue # unparseable file is skipped, not fatal
89
+ entrypoints: list[Entrypoint] = []
90
+ for path, tree in asts.items():
91
+ entrypoints.extend(_decorator_entrypoints_in(path, tree, sigs.decorators))
92
+ # route-call entrypoints (Django path()): resolved across files so an
93
+ # include() mount contributes its sub-routes under the mount's prefix
94
+ entrypoints.extend(_route_call_entrypoints(asts, sigs.calls))
95
+ # an entrypoint must name a reviewable handler function; drop entries with no
96
+ # resolvable function (an unresolved view) so the seeded inventory stays clean
97
+ entrypoints = [e for e in entrypoints if e.function]
93
98
  return RepoModel(root=str(root), files=tuple(sorted(files)), entrypoints=tuple(entrypoints))
94
99
 
95
100
 
96
- def _entrypoints_in(path: str, tree: ast.Module, sigs: _Signatures) -> list[Entrypoint]:
101
+ def _decorator_entrypoints_in(path: str, tree: ast.Module, deco_sigs) -> list[Entrypoint]:
97
102
  out: list[Entrypoint] = []
98
103
  for node in ast.walk(tree):
99
104
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
100
105
  for dec in node.decorator_list:
101
- ep = _decorator_entrypoint(path, node, dec, sigs.decorators)
106
+ ep = _decorator_entrypoint(path, node, dec, deco_sigs)
102
107
  if ep is not None:
103
108
  out.append(ep)
104
- elif isinstance(node, ast.Call):
105
- ep = _call_entrypoint(path, node, sigs.calls)
106
- if ep is not None:
107
- out.append(ep)
108
109
  return out
109
110
 
110
111
 
@@ -136,21 +137,91 @@ def _decorator_entrypoint(path, func, dec, deco_sigs) -> Entrypoint | None:
136
137
  )
137
138
 
138
139
 
139
- def _call_entrypoint(path, call, call_sigs) -> Entrypoint | None:
140
- name = call.func.attr if isinstance(call.func, ast.Attribute) else (
141
- call.func.id if isinstance(call.func, ast.Name) else None
142
- )
143
- sig = next((s for s in call_sigs if name in s["names"]), None)
144
- if sig is None:
140
+ # names of the call that mounts a sub-urlconf (Django include()); a route call
141
+ # whose view position is one of these recurses into the included module
142
+ _INCLUDE_NAMES = frozenset({"include"})
143
+
144
+
145
+ def _call_name(call: ast.Call) -> str | None:
146
+ f = call.func
147
+ if isinstance(f, ast.Attribute):
148
+ return f.attr
149
+ if isinstance(f, ast.Name):
150
+ return f.id
151
+ return None
152
+
153
+
154
+ def _route_calls(tree: ast.Module, names: set[str]) -> list[ast.Call]:
155
+ return [n for n in ast.walk(tree) if isinstance(n, ast.Call) and _call_name(n) in names]
156
+
157
+
158
+ def _include_module(call: ast.Call) -> str | None:
159
+ """If this route call mounts a sub-urlconf via include('a.b.urls'), return the
160
+ dotted module; else None. Handles include('mod') and include(('mod', 'ns'))."""
161
+ if len(call.args) < 2 or not isinstance(call.args[1], ast.Call):
145
162
  return None
146
- return Entrypoint(
147
- file=path,
148
- line=call.lineno,
149
- function=_view_name(call),
150
- kind=sig["kind"],
151
- framework=sig["framework"],
152
- route=_first_str_arg(call),
153
- )
163
+ inc = call.args[1]
164
+ if _call_name(inc) not in _INCLUDE_NAMES or not inc.args:
165
+ return None
166
+ a = inc.args[0]
167
+ if isinstance(a, ast.Constant) and isinstance(a.value, str):
168
+ return a.value
169
+ if isinstance(a, ast.Tuple) and a.elts and isinstance(a.elts[0], ast.Constant) and isinstance(a.elts[0].value, str):
170
+ return a.elts[0].value
171
+ return None
172
+
173
+
174
+ def _module_to_file(module: str, files: set[str]) -> str | None:
175
+ """Resolve a dotted module ('app.urls') to a file in the repo, by exact path or
176
+ a suffix match (the dotted path may be rooted above the scanned tree)."""
177
+ candidate = module.replace(".", "/") + ".py"
178
+ if candidate in files:
179
+ return candidate
180
+ matches = sorted(f for f in files if f.endswith("/" + candidate))
181
+ return matches[0] if matches else None
182
+
183
+
184
+ def _route_call_entrypoints(asts: dict[str, ast.Module], call_sigs) -> list[Entrypoint]:
185
+ if not call_sigs:
186
+ return []
187
+ names = {n for s in call_sigs for n in s["names"]}
188
+ sig_by_name = {n: s for s in call_sigs for n in s["names"]}
189
+ files = set(asts)
190
+
191
+ # files mounted by another file's include() are not roots; they are emitted
192
+ # only through the include chain, with the mount prefix, never standalone
193
+ mounted: set[str] = set()
194
+ for tree in asts.values():
195
+ for call in _route_calls(tree, names):
196
+ mod = _include_module(call)
197
+ target = _module_to_file(mod, files) if mod else None
198
+ if target:
199
+ mounted.add(target)
200
+
201
+ out: list[Entrypoint] = []
202
+ for path in sorted(asts):
203
+ if path not in mounted:
204
+ _emit_routes(path, "", asts, files, names, sig_by_name, {path}, out)
205
+ return out
206
+
207
+
208
+ def _emit_routes(path, prefix, asts, files, names, sig_by_name, visited, out) -> None:
209
+ for call in _route_calls(asts[path], names):
210
+ seg = _first_str_arg(call)
211
+ mod = _include_module(call)
212
+ if mod:
213
+ target = _module_to_file(mod, files)
214
+ if target and target not in visited:
215
+ _emit_routes(target, prefix + seg, asts, files, names, sig_by_name, visited | {target}, out)
216
+ continue
217
+ view = _view_name(call)
218
+ if not view:
219
+ continue
220
+ sig = sig_by_name[_call_name(call)]
221
+ out.append(Entrypoint(
222
+ file=path, line=call.lineno, function=view,
223
+ kind=sig["kind"], framework=sig["framework"], route=prefix + seg,
224
+ ))
154
225
 
155
226
 
156
227
  def _first_str_arg(call: ast.Call) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.14.5
3
+ Version: 0.14.6
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.14.5"
3
+ version = "0.14.6"
4
4
  description = "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
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -115,6 +115,54 @@ def test_django_path_calls_capture_view_and_route():
115
115
  }
116
116
 
117
117
 
118
+ DJANGO_ROOT = '''
119
+ from django.urls import path, include
120
+ urlpatterns = [
121
+ path("", include("introduction.urls")),
122
+ path("api/", include("api.urls")),
123
+ path("health/", views.health),
124
+ ]
125
+ '''
126
+ DJANGO_INTRO = '''
127
+ from django.urls import path
128
+ from . import views
129
+ urlpatterns = [
130
+ path("home/", views.home),
131
+ path("xss", views.xss),
132
+ ]
133
+ '''
134
+ DJANGO_API = '''
135
+ from django.urls import path, include
136
+ urlpatterns = [
137
+ path("v1/", include("api.v1.urls")),
138
+ ]
139
+ '''
140
+ DJANGO_API_V1 = '''
141
+ from django.urls import path
142
+ urlpatterns = [
143
+ path("users", views.users),
144
+ ]
145
+ '''
146
+
147
+
148
+ def test_django_include_mounts_subroutes_under_prefix():
149
+ model = _model({
150
+ "urls.py": DJANGO_ROOT,
151
+ "introduction/urls.py": DJANGO_INTRO,
152
+ "api/urls.py": DJANGO_API,
153
+ "api/v1/urls.py": DJANGO_API_V1,
154
+ })
155
+ routes = {(e.function, e.route) for e in model.entrypoints}
156
+ assert routes == {
157
+ ("health", "health/"), # direct route on the root urlconf
158
+ ("home", "home/"), # included at "" prefix
159
+ ("xss", "xss"),
160
+ ("users", "api/v1/users"), # nested include: "api/" + "v1/" + "users"
161
+ }
162
+ # the included sub-urlconfs are not also emitted standalone (no bare "users")
163
+ assert all(e.route != "users" for e in model.entrypoints)
164
+
165
+
118
166
  def test_django_include_and_cbv_with_no_resolvable_function_are_dropped():
119
167
  # include() mounts and an unresolved .as_view() carry no handler function;
120
168
  # only the plain function-based view survives, no empty `- -` noise entries
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes