codejury 0.13.1__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 (81) hide show
  1. {codejury-0.13.1 → codejury-0.13.2}/PKG-INFO +1 -1
  2. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/debate.py +11 -6
  3. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/engine.py +15 -1
  4. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/retry.py +16 -2
  5. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/PKG-INFO +1 -1
  6. {codejury-0.13.1 → codejury-0.13.2}/pyproject.toml +1 -1
  7. {codejury-0.13.1 → codejury-0.13.2}/tests/test_diff_debate.py +22 -0
  8. {codejury-0.13.1 → codejury-0.13.2}/tests/test_diff_engine.py +11 -2
  9. {codejury-0.13.1 → codejury-0.13.2}/tests/test_retry_provider.py +28 -1
  10. {codejury-0.13.1 → codejury-0.13.2}/LICENSE +0 -0
  11. {codejury-0.13.1 → codejury-0.13.2}/README.md +0 -0
  12. {codejury-0.13.1 → codejury-0.13.2}/codejury/__init__.py +0 -0
  13. {codejury-0.13.1 → codejury-0.13.2}/codejury/analysis/__init__.py +0 -0
  14. {codejury-0.13.1 → codejury-0.13.2}/codejury/analysis/repo_model.py +0 -0
  15. {codejury-0.13.1 → codejury-0.13.2}/codejury/cli.py +0 -0
  16. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/agent/full-review.md +0 -0
  17. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/agent/security-review-memory.md +0 -0
  18. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/entrypoints.yaml +0 -0
  19. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/SKILL.md +0 -0
  20. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/business-logic.md +0 -0
  21. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/code-injection.md +0 -0
  22. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/command-injection.md +0 -0
  23. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/cross-site-request-forgery.md +0 -0
  24. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/cross-site-scripting.md +0 -0
  25. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/hardcoded-secrets.md +0 -0
  26. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/http-response-splitting.md +0 -0
  27. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/improper-authentication.md +0 -0
  28. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/information-exposure.md +0 -0
  29. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/insecure-cryptography.md +0 -0
  30. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/insecure-deserialization.md +0 -0
  31. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
  32. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/insecure-transport.md +0 -0
  33. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/jwt-validation.md +0 -0
  34. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/mass-assignment.md +0 -0
  35. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/missing-authorization.md +0 -0
  36. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/open-redirect.md +0 -0
  37. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/path-traversal.md +0 -0
  38. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/race-condition.md +0 -0
  39. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/replay-attack.md +0 -0
  40. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/server-side-request-forgery.md +0 -0
  41. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/server-side-template-injection.md +0 -0
  42. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/session-fixation.md +0 -0
  43. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/sql-injection.md +0 -0
  44. {codejury-0.13.1 → codejury-0.13.2}/codejury/data/rules/xml-external-entity.md +0 -0
  45. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/__init__.py +0 -0
  46. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/debate_prompts.py +0 -0
  47. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/findings_filter.py +0 -0
  48. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/prompts.py +0 -0
  49. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/report.py +0 -0
  50. {codejury-0.13.1 → codejury-0.13.2}/codejury/diff/rules.py +0 -0
  51. {codejury-0.13.1 → codejury-0.13.2}/codejury/domain/__init__.py +0 -0
  52. {codejury-0.13.1 → codejury-0.13.2}/codejury/domain/finding.py +0 -0
  53. {codejury-0.13.1 → codejury-0.13.2}/codejury/fullreview/__init__.py +0 -0
  54. {codejury-0.13.1 → codejury-0.13.2}/codejury/fullreview/scaffold.py +0 -0
  55. {codejury-0.13.1 → codejury-0.13.2}/codejury/infrastructure/__init__.py +0 -0
  56. {codejury-0.13.1 → codejury-0.13.2}/codejury/infrastructure/json_parse.py +0 -0
  57. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/__init__.py +0 -0
  58. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/anthropic.py +0 -0
  59. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/base.py +0 -0
  60. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/factory.py +0 -0
  61. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/litellm.py +0 -0
  62. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/mock.py +0 -0
  63. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/openai.py +0 -0
  64. {codejury-0.13.1 → codejury-0.13.2}/codejury/providers/openai_format.py +0 -0
  65. {codejury-0.13.1 → codejury-0.13.2}/codejury/resources.py +0 -0
  66. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/SOURCES.txt +0 -0
  67. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/dependency_links.txt +0 -0
  68. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/entry_points.txt +0 -0
  69. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/requires.txt +0 -0
  70. {codejury-0.13.1 → codejury-0.13.2}/codejury.egg-info/top_level.txt +0 -0
  71. {codejury-0.13.1 → codejury-0.13.2}/setup.cfg +0 -0
  72. {codejury-0.13.1 → codejury-0.13.2}/tests/test_anthropic_provider.py +0 -0
  73. {codejury-0.13.1 → codejury-0.13.2}/tests/test_cli_audit.py +0 -0
  74. {codejury-0.13.1 → codejury-0.13.2}/tests/test_diff_report.py +0 -0
  75. {codejury-0.13.1 → codejury-0.13.2}/tests/test_full_review_scaffold.py +0 -0
  76. {codejury-0.13.1 → codejury-0.13.2}/tests/test_json_parse.py +0 -0
  77. {codejury-0.13.1 → codejury-0.13.2}/tests/test_litellm_provider.py +0 -0
  78. {codejury-0.13.1 → codejury-0.13.2}/tests/test_openai_format.py +0 -0
  79. {codejury-0.13.1 → codejury-0.13.2}/tests/test_openai_provider.py +0 -0
  80. {codejury-0.13.1 → codejury-0.13.2}/tests/test_repo_model.py +0 -0
  81. {codejury-0.13.1 → codejury-0.13.2}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.13.1
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
@@ -79,12 +79,17 @@ class AdversarialAuditRunner:
79
79
  """Returns (parsed object, ok). ok is False when the response could not be
80
80
  parsed into a JSON object (a provider error page, a blocked request, prose),
81
81
  so the caller can avoid treating an unusable reply as an empty result."""
