codejury 0.14.1__tar.gz → 0.14.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.14.1 → codejury-0.14.3}/PKG-INFO +1 -1
- {codejury-0.14.1 → codejury-0.14.3}/codejury/cli.py +10 -83
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/debate.py +35 -10
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/engine.py +1 -1
- codejury-0.14.3/codejury/diff/runner.py +85 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/PKG-INFO +1 -1
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/SOURCES.txt +4 -4
- {codejury-0.14.1 → codejury-0.14.3}/pyproject.toml +1 -1
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_cli_audit.py +8 -7
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_diff_debate.py +23 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_json_parse.py +1 -1
- codejury-0.14.1/tests/test_diff_report.py → codejury-0.14.3/tests/test_report.py +1 -1
- codejury-0.14.1/codejury/infrastructure/__init__.py +0 -4
- {codejury-0.14.1 → codejury-0.14.3}/LICENSE +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/README.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/__init__.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/agent/full-review.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/agent/security-review-memory.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/entrypoints.yaml +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/SKILL.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/business-logic.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/code-injection.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/command-injection.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/cross-site-request-forgery.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/cross-site-scripting.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/hardcoded-secrets.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/http-response-splitting.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/improper-authentication.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/information-exposure.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-cryptography.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-deserialization.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/insecure-transport.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/jwt-validation.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/mass-assignment.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/missing-authorization.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/open-redirect.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/path-traversal.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/race-condition.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/replay-attack.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/server-side-request-forgery.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/server-side-template-injection.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/session-fixation.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/sql-injection.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/data/rules/xml-external-entity.md +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/__init__.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/debate_prompts.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/findings_filter.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/prompts.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/diff/rules.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/domain/__init__.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/domain/finding.py +0 -0
- {codejury-0.14.1/codejury/infrastructure → codejury-0.14.3/codejury}/json_parse.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/__init__.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/anthropic.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/base.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/factory.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/litellm.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/mock.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/openai.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/openai_format.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/providers/retry.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/__init__.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/model.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/repo/scaffold.py +0 -0
- {codejury-0.14.1/codejury/diff → codejury-0.14.3/codejury}/report.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury/resources.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/setup.cfg +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_diff_engine.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_litellm_provider.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_openai_format.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_openai_provider.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_repo_model.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_repo_scaffold.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_retry_provider.py +0 -0
- {codejury-0.14.1 → codejury-0.14.3}/tests/test_rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.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,14 +1,15 @@
|
|
|
1
|
-
"""codejury CLI.
|
|
1
|
+
"""codejury CLI: thin argument parsing and dispatch.
|
|
2
2
|
|
|
3
|
-
Two
|
|
3
|
+
Two paths matched to their nature:
|
|
4
4
|
|
|
5
|
-
- ``
|
|
6
|
-
call (standard) or the adversarial Finder/Challenger/Judge pass.
|
|
7
|
-
- ``
|
|
5
|
+
- ``review diff`` runs the coded diff engine over a unified diff: a single
|
|
6
|
+
balanced call (standard) or the adversarial Finder/Challenger/Judge pass.
|
|
7
|
+
- ``review repo`` scaffolds a workspace and prints the methodology for an
|
|
8
8
|
interactive agent to run a whole-repo review (it does not run an LLM pipeline,
|
|
9
9
|
which a single call cannot do for a whole codebase).
|
|
10
10
|
|
|
11
|
-
``dry-run`` exercises the
|
|
11
|
+
``review diff --dry-run`` exercises the engine with a mock provider and no key.
|
|
12
|
+
The audit orchestration itself lives in ``codejury.diff.runner``.
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
from __future__ import annotations
|
|
@@ -17,20 +18,9 @@ import argparse
|
|
|
17
18
|
import subprocess
|
|
18
19
|
import sys
|
|
19
20
|
|
|
20
|
-
import dataclasses
|
|
21
|
-
|
|
22
21
|
from codejury import __version__
|
|
23
|
-
from codejury.
|
|
24
|
-
from codejury.diff.
|
|
25
|
-
from codejury.diff.findings_filter import FindingsFilter
|
|
26
|
-
from codejury.diff.report import gate, render
|
|
27
|
-
from codejury.diff.rules import allowed_categories, normalize_category
|
|
28
|
-
from codejury.domain.finding import Finding
|
|
29
|
-
|
|
30
|
-
# A diff larger than this is audited file-by-file so a big PR does not overflow
|
|
31
|
-
# the model's context and silently truncate the reply.
|
|
32
|
-
_MAX_DIFF_CHARS = 60_000
|
|
33
|
-
from codejury.repo.scaffold import scaffold
|
|
22
|
+
from codejury.report import gate, render
|
|
23
|
+
from codejury.diff.runner import audit_diff
|
|
34
24
|
from codejury.providers.factory import (
|
|
35
25
|
DEFAULT_API_BASE,
|
|
36
26
|
DEFAULT_API_KEY,
|
|
@@ -42,75 +32,12 @@ from codejury.providers.factory import (
|
|
|
42
32
|
make_provider,
|
|
43
33
|
)
|
|
44
34
|
from codejury.providers.mock import MockProvider
|
|
35
|
+
from codejury.repo.scaffold import scaffold
|
|
45
36
|
|
|
46
37
|
_FORMATS = ("text", "markdown", "json", "sarif")
|
|
47
38
|
_FAIL_ON = ("critical", "high", "medium", "low")
|
|
48
39
|
|
|
49
40
|
|
|
50
|
-
def _split_diff_by_file(diff: str) -> list[str]:
|
|
51
|
-
"""Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
|
|
52
|
-
chunks: list[str] = []
|
|
53
|
-
cur: list[str] = []
|
|
54
|
-
for line in diff.splitlines(keepends=True):
|
|
55
|
-
if line.startswith("diff --git ") and cur:
|
|
56
|
-
chunks.append("".join(cur))
|
|
57
|
-
cur = []
|
|
58
|
-
cur.append(line)
|
|
59
|
-
if cur:
|
|
60
|
-
chunks.append("".join(cur))
|
|
61
|
-
return chunks or ([diff] if diff.strip() else [])
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _dedup_findings(findings: list[Finding]) -> list[Finding]:
|
|
65
|
-
seen: set = set()
|
|
66
|
-
out: list[Finding] = []
|
|
67
|
-
for f in findings:
|
|
68
|
-
k = (f.file, f.line, f.category, f.description)
|
|
69
|
-
if k not in seen:
|
|
70
|
-
seen.add(k)
|
|
71
|
-
out.append(f)
|
|
72
|
-
return out
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def audit_diff(
|
|
76
|
-
diff: str,
|
|
77
|
-
*,
|
|
78
|
-
provider,
|
|
79
|
-
model: str,
|
|
80
|
-
mode: str = "standard",
|
|
81
|
-
max_rounds: int = 3,
|
|
82
|
-
filter_findings: bool = True,
|
|
83
|
-
finder_model: str | None = None,
|
|
84
|
-
challenger_model: str | None = None,
|
|
85
|
-
judge_model: str | None = None,
|
|
86
|
-
exclude_paths: tuple[str, ...] = (),
|
|
87
|
-
) -> tuple[list[Finding], list[tuple[Finding, str]]]:
|
|
88
|
-
"""Audit a diff and return (kept findings, dropped (finding, reason)).
|
|
89
|
-
|
|
90
|
-
A diff over the size budget is audited one file at a time so it does not
|
|
91
|
-
overflow the context. Finding categories are normalized to the rule-id set.
|
|
92
|
-
``exclude_paths`` are operator-supplied path substrings to drop."""
|
|
93
|
-
def _run_one(d: str) -> list[Finding]:
|
|
94
|
-
if mode == "adversarial":
|
|
95
|
-
return AdversarialAuditRunner(
|
|
96
|
-
provider=provider, model=model,
|
|
97
|
-
finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
|
|
98
|
-
).run(d, max_rounds=max_rounds).findings
|
|
99
|
-
return AuditRunner(provider=provider, model=model).run(d)
|
|
100
|
-
|
|
101
|
-
if len(diff) > _MAX_DIFF_CHARS:
|
|
102
|
-
chunks = _split_diff_by_file(diff)
|
|
103
|
-
findings = _dedup_findings([f for c in chunks for f in _run_one(c)])
|
|
104
|
-
else:
|
|
105
|
-
findings = _run_one(diff)
|
|
106
|
-
|
|
107
|
-
allowed = set(allowed_categories())
|
|
108
|
-
findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
|
|
109
|
-
if filter_findings:
|
|
110
|
-
return FindingsFilter(exclude_paths=exclude_paths).filter(findings)
|
|
111
|
-
return findings, []
|
|
112
|
-
|
|
113
|
-
|
|
114
41
|
def _read_diff(args) -> str:
|
|
115
42
|
if args.diff_file:
|
|
116
43
|
with open(args.diff_file, encoding="utf-8") as f:
|
|
@@ -21,7 +21,7 @@ from codejury.diff.debate_prompts import (
|
|
|
21
21
|
)
|
|
22
22
|
from codejury.diff.rules import rules_for_diff
|
|
23
23
|
from codejury.domain.finding import Finding, findings_from_list
|
|
24
|
-
from codejury.
|
|
24
|
+
from codejury.json_parse import extract_json_object
|
|
25
25
|
from codejury.providers.base import Message, Provider
|
|
26
26
|
|
|
27
27
|
|
|
@@ -56,6 +56,28 @@ def _dedup(items: list[dict]) -> list[dict]:
|
|
|
56
56
|
return out
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
_DISMISS_VERDICTS = frozenset({"dismiss", "dismissed", "false_positive", "reject", "rejected"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _loc(d: dict) -> str:
|
|
63
|
+
line = d.get("line")
|
|
64
|
+
return f"{d.get('file')}:{line}" if line else str(d.get("file") or "")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _apply_dismissals(findings: list[dict], rebuttals: list[dict]) -> list[dict]:
|
|
68
|
+
"""Drop findings the challenger dismissed. The challenger is recall-safe (it
|
|
69
|
+
dismisses only when the diff shows a safe pattern: a parameterized query,
|
|
70
|
+
basename, an allowlist, shell=False), so honoring its dismissals is sound even
|
|
71
|
+
when the judge is unavailable."""
|
|
72
|
+
dismissed = {
|
|
73
|
+
str(r.get("target")) for r in rebuttals
|
|
74
|
+
if str(r.get("verdict", "")).strip().lower() in _DISMISS_VERDICTS
|
|
75
|
+
}
|
|
76
|
+
if not dismissed:
|
|
77
|
+
return findings
|
|
78
|
+
return [f for f in findings if _loc(f) not in dismissed and str(f.get("file") or "") not in dismissed]
|
|
79
|
+
|
|
80
|
+
|
|
59
81
|
class AdversarialAuditRunner:
|
|
60
82
|
def __init__(
|
|
61
83
|
self,
|
|
@@ -116,16 +138,19 @@ class AdversarialAuditRunner:
|
|
|
116
138
|
rebuttals = _dicts(challenger.get("rebuttals"))
|
|
117
139
|
new_findings = _dicts(challenger.get("new_findings"))
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
jp = judge_prompt(diff, finder_findings, rebuttals, new_findings, context=context)
|
|
142
|
+
verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
|
|
143
|
+
if not judge_ok:
|
|
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
|
|
147
|
+
verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
|
|
124
148
|
if not judge_ok:
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
149
|
+
# 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)
|
|
153
|
+
fallback = _dedup(_apply_dismissals(finder_findings, rebuttals) + new_findings)
|
|
129
154
|
judged = AdversarialResult(findings=findings_from_list(fallback), rounds=rounds, degraded=True)
|
|
130
155
|
degraded = True
|
|
131
156
|
break
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from codejury.domain.finding import Finding, findings_from_list
|
|
11
11
|
from codejury.diff.prompts import SYSTEM, standard_audit_prompt
|
|
12
12
|
from codejury.diff.rules import rules_for_diff
|
|
13
|
-
from codejury.
|
|
13
|
+
from codejury.json_parse import extract_json_object
|
|
14
14
|
from codejury.providers.base import Message, Provider
|
|
15
15
|
|
|
16
16
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Diff-audit orchestration: run a diff through the engine and clean the result.
|
|
2
|
+
|
|
3
|
+
The library entry point behind `review diff`. Picks the standard or adversarial
|
|
4
|
+
engine, audits a large diff one file at a time so a big PR does not overflow the
|
|
5
|
+
model context, normalizes finding categories onto the rule-id set, and applies
|
|
6
|
+
the false-positive filter. Kept out of the CLI so it can be called as a library.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
|
|
13
|
+
from codejury.diff.debate import AdversarialAuditRunner
|
|
14
|
+
from codejury.diff.engine import AuditRunner
|
|
15
|
+
from codejury.diff.findings_filter import FindingsFilter
|
|
16
|
+
from codejury.diff.rules import allowed_categories, normalize_category
|
|
17
|
+
from codejury.domain.finding import Finding
|
|
18
|
+
|
|
19
|
+
# A diff larger than this is audited file-by-file so a big PR does not overflow
|
|
20
|
+
# the model's context and silently truncate the reply.
|
|
21
|
+
_MAX_DIFF_CHARS = 60_000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def split_diff_by_file(diff: str) -> list[str]:
|
|
25
|
+
"""Split a unified diff into one diff per file (`diff --git ...` boundaries)."""
|
|
26
|
+
chunks: list[str] = []
|
|
27
|
+
cur: list[str] = []
|
|
28
|
+
for line in diff.splitlines(keepends=True):
|
|
29
|
+
if line.startswith("diff --git ") and cur:
|
|
30
|
+
chunks.append("".join(cur))
|
|
31
|
+
cur = []
|
|
32
|
+
cur.append(line)
|
|
33
|
+
if cur:
|
|
34
|
+
chunks.append("".join(cur))
|
|
35
|
+
return chunks or ([diff] if diff.strip() else [])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dedup_findings(findings: list[Finding]) -> list[Finding]:
|
|
39
|
+
seen: set = set()
|
|
40
|
+
out: list[Finding] = []
|
|
41
|
+
for f in findings:
|
|
42
|
+
k = (f.file, f.line, f.category, f.description)
|
|
43
|
+
if k not in seen:
|
|
44
|
+
seen.add(k)
|
|
45
|
+
out.append(f)
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def audit_diff(
|
|
50
|
+
diff: str,
|
|
51
|
+
*,
|
|
52
|
+
provider,
|
|
53
|
+
model: str,
|
|
54
|
+
mode: str = "standard",
|
|
55
|
+
max_rounds: int = 3,
|
|
56
|
+
filter_findings: bool = True,
|
|
57
|
+
finder_model: str | None = None,
|
|
58
|
+
challenger_model: str | None = None,
|
|
59
|
+
judge_model: str | None = None,
|
|
60
|
+
exclude_paths: tuple[str, ...] = (),
|
|
61
|
+
) -> tuple[list[Finding], list[tuple[Finding, str]]]:
|
|
62
|
+
"""Audit a diff and return (kept findings, dropped (finding, reason)).
|
|
63
|
+
|
|
64
|
+
A diff over the size budget is audited one file at a time so it does not
|
|
65
|
+
overflow the context. Finding categories are normalized to the rule-id set.
|
|
66
|
+
``exclude_paths`` are operator-supplied path substrings to drop."""
|
|
67
|
+
def _run_one(d: str) -> list[Finding]:
|
|
68
|
+
if mode == "adversarial":
|
|
69
|
+
return AdversarialAuditRunner(
|
|
70
|
+
provider=provider, model=model,
|
|
71
|
+
finder_model=finder_model, challenger_model=challenger_model, judge_model=judge_model,
|
|
72
|
+
).run(d, max_rounds=max_rounds).findings
|
|
73
|
+
return AuditRunner(provider=provider, model=model).run(d)
|
|
74
|
+
|
|
75
|
+
if len(diff) > _MAX_DIFF_CHARS:
|
|
76
|
+
chunks = split_diff_by_file(diff)
|
|
77
|
+
findings = dedup_findings([f for c in chunks for f in _run_one(c)])
|
|
78
|
+
else:
|
|
79
|
+
findings = _run_one(diff)
|
|
80
|
+
|
|
81
|
+
allowed = set(allowed_categories())
|
|
82
|
+
findings = [dataclasses.replace(f, category=normalize_category(f.category, allowed)) for f in findings]
|
|
83
|
+
if filter_findings:
|
|
84
|
+
return FindingsFilter(exclude_paths=exclude_paths).filter(findings)
|
|
85
|
+
return findings, []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -3,6 +3,8 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
codejury/__init__.py
|
|
5
5
|
codejury/cli.py
|
|
6
|
+
codejury/json_parse.py
|
|
7
|
+
codejury/report.py
|
|
6
8
|
codejury/resources.py
|
|
7
9
|
codejury.egg-info/PKG-INFO
|
|
8
10
|
codejury.egg-info/SOURCES.txt
|
|
@@ -45,12 +47,10 @@ codejury/diff/debate_prompts.py
|
|
|
45
47
|
codejury/diff/engine.py
|
|
46
48
|
codejury/diff/findings_filter.py
|
|
47
49
|
codejury/diff/prompts.py
|
|
48
|
-
codejury/diff/report.py
|
|
49
50
|
codejury/diff/rules.py
|
|
51
|
+
codejury/diff/runner.py
|
|
50
52
|
codejury/domain/__init__.py
|
|
51
53
|
codejury/domain/finding.py
|
|
52
|
-
codejury/infrastructure/__init__.py
|
|
53
|
-
codejury/infrastructure/json_parse.py
|
|
54
54
|
codejury/providers/__init__.py
|
|
55
55
|
codejury/providers/anthropic.py
|
|
56
56
|
codejury/providers/base.py
|
|
@@ -67,12 +67,12 @@ tests/test_anthropic_provider.py
|
|
|
67
67
|
tests/test_cli_audit.py
|
|
68
68
|
tests/test_diff_debate.py
|
|
69
69
|
tests/test_diff_engine.py
|
|
70
|
-
tests/test_diff_report.py
|
|
71
70
|
tests/test_json_parse.py
|
|
72
71
|
tests/test_litellm_provider.py
|
|
73
72
|
tests/test_openai_format.py
|
|
74
73
|
tests/test_openai_provider.py
|
|
75
74
|
tests/test_repo_model.py
|
|
76
75
|
tests/test_repo_scaffold.py
|
|
76
|
+
tests/test_report.py
|
|
77
77
|
tests/test_retry_provider.py
|
|
78
78
|
tests/test_rules.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codejury"
|
|
3
|
-
version = "0.14.
|
|
3
|
+
version = "0.14.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"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Diff-audit orchestration (codejury.diff.runner) plus the thin CLI surface.
|
|
2
2
|
|
|
3
3
|
A diff over the size budget is split per file and audited one file at a time so a
|
|
4
4
|
big PR does not overflow the model context and silently truncate the reply; the
|
|
@@ -7,7 +7,8 @@ per-file findings are then de-duplicated.
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from codejury.cli import
|
|
10
|
+
from codejury.cli import main
|
|
11
|
+
from codejury.diff.runner import audit_diff, dedup_findings, split_diff_by_file
|
|
11
12
|
from codejury.domain.finding import Finding
|
|
12
13
|
from codejury.providers.mock import MockProvider
|
|
13
14
|
|
|
@@ -16,14 +17,14 @@ _FILE_B = "diff --git a/b.py b/b.py\n@@ -0,0 +1 @@\n+y = 2\n"
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def test_split_diff_by_file():
|
|
19
|
-
chunks =
|
|
20
|
+
chunks = split_diff_by_file(_FILE_A + _FILE_B)
|
|
20
21
|
assert chunks == [_FILE_A, _FILE_B]
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def test_split_diff_empty_and_unbounded():
|
|
24
|
-
assert
|
|
25
|
+
assert split_diff_by_file("") == []
|
|
25
26
|
# a fragment with no `diff --git` boundary is returned as one chunk
|
|
26
|
-
assert
|
|
27
|
+
assert split_diff_by_file("just text\n") == ["just text\n"]
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def test_dedup_findings_collapses_identical():
|
|
@@ -31,12 +32,12 @@ def test_dedup_findings_collapses_identical():
|
|
|
31
32
|
description="d", confidence=0.9)
|
|
32
33
|
g = Finding(file="a.py", line=2, severity="HIGH", category="sql-injection",
|
|
33
34
|
description="d", confidence=0.9)
|
|
34
|
-
assert
|
|
35
|
+
assert dedup_findings([f, f, g]) == [f, g]
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def test_large_diff_is_audited_per_file(monkeypatch):
|
|
38
39
|
# force the chunking path on a small diff
|
|
39
|
-
monkeypatch.setattr("codejury.
|
|
40
|
+
monkeypatch.setattr("codejury.diff.runner._MAX_DIFF_CHARS", 1)
|
|
40
41
|
resp = ('{"findings": [{"file": "a.py", "line": 1, "severity": "HIGH", '
|
|
41
42
|
'"category": "sql_injection", "description": "x", "confidence": 0.9}]}')
|
|
42
43
|
provider = MockProvider(default=resp)
|
|
@@ -156,6 +156,29 @@ def test_provider_exception_degrades_rather_than_crashes():
|
|
|
156
156
|
assert out.degraded is True
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def test_judge_retry_recovers_from_a_transient_unusable_reply():
|
|
160
|
+
# the judge's first reply is unusable, the retry succeeds: not degraded,
|
|
161
|
+
# and the judge verdict is applied normally
|
|
162
|
+
_, out = _run([_finder([_VULN]), _challenger(), "blocked by waf", _judge([_VULN])], max_rounds=1)
|
|
163
|
+
assert out.degraded is False
|
|
164
|
+
assert [f.category for f in out.findings] == ["sql_injection"]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_degraded_fallback_drops_challenger_dismissed_findings():
|
|
168
|
+
# judge stays unusable (both the call and its retry); the degraded fallback
|
|
169
|
+
# must still honor the challenger's recall-safe dismissals rather than pass
|
|
170
|
+
# every finder finding through, which is what inflates false positives
|
|
171
|
+
second = {**_VULN, "line": 5, "category": "xss"}
|
|
172
|
+
_, out = _run(
|
|
173
|
+
[_finder([_VULN, second]),
|
|
174
|
+
_challenger(rebuttals=[{"target": "app.py:5", "verdict": "dismiss", "reason": "output is escaped"}]),
|
|
175
|
+
"blocked", "blocked"], # judge call + its one retry both unusable
|
|
176
|
+
max_rounds=1,
|
|
177
|
+
)
|
|
178
|
+
assert out.degraded is True
|
|
179
|
+
assert [f.category for f in out.findings] == ["sql_injection"] # the dismissed xss at app.py:5 is dropped
|
|
180
|
+
|
|
181
|
+
|
|
159
182
|
def test_per_role_models_are_used():
|
|
160
183
|
provider = MockProvider(responses=[_finder([]), _challenger(), _judge([])], default="{}")
|
|
161
184
|
AdversarialAuditRunner(
|
|
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
|