Skip to content

Commit c2b908c

Browse files
committed
Add rules and a reasoning pass for weaknesses the code patterns missed
Twenty three new code rules, each written for a class of weakness rather than one application: checks that return true before checking anything, CSV exports without formula neutralizing, redirects followed after checking only the first address, a host checked and then fetched by name, certificate checking turned off, secrets on a command line, headers set for a whole browser session, links printed with HTML escaping only, strip_tags and regular expressions used as sanitizers, archives read without a size limit, SAML responses not tied to a sign in, privileged containers, and editor settings a workspace can redirect. Fixtures say what each rule must find and what it must leave alone. Code reasoning, on the AI page and off by default, asks the model about the request handlers that take an id or do something privileged, with the access checks they call and nothing else. Answers are cached against exactly what was sent, only high severity claims get a second question, and a monthly token limit is checked before every question. It only adds findings for a person to confirm, and a scan never closes them. Three read only dynamic rules: a sign in form that sends the password in the address, single sign on discovery that names plain http addresses, and a session cookie sent on requests from other sites. The rule language gains body_matches and cookie_matches, both bounded.
1 parent 304c603 commit c2b908c

16 files changed

Lines changed: 1573 additions & 7 deletions

‎app/core/ai.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@
107107
"ai.auto_review_max": (200, False,
108108
"Most new findings one scan hands to the automatic "
109109
"review, worst first. 0 for no limit."),
110+
# Off by default for the same reason. On, the handlers that read the
111+
# request and touch stored data are read after every scan, a few at a
112+
# time, for access checks a pattern cannot judge.
113+
"ai.code_reasoning": (False, False,
114+
"After a scan, ask the model about request handlers for "
115+
"missing access checks and similar logic mistakes."),
116+
"ai.code_reasoning_monthly_tokens": (1_000_000, False,
117+
"Most tokens the code reasoning pass may "
118+
"use in a calendar month, in and out "
119+
"together."),
120+
"ai.code_reasoning_per_repo": (25, False,
121+
"Most handlers asked about in one repository per "
122+
"run, the likeliest first."),
110123
# What the custom service charges, per million tokens, for the cost page.
111124
# Kept as text so 0.10 stays 0.10. Empty is not counted.
112125
"ai.price_input": ("", False, "Price per million input tokens."),
@@ -190,6 +203,12 @@ def read_form(form, text, number, url, checked) -> dict:
190203
"Questions per minute", 1, 600),
191204
"ai.auto_review_max": number(form.get("ai.auto_review_max") or 200,
192205
"New findings per scan", 0, 100_000),
206+
"ai.code_reasoning": checked("ai.code_reasoning"),
207+
"ai.code_reasoning_monthly_tokens": number(
208+
form.get("ai.code_reasoning_monthly_tokens") or 0,
209+
"Monthly token limit", 0, 2_000_000_000),
210+
"ai.code_reasoning_per_repo": number(form.get("ai.code_reasoning_per_repo") or 25,
211+
"Handlers per repository", 1, 500),
193212
}
194213

195214

@@ -295,6 +314,18 @@ def describe_model(cfg: dict) -> str:
295314
return ""
296315

297316

317+
def low_effort(cfg: dict) -> dict:
318+
"""A copy of the settings asking for the least effort the provider allows.
319+
320+
Only a provider with an effort setting is changed, and only when one is
321+
set, because an empty one means the model does not take it.
322+
"""
323+
out = dict(cfg)
324+
if out.get("ai.anthropic_effort"):
325+
out["ai.anthropic_effort"] = "low"
326+
return out
327+
328+
298329
def complete(cfg: dict, system: str, prompt: str, *, max_tokens: int = None,
299330
timeout: float = None, purpose: str = "other") -> dict:
300331
"""Ask once. -> {ok, text, error, provider, model, input_tokens,

‎app/core/assessment.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,9 @@ def progress():
11061106

11071107

11081108
REPORT_ENGINE = "security-report"
1109+
# Findings a model raised rather than a scanner. A scan never closes them,
1110+
# and they step aside when a scanner reports the same place.
1111+
MODEL_ENGINES = (REPORT_ENGINE, "code-reasoning")
11091112
MAX_LINE_FILES = 20_000
11101113
NEAR_LINES = 3 # the same place, give or take a line or two
11111114
SAME_CWE_LINES = 30 # the same weakness in the same part of the file
@@ -1169,7 +1172,7 @@ def hand_over(session, repositories) -> int:
11691172
return 0
11701173
ours = (session.query(Finding)
11711174
.filter(Finding.repository.in_(list(repositories)),
1172-
Finding.engines == REPORT_ENGINE,
1175+
Finding.engines.in_(MODEL_ENGINES),
11731176
Finding.state == FindingState.OPEN)
11741177
.all())
11751178
handed = 0
@@ -1184,13 +1187,15 @@ def hand_over(session, repositories) -> int:
11841187
Finding.kind.in_([FindingKind.CODE,
11851188
FindingKind.MISCONFIGURATION]),
11861189
Finding.state == FindingState.OPEN,
1187-
Finding.engines != REPORT_ENGINE)
1190+
Finding.engines.notin_(MODEL_ENGINES))
11881191
.all() if _near(g, place)), None)
11891192
if scanner is None:
11901193
continue
11911194
f.state, f.excepted = FindingState.WAIVED, True
11921195
f.exception_reason = f"Now reported by the scanner as finding {scanner.id}."
1193-
f.exception_by, f.exception_at = "the security report", _now()
1196+
f.exception_by = ("the security report" if f.engines == REPORT_ENGINE
1197+
else "the code reasoning pass")
1198+
f.exception_at = _now()
11941199
handed += 1
11951200
return handed
11961201

0 commit comments

Comments
 (0)