82
- result = self._provider.complete(
83
- system=system,
84
- messages=[Message(role="user", content=prompt)],
85
- model=model,
86
- max_tokens=self._max_tokens,
87
- )
82
+ try:
83
+ result = self._provider.complete(
84
+ system=system,
85
+ messages=[Message(role="user", content=prompt)],
86
+ model=model,
87
+ max_tokens=self._max_tokens,
88
+ )
89
+ except Exception:
90
+ # a provider error (exhausted retries, blank body, transport failure)
91
+ # is an unusable reply, not an empty result; degrade gracefully
92
+ return {}, False
88
93
  obj = extract_json_object(result.text)
89
94
  return (obj or {}), bool(obj)
90
95
 
@@ -14,6 +14,15 @@ from codejury.infrastructure.json_parse import extract_json_object
14
14
  from codejury.providers.base import Message, Provider
15
15
 
16
16
 
17
+ class AuditError(RuntimeError):
18
+ """The model reply could not be parsed into an audit result.
19
+
20
+ Raised instead of returning an empty findings list, so a failed or blank
21
+ call is never reported as a clean audit. The prompt requires a JSON object
22
+ (an empty ``{"findings": []}`` when there is nothing to report), so a reply
23
+ that yields no object is a failure, not a pass."""
24
+
25
+
17
26
  class AuditRunner:
18
27
  def __init__(self, *, provider: Provider, model: str, max_tokens: int = 4096) -> None:
19
28
  self._provider = provider
@@ -29,5 +38,10 @@ class AuditRunner:
29
38
  model=self._model,
30
39
  max_tokens=self._max_tokens,
31
40
  )
32
- obj = extract_json_object(result.text) or {}
41
+ obj = extract_json_object(result.text)
42
+ if obj is None:
43
+ raise AuditError(
44
+ "could not parse a JSON audit result from the model reply; "
45
+ "treating it as a failed audit rather than a clean pass"
46
+ )
33
47
  return findings_from_list(obj.get("findings"))
@@ -2,7 +2,10 @@
2
2
 
3
3
  Real model calls fail intermittently, for example on timeouts or rate limits.
4
4
  This decorator retries with linear backoff and re-raises the last error once
5
- attempts are exhausted. ``sleep`` is injectable so tests do not actually wait.
5
+ attempts are exhausted. A 200 response with a blank body is treated as a
6
+ transient failure and retried too, since an empty reply is unusable and must
7
+ not be passed downstream as a clean (no-findings) result. ``sleep`` is
8
+ injectable so tests do not actually wait.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
@@ -13,6 +16,10 @@ from typing import Callable
13
16
  from codejury.providers.base import CompletionResult, Message, Provider
14
17
 
15
18
 
19
+ class EmptyResponseError(RuntimeError):
20
+ """The provider returned a blank body on every attempt."""
21
+
22
+
16
23
  class RetryProvider(Provider):
17
24
  def __init__(
18
25
  self,
@@ -45,11 +52,18 @@ class RetryProvider(Provider):
45
52
  ) -> CompletionResult:
46
53
  for attempt in range(1, self._max_attempts + 1):
47
54
  try:
