codejury 0.15.0__tar.gz → 0.16.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 (84) hide show
  1. {codejury-0.15.0 → codejury-0.16.1}/PKG-INFO +1 -1
  2. {codejury-0.15.0 → codejury-0.16.1}/codejury/cli.py +2 -0
  3. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/agent/repo-review.md +4 -1
  4. codejury-0.16.1/codejury/data/frameworks/django.md +30 -0
  5. codejury-0.16.1/codejury/data/languages/python.md +24 -0
  6. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/engine.py +3 -1
  7. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/prompts.py +3 -1
  8. codejury-0.16.1/codejury/guides.py +97 -0
  9. {codejury-0.15.0 → codejury-0.16.1}/codejury/repo/scaffold.py +40 -0
  10. {codejury-0.15.0 → codejury-0.16.1}/codejury/resources.py +3 -1
  11. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/PKG-INFO +1 -1
  12. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/SOURCES.txt +4 -0
  13. {codejury-0.15.0 → codejury-0.16.1}/pyproject.toml +1 -1
  14. {codejury-0.15.0 → codejury-0.16.1}/tests/test_diff_engine.py +2 -1
  15. codejury-0.16.1/tests/test_guides.py +45 -0
  16. {codejury-0.15.0 → codejury-0.16.1}/tests/test_repo_scaffold.py +20 -0
  17. {codejury-0.15.0 → codejury-0.16.1}/LICENSE +0 -0
  18. {codejury-0.15.0 → codejury-0.16.1}/README.md +0 -0
  19. {codejury-0.15.0 → codejury-0.16.1}/codejury/__init__.py +0 -0
  20. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/agent/security-review-memory.md +0 -0
  21. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/entrypoints.yaml +0 -0
  22. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/SKILL.md +0 -0
  23. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/business-logic.md +0 -0
  24. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/code-injection.md +0 -0
  25. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/command-injection.md +0 -0
  26. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/cross-site-request-forgery.md +0 -0
  27. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/cross-site-scripting.md +0 -0
  28. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/hardcoded-secrets.md +0 -0
  29. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/http-response-splitting.md +0 -0
  30. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/improper-authentication.md +0 -0
  31. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/information-exposure.md +0 -0
  32. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/insecure-cryptography.md +0 -0
  33. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/insecure-deserialization.md +0 -0
  34. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
  35. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/insecure-transport.md +0 -0
  36. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/jwt-validation.md +0 -0
  37. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/mass-assignment.md +0 -0
  38. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/missing-authorization.md +0 -0
  39. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/open-redirect.md +0 -0
  40. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/path-traversal.md +0 -0
  41. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/race-condition.md +0 -0
  42. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/replay-attack.md +0 -0
  43. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/server-side-request-forgery.md +0 -0
  44. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/server-side-template-injection.md +0 -0
  45. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/session-fixation.md +0 -0
  46. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/sql-injection.md +0 -0
  47. {codejury-0.15.0 → codejury-0.16.1}/codejury/data/rules/xml-external-entity.md +0 -0
  48. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/__init__.py +0 -0
  49. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/debate.py +0 -0
  50. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/debate_prompts.py +0 -0
  51. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/findings_filter.py +0 -0
  52. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/rules.py +0 -0
  53. {codejury-0.15.0 → codejury-0.16.1}/codejury/diff/runner.py +0 -0
  54. {codejury-0.15.0 → codejury-0.16.1}/codejury/domain/__init__.py +0 -0
  55. {codejury-0.15.0 → codejury-0.16.1}/codejury/domain/finding.py +0 -0
  56. {codejury-0.15.0 → codejury-0.16.1}/codejury/json_parse.py +0 -0
  57. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/__init__.py +0 -0
  58. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/anthropic.py +0 -0
  59. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/base.py +0 -0
  60. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/factory.py +0 -0
  61. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/litellm.py +0 -0
  62. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/mock.py +0 -0
  63. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/openai.py +0 -0
  64. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/openai_format.py +0 -0
  65. {codejury-0.15.0 → codejury-0.16.1}/codejury/providers/retry.py +0 -0
  66. {codejury-0.15.0 → codejury-0.16.1}/codejury/repo/__init__.py +0 -0
  67. {codejury-0.15.0 → codejury-0.16.1}/codejury/repo/model.py +0 -0
  68. {codejury-0.15.0 → codejury-0.16.1}/codejury/report.py +0 -0
  69. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/dependency_links.txt +0 -0
  70. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/entry_points.txt +0 -0
  71. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/requires.txt +0 -0
  72. {codejury-0.15.0 → codejury-0.16.1}/codejury.egg-info/top_level.txt +0 -0
  73. {codejury-0.15.0 → codejury-0.16.1}/setup.cfg +0 -0
  74. {codejury-0.15.0 → codejury-0.16.1}/tests/test_anthropic_provider.py +0 -0
  75. {codejury-0.15.0 → codejury-0.16.1}/tests/test_cli_audit.py +0 -0
  76. {codejury-0.15.0 → codejury-0.16.1}/tests/test_diff_debate.py +0 -0
  77. {codejury-0.15.0 → codejury-0.16.1}/tests/test_json_parse.py +0 -0
  78. {codejury-0.15.0 → codejury-0.16.1}/tests/test_litellm_provider.py +0 -0
  79. {codejury-0.15.0 → codejury-0.16.1}/tests/test_openai_format.py +0 -0
  80. {codejury-0.15.0 → codejury-0.16.1}/tests/test_openai_provider.py +0 -0
  81. {codejury-0.15.0 → codejury-0.16.1}/tests/test_repo_model.py +0 -0
  82. {codejury-0.15.0 → codejury-0.16.1}/tests/test_report.py +0 -0
  83. {codejury-0.15.0 → codejury-0.16.1}/tests/test_retry_provider.py +0 -0
  84. {codejury-0.15.0 → codejury-0.16.1}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -133,6 +133,8 @@ def _dispatch(args, parser) -> int:
