codejury 0.13.1__tar.gz → 0.13.3__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.
- {codejury-0.13.1 → codejury-0.13.3}/PKG-INFO +1 -1
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/debate.py +11 -6
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/engine.py +17 -1
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/retry.py +16 -2
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/PKG-INFO +1 -1
- {codejury-0.13.1 → codejury-0.13.3}/pyproject.toml +1 -1
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_diff_debate.py +22 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_diff_engine.py +22 -2
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_retry_provider.py +28 -1
- {codejury-0.13.1 → codejury-0.13.3}/LICENSE +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/README.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/analysis/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/analysis/repo_model.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/cli.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/agent/full-review.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/agent/security-review-memory.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/entrypoints.yaml +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/SKILL.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/business-logic.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/code-injection.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/command-injection.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/cross-site-request-forgery.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/cross-site-scripting.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/hardcoded-secrets.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/http-response-splitting.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/improper-authentication.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/information-exposure.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/insecure-cryptography.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/insecure-deserialization.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/insecure-transport.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/jwt-validation.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/mass-assignment.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/missing-authorization.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/open-redirect.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/path-traversal.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/race-condition.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/replay-attack.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/server-side-request-forgery.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/server-side-template-injection.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/session-fixation.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/sql-injection.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/data/rules/xml-external-entity.md +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/debate_prompts.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/findings_filter.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/prompts.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/report.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/diff/rules.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/domain/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/domain/finding.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/fullreview/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/fullreview/scaffold.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/infrastructure/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/infrastructure/json_parse.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/__init__.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/anthropic.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/base.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/factory.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/litellm.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/mock.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/openai.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/providers/openai_format.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury/resources.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/SOURCES.txt +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/setup.cfg +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_cli_audit.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_diff_report.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_full_review_scaffold.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_json_parse.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_litellm_provider.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_openai_format.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_openai_provider.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_repo_model.py +0 -0
- {codejury-0.13.1 → codejury-0.13.3}/tests/test_rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,16 @@ 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
|
+
carrying a ``findings`` key (an empty ``{"findings": []}`` when there is
|
|
23
|
+
nothing to report), so a reply that yields no object, or an object without
|
|
24
|
+
that key, is a failure, not a pass."""
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
class AuditRunner:
|
|
18
28
|
def __init__(self, *, provider: Provider, model: str, max_tokens: int = 4096) -> None:
|
|
19
29
|
self._provider = provider
|
|
@@ -29,5 +39,11 @@ class AuditRunner:
|
|
|
29
39
|
model=self._model,
|
|
30
40
|
max_tokens=self._max_tokens,
|
|
31
41
|
)
|
|
32
|
-
obj = extract_json_object(result.text)
|
|
42
|
+
obj = extract_json_object(result.text)
|
|
43
|
+
if obj is None or "findings" not in obj:
|
|
44
|
+
raise AuditError(
|
|
45
|
+
"the model reply was not a valid audit result (no JSON object, or a "
|
|
46
|
+
"JSON object without a `findings` key); treating it as a failed "
|
|
47
|
+
"audit rather than a clean pass"
|
|
48
|
+
)
|
|
33
49
|
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.
|
|
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
|
-
|
|
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.
|
|
3
|
+
Version: 0.13.3
|
|
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.
|
|
3
|
+
version = "0.13.3"
|
|
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,28 @@ 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
|
|
64
|
-
|
|
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)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_engine_raises_on_wrong_shape_json():
|
|
77
|
+
# valid JSON but no `findings` key is a malformed reply, not a clean audit
|
|
78
|
+
import pytest
|
|
79
|
+
|
|
80
|
+
from codejury.diff.engine import AuditError
|
|
81
|
+
|
|
82
|
+
for bad in ("{}", '{"result": "ok"}'):
|
|
83
|
+
with pytest.raises(AuditError):
|
|
84
|
+
AuditRunner(provider=MockProvider(default=bad), model="m").run(_DIFF)
|
|
65
85
|
|
|
66
86
|
|
|
67
87
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|