48
- return self._inner.complete(
55
+ result = self._inner.complete(
49
56
  system=system, messages=messages, model=model, max_tokens=max_tokens, cache=cache
50
57
  )
51
58
  except self._retryable:
52
59
  if attempt == self._max_attempts:
53
60
  raise
54
61
  self._sleep(self._base_delay * attempt)
62
+ continue
63
+ if result.text.strip():
64
+ return result
65
+ # 200 OK but blank body: a transient upstream hiccup, retry it
66
+ if attempt == self._max_attempts:
67
+ raise EmptyResponseError("provider returned a blank response after all attempts")
68
+ self._sleep(self._base_delay * attempt)
55
69
  raise AssertionError("unreachable") # pragma: no cover
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.13.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.13.1"
3
+ version = "0.13.2"
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"
@@ -134,6 +134,28 @@ def test_unusable_judge_includes_challenger_independent_findings():
134
134
  assert [f.category for f in out.findings] == ["idor"] and out.degraded is True
135
135
 
136
136
 
137
+ def test_provider_exception_degrades_rather_than_crashes():
138
+ # a raising provider (exhausted retries, transport error) on the judge call
139
+ # must degrade to the unjudged finder set, not propagate and abort the run.
140
+ from codejury.providers.base import CompletionResult, Provider
141
+
142
+ class _RaiseOnJudge(Provider):
143
+ def __init__(self):
144
+ self.calls = 0
145
+
146
+ def complete(self, **kwargs):
147
+ self.calls += 1
148
+ if self.calls == 1:
149
+ return CompletionResult(text=_finder([_VULN]))
150
+ if self.calls == 2:
151
+ return CompletionResult(text=_challenger())
152
+ raise RuntimeError("provider down")
153
+
154
+ out = AdversarialAuditRunner(provider=_RaiseOnJudge(), model="m").run(_DIFF, max_rounds=1)
155
+ assert [f.category for f in out.findings] == ["sql_injection"]
156
+ assert out.degraded is True
157
+
158
+
137
159
  def test_per_role_models_are_used():
138
160
  provider = MockProvider(responses=[_finder([]), _challenger(), _judge([])], default="{}")
139
161
  AdversarialAuditRunner(
@@ -60,8 +60,17 @@ def test_engine_empty_on_no_findings():
60
60
  assert AuditRunner(provider=MockProvider(default='{"findings": []}'), model="m").run(_DIFF) == []
61
61
 
62
62
 
63
- def test_engine_tolerates_garbage_reply():
64
- assert AuditRunner(provider=MockProvider(default="not json"), model="m").run(_DIFF) == []
63
+ def test_engine_raises_on_unparseable_reply():
64
+ # an unusable reply (provider error page, blank body, prose) must not be
65
+ # reported as a clean audit; it is a failure
66
+ import pytest
67
+
68
+ from codejury.diff.engine import AuditError
69
+
70
+ with pytest.raises(AuditError):
71
+ AuditRunner(provider=MockProvider(default="not json"), model="m").run(_DIFF)
72
+ with pytest.raises(AuditError):
73
+ AuditRunner(provider=MockProvider(default=""), model="m").run(_DIFF)
65
74
 
66
75
 
67
76
  def test_prompt_carries_diff_focus_and_do_not_report():
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from codejury.providers.base import CompletionResult, Message, Provider
4
- from codejury.providers.retry import RetryProvider
4
+ from codejury.providers.retry import EmptyResponseError, RetryProvider
5
5
 
6
6
 
7
7
  class _Flaky(Provider):
@@ -47,3 +47,30 @@ def test_no_retry_on_first_success():
47
47
  )
48
48
  assert inner.calls == 1
49
49
  assert slept == []
50
+
51
+
52
+ class _Blank(Provider):
53
+ """Returns a blank body `blank_times` times, then a real reply."""
54
+
55
+ def __init__(self, blank_times):
56
+ self._blank_times = blank_times
57
+ self.calls = 0
58
+
59
+ def complete(self, **kwargs):
60
+ self.calls += 1
61
+ return CompletionResult(text="" if self.calls <= self._blank_times else "ok")
62
+
63
+
64
+ def test_retries_blank_body_then_succeeds():
65
+ inner = _Blank(blank_times=1)
66
+ provider = RetryProvider(inner, max_attempts=3, sleep=lambda _: None)
67
+ assert _call(provider).text == "ok" # the blank 200 is retried, not passed through
68
+ assert inner.calls == 2
69
+
70
+
71
+ def test_raises_when_body_blank_every_attempt():
72
+ inner = _Blank(blank_times=5)
73
+ provider = RetryProvider(inner, max_attempts=3, sleep=lambda _: None)
74
+ with pytest.raises(EmptyResponseError):
75
+ _call(provider)
76
+ assert inner.calls == 3
File without changes
File without changes
File without changes
File without changes
File without changes