133
133
  res = scaffold(args.directory, args.workspace)
134
134
  print(f"Workspace: {res.workspace}", file=sys.stderr)
135
135
  print(f"Seeded {res.entrypoints} entrypoints into {res.workspace}/entrypoints/_entrypoints.md", file=sys.stderr)
136
+ if res.guides:
137
+ print(f"Detected stack ({', '.join(res.guides)}); notes in {res.workspace}/_stack.md", file=sys.stderr)
136
138
  print(f"Memory: {res.memory_path}", file=sys.stderr)
137
139
  print("\nRun this review with an interactive agent (Claude Code / Codex) using the methodology below.\n")
138
140
  print(res.methodology)
@@ -21,7 +21,10 @@ Workspace: `<workspace>/<project>/` (created for you), holding `entrypoints/`,
21
21
  2. Read `entrypoints/_entrypoints.md` (seeded for you from a deterministic AST
22
22
  scan) as a *starting* map of the attack surface. It lists HTTP routes and CLI
23
23
  commands only, a subset, not the whole surface (see "Map the attack surface").
24
- 3. Read the relevant rule files under the shipped `rules/` for the target's stack
24
+ 3. Read `_stack.md` (seeded): the detected languages and frameworks and review
25
+ notes for them, so you know where this stack's entrypoints, sinks, and auth
26
+ checks live. If it matched nothing, lean on your own knowledge of the stack.
27
+ 4. Read the relevant rule files under the shipped `rules/` for the target's stack
25
28
  (sql-injection, idor, ssrf, authentication-jwt, insecure-deserialization, ...).
26
29
 
27
30
  ## Map the attack surface
