codejury 0.1.0__py3-none-any.whl
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/__init__.py +8 -0
- codejury/agents/__init__.py +6 -0
- codejury/agents/base.py +21 -0
- codejury/agents/debate.py +188 -0
- codejury/agents/mock.py +38 -0
- codejury/agents/parsing.py +42 -0
- codejury/agents/verifier.py +106 -0
- codejury/assembly.py +76 -0
- codejury/cli.py +196 -0
- codejury/data/capabilities/authentication.yaml +67 -0
- codejury/data/capabilities/authorization.yaml +55 -0
- codejury/data/capabilities/business_logic.yaml +58 -0
- codejury/data/capabilities/crypto.yaml +78 -0
- codejury/data/capabilities/data_protection.yaml +57 -0
- codejury/data/capabilities/dependency_config.yaml +52 -0
- codejury/data/capabilities/error_logging.yaml +49 -0
- codejury/data/capabilities/input_validation.yaml +92 -0
- codejury/data/capabilities/output_encoding.yaml +56 -0
- codejury/data/capabilities/secrets.yaml +51 -0
- codejury/data/capabilities/session.yaml +60 -0
- codejury/data/golden/authn_bcrypt_password.yaml +5 -0
- codejury/data/golden/authn_sha256_password.yaml +5 -0
- codejury/data/golden/sqli_fstring_query.yaml +5 -0
- codejury/data/golden/sqli_parameterized_query.yaml +5 -0
- codejury/data/tasks/audit_diff_debate.yaml +4 -0
- codejury/data/tasks/quick_scan_single.yaml +4 -0
- codejury/domain/__init__.py +5 -0
- codejury/domain/artifact.py +20 -0
- codejury/domain/capability.py +123 -0
- codejury/domain/context.py +26 -0
- codejury/domain/observation.py +104 -0
- codejury/domain/result.py +19 -0
- codejury/evaluation.py +107 -0
- codejury/infrastructure/__init__.py +4 -0
- codejury/infrastructure/json_parse.py +57 -0
- codejury/orchestrators/__init__.py +6 -0
- codejury/orchestrators/base.py +19 -0
- codejury/orchestrators/debate.py +57 -0
- codejury/orchestrators/pipeline.py +32 -0
- codejury/orchestrators/reflexion.py +58 -0
- codejury/orchestrators/single.py +24 -0
- codejury/providers/__init__.py +5 -0
- codejury/providers/anthropic.py +68 -0
- codejury/providers/base.py +42 -0
- codejury/providers/litellm.py +68 -0
- codejury/providers/mock.py +32 -0
- codejury/providers/openai.py +57 -0
- codejury/providers/openai_format.py +30 -0
- codejury/providers/retry.py +48 -0
- codejury/reporting.py +114 -0
- codejury/resources.py +13 -0
- codejury/sources/__init__.py +6 -0
- codejury/sources/base.py +17 -0
- codejury/sources/chunker.py +33 -0
- codejury/sources/diff.py +69 -0
- codejury/sources/function.py +35 -0
- codejury/sources/mock.py +25 -0
- codejury/sources/repo.py +44 -0
- codejury/tasks/__init__.py +6 -0
- codejury/tasks/base.py +55 -0
- codejury/tasks/registry.py +22 -0
- codejury-0.1.0.dist-info/METADATA +110 -0
- codejury-0.1.0.dist-info/RECORD +67 -0
- codejury-0.1.0.dist-info/WHEEL +5 -0
- codejury-0.1.0.dist-info/entry_points.txt +2 -0
- codejury-0.1.0.dist-info/licenses/LICENSE +21 -0
- codejury-0.1.0.dist-info/top_level.txt +1 -0
codejury/cli.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Command-line entry point.
|
|
2
|
+
|
|
3
|
+
``dry-run`` wires every mock layer together with no API key, proving the
|
|
4
|
+
contracts compose. ``audit`` runs the real pipeline against the capability
|
|
5
|
+
library, backed by the Anthropic provider, under a chosen orchestration strategy
|
|
6
|
+
(single verifier, or finder/challenger/judge debate).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from codejury.agents.mock import MockAgent
|
|
15
|
+
from codejury.assembly import (
|
|
16
|
+
DEFAULT_MODEL,
|
|
17
|
+
PROVIDERS,
|
|
18
|
+
STRATEGIES,
|
|
19
|
+
build_orchestration,
|
|
20
|
+
make_provider,
|
|
21
|
+
run_over_source,
|
|
22
|
+
)
|
|
23
|
+
from codejury.domain.artifact import CodeArtifact
|
|
24
|
+
from codejury.domain.capability import Capability, load_capabilities
|
|
25
|
+
from codejury.domain.context import AnalysisContext
|
|
26
|
+
from codejury.domain.observation import Observation
|
|
27
|
+
from codejury.domain.result import AnalysisResult
|
|
28
|
+
from codejury.evaluation import Metrics, evaluate, load_cases
|
|
29
|
+
from codejury.orchestrators.single import SingleOrchestrator
|
|
30
|
+
from codejury.providers.base import Provider
|
|
31
|
+
from codejury.providers.mock import MockProvider
|
|
32
|
+
from codejury.reporting import to_json, to_markdown
|
|
33
|
+
from codejury.resources import CAPABILITIES_DIR, GOLDEN_DIR, TASKS_DIR
|
|
34
|
+
from codejury.sources.diff import DiffSource
|
|
35
|
+
from codejury.tasks.base import run_task
|
|
36
|
+
from codejury.tasks.registry import load_tasks
|
|
37
|
+
|
|
38
|
+
_FORMATS = ("text", "markdown", "json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def dry_run() -> AnalysisResult:
|
|
42
|
+
provider = MockProvider(default="[mock] no real backend was called")
|
|
43
|
+
agent = MockAgent(provider=provider, role="verifier")
|
|
44
|
+
orchestrator = SingleOrchestrator()
|
|
45
|
+
capabilities = [
|
|
46
|
+
Capability(id="authn", name="Authentication"),
|
|
47
|
+
Capability(id="crypto", name="Cryptography"),
|
|
48
|
+
]
|
|
49
|
+
ctx = AnalysisContext(
|
|
50
|
+
artifact=CodeArtifact(kind="diff", path="auth.py", content="+ hashlib.sha256(pwd)"),
|
|
51
|
+
capabilities=capabilities,
|
|
52
|
+
)
|
|
53
|
+
return orchestrator.run({"verifier": agent}, ctx)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def audit(
|
|
57
|
+
diff_text: str,
|
|
58
|
+
capabilities: list[Capability],
|
|
59
|
+
*,
|
|
60
|
+
provider: Provider,
|
|
61
|
+
model: str,
|
|
62
|
+
max_tokens: int = 2048,
|
|
63
|
+
strategy: str = "single",
|
|
64
|
+
) -> list[tuple[str, AnalysisResult]]:
|
|
65
|
+
"""Audit each changed file in `diff_text`, returning (path, result) per file."""
|
|
66
|
+
agents, orchestrator = build_orchestration(strategy, provider=provider, model=model, max_tokens=max_tokens)
|
|
67
|
+
return run_over_source(DiffSource(diff_text), capabilities, agents, orchestrator)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _render_dry_run(result: AnalysisResult) -> str:
|
|
71
|
+
lines = [f"observations: {len(result.observations)}"]
|
|
72
|
+
for o in result.observations:
|
|
73
|
+
lines.append(f" [{o.kind}] {o.capability} by {o.produced_by} -> {getattr(o, 'status', '-')}")
|
|
74
|
+
if result.error:
|
|
75
|
+
lines.append(f"error: {result.error}")
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _render_audit(results: list[tuple[str, AnalysisResult]]) -> str:
|
|
80
|
+
if not results:
|
|
81
|
+
return "no changed files in diff"
|
|
82
|
+
lines = []
|
|
83
|
+
for path, result in results:
|
|
84
|
+
lines.append(f"== {path} ==")
|
|
85
|
+
if result.error:
|
|
86
|
+
lines.append(f" error: {result.error}")
|
|
87
|
+
for o in result.observations:
|
|
88
|
+
lines.append(" " + _render_observation(o))
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _render_observation(o: Observation) -> str:
|
|
93
|
+
if o.kind == "verdict":
|
|
94
|
+
matched = o.matched_anti or o.matched_correct
|
|
95
|
+
suffix = f" [{', '.join(matched)}]" if matched else ""
|
|
96
|
+
return f"{o.status:<11} {o.capability}{suffix}"
|
|
97
|
+
if o.kind == "finding":
|
|
98
|
+
cwe = f" {o.cwe}" if o.cwe else ""
|
|
99
|
+
return f"{'FINDING':<11} [{o.severity}{cwe}] {o.title}"
|
|
100
|
+
if o.kind == "concession":
|
|
101
|
+
return f"{'DISMISSED':<11} {o.target}: {o.reason}"
|
|
102
|
+
return f"{o.kind}: {o.capability}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _render_results(fmt: str, results: list[tuple[str, AnalysisResult]]) -> str:
|
|
106
|
+
return {"text": _render_audit, "markdown": to_markdown, "json": to_json}[fmt](results)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_metrics(m: Metrics) -> str:
|
|
110
|
+
return (
|
|
111
|
+
f"cases: {m.total} (tp={m.tp} fp={m.fp} tn={m.tn} fn={m.fn})\n"
|
|
112
|
+
f"precision: {m.precision:.2f} recall: {m.recall:.2f} accuracy: {m.accuracy:.2f}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _read_diff(path: str) -> str:
|
|
117
|
+
if path == "-":
|
|
118
|
+
return sys.stdin.read()
|
|
119
|
+
with open(path, encoding="utf-8") as f:
|
|
120
|
+
return f.read()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv: list[str] | None = None) -> int:
|
|
124
|
+
parser = argparse.ArgumentParser(prog="codejury")
|
|
125
|
+
sub = parser.add_subparsers(dest="command")
|
|
126
|
+
|
|
127
|
+
sub.add_parser("dry-run", help="run the mock pipeline end to end")
|
|
128
|
+
|
|
129
|
+
audit_p = sub.add_parser("audit", help="audit a unified diff against the capability library")
|
|
130
|
+
audit_p.add_argument("diff", nargs="?", default="-", help="unified diff file, or - for stdin")
|
|
131
|
+
audit_p.add_argument("--capabilities", default=CAPABILITIES_DIR, help="capability YAML directory")
|
|
132
|
+
audit_p.add_argument("--orchestrator", choices=STRATEGIES, default="single")
|
|
133
|
+
audit_p.add_argument("--provider", choices=PROVIDERS, default="anthropic")
|
|
134
|
+
audit_p.add_argument("--format", choices=_FORMATS, default="text", dest="fmt")
|
|
135
|
+
audit_p.add_argument("--model", default=DEFAULT_MODEL)
|
|
136
|
+
audit_p.add_argument("--max-tokens", type=int, default=2048)
|
|
137
|
+
audit_p.add_argument("--retries", type=int, default=0, help="provider retry attempts on failure")
|
|
138
|
+
|
|
139
|
+
run_p = sub.add_parser("run", help="run a named task preset against a unified diff")
|
|
140
|
+
run_p.add_argument("task", help="task name")
|
|
141
|
+
run_p.add_argument("diff", nargs="?", default="-", help="unified diff file, or - for stdin")
|
|
142
|
+
run_p.add_argument("--tasks", default=TASKS_DIR, help="task YAML directory")
|
|
143
|
+
run_p.add_argument("--capabilities", default=CAPABILITIES_DIR, help="capability YAML directory")
|
|
144
|
+
run_p.add_argument("--format", choices=_FORMATS, default="text", dest="fmt")
|
|
145
|
+
|
|
146
|
+
eval_p = sub.add_parser("eval", help="score golden cases and report precision/recall")
|
|
147
|
+
eval_p.add_argument("--golden", default=GOLDEN_DIR, help="golden case YAML directory")
|
|
148
|
+
eval_p.add_argument("--capabilities", default=CAPABILITIES_DIR, help="capability YAML directory")
|
|
149
|
+
eval_p.add_argument("--provider", choices=PROVIDERS, default="anthropic")
|
|
150
|
+
eval_p.add_argument("--model", default=DEFAULT_MODEL)
|
|
151
|
+
|
|
152
|
+
args = parser.parse_args(argv)
|
|
153
|
+
|
|
154
|
+
if args.command == "audit":
|
|
155
|
+
results = audit(
|
|
156
|
+
_read_diff(args.diff),
|
|
157
|
+
load_capabilities(args.capabilities),
|
|
158
|
+
provider=make_provider(args.provider, retries=args.retries),
|
|
159
|
+
model=args.model,
|
|
160
|
+
max_tokens=args.max_tokens,
|
|
161
|
+
strategy=args.orchestrator,
|
|
162
|
+
)
|
|
163
|
+
print(_render_results(args.fmt, results))
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
if args.command == "run":
|
|
167
|
+
tasks = load_tasks(args.tasks)
|
|
168
|
+
if args.task not in tasks:
|
|
169
|
+
print(f"unknown task {args.task!r}; available: {', '.join(sorted(tasks)) or '(none)'}")
|
|
170
|
+
return 1
|
|
171
|
+
results = run_task(
|
|
172
|
+
tasks[args.task], DiffSource(_read_diff(args.diff)), load_capabilities(args.capabilities)
|
|
173
|
+
)
|
|
174
|
+
print(_render_results(args.fmt, results))
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
if args.command == "eval":
|
|
178
|
+
metrics = evaluate(
|
|
179
|
+
load_cases(args.golden),
|
|
180
|
+
load_capabilities(args.capabilities),
|
|
181
|
+
provider=make_provider(args.provider),
|
|
182
|
+
model=args.model,
|
|
183
|
+
)
|
|
184
|
+
print(_render_metrics(metrics))
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
if args.command in (None, "dry-run"):
|
|
188
|
+
print(_render_dry_run(dry_run()))
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
parser.print_help()
|
|
192
|
+
return 1
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
id: authn
|
|
2
|
+
name: Authentication
|
|
3
|
+
asvs_chapter: V2
|
|
4
|
+
description: Mechanisms that verify a caller's claimed identity.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
password_storage:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: PWD-OK-1
|
|
10
|
+
description: Hash passwords with bcrypt, scrypt, or Argon2id at an OWASP-recommended cost
|
|
11
|
+
signals: ["bcrypt.hashpw", "argon2.PasswordHasher", "hashlib.scrypt", "passlib"]
|
|
12
|
+
why_ok: Slow, salted, memory-hard hashing resists GPU brute force and rainbow tables
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: PWD-BAD-1
|
|
16
|
+
cwe: CWE-916
|
|
17
|
+
severity: HIGH
|
|
18
|
+
description: Hash passwords with a fast general-purpose digest such as MD5, SHA-1, or SHA-256
|
|
19
|
+
signals: ["hashlib.md5(", "hashlib.sha1(", "hashlib.sha256("]
|
|
20
|
+
why_bad: Unsalted fast hashes are brute-forced at billions of guesses per second on commodity GPUs
|
|
21
|
+
example_bad: |
|
|
22
|
+
hashlib.sha256(password.encode()).hexdigest()
|
|
23
|
+
example_good: |
|
|
24
|
+
bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
|
25
|
+
|
|
26
|
+
- id: PWD-BAD-2
|
|
27
|
+
cwe: CWE-256
|
|
28
|
+
severity: HIGH
|
|
29
|
+
description: Store passwords in plaintext or with reversible encryption
|
|
30
|
+
why_bad: A single database leak exposes every credential directly
|
|
31
|
+
|
|
32
|
+
- id: PWD-BAD-3
|
|
33
|
+
cwe: CWE-759
|
|
34
|
+
severity: MEDIUM
|
|
35
|
+
description: Hash without a per-user salt, using a global salt or no salt
|
|
36
|
+
why_bad: Identical passwords produce identical hashes, so precomputed tables apply across users
|
|
37
|
+
|
|
38
|
+
jwt_verification:
|
|
39
|
+
correct_patterns:
|
|
40
|
+
- id: JWT-OK-1
|
|
41
|
+
description: Verify the signature and validate iss, aud, exp, and nbf before trusting any claim
|
|
42
|
+
signals: ["algorithms=", "audience=", "issuer="]
|
|
43
|
+
why_ok: Rejects forged, expired, and misrouted tokens before their claims are used
|
|
44
|
+
|
|
45
|
+
anti_patterns:
|
|
46
|
+
- id: JWT-BAD-1
|
|
47
|
+
cwe: CWE-347
|
|
48
|
+
severity: HIGH
|
|
49
|
+
description: Accept the "none" algorithm, or sign with a weak or hardcoded HS256 secret
|
|
50
|
+
signals: ['algorithms=["none"]', 'algorithms=["HS256"]']
|
|
51
|
+
why_bad: An attacker can forge tokens that the server accepts as authentic
|
|
52
|
+
|
|
53
|
+
- id: JWT-BAD-2
|
|
54
|
+
cwe: CWE-345
|
|
55
|
+
severity: HIGH
|
|
56
|
+
description: Read claims before verifying the signature, or skip verification entirely
|
|
57
|
+
signals: ['verify_signature": False', "verify=False"]
|
|
58
|
+
why_bad: Attacker-controlled claims drive trust and authorization decisions
|
|
59
|
+
example_bad: |
|
|
60
|
+
claims = jwt.decode(token, options={"verify_signature": False})
|
|
61
|
+
example_good: |
|
|
62
|
+
claims = jwt.decode(token, key, algorithms=["RS256"], audience=AUD, issuer=ISS)
|
|
63
|
+
|
|
64
|
+
trigger_signals:
|
|
65
|
+
- routes matching /login, /register, /auth, or /token appear
|
|
66
|
+
- imports of jwt, pyjwt, python-jose, or authlib
|
|
67
|
+
- a user model with a password or password_hash field
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
id: authz
|
|
2
|
+
name: Authorization
|
|
3
|
+
asvs_chapter: V4
|
|
4
|
+
description: Deciding whether an authenticated caller is allowed to perform an action on a resource.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
object_level:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: OBJ-OK-1
|
|
10
|
+
description: Confirm the authenticated user owns or may access the object before acting on it
|
|
11
|
+
signals: ["filter(owner=request.user", "get_object_or_404(..., user=", "current_user.id =="]
|
|
12
|
+
why_ok: Ties every object access to the caller's identity, closing direct-object-reference holes
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: IDOR-BAD-1
|
|
16
|
+
cwe: CWE-639
|
|
17
|
+
severity: HIGH
|
|
18
|
+
description: Fetch or mutate a record by a user-supplied id with no ownership or access check
|
|
19
|
+
signals: ["get(id=request", "objects.get(pk=", "WHERE id ="]
|
|
20
|
+
why_bad: Any user can read or change another user's data by changing the id (IDOR)
|
|
21
|
+
example_bad: |
|
|
22
|
+
account = Account.objects.get(id=request.GET["account_id"])
|
|
23
|
+
example_good: |
|
|
24
|
+
account = Account.objects.get(id=request.GET["account_id"], owner=request.user)
|
|
25
|
+
|
|
26
|
+
- id: AUTHZ-BAD-1
|
|
27
|
+
cwe: CWE-862
|
|
28
|
+
severity: HIGH
|
|
29
|
+
description: Missing function or endpoint level authorization, so any authenticated user reaches privileged actions
|
|
30
|
+
why_bad: Authentication is checked but authorization is not; non-admins can hit admin routes
|
|
31
|
+
|
|
32
|
+
privilege:
|
|
33
|
+
correct_patterns:
|
|
34
|
+
- id: PRIV-OK-1
|
|
35
|
+
description: Derive roles and permissions server-side from a trusted store, checked per request
|
|
36
|
+
why_ok: The client cannot grant itself privileges it was not assigned
|
|
37
|
+
|
|
38
|
+
anti_patterns:
|
|
39
|
+
- id: PRIV-BAD-1
|
|
40
|
+
cwe: CWE-269
|
|
41
|
+
severity: HIGH
|
|
42
|
+
description: Derive role or permission from a client-controlled field (request body, query param, client-set claim)
|
|
43
|
+
signals: ["request.json[\"role\"]", "is_admin = request", "request.POST.get(\"role\""]
|
|
44
|
+
why_bad: An attacker sets the field and escalates to admin
|
|
45
|
+
|
|
46
|
+
- id: PRIV-BAD-2
|
|
47
|
+
cwe: CWE-602
|
|
48
|
+
severity: MEDIUM
|
|
49
|
+
description: Enforce authorization only in the client or UI, not on the server
|
|
50
|
+
why_bad: The server is the only trust boundary; hidden buttons stop nothing
|
|
51
|
+
|
|
52
|
+
trigger_signals:
|
|
53
|
+
- routes with an :id or <pk> path parameter
|
|
54
|
+
- admin or privileged endpoints
|
|
55
|
+
- role, permission, or is_admin checks
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
id: business_logic
|
|
2
|
+
name: Business Logic
|
|
3
|
+
asvs_chapter: V11
|
|
4
|
+
description: Correctness of stateful workflows against abuse -- ordering, races, replay, and value validation.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
state_and_sequence:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: BL-OK-1
|
|
10
|
+
description: Enforce the workflow state machine server-side and make sensitive actions idempotent
|
|
11
|
+
signals: ["idempotency_key", "select_for_update", "with lock"]
|
|
12
|
+
why_ok: Steps cannot be skipped or applied twice regardless of client behavior
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: SEQ-BAD-1
|
|
16
|
+
cwe: CWE-841
|
|
17
|
+
severity: MEDIUM
|
|
18
|
+
description: Do not enforce step ordering, so a later step can be invoked without the earlier ones
|
|
19
|
+
why_bad: An attacker reaches checkout or fulfillment without payment or validation
|
|
20
|
+
|
|
21
|
+
- id: RACE-BAD-1
|
|
22
|
+
cwe: CWE-362
|
|
23
|
+
severity: HIGH
|
|
24
|
+
description: Check-then-act on shared state without a lock or atomic update
|
|
25
|
+
signals: ["if balance >=", "balance -=", ".get(...)\n ...save()"]
|
|
26
|
+
why_bad: Concurrent requests both pass the check, enabling double spend
|
|
27
|
+
example_bad: |
|
|
28
|
+
if account.balance >= amount:
|
|
29
|
+
account.balance -= amount
|
|
30
|
+
example_good: |
|
|
31
|
+
with transaction.atomic():
|
|
32
|
+
acct = Account.objects.select_for_update().get(pk=id)
|
|
33
|
+
if acct.balance >= amount: acct.balance -= amount
|
|
34
|
+
|
|
35
|
+
limits_and_replay:
|
|
36
|
+
correct_patterns:
|
|
37
|
+
- id: LIM-OK-1
|
|
38
|
+
description: Validate amounts, quantities, and ranges server-side and rate-limit sensitive actions
|
|
39
|
+
why_ok: Client-supplied values cannot drive the outcome
|
|
40
|
+
|
|
41
|
+
anti_patterns:
|
|
42
|
+
- id: AMT-BAD-1
|
|
43
|
+
cwe: CWE-840
|
|
44
|
+
severity: MEDIUM
|
|
45
|
+
description: Trust a client-supplied price, amount, or quantity without server validation
|
|
46
|
+
signals: ["request.json[\"price\"]", "total = request", "quantity = request"]
|
|
47
|
+
why_bad: A user sets a negative or tiny amount and underpays
|
|
48
|
+
|
|
49
|
+
- id: REPLAY-BAD-1
|
|
50
|
+
cwe: CWE-799
|
|
51
|
+
severity: MEDIUM
|
|
52
|
+
description: No rate limiting or replay protection on sensitive actions
|
|
53
|
+
why_bad: Requests can be replayed or brute-forced without restriction
|
|
54
|
+
|
|
55
|
+
trigger_signals:
|
|
56
|
+
- multi-step flows such as checkout, transfer, or approval
|
|
57
|
+
- balance, amount, price, or quantity arithmetic
|
|
58
|
+
- shared-state updates without locking or transactions
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
id: crypto
|
|
2
|
+
name: Cryptography
|
|
3
|
+
asvs_chapter: V6
|
|
4
|
+
description: Correct choice and use of cryptographic primitives for confidentiality and integrity.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
algorithm:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: CRYPTO-OK-1
|
|
10
|
+
description: Use vetted authenticated encryption such as AES-GCM or ChaCha20-Poly1305
|
|
11
|
+
signals: ["AESGCM", "ChaCha20Poly1305", "AES.MODE_GCM"]
|
|
12
|
+
why_ok: Modern AEAD provides confidentiality and integrity with no mode pitfalls
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: CRYPTO-BAD-1
|
|
16
|
+
cwe: CWE-327
|
|
17
|
+
severity: HIGH
|
|
18
|
+
description: Use a broken or obsolete cipher or hash for security (DES, 3DES, RC4, MD5, SHA-1)
|
|
19
|
+
signals: ["DES.new", "ARC4", "Crypto.Cipher.DES", "hashlib.md5", "hashlib.sha1"]
|
|
20
|
+
why_bad: These are practically attackable and unfit for protecting data
|
|
21
|
+
example_bad: |
|
|
22
|
+
cipher = DES.new(key, DES.MODE_ECB)
|
|
23
|
+
example_good: |
|
|
24
|
+
cipher = AESGCM(key); ct = cipher.encrypt(nonce, data, None)
|
|
25
|
+
|
|
26
|
+
- id: CRYPTO-BAD-2
|
|
27
|
+
cwe: CWE-327
|
|
28
|
+
severity: HIGH
|
|
29
|
+
description: Use ECB mode for a block cipher
|
|
30
|
+
signals: ["MODE_ECB"]
|
|
31
|
+
why_bad: ECB leaks plaintext structure because identical blocks encrypt identically
|
|
32
|
+
|
|
33
|
+
- id: CRYPTO-BAD-3
|
|
34
|
+
cwe: CWE-326
|
|
35
|
+
severity: MEDIUM
|
|
36
|
+
description: Use an inadequate key size (e.g. RSA shorter than 2048 bits)
|
|
37
|
+
why_bad: Undersized keys are within reach of feasible attacks
|
|
38
|
+
|
|
39
|
+
key_and_nonce:
|
|
40
|
+
correct_patterns:
|
|
41
|
+
- id: KEY-OK-1
|
|
42
|
+
description: Load keys from a KMS or secret store and rotate them; derive a fresh random nonce per message
|
|
43
|
+
why_ok: Keys are not exposed in code and nonces stay unique
|
|
44
|
+
|
|
45
|
+
anti_patterns:
|
|
46
|
+
- id: KEY-BAD-1
|
|
47
|
+
cwe: CWE-321
|
|
48
|
+
severity: HIGH
|
|
49
|
+
description: Hardcode a cryptographic key in source
|
|
50
|
+
signals: ["key = b\"", "SECRET_KEY = \"", "AES_KEY ="]
|
|
51
|
+
why_bad: Anyone with the source can decrypt everything
|
|
52
|
+
|
|
53
|
+
- id: IV-BAD-1
|
|
54
|
+
cwe: CWE-329
|
|
55
|
+
severity: HIGH
|
|
56
|
+
description: Use a static or reused IV/nonce
|
|
57
|
+
signals: ["iv = b\"\\x00", "nonce = b\"", "IV = bytes(16)"]
|
|
58
|
+
why_bad: Nonce reuse breaks GCM and reveals patterns under CBC
|
|
59
|
+
|
|
60
|
+
randomness:
|
|
61
|
+
correct_patterns:
|
|
62
|
+
- id: RND-OK-1
|
|
63
|
+
description: Use a CSPRNG (secrets, os.urandom) for tokens, keys, and salts
|
|
64
|
+
signals: ["secrets.token_", "os.urandom("]
|
|
65
|
+
why_ok: Cryptographically strong randomness is unpredictable
|
|
66
|
+
|
|
67
|
+
anti_patterns:
|
|
68
|
+
- id: RND-BAD-1
|
|
69
|
+
cwe: CWE-338
|
|
70
|
+
severity: MEDIUM
|
|
71
|
+
description: Use a non-cryptographic PRNG for security-sensitive values
|
|
72
|
+
signals: ["random.random(", "random.randint(", "random.choice("]
|
|
73
|
+
why_bad: Mersenne Twister output is predictable from a few samples
|
|
74
|
+
|
|
75
|
+
trigger_signals:
|
|
76
|
+
- imports of hashlib, Crypto, cryptography, or ssl
|
|
77
|
+
- encrypt, decrypt, sign, or token generation
|
|
78
|
+
- key, iv, nonce, or salt literals
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
id: data_protection
|
|
2
|
+
name: Data Protection
|
|
3
|
+
asvs_chapter: V8
|
|
4
|
+
description: Protecting sensitive data in transit, at rest, and in use, including PII handling.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
in_transit:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: TLS-OK-1
|
|
10
|
+
description: Send sensitive data only over TLS and verify the peer certificate
|
|
11
|
+
signals: ["https://", "verify=True", "ssl.create_default_context"]
|
|
12
|
+
why_ok: Confidentiality and integrity on the wire, with a checked endpoint identity
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: TLS-BAD-1
|
|
16
|
+
cwe: CWE-319
|
|
17
|
+
severity: HIGH
|
|
18
|
+
description: Transmit sensitive data over plaintext HTTP
|
|
19
|
+
signals: ["http://"]
|
|
20
|
+
why_bad: Anyone on the path can read or alter the data
|
|
21
|
+
|
|
22
|
+
- id: TLS-BAD-2
|
|
23
|
+
cwe: CWE-295
|
|
24
|
+
severity: HIGH
|
|
25
|
+
description: Disable TLS certificate verification
|
|
26
|
+
signals: ["verify=False", "ssl._create_unverified_context", "CERT_NONE"]
|
|
27
|
+
why_bad: Removes protection against man-in-the-middle interception
|
|
28
|
+
example_bad: |
|
|
29
|
+
requests.get(url, verify=False)
|
|
30
|
+
example_good: |
|
|
31
|
+
requests.get(url) # verification on by default
|
|
32
|
+
|
|
33
|
+
at_rest:
|
|
34
|
+
correct_patterns:
|
|
35
|
+
- id: REST-OK-1
|
|
36
|
+
description: Encrypt sensitive fields and backups at rest
|
|
37
|
+
why_ok: A storage or backup leak does not expose plaintext
|
|
38
|
+
|
|
39
|
+
anti_patterns:
|
|
40
|
+
- id: REST-BAD-1
|
|
41
|
+
cwe: CWE-311
|
|
42
|
+
severity: MEDIUM
|
|
43
|
+
description: Store sensitive data (PII, tokens, financial) unencrypted
|
|
44
|
+
why_bad: Any database or disk access reveals it directly
|
|
45
|
+
|
|
46
|
+
pii:
|
|
47
|
+
anti_patterns:
|
|
48
|
+
- id: PII-BAD-1
|
|
49
|
+
cwe: CWE-359
|
|
50
|
+
severity: MEDIUM
|
|
51
|
+
description: Expose PII in URLs, responses, or logs beyond what the operation needs
|
|
52
|
+
why_bad: PII spreads into caches, history, and logs, widening breach impact
|
|
53
|
+
|
|
54
|
+
trigger_signals:
|
|
55
|
+
- outbound requests or URLs carrying user data
|
|
56
|
+
- models or tables with PII, token, or financial fields
|
|
57
|
+
- TLS or certificate verification configuration
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
id: dependency_config
|
|
2
|
+
name: Dependencies and Configuration
|
|
3
|
+
asvs_chapter: V14
|
|
4
|
+
description: Software supply chain and deployment configuration -- known-vulnerable components and unsafe defaults.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
dependencies:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: DEP-OK-1
|
|
10
|
+
description: Pin dependency versions and scan them for known vulnerabilities
|
|
11
|
+
signals: ["==", "requirements.txt", "poetry.lock", "pip-audit"]
|
|
12
|
+
why_ok: Builds are reproducible and known-vulnerable versions are caught
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: DEP-BAD-1
|
|
16
|
+
cwe: CWE-1104
|
|
17
|
+
severity: MEDIUM
|
|
18
|
+
description: Depend on unmaintained or known-vulnerable components
|
|
19
|
+
why_bad: Public CVEs in shipped dependencies are directly exploitable
|
|
20
|
+
|
|
21
|
+
- id: DEP-BAD-2
|
|
22
|
+
cwe: CWE-494
|
|
23
|
+
severity: MEDIUM
|
|
24
|
+
description: Download or install code at runtime without an integrity or signature check
|
|
25
|
+
signals: ["curl | sh", "pip install http", "urlretrieve"]
|
|
26
|
+
why_bad: A tampered or hijacked source injects code into the build or host
|
|
27
|
+
|
|
28
|
+
configuration:
|
|
29
|
+
correct_patterns:
|
|
30
|
+
- id: CFG-OK-1
|
|
31
|
+
description: Ship secure defaults with least privilege and a minimal exposed surface
|
|
32
|
+
why_ok: Misconfiguration is the default-off state, not something to remember
|
|
33
|
+
|
|
34
|
+
anti_patterns:
|
|
35
|
+
- id: CFG-BAD-1
|
|
36
|
+
cwe: CWE-732
|
|
37
|
+
severity: MEDIUM
|
|
38
|
+
description: Grant overly permissive permissions (world-writable files, public storage buckets)
|
|
39
|
+
signals: ["chmod 0777", "0o777", "ACL: public-read", "AllUsers"]
|
|
40
|
+
why_bad: Anyone can read or modify resources that should be restricted
|
|
41
|
+
|
|
42
|
+
- id: CFG-BAD-2
|
|
43
|
+
cwe: CWE-1392
|
|
44
|
+
severity: HIGH
|
|
45
|
+
description: Ship default or sample credentials
|
|
46
|
+
signals: ["admin:admin", "password=admin", "changeme"]
|
|
47
|
+
why_bad: Default credentials are public knowledge and trivially abused
|
|
48
|
+
|
|
49
|
+
trigger_signals:
|
|
50
|
+
- dependency manifests and lock files
|
|
51
|
+
- install or bootstrap scripts fetching remote code
|
|
52
|
+
- file permission, bucket ACL, or default credential settings
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
id: error_logging
|
|
2
|
+
name: Error Handling and Logging
|
|
3
|
+
asvs_chapter: V7
|
|
4
|
+
description: Failing without leaking information, and recording enough to investigate incidents.
|
|
5
|
+
|
|
6
|
+
sub_capabilities:
|
|
7
|
+
information_leakage:
|
|
8
|
+
correct_patterns:
|
|
9
|
+
- id: ERR-OK-1
|
|
10
|
+
description: Return a generic error to the client and log the detail server-side
|
|
11
|
+
signals: ["except Exception", "return 500", "abort(500)"]
|
|
12
|
+
why_ok: The caller learns nothing exploitable while operators keep the detail
|
|
13
|
+
|
|
14
|
+
anti_patterns:
|
|
15
|
+
- id: ERR-BAD-1
|
|
16
|
+
cwe: CWE-209
|
|
17
|
+
severity: MEDIUM
|
|
18
|
+
description: Return a stack trace or exception detail to the client
|
|
19
|
+
signals: ["traceback.format_exc()", "str(e)", "return jsonify(error=str"]
|
|
20
|
+
why_bad: Internal paths, queries, and versions help an attacker map the system
|
|
21
|
+
example_bad: |
|
|
22
|
+
return jsonify(error=traceback.format_exc()), 500
|
|
23
|
+
example_good: |
|
|
24
|
+
app.logger.exception("checkout failed"); return jsonify(error="internal error"), 500
|
|
25
|
+
|
|
26
|
+
- id: ERR-BAD-2
|
|
27
|
+
cwe: CWE-489
|
|
28
|
+
severity: LOW
|
|
29
|
+
description: Leave a debug feature or verbose mode enabled in production
|
|
30
|
+
signals: ["DEBUG = True", "app.run(debug=True", "FLASK_DEBUG=1"]
|
|
31
|
+
why_bad: Debug pages expose internals and sometimes allow code execution
|
|
32
|
+
|
|
33
|
+
audit_trail:
|
|
34
|
+
correct_patterns:
|
|
35
|
+
- id: AUDIT-OK-1
|
|
36
|
+
description: Log security-relevant events (auth attempts, access denials, admin actions) with timestamp and actor
|
|
37
|
+
why_ok: Gives a forensic trail to detect and reconstruct abuse
|
|
38
|
+
|
|
39
|
+
anti_patterns:
|
|
40
|
+
- id: AUDIT-BAD-1
|
|
41
|
+
cwe: CWE-778
|
|
42
|
+
severity: MEDIUM
|
|
43
|
+
description: Do not log security-relevant events
|
|
44
|
+
why_bad: Without an audit trail, intrusions go unnoticed and uninvestigable
|
|
45
|
+
|
|
46
|
+
trigger_signals:
|
|
47
|
+
- exception handlers and error responses
|
|
48
|
+
- logging configuration and DEBUG flags
|
|
49
|
+
- authentication, authorization, or admin actions
|