codejury 0.16.1__tar.gz → 0.17.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.
- {codejury-0.16.1 → codejury-0.17.1}/PKG-INFO +4 -4
- {codejury-0.16.1 → codejury-0.17.1}/README.md +3 -3
- {codejury-0.16.1 → codejury-0.17.1}/codejury/cli.py +3 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/frameworks/django.md +4 -3
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/languages/python.md +5 -5
- {codejury-0.16.1/codejury/data/agent → codejury-0.17.1/codejury/data/methodology}/repo-review.md +17 -16
- {codejury-0.16.1/codejury/data/agent → codejury-0.17.1/codejury/data/methodology}/security-review-memory.md +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/SKILL.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/code-injection.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/command-injection.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/cross-site-request-forgery.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/cross-site-scripting.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/hardcoded-secrets.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/http-response-splitting.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/improper-authentication.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/missing-authorization.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/server-side-template-injection.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/sql-injection.md +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/debate.py +13 -14
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/debate_prompts.py +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/engine.py +19 -6
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/prompts.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/rules.py +13 -26
- {codejury-0.16.1 → codejury-0.17.1}/codejury/domain/__init__.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/domain/finding.py +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/guides.py +15 -31
- {codejury-0.16.1 → codejury-0.17.1}/codejury/json_parse.py +1 -1
- codejury-0.17.1/codejury/mddoc.py +38 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/__init__.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/anthropic.py +3 -3
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/factory.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/litellm.py +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/mock.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/openai.py +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/retry.py +1 -1
- codejury-0.17.1/codejury/repo/model.py +61 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/repo/scaffold.py +26 -28
- {codejury-0.16.1 → codejury-0.17.1}/codejury/report.py +2 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury/resources.py +1 -2
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/PKG-INFO +4 -4
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/SOURCES.txt +4 -3
- {codejury-0.16.1 → codejury-0.17.1}/pyproject.toml +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_cli_audit.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_diff_debate.py +1 -1
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_diff_engine.py +12 -3
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_guides.py +2 -13
- codejury-0.17.1/tests/test_mddoc.py +28 -0
- codejury-0.17.1/tests/test_repo_model.py +38 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_repo_scaffold.py +23 -9
- codejury-0.16.1/codejury/data/entrypoints.yaml +0 -16
- codejury-0.16.1/codejury/repo/model.py +0 -262
- codejury-0.16.1/tests/test_repo_model.py +0 -227
- {codejury-0.16.1 → codejury-0.17.1}/LICENSE +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/__init__.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/business-logic.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/information-exposure.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-cryptography.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-deserialization.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-direct-object-reference.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/insecure-transport.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/jwt-validation.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/mass-assignment.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/open-redirect.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/path-traversal.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/race-condition.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/replay-attack.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/server-side-request-forgery.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/session-fixation.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/data/rules/xml-external-entity.md +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/__init__.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/findings_filter.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/diff/runner.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/base.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/providers/openai_format.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury/repo/__init__.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/setup.cfg +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_json_parse.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_litellm_provider.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_openai_format.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_openai_provider.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_report.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_retry_provider.py +0 -0
- {codejury-0.16.1 → codejury-0.17.1}/tests/test_rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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
|
|
@@ -104,7 +104,7 @@ Audit every pull request and surface findings in the code scanning tab. Copy
|
|
|
104
104
|
2. write SARIF and upload it with `github/codeql-action/upload-sarif`,
|
|
105
105
|
3. fail the check on a HIGH or CRITICAL finding (`--fail-on high`).
|
|
106
106
|
|
|
107
|
-
The job makes one model call per PR (standard mode)
|
|
107
|
+
The job makes one model call per PR (standard mode). The SARIF is uploaded even
|
|
108
108
|
when the gate fails, so findings always show up on the PR.
|
|
109
109
|
|
|
110
110
|
## Whole-repo review
|
|
@@ -125,11 +125,11 @@ against production.
|
|
|
125
125
|
|
|
126
126
|
Each finding carries a file and line, a severity and category, a concrete
|
|
127
127
|
exploit scenario, a recommendation, and a confidence. A false-positive filter
|
|
128
|
-
drops test/mock-path and low-confidence noise
|
|
128
|
+
drops test/mock-path and low-confidence noise. The model is also told not to
|
|
129
129
|
report dependency CVEs, style notes, speculation, or config-leak-only risks.
|
|
130
130
|
|
|
131
131
|
## Extending
|
|
132
132
|
|
|
133
133
|
Add a vulnerability class by dropping a new `codejury/data/rules/<class>.md` with
|
|
134
134
|
the standard frontmatter (title, impact, tags, triggers) and vulnerable/secure
|
|
135
|
-
examples. It is data
|
|
135
|
+
examples. It is data, no code change needed.
|
|
@@ -73,7 +73,7 @@ Audit every pull request and surface findings in the code scanning tab. Copy
|
|
|
73
73
|
2. write SARIF and upload it with `github/codeql-action/upload-sarif`,
|
|
74
74
|
3. fail the check on a HIGH or CRITICAL finding (`--fail-on high`).
|
|
75
75
|
|
|
76
|
-
The job makes one model call per PR (standard mode)
|
|
76
|
+
The job makes one model call per PR (standard mode). The SARIF is uploaded even
|
|
77
77
|
when the gate fails, so findings always show up on the PR.
|
|
78
78
|
|
|
79
79
|
## Whole-repo review
|
|
@@ -94,11 +94,11 @@ against production.
|
|
|
94
94
|
|
|
95
95
|
Each finding carries a file and line, a severity and category, a concrete
|
|
96
96
|
exploit scenario, a recommendation, and a confidence. A false-positive filter
|
|
97
|
-
drops test/mock-path and low-confidence noise
|
|
97
|
+
drops test/mock-path and low-confidence noise. The model is also told not to
|
|
98
98
|
report dependency CVEs, style notes, speculation, or config-leak-only risks.
|
|
99
99
|
|
|
100
100
|
## Extending
|
|
101
101
|
|
|
102
102
|
Add a vulnerability class by dropping a new `codejury/data/rules/<class>.md` with
|
|
103
103
|
the standard frontmatter (title, impact, tags, triggers) and vulnerable/secure
|
|
104
|
-
examples. It is data
|
|
104
|
+
examples. It is data, no code change needed.
|
|
@@ -132,9 +132,10 @@ def _dispatch(args, parser) -> int:
|
|
|
132
132
|
if args.command == "review" and scope == "repo":
|
|
133
133
|
res = scaffold(args.directory, args.workspace)
|
|
134
134
|
print(f"Workspace: {res.workspace}", file=sys.stderr)
|
|
135
|
-
print(f"Seeded {res.entrypoints} entrypoints into {res.workspace}/entrypoints/_entrypoints.md", file=sys.stderr)
|
|
136
135
|
if res.guides:
|
|
137
|
-
print(f"Detected stack
|
|
136
|
+
print(f"Detected stack: {', '.join(res.guides)}. Notes in {res.workspace}/_stack.md", file=sys.stderr)
|
|
137
|
+
print(f"Flagged {len(res.candidate_files)} candidate entrypoint files into "
|
|
138
|
+
f"{res.workspace}/entrypoints/_entrypoints.md", file=sys.stderr)
|
|
138
139
|
print(f"Memory: {res.memory_path}", file=sys.stderr)
|
|
139
140
|
print("\nRun this review with an interactive agent (Claude Code / Codex) using the methodology below.\n")
|
|
140
141
|
print(res.methodology)
|
|
@@ -6,8 +6,9 @@ detect:
|
|
|
6
6
|
files: ["*urls.py", "manage.py", "*settings.py"]
|
|
7
7
|
manifest: ["django"]
|
|
8
8
|
imports: ["from django", "import django"]
|
|
9
|
+
entrypoint_files: ["*urls.py"]
|
|
9
10
|
---
|
|
10
|
-
# Django
|
|
11
|
+
# Django review notes
|
|
11
12
|
|
|
12
13
|
## Entrypoints
|
|
13
14
|
- Routes live in `urls.py`: `path()` / `re_path()` map a URL to a view.
|
|
@@ -25,6 +26,6 @@ detect:
|
|
|
25
26
|
|
|
26
27
|
## Common sinks / gotchas
|
|
27
28
|
- SQL: `.raw()`, `.extra()`, `RawSQL`, or string-built SQL via `connection.cursor()`.
|
|
28
|
-
- Templates: `mark_safe`, `|safe`, `format_html` on unescaped user input
|
|
29
|
-
- `pickle` / `yaml.load` on a cookie or upload
|
|
29
|
+
- Templates: `mark_safe`, `|safe`, `format_html` on unescaped user input, autoescape off.
|
|
30
|
+
- `pickle` / `yaml.load` on a cookie or upload, `DEBUG=True` leaking internals,
|
|
30
31
|
a hardcoded `SECRET_KEY`.
|
|
@@ -7,16 +7,16 @@ detect:
|
|
|
7
7
|
manifest: []
|
|
8
8
|
imports: []
|
|
9
9
|
---
|
|
10
|
-
# Python
|
|
10
|
+
# Python review notes
|
|
11
11
|
|
|
12
12
|
Where untrusted input enters (beyond web routes, which the framework guide covers):
|
|
13
13
|
CLI (`argparse`/`click`), scheduled jobs, queue consumers, and any function fed an
|
|
14
14
|
external value. Non-HTTP sources matter as much as routes:
|
|
15
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
|
|
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
20
|
- network: `requests.get(user_url)` and friends (SSRF).
|
|
21
21
|
|
|
22
22
|
Common sinks: string-built SQL handed to a DB cursor or ORM `.raw()`/`.extra()`,
|
{codejury-0.16.1/codejury/data/agent → codejury-0.17.1/codejury/data/methodology}/repo-review.md
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Repo Security Review
|
|
1
|
+
# Repo Security Review: Agent Methodology
|
|
2
2
|
|
|
3
3
|
The `review repo` path: a whole-repository security audit, run by an interactive
|
|
4
|
-
coding agent
|
|
4
|
+
coding agent such as Claude Code or Codex, not a one-shot LLM call. It maps the
|
|
5
5
|
attack surface, traces inputs to sinks across files, verifies issues with a real
|
|
6
6
|
PoC, and iterates over multiple rounds with a persistent memory. One round is
|
|
7
|
-
roughly 30 minutes
|
|
7
|
+
roughly 30 minutes. Run as many rounds as needed.
|
|
8
8
|
|
|
9
9
|
Target repository: the directory you were given.
|
|
10
10
|
Workspace: `<workspace>/<project>/` (created for you), holding `entrypoints/`,
|
|
@@ -12,15 +12,16 @@ Workspace: `<workspace>/<project>/` (created for you), holding `entrypoints/`,
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## On start
|
|
15
|
+
## On start
|
|
16
16
|
|
|
17
17
|
1. Read `security-review-memory.md` in the workspace if it exists:
|
|
18
|
-
- skip every pattern under "Confirmed false positives"
|
|
19
|
-
- do not re-report anything under "Fixed"
|
|
18
|
+
- skip every pattern under "Confirmed false positives".
|
|
19
|
+
- do not re-report anything under "Fixed".
|
|
20
20
|
- weight the files under "High-risk areas" more heavily.
|
|
21
|
-
2. Read `entrypoints/_entrypoints.md` (seeded
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
2. Read `entrypoints/_entrypoints.md` (seeded): the files the detected stack
|
|
22
|
+
flags as likely to define entrypoints. Open them to find the actual
|
|
23
|
+
entrypoints. It is a *starting* subset, not the whole surface (see "Map the
|
|
24
|
+
attack surface").
|
|
24
25
|
3. Read `_stack.md` (seeded): the detected languages and frameworks and review
|
|
25
26
|
notes for them, so you know where this stack's entrypoints, sinks, and auth
|
|
26
27
|
checks live. If it matched nothing, lean on your own knowledge of the stack.
|
|
@@ -33,12 +34,12 @@ The seeded inventory lists HTTP routes and CLI commands only. Before analysing,
|
|
|
33
34
|
complete the surface: untrusted input enters at more than HTTP. Enumerate every
|
|
34
35
|
source the attacker can influence and add it to `entrypoints/`:
|
|
35
36
|
|
|
36
|
-
- HTTP routes, GraphQL resolvers, gRPC / RPC handlers, WebSocket handlers
|
|
37
|
+
- HTTP routes, GraphQL resolvers, gRPC / RPC handlers, WebSocket handlers.
|
|
37
38
|
- CLI commands, scheduled jobs / cron, queue and topic consumers, webhooks and
|
|
38
|
-
third-party callbacks
|
|
39
|
+
third-party callbacks.
|
|
39
40
|
- deserialization points (pickle, yaml.load, marshal), file and document parsers
|
|
40
|
-
(XML / XXE, YAML, CSV, zip, image / office), template rendering of user input
|
|
41
|
-
- file uploads, archive extraction, and any filesystem path built from user input
|
|
41
|
+
(XML / XXE, YAML, CSV, zip, image / office), template rendering of user input.
|
|
42
|
+
- file uploads, archive extraction, and any filesystem path built from user input.
|
|
42
43
|
- headers, cookies, environment, and config read as trusted, and inbound
|
|
43
44
|
inter-service calls.
|
|
44
45
|
|
|
@@ -60,11 +61,11 @@ Read the implementation reachable from each source. For every one ask:
|
|
|
60
61
|
- Mass assignment: is a user-controlled body bound wholesale into a model?
|
|
61
62
|
- Signature: is a caller-supplied key trusted as the trust anchor?
|
|
62
63
|
|
|
63
|
-
## Trace attack paths
|
|
64
|
+
## Trace attack paths, the core work
|
|
64
65
|
|
|
65
66
|
A whole-repo review earns its keep by reasoning *across files*: a flaw is usually
|
|
66
67
|
a source in one file reaching a dangerous sink in another, past a control defined
|
|
67
|
-
in a third (a route that trusts a helper which skips signature checks
|
|
68
|
+
in a third (a route that trusts a helper which skips signature checks, an id that
|
|
68
69
|
reaches a query with no ownership check). For each promising source, trace the
|
|
69
70
|
path and record it in `analysis/`:
|
|
70
71
|
|
|
@@ -128,7 +129,7 @@ destructive action without the operator's explicit go-ahead.
|
|
|
128
129
|
## Iteration
|
|
129
130
|
|
|
130
131
|
Each round, read the workspace history first and do not repeat finished work.
|
|
131
|
-
Process leftover TODOs
|
|
132
|
+
Process leftover TODOs, otherwise pick an unreviewed (❌) or to-deepen (⚠️)
|
|
132
133
|
source from the inventory. When every source is ✅ and there are no TODOs,
|
|
133
134
|
report the review complete.
|
|
134
135
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Security Review Memory
|
|
1
|
+
# Security Review Memory: <project>
|
|
2
2
|
|
|
3
3
|
> Maintained by the security-review agent. Read at the start of every round,
|
|
4
4
|
> updated at the end. It skips confirmed false positives, avoids re-reporting
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
## High-risk areas (weight every round)
|
|
24
24
|
|
|
25
|
-
<!-- - `<path
|
|
25
|
+
<!-- - `<path>`: <reason, for example a past auth bypass> -->
|
|
26
26
|
|
|
27
27
|
## Audit history
|
|
28
28
|
|
|
@@ -8,7 +8,7 @@ description: "Application security rules for reviewing code for exploitable vuln
|
|
|
8
8
|
Application-security rules, one file per weakness class under `rules/`, named by
|
|
9
9
|
the specific weakness (CWE-style). Each rule states impact, the markers to hunt
|
|
10
10
|
(`triggers`), and vulnerable-vs-secure examples. The diff-audit engine injects the
|
|
11
|
-
rules relevant to a change into the prompt
|
|
11
|
+
rules relevant to a change into the prompt. The repo-review agent reads them for
|
|
12
12
|
the target's stack. A finding's `category` is one of these ids.
|
|
13
13
|
|
|
14
14
|
## Rules by OWASP category
|
|
@@ -8,7 +8,7 @@ triggers: ["eval(", "exec(", "compile(", "pickle.loads", "new Function", "setTim
|
|
|
8
8
|
|
|
9
9
|
## Code Injection
|
|
10
10
|
|
|
11
|
-
Passing untrusted input to a language evaluation primitive (eval, exec, compile, dynamic import, JS Function) lets an attacker execute arbitrary code. Never evaluate untrusted input
|
|
11
|
+
Passing untrusted input to a language evaluation primitive (eval, exec, compile, dynamic import, JS Function) lets an attacker execute arbitrary code. Never evaluate untrusted input. Parse it with a data-only parser or dispatch through an allowlist.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["os.system", "os.popen", "subprocess", "shell=True", "Runtime.getRunt
|
|
|
8
8
|
|
|
9
9
|
## Command Injection
|
|
10
10
|
|
|
11
|
-
Passing untrusted input to a shell lets an attacker run arbitrary commands. Never build a shell string from input
|
|
11
|
+
Passing untrusted input to a shell lets an attacker run arbitrary commands. Never build a shell string from input. Pass an argument list with the shell disabled.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -18,4 +18,4 @@ Vulnerable:
|
|
|
18
18
|
def change_email():
|
|
19
19
|
current_user.email = request.form["email"]; db.commit()
|
|
20
20
|
```
|
|
21
|
-
Secure: keep CSRF protection on
|
|
21
|
+
Secure: keep CSRF protection on. Validate the token, or set the session cookie `SameSite="Lax"` and check the Origin header.
|
|
@@ -22,4 +22,4 @@ el.textContent = "Hello " + username;
|
|
|
22
22
|
|
|
23
23
|
### Python (templates)
|
|
24
24
|
Vulnerable: `return render_template_string("<div>" + user_input + "</div>")`
|
|
25
|
-
Secure: rely on Jinja auto-escaping
|
|
25
|
+
Secure: rely on Jinja auto-escaping. Never apply `| safe` to user data.
|
|
@@ -8,7 +8,7 @@ triggers: ["api_key =", "API_KEY =", "password =", "secret =", "token =", "sk_li
|
|
|
8
8
|
|
|
9
9
|
## Hardcoded Secrets
|
|
10
10
|
|
|
11
|
-
A literal credential, key, or token in source leaks with the code and cannot be rotated easily. Load secrets from environment variables or a secret manager. A variable that reads from the environment or is passed in as a parameter is fine
|
|
11
|
+
A literal credential, key, or token in source leaks with the code and cannot be rotated easily. Load secrets from environment variables or a secret manager. A variable that reads from the environment or is passed in as a parameter is fine. Only an actual literal value is a finding.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["set_header", "add_header", "Location", "Set-Cookie", "response.heade
|
|
|
8
8
|
|
|
9
9
|
## HTTP Response Splitting / Header Injection
|
|
10
10
|
|
|
11
|
-
Putting untrusted input into a response header (or redirect Location, Set-Cookie) without stripping CR/LF lets an attacker inject headers or split the response. Strip or reject newline characters in any header value built from input
|
|
11
|
+
Putting untrusted input into a response header (or redirect Location, Set-Cookie) without stripping CR/LF lets an attacker inject headers or split the response. Strip or reject newline characters in any header value built from input. Frameworks often do this, but manual header construction may not.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["login", "authenticate", "password ==", "== token", "auth", "bypass",
|
|
|
8
8
|
|
|
9
9
|
## Improper Authentication
|
|
10
10
|
|
|
11
|
-
Authentication is missing on a protected path, can be bypassed by a logic flaw, or compares credentials unsafely (a hardcoded bypass token, a non-constant-time compare, trusting a client-asserted identity). Verify identity server-side against a trusted store before granting access
|
|
11
|
+
Authentication is missing on a protected path, can be bypassed by a logic flaw, or compares credentials unsafely (a hardcoded bypass token, a non-constant-time compare, trusting a client-asserted identity). Verify identity server-side against a trusted store before granting access. Compare secrets in constant time.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["@app.route", "@router", "@login_required", "requires_", "permission"
|
|
|
8
8
|
|
|
9
9
|
## Missing Authorization
|
|
10
10
|
|
|
11
|
-
A privileged or state-changing endpoint performs its action without verifying the caller is allowed to, or derives the role/permission from a client-controlled value. Enforce authorization server-side, per request, from a trusted store
|
|
11
|
+
A privileged or state-changing endpoint performs its action without verifying the caller is allowed to, or derives the role/permission from a client-controlled value. Enforce authorization server-side, per request, from a trusted store. A route that mutates or exposes privileged data must check the caller's rights the same way its peers do.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["render_template_string", "Template(", "from_string", "Jinja2", "{{",
|
|
|
8
8
|
|
|
9
9
|
## Server-Side Template Injection (SSTI)
|
|
10
10
|
|
|
11
|
-
Building a template from untrusted input (rather than passing input as template data) lets an attacker inject template syntax that the engine evaluates, often reaching RCE. Pass untrusted values as data to a fixed template
|
|
11
|
+
Building a template from untrusted input (rather than passing input as template data) lets an attacker inject template syntax that the engine evaluates, often reaching RCE. Pass untrusted values as data to a fixed template. Never compile a template from user input.
|
|
12
12
|
|
|
13
13
|
### Python (Jinja2)
|
|
14
14
|
Vulnerable:
|
|
@@ -8,7 +8,7 @@ triggers: ["execute(", "executemany", ".raw(", "cursor", "SELECT ", "INSERT ", "
|
|
|
8
8
|
|
|
9
9
|
## SQL Injection
|
|
10
10
|
|
|
11
|
-
Untrusted input concatenated or interpolated into a SQL statement lets an attacker change the query. Use parameterized queries / bound parameters
|
|
11
|
+
Untrusted input concatenated or interpolated into a SQL statement lets an attacker change the query. Use parameterized queries / bound parameters. Never build SQL from input. Identifiers (table/column names) cannot be parameterized, so validate them against an allowlist.
|
|
12
12
|
|
|
13
13
|
### Python
|
|
14
14
|
Vulnerable:
|
|
@@ -34,7 +34,7 @@ class AdversarialResult:
|
|
|
34
34
|
investigate: list[dict] = field(default_factory=list)
|
|
35
35
|
rounds: int = 0
|
|
36
36
|
converged: bool = False
|
|
37
|
-
degraded: bool = False # the judge response was unusable
|
|
37
|
+
degraded: bool = False # the judge response was unusable, findings are the unjudged fallback
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _dicts(items: object) -> list[dict]:
|
|
@@ -91,16 +91,16 @@ class AdversarialAuditRunner:
|
|
|
91
91
|
) -> None:
|
|
92
92
|
self._provider = provider
|
|
93
93
|
self._max_tokens = max_tokens
|
|
94
|
-
# each role can run on its own model
|
|
95
|
-
# challenger, a careful judge); each defaults to the base model.
|
|
94
|
+
# each role can run on its own model, each defaults to the base model
|
|
96
95
|
self._finder_model = finder_model or model
|
|
97
96
|
self._challenger_model = challenger_model or model
|
|
98
97
|
self._judge_model = judge_model or model
|
|
99
98
|
|
|
100
99
|
def _ask(self, system: str, prompt: str, model: str) -> tuple[dict, bool]:
|
|
101
|
-
"""
|
|
102
|
-
parsed into a JSON object
|
|
103
|
-
so the caller
|
|
100
|
+
"""Return the parsed object and an ok flag. ok is False when the response
|
|
101
|
+
could not be parsed into a JSON object, for example a provider error page,
|
|
102
|
+
a blocked request, or prose, so the caller does not treat an unusable reply
|
|
103
|
+
as an empty result."""
|
|
104
104
|
try:
|
|
105
105
|
result = self._provider.complete(
|
|
106
106
|
system=system,
|
|
@@ -109,15 +109,15 @@ class AdversarialAuditRunner:
|
|
|
109
109
|
max_tokens=self._max_tokens,
|
|
110
110
|
)
|
|
111
111
|
except Exception:
|
|
112
|
-
# a provider error
|
|
113
|
-
# is an unusable reply, not an empty result
|
|
112
|
+
# a provider error such as exhausted retries or a transport failure
|
|
113
|
+
# is an unusable reply, not an empty result, so degrade gracefully
|
|
114
114
|
return {}, False
|
|
115
115
|
obj = extract_json_object(result.text)
|
|
116
116
|
return (obj or {}), bool(obj)
|
|
117
117
|
|
|
118
118
|
def run(self, diff: str, *, rules: str = "", context: str = "", max_rounds: int = 3) -> AdversarialResult:
|
|
119
119
|
if not rules:
|
|
120
|
-
rules = rules_for_diff(diff)
|
|
120
|
+
rules = rules_for_diff(diff)
|
|
121
121
|
prior: list[dict] = []
|
|
122
122
|
prev_keys: set | None = None
|
|
123
123
|
judged = AdversarialResult(findings=[])
|
|
@@ -142,14 +142,13 @@ class AdversarialAuditRunner:
|
|
|
142
142
|
verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
|
|
143
143
|
if not judge_ok:
|
|
144
144
|
# the judge is the filter that controls false positives, and an
|
|
145
|
-
# unusable reply is
|
|
146
|
-
# request); re-ask once before giving up on it
|
|
145
|
+
# unusable reply is usually transient, so re-ask once before degrading
|
|
147
146
|
verdict, judge_ok = self._ask(JUDGE_SYSTEM, jp, self._judge_model)
|
|
148
147
|
if not judge_ok:
|
|
149
148
|
# judge still unusable: degrade, but apply the recall-safe
|
|
150
|
-
# challenger
|
|
151
|
-
# through findings the challenger already showed are safe
|
|
152
|
-
#
|
|
149
|
+
# challenger dismissals so a transient judge outage does not pass
|
|
150
|
+
# through findings the challenger already showed are safe. without
|
|
151
|
+
# this the degraded fallback inflates false positives
|
|
153
152
|
fallback = _dedup(_apply_dismissals(finder_findings, rebuttals) + new_findings)
|
|
154
153
|
judged = AdversarialResult(findings=findings_from_list(fallback), rounds=rounds, degraded=True)
|
|
155
154
|
degraded = True
|
|
@@ -26,7 +26,7 @@ _FINDING_FIELDS = (
|
|
|
26
26
|
|
|
27
27
|
FINDER_SYSTEM = (
|
|
28
28
|
"You are a red-team application security engineer. Enumerate every plausible "
|
|
29
|
-
"exploitable vulnerability an attacker could reach
|
|
29
|
+
"exploitable vulnerability an attacker could reach. Do not self-censor or pre-filter "
|
|
30
30
|
"for fear of false positives, the Challenger and Judge will do that. Respond with a "
|
|
31
31
|
"single JSON object and nothing else."
|
|
32
32
|
)
|
|
@@ -102,7 +102,7 @@ def judge_prompt(diff: str, finder_findings: list, rebuttals: list, new_findings
|
|
|
102
102
|
"- UNRESOLVED: cannot decide from the code shown -> put it in `unresolved`.\n"
|
|
103
103
|
"- INVESTIGATE: needs a dynamic/runtime check to confirm -> put it in `investigate`.\n"
|
|
104
104
|
"Set `converged` to true when this round surfaced no new confirmed finding and nothing is left to "
|
|
105
|
-
"investigate
|
|
105
|
+
"investigate. Set it to false if another round could still change the ruling.\n\n"
|
|
106
106
|
f"Code change (unified diff):\n```diff\n{diff}\n```\n\n{context_block}"
|
|
107
107
|
f"Finder findings:\n{json.dumps(finder_findings, ensure_ascii=False)}\n\n"
|
|
108
108
|
f"Challenger rebuttals:\n{json.dumps(rebuttals, ensure_ascii=False)}\n\n"
|
|
@@ -7,12 +7,25 @@ coverage and lower false positives.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import re
|
|
11
|
+
|
|
10
12
|
from codejury.domain.finding import Finding, findings_from_list
|
|
11
13
|
from codejury.diff.prompts import SYSTEM, standard_audit_prompt
|
|
12
14
|
from codejury.diff.rules import rules_for_diff
|
|
15
|
+
from codejury.guides import select_guides
|
|
13
16
|
from codejury.json_parse import extract_json_object
|
|
14
17
|
from codejury.providers.base import Message, Provider
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
_DIFF_PATH = re.compile(r"^(?:\+\+\+ b/|diff --git a/\S+ b/)(\S+)", re.MULTILINE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def guides_for_diff(diff: str) -> str:
|
|
23
|
+
"""Concatenated bodies of the language/framework guides relevant to a diff,
|
|
24
|
+
selected by its changed paths and its content. Empty when nothing matches.
|
|
25
|
+
Lives here, not in the shared guides module, because parsing a diff is a
|
|
26
|
+
diff-path concern."""
|
|
27
|
+
paths = _DIFF_PATH.findall(diff)
|
|
28
|
+
return "\n\n---\n\n".join(g.body for g in select_guides(paths, text=diff))
|
|
16
29
|
|
|
17
30
|
|
|
18
31
|
class AuditError(RuntimeError):
|
|
@@ -33,8 +46,8 @@ class AuditRunner:
|
|
|
33
46
|
|
|
34
47
|
def run(self, diff: str, *, rules: str = "", context: str = "") -> list[Finding]:
|
|
35
48
|
if not rules:
|
|
36
|
-
rules = rules_for_diff(diff)
|
|
37
|
-
stack = guides_for_diff(diff)
|
|
49
|
+
rules = rules_for_diff(diff)
|
|
50
|
+
stack = guides_for_diff(diff)
|
|
38
51
|
result = self._provider.complete(
|
|
39
52
|
system=SYSTEM,
|
|
40
53
|
messages=[Message(role="user", content=standard_audit_prompt(diff, rules=rules, context=context, stack=stack))],
|
|
@@ -44,8 +57,8 @@ class AuditRunner:
|
|
|
44
57
|
obj = extract_json_object(result.text)
|
|
45
58
|
if obj is None or "findings" not in obj:
|
|
46
59
|
raise AuditError(
|
|
47
|
-
"the model reply was not a valid audit result
|
|
48
|
-
"JSON object without a
|
|
49
|
-
"
|
|
60
|
+
"the model reply was not a valid audit result. it had no JSON object, "
|
|
61
|
+
"or a JSON object without a findings key, so it is a failed audit "
|
|
62
|
+
"rather than a clean pass"
|
|
50
63
|
)
|
|
51
64
|
return findings_from_list(obj.get("findings"))
|
|
@@ -33,7 +33,7 @@ Do NOT report, regardless of severity:
|
|
|
33
33
|
- Risks that only matter if a production config is leaked (do not assume the code
|
|
34
34
|
shown reflects production configuration).
|
|
35
35
|
For input-driven issues, flag only when untrusted input can plausibly reach the
|
|
36
|
-
sink
|
|
36
|
+
sink. A constant, a stored field, trusted config, or an operator-supplied CLI
|
|
37
37
|
argument is not attacker-controlled.
|
|
38
38
|
"""
|
|
39
39
|
|
|
@@ -12,8 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
|
|
15
|
+
from codejury.mddoc import iter_md_docs
|
|
17
16
|
from codejury.resources import RULES_DIR
|
|
18
17
|
|
|
19
18
|
_IMPACT_RANK = {"CRITICAL": 3, "HIGH": 2, "MEDIUM": 1, "LOW": 0}
|
|
@@ -29,31 +28,19 @@ class Rule:
|
|
|
29
28
|
body: str
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
def _parse(path: Path) -> Rule | None:
|
|
33
|
-
text = path.read_text(encoding="utf-8")
|
|
34
|
-
meta: dict = {}
|
|
35
|
-
body = text
|
|
36
|
-
if text.startswith("---"):
|
|
37
|
-
parts = text.split("---", 2)
|
|
38
|
-
if len(parts) == 3:
|
|
39
|
-
meta = yaml.safe_load(parts[1]) or {}
|
|
40
|
-
body = parts[2].strip()
|
|
41
|
-
return Rule(
|
|
42
|
-
id=path.stem,
|
|
43
|
-
title=str(meta.get("title", path.stem)),
|
|
44
|
-
impact=str(meta.get("impact", "MEDIUM")).upper(),
|
|
45
|
-
tags=tuple(meta.get("tags", [])),
|
|
46
|
-
triggers=tuple(str(t) for t in meta.get("triggers", [])),
|
|
47
|
-
body=body,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
31
|
def load_rules(directory: str | Path = RULES_DIR) -> list[Rule]:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
32
|
+
rules = [
|
|
33
|
+
Rule(
|
|
34
|
+
id=path.stem,
|
|
35
|
+
title=str(meta.get("title", path.stem)),
|
|
36
|
+
impact=str(meta.get("impact", "MEDIUM")).upper(),
|
|
37
|
+
tags=tuple(meta.get("tags", [])),
|
|
38
|
+
triggers=tuple(str(t) for t in meta.get("triggers", [])),
|
|
39
|
+
body=body,
|
|
40
|
+
)
|
|
41
|
+
for path, meta, body in iter_md_docs(directory)
|
|
42
|
+
]
|
|
43
|
+
return sorted(rules, key=lambda r: r.id)
|
|
57
44
|
|
|
58
45
|
|
|
59
46
|
def select_rules(diff: str, rules: list[Rule], *, limit: int = 6) -> list[Rule]:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""codejury.domain: the framework's typed data model.
|
|
2
2
|
|
|
3
|
-
Layers communicate only through these structures
|
|
3
|
+
Layers communicate only through these structures. This package depends on no
|
|
4
4
|
concrete implementation (Provider / Source / ...).
|
|
5
5
|
"""
|
|
@@ -30,10 +30,10 @@ class Finding:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def _to_float(value: object, default: float) -> float:
|
|
33
|
-
if isinstance(value, bool):
|
|
33
|
+
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
|
|
34
34
|
return default
|
|
35
35
|
try:
|
|
36
|
-
f = float(value)
|
|
36
|
+
f = float(value)
|
|
37
37
|
except (TypeError, ValueError):
|
|
38
38
|
return default
|
|
39
39
|
return f if 0.0 <= f <= 1.0 else default
|
|
@@ -13,16 +13,11 @@ code change, which keeps the unbounded language/framework axis out of code.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import fnmatch
|
|
16
|
-
import re
|
|
17
16
|
from dataclasses import dataclass
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
import yaml
|
|
21
17
|
|
|
18
|
+
from codejury.mddoc import iter_md_docs
|
|
22
19
|
from codejury.resources import FRAMEWORKS_DIR, LANGUAGES_DIR
|
|
23
20
|
|
|
24
|
-
_DIFF_PATH = re.compile(r"^(?:\+\+\+ b/|diff --git a/\S+ b/)(\S+)", re.MULTILINE)
|
|
25
|
-
|
|
26
21
|
|
|
27
22
|
@dataclass(frozen=True, kw_only=True)
|
|
28
23
|
class Guide:
|
|
@@ -32,18 +27,11 @@ class Guide:
|
|
|
32
27
|
detect_files: tuple[str, ...]
|
|
33
28
|
detect_manifest: tuple[str, ...]
|
|
34
29
|
detect_imports: tuple[str, ...]
|
|
30
|
+
entrypoint_files: tuple[str, ...] # globs for files likely to define entrypoints
|
|
35
31
|
body: str
|
|
36
32
|
|
|
37
33
|
|
|
38
|
-
def
|
|
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()
|
|
34
|
+
def _guide(path, meta: dict, body: str, kind: str) -> Guide:
|
|
47
35
|
detect = meta.get("detect", {}) or {}
|
|
48
36
|
return Guide(
|
|
49
37
|
id=str(meta.get("id", path.stem)),
|
|
@@ -52,16 +40,24 @@ def _parse(path: Path, kind: str) -> Guide:
|
|
|
52
40
|
detect_files=tuple(str(f) for f in detect.get("files", [])),
|
|
53
41
|
detect_manifest=tuple(str(m).lower() for m in detect.get("manifest", [])),
|
|
54
42
|
detect_imports=tuple(str(i) for i in detect.get("imports", [])),
|
|
43
|
+
entrypoint_files=tuple(str(g) for g in meta.get("entrypoint_files", [])),
|
|
55
44
|
body=body,
|
|
56
45
|
)
|
|
57
46
|
|
|
58
47
|
|
|
48
|
+
def entrypoint_globs(guides: list[Guide]) -> tuple[str, ...]:
|
|
49
|
+
"""The entrypoint-file globs declared by a set of guides, deduplicated."""
|
|
50
|
+
seen: dict[str, None] = {}
|
|
51
|
+
for g in guides:
|
|
52
|
+
for pat in g.entrypoint_files:
|
|
53
|
+
seen.setdefault(pat, None)
|
|
54
|
+
return tuple(seen)
|
|
55
|
+
|
|
56
|
+
|
|
59
57
|
def load_guides(languages_dir=LANGUAGES_DIR, frameworks_dir=FRAMEWORKS_DIR) -> list[Guide]:
|
|
60
58
|
out: list[Guide] = []
|
|
61
59
|
for directory, kind in ((languages_dir, "language"), (frameworks_dir, "framework")):
|
|
62
|
-
|
|
63
|
-
if root.is_dir():
|
|
64
|
-
out += [_parse(p, kind) for p in sorted(root.glob("*.md")) if p.name != "SKILL.md"]
|
|
60
|
+
out += [_guide(path, meta, body, kind) for path, meta, body in iter_md_docs(directory)]
|
|
65
61
|
return out
|
|
66
62
|
|
|
67
63
|
|
|
@@ -77,21 +73,9 @@ def _matches(guide: Guide, files: list[str], text: str) -> bool:
|
|
|
77
73
|
|
|
78
74
|
def select_guides(files, *, text: str = "", guides: list[Guide] | None = None) -> list[Guide]:
|
|
79
75
|
"""The guides whose detect signals fire on the target, languages first then
|
|
80
|
-
frameworks. `files` are the target's file paths
|
|
76
|
+
frameworks. `files` are the target's file paths. `text` is content to scan for
|
|
81
77
|
manifest substrings and import markers (a repo's manifests, or a diff body)."""
|
|
82
78
|
pool = load_guides() if guides is None else guides
|
|
83
79
|
file_list = list(files)
|
|
84
80
|
blob = text.lower()
|
|
85
81
|
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)
|