@@ -0,0 +1,30 @@
1
+ ---
2
+ id: django
3
+ title: Django
4
+ kind: framework
5
+ detect:
6
+ files: ["*urls.py", "manage.py", "*settings.py"]
7
+ manifest: ["django"]
8
+ imports: ["from django", "import django"]
9
+ ---
10
+ # Django — review notes
11
+
12
+ ## Entrypoints
13
+ - Routes live in `urls.py`: `path()` / `re_path()` map a URL to a view.
14
+ `include('app.urls')` mounts a sub-urlconf and the URL prefix accumulates.
15
+ Class-based views are wired as `SomeView.as_view()`.
16
+ - Also: Django REST Framework viewsets/routers/serializers, management commands,
17
+ signals, and middleware.
18
+
19
+ ## Authorization / IDOR
20
+ - Auth is enforced by decorators (`@login_required`), DRF permission classes, or
21
+ middleware. Note where it is and where it is missing.
22
+ - Classic IDOR shape: `Model.objects.get(pk=<user input>)` (or `filter(id=...)`)
23
+ with no owner/tenant scoping, then returned to the caller. Inspect every object
24
+ fetch keyed by a user-supplied id.
25
+
26
+ ## Common sinks / gotchas
27
+ - 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;
30
+ a hardcoded `SECRET_KEY`.
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: python
3
+ title: Python
4
+ kind: language
5
+ detect:
6
+ files: ["*.py"]
7
+ manifest: []
8
+ imports: []
9
+ ---
10
+ # Python — review notes
11
+
12
+ Where untrusted input enters (beyond web routes, which the framework guide covers):
13
+ CLI (`argparse`/`click`), scheduled jobs, queue consumers, and any function fed an
14
+ external value. Non-HTTP sources matter as much as routes:
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;
20
+ - network: `requests.get(user_url)` and friends (SSRF).
21
+
22
+ Common sinks: string-built SQL handed to a DB cursor or ORM `.raw()`/`.extra()`,
23
+ a shell command, `eval`/`exec`, a user-controlled file path, a fetch of a
24
+ user-controlled URL, and template rendering of user input.
@@ -12,6 +12,7 @@ from codejury.diff.prompts import SYSTEM, standard_audit_prompt
12
12
  from codejury.diff.rules import rules_for_diff
13
13
  from codejury.json_parse import extract_json_object
14
14
  from codejury.providers.base import Message, Provider
15
+ from codejury.guides import guides_for_diff
15
16
 
16
17
 
17
18
  class AuditError(RuntimeError):
@@ -33,9 +34,10 @@ class AuditRunner:
33
34
  def run(self, diff: str, *, rules: str = "", context: str = "") -> list[Finding]:
34
35
  if not rules:
35
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
36
38
  result = self._provider.complete(
37
39
  system=SYSTEM,
38
- messages=[Message(role="user", content=standard_audit_prompt(diff, rules=rules, context=context))],
40
+ messages=[Message(role="user", content=standard_audit_prompt(diff, rules=rules, context=context, stack=stack))],
39
41
  model=self._model,
40
42
  max_tokens=self._max_tokens,
41
43
  )
@@ -57,7 +57,8 @@ def category_block() -> str:
57
57
  )
58
58
 
59
59
 
60
- def standard_audit_prompt(diff: str, *, rules: str = "", context: str = "") -> str:
60
+ def standard_audit_prompt(diff: str, *, rules: str = "", context: str = "", stack: str = "") -> str:
61
+ stack_block = f"Conventions of the target's language/framework:\n{stack}\n\n" if stack else ""
61
62
  rules_block = f"Relevant security rules for reference:\n{rules}\n\n" if rules else ""
62
63
  context_block = (
63
64
  f"Surrounding code for tracing where values come from (not under review):\n"
@@ -69,6 +70,7 @@ def standard_audit_prompt(diff: str, *, rules: str = "", context: str = "") -> s
69
70
  "Review the following code change for security vulnerabilities.\n\n"
70
71
  f"{FOCUS}\n{DO_NOT_REPORT}\n"
71
72
  f"{category_block()}"
73
+ f"{stack_block}"
72
74
  f"{rules_block}"
73
75
  f"Code change (unified diff):\n```diff\n{diff}\n```\n\n"
74
76
  f"{context_block}"
@@ -0,0 +1,97 @@
1
+ """Language and framework review guides as data.
2
+
3
+ Each `data/languages/*.md` and `data/frameworks/*.md` is a knowledge unit: YAML
4
+ frontmatter declaring how to detect the language or framework in a target repo
5
+ (file-name globs, dependency-manifest substrings, import markers), and a body of
6
+ review guidance (where input enters, common sinks, auth conventions, gotchas).
7
+
8
+ Selection is generic: a guide applies when its detect signals fire on the repo.
9
+ Adding a language or framework is a drop-in file under the right directory, no
10
+ code change, which keeps the unbounded language/framework axis out of code.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import fnmatch
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ import yaml
21
+
22
+ from codejury.resources import FRAMEWORKS_DIR, LANGUAGES_DIR
23
+
24
+ _DIFF_PATH = re.compile(r"^(?:\+\+\+ b/|diff --git a/\S+ b/)(\S+)", re.MULTILINE)
25
+
26
+
27
+ @dataclass(frozen=True, kw_only=True)
28
+ class Guide:
29
+ id: str
30
+ kind: str # "language" or "framework"
31
+ title: str
32
+ detect_files: tuple[str, ...]
33
+ detect_manifest: tuple[str, ...]
34
+ detect_imports: tuple[str, ...]
35
+ body: str
36
+
37
+
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()
47
+ detect = meta.get("detect", {}) or {}
48
+ return Guide(
49
+ id=str(meta.get("id", path.stem)),
50
+ kind=kind,
51
+ title=str(meta.get("title", path.stem)),
52
+ detect_files=tuple(str(f) for f in detect.get("files", [])),
53
+ detect_manifest=tuple(str(m).lower() for m in detect.get("manifest", [])),
54
+ detect_imports=tuple(str(i) for i in detect.get("imports", [])),
55
+ body=body,
56
+ )
57
+
58
+
59
+ def load_guides(languages_dir=LANGUAGES_DIR, frameworks_dir=FRAMEWORKS_DIR) -> list[Guide]:
60
+ out: list[Guide] = []
61
+ 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"]
65
+ return out
66
+
67
+
68
+ def _matches(guide: Guide, files: list[str], text: str) -> bool:
69
+ if any(fnmatch.fnmatch(f, pat) for pat in guide.detect_files for f in files):
70
+ return True
71
+ if any(m in text for m in guide.detect_manifest):
72
+ return True
73
+ if any(i in text for i in guide.detect_imports):
74
+ return True
75
+ return False
76
+
77
+
78
+ def select_guides(files, *, text: str = "", guides: list[Guide] | None = None) -> list[Guide]:
79
+ """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
81
+ manifest substrings and import markers (a repo's manifests, or a diff body)."""
82
+ pool = load_guides() if guides is None else guides
83
+ file_list = list(files)
84
+ blob = text.lower()
85
+ 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)
@@ -13,12 +13,19 @@ from __future__ import annotations
13
13
  from dataclasses import dataclass, field
14
14
  from pathlib import Path
15
15
 
16
+ from codejury.guides import Guide, select_guides
16
17
  from codejury.repo.model import build_repo_model_from_dir
17
18
  from codejury.resources import AGENT_DIR
18
19
 
19
20
  _METHODOLOGY = AGENT_DIR / "repo-review.md"
20
21
  _MEMORY_TEMPLATE = AGENT_DIR / "security-review-memory.md"
21
22
 
23
+ # top-level dependency manifests scanned to detect the stack (content, not names)
24
+ _MANIFESTS = (
25
+ "requirements.txt", "requirements-dev.txt", "pyproject.toml", "setup.py", "Pipfile",
26
+ "package.json", "go.mod", "Gemfile", "pom.xml", "build.gradle", "Cargo.toml", "composer.json",
27
+ )
28
+
22
29
 
23
30
  @dataclass(frozen=True, kw_only=True)
24
31
  class ScaffoldResult:
@@ -27,9 +34,37 @@ class ScaffoldResult:
27
34
  methodology: str
28
35
  memory_path: Path
29
36
  entrypoints: int
37
+ guides: tuple[str, ...] = ()
30
38
  created: list[str] = field(default_factory=list)
31
39
 
32
40
 
41
+ def _read_manifests(target: Path) -> str:
42
+ parts: list[str] = []
43
+ for name in _MANIFESTS:
44
+ p = target / name
45
+ try:
46
+ if p.is_file():
47
+ parts.append(p.read_text(encoding="utf-8"))
48
+ except OSError:
49
+ pass
50
+ return "\n".join(parts)
51
+
52
+
53
+ def _stack_md(guides: list[Guide]) -> str:
54
+ if not guides:
55
+ return ("# Detected stack — review notes\n\n"
56
+ "(no language or framework guide matched; rely on the methodology and "
57
+ "your own knowledge of the stack)\n")
58
+ langs = [g.id for g in guides if g.kind == "language"]
59
+ fws = [g.id for g in guides if g.kind == "framework"]
60
+ lines = ["# Detected stack — review notes", "",
61
+ f"Languages: {', '.join(langs) or '-'}",
62
+ f"Frameworks: {', '.join(fws) or '-'}", ""]
63
+ for g in guides:
64
+ lines += ["---", "", g.body, ""]
65
+ return "\n".join(lines) + "\n"
66
+
67
+
33
68
  def _entrypoints_md(model) -> str:
34
69
  http = [e for e in model.entrypoints if e.kind == "http"]
35
70
  cli = [e for e in model.entrypoints if e.kind == "cli"]
@@ -69,11 +104,16 @@ def scaffold(target: str | Path, workspace: str | Path) -> ScaffoldResult:
69
104
  model = build_repo_model_from_dir(target)
70
105
  (ws / "entrypoints" / "_entrypoints.md").write_text(_entrypoints_md(model), encoding="utf-8")
71
106
 
107
+ # detect the stack and seed its review guides (languages + frameworks)
108
+ guides = select_guides(model.files, text=_read_manifests(target))
109
+ (ws / "_stack.md").write_text(_stack_md(guides), encoding="utf-8")
110
+
72
111
  return ScaffoldResult(
73
112
  project=project,
74
113
  workspace=ws,
75
114
  methodology=_METHODOLOGY.read_text(encoding="utf-8"),
76
115
  memory_path=memory_path,
77
116
  entrypoints=len(model.entrypoints),
117
+ guides=tuple(g.id for g in guides),
78
118
  created=created,
79
119
  )
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
 
9
9
  _DATA = Path(__file__).resolve().parent / "data"
10
10
 
11
- RULES_DIR = _DATA / "rules" # security rules injected into the audit prompt
11
+ RULES_DIR = _DATA / "rules" # vulnerability-class detection rules (what to find)
12
+ LANGUAGES_DIR = _DATA / "languages" # per-language review guides (how the target works)
13
+ FRAMEWORKS_DIR = _DATA / "frameworks" # per-framework review guides (how the target works)
12
14
  AGENT_DIR = _DATA / "agent" # repo-review methodology and memory template
13
15
  ENTRYPOINTS_FILE = _DATA / "entrypoints.yaml" # framework signatures for RepoModel
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  codejury/__init__.py
5
5
  codejury/cli.py
6
+ codejury/guides.py
6
7
  codejury/json_parse.py
7
8
  codejury/report.py
8
9
  codejury/resources.py
@@ -15,6 +16,8 @@ codejury.egg-info/top_level.txt
15
16
  codejury/data/entrypoints.yaml
16
17
  codejury/data/agent/repo-review.md
17
18
  codejury/data/agent/security-review-memory.md
19
+ codejury/data/frameworks/django.md
20
+ codejury/data/languages/python.md
18
21
  codejury/data/rules/SKILL.md
19
22
  codejury/data/rules/business-logic.md
20
23
  codejury/data/rules/code-injection.md
@@ -67,6 +70,7 @@ tests/test_anthropic_provider.py
67
70
  tests/test_cli_audit.py
68
71
  tests/test_diff_debate.py
69
72
  tests/test_diff_engine.py
73
+ tests/test_guides.py
70
74
  tests/test_json_parse.py
71
75
  tests/test_litellm_provider.py
72
76
  tests/test_openai_format.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.15.0"
3
+ version = "0.16.1"
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"
@@ -85,11 +85,12 @@ def test_engine_raises_on_wrong_shape_json():
85
85
 
86
86
 
87
87
  def test_prompt_carries_diff_focus_and_do_not_report():
88
- p = standard_audit_prompt(_DIFF, rules="RULE-X", context="def caller(): ...")
88
+ p = standard_audit_prompt(_DIFF, rules="RULE-X", context="def caller(): ...", stack="STACK-NOTE")
89
89
  assert "SELECT * FROM u" in p # the diff
90
90
  assert "Do NOT report" in p # the noise-control list
91
91
  assert "IDOR" in p # the focus
92
92
  assert "RULE-X" in p # rules excerpt
93
+ assert "STACK-NOTE" in p # language/framework conventions block
93
94
  assert "def caller()" in p # context block
94
95
 
95
96
 
@@ -0,0 +1,45 @@
1
+ """Language/framework review guides load and are selected by detection signals
2
+ (file globs + dependency-manifest substrings), so adding one is a drop-in file."""
3
+
4
+ from codejury.guides import Guide, guides_for_diff, load_guides, select_guides
5
+
6
+
7
+ def test_shipped_guides_load():
8
+ by_id = {g.id: g for g in load_guides()}
9
+ assert {"python", "django"} <= set(by_id)
10
+ assert by_id["python"].kind == "language"
11
+ assert by_id["django"].kind == "framework"
12
+ assert "IDOR" in by_id["django"].body or "idor" in by_id["django"].body.lower()
13
+
14
+
15
+ def test_select_by_file_glob():
16
+ matched = {g.id for g in select_guides(["app/urls.py", "app/views.py", "manage.py"])}
17
+ assert "python" in matched # *.py
18
+ assert "django" in matched # *urls.py / manage.py
19
+
20
+
21
+ def test_select_by_manifest_substring():
22
+ matched = {g.id for g in select_guides(["main.py"], text="Django==4.2\nrequests\n")}
23
+ assert "django" in matched and "python" in matched
24
+
25
+
26
+ def test_no_signal_no_match():
27
+ assert select_guides(["index.html", "style.css"]) == [] # no .py, no django manifest
28
+
29
+
30
+ def test_select_respects_injected_pool():
31
+ only = [Guide(id="x", kind="framework", title="X", detect_files=("*.xyz",),
32
+ detect_manifest=(), detect_imports=(), body="b")]
33
+ assert [g.id for g in select_guides(["a.xyz"], guides=only)] == ["x"]
34
+ assert select_guides(["a.py"], guides=only) == []
35
+
36
+
37
+ def test_guides_for_diff_by_path_and_content():
38
+ diff = ("diff --git a/app/urls.py b/app/urls.py\n"
39
+ "+from django.urls import path\n+urlpatterns = []\n")
40
+ notes = guides_for_diff(diff)
41
+ assert "Django" in notes and "Python" in notes # urls.py + .py + the django import
42
+
43
+
44
+ def test_guides_for_diff_empty_when_irrelevant():
45
+ assert guides_for_diff("+++ b/README.md\n+hello\n") == ""
@@ -47,6 +47,26 @@ def test_scaffold_seeds_entrypoints_from_repomodel(tmp_path):
47
47
  assert "list_users" in seeded and "❌" in seeded
48
48
 
49
49
 
50
+ def test_scaffold_seeds_stack_guides(tmp_path):
51
+ # the Flask target is Python; the python language guide should be detected and
52
+ # its notes written to _stack.md
53
+ res = scaffold(_target(tmp_path), tmp_path / "work")
54
+ assert "python" in res.guides
55
+ stack = (res.workspace / "_stack.md").read_text()
56
+ assert "python" in stack.lower()
57
+
58
+
59
+ def test_scaffold_detects_framework_from_files_and_manifest(tmp_path):
60
+ d = tmp_path / "dj"
61
+ d.mkdir()
62
+ (d / "manage.py").write_text("import django\n")
63
+ (d / "urls.py").write_text("from django.urls import path\nurlpatterns = []\n")
64
+ (d / "requirements.txt").write_text("Django==4.2\n")
65
+ res = scaffold(d, tmp_path / "work")
66
+ assert "django" in res.guides
67
+ assert "Django" in (res.workspace / "_stack.md").read_text()
68
+
69
+
50
70
  def test_methodology_is_returned(tmp_path):
51
71
  # the methodology text ships and is handed back for the agent to follow
52
72
  res = scaffold(_target(tmp_path), tmp_path / "work")
File without changes
File without changes
File without changes
File without changes
File without changes