mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(authorization-obligations): harvest mode handler_context and the obligation label
Task 4.1. A new excerpt mode keeps the labeled handler with its decorators and every statement outside it that registers the handler by name, so guards living in decorators, routers and middleware travel with the pair. Comment and string mentions never count. The closed mechanism vocabulary gains the three obligation ids, catalog rows accept an additive obligation field validated against the closed operation kinds, and the pair scorer counts a finding tagged with that obligation inside the labeled function as a detection. Rows without the label run the same code as before. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
4bd59555ae
commit
e219dd553d
@@ -97,8 +97,9 @@ when a hunter model is configured. The improve loop rejects a change that
|
||||
regresses any profile on the holdout split.
|
||||
|
||||
Harvest for every slice goes through `benchmarks/pairs/harvest.py --slice <name>`
|
||||
(modes `name`, `line_range`, `hunk`, `enclosing`; comment and string mentions
|
||||
never anchor) and `benchmarks/pairs/catalog_gen.py --slice <name>` regenerates
|
||||
(modes `name`, `line_range`, `hunk`, `enclosing`, `handler_context`; comment
|
||||
and string mentions never anchor; `handler_context` keeps the labeled handler
|
||||
with its decorators and the statements that register it) and `benchmarks/pairs/catalog_gen.py --slice <name>` regenerates
|
||||
the catalog from `recipes.toml`. Maintainer only; CI never fetches.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -37,7 +37,7 @@ ALLOWED_HOSTS = {
|
||||
"hg-edge.mozilla.org",
|
||||
}
|
||||
BLOCKED_TOKENS = ("fixfox", "zenodo.org/record")
|
||||
MODES = ("name", "line_range", "hunk", "enclosing")
|
||||
MODES = ("name", "line_range", "hunk", "enclosing", "handler_context")
|
||||
_SUFFIX_LANGUAGE = {
|
||||
".c": "c",
|
||||
".h": "c",
|
||||
@@ -194,6 +194,28 @@ def extract_python_block(source: str, function_name: str) -> str:
|
||||
raise RecipeError(f"function not found: {function_name}")
|
||||
|
||||
|
||||
def extract_handler_context(source: str, function_name: str, language: str = "c") -> str:
|
||||
"""The named function (with its decorators) followed by every statement outside it that names the function as an
|
||||
identifier: `app.add_url_rule(..., view_func=fn)`, `router.get('/x', fn)`, `path('x', fn)`, `app.use(fn)`. Comment and
|
||||
string mentions never count (the masked text is searched). One excerpt per side, so guards living in decorators,
|
||||
routers and middleware are visible to the teacher and the scorer (authorization-obligations, Req 7.1)."""
|
||||
body = extract_function(source, function_name, language)
|
||||
lines = source.splitlines()
|
||||
start = _start_line_of(source, body) - 1
|
||||
end = start + len(body.splitlines())
|
||||
masked_lines = mask_comments_and_strings(source, language).splitlines()
|
||||
mention = re.compile(rf"(?<![\w.]){re.escape(function_name)}(?!\w)")
|
||||
declaration = re.compile(rf"^\s*(?:async\s+)?(?:def|function|sub)\s+{re.escape(function_name)}\b")
|
||||
registrations = [
|
||||
lines[index]
|
||||
for index, masked in enumerate(masked_lines)
|
||||
if not (start <= index < end) and index < len(lines) and mention.search(masked) and not declaration.match(masked)
|
||||
]
|
||||
if not registrations:
|
||||
return body
|
||||
return body.rstrip("\n") + "\n\n" + "\n".join(line.strip() for line in registrations) + "\n"
|
||||
|
||||
|
||||
def extract_line_range(source: str, start: int, end: int) -> str:
|
||||
"""Inclusive 1-based line range."""
|
||||
lines = source.splitlines(keepends=True)
|
||||
@@ -384,7 +406,7 @@ def validate_recipe(recipe: dict[str, Any], *, require_license: bool = True) ->
|
||||
raise RecipeError(f"{name}: unknown mode {mode!r}; expected one of {MODES}")
|
||||
if not recipe.get("path"):
|
||||
raise RecipeError(f"{name}: path required")
|
||||
if mode == "name" and not recipe.get("function"):
|
||||
if mode in {"name", "handler_context"} and not recipe.get("function"):
|
||||
raise RecipeError(f"{name}: path and function required")
|
||||
if mode == "line_range" and not (isinstance(recipe.get("line_start"), int) and isinstance(recipe.get("line_end"), int)):
|
||||
raise RecipeError(f"{name}: line_range mode needs integer line_start and line_end")
|
||||
@@ -456,8 +478,9 @@ def extract_pair_with_lines(recipe: dict[str, Any], parent_src: str, fix_src: st
|
||||
raise RecipeError(f"{recipe.get('name')}: line {line}/{fix_line} is not inside a function")
|
||||
return (extract_line_range(parent_src, *parent_bounds), parent_bounds[0]), (extract_line_range(fix_src, *fix_bounds), fix_bounds[0])
|
||||
function = str(recipe["function"])
|
||||
parent_text = extract_function(parent_src, function, language)
|
||||
fix_text = extract_function(fix_src, function, language)
|
||||
extract = extract_handler_context if mode == "handler_context" else extract_function
|
||||
parent_text = extract(parent_src, function, language)
|
||||
fix_text = extract(fix_src, function, language)
|
||||
return (parent_text, _start_line_of(parent_src, parent_text)), (fix_text, _start_line_of(fix_src, fix_text))
|
||||
|
||||
|
||||
@@ -511,7 +534,8 @@ def extract_pair(recipe: dict[str, Any], parent_src: str, fix_src: str) -> tuple
|
||||
raise RecipeError(f"{recipe.get('name')}: line {line}/{fix_line} is not inside a function")
|
||||
return extract_line_range(parent_src, *parent_bounds), extract_line_range(fix_src, *fix_bounds)
|
||||
function = str(recipe["function"])
|
||||
return extract_function(parent_src, function, language), extract_function(fix_src, function, language)
|
||||
extract = extract_handler_context if mode == "handler_context" else extract_function
|
||||
return extract(parent_src, function, language), extract(fix_src, function, language)
|
||||
|
||||
|
||||
def harvest_recipe(recipe: dict[str, Any], root: Path, *, require_license: bool = True) -> tuple[Path, Path]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Closed mechanism vocabulary for pair labels (pair-corpus-honesty, Req 2.1).
|
||||
# Every [[pair.expected]] row names one of these ids. Bump `version` when the set changes.
|
||||
version = "1"
|
||||
version = "2"
|
||||
|
||||
[[mechanism]]
|
||||
id = "source_reaches_sink"
|
||||
@@ -78,6 +78,18 @@ definition = "A validator exists but accepts inputs it should reject; the fix st
|
||||
id = "cross_artifact"
|
||||
definition = "The bug is only decidable with an artifact outside the vendored files (a properties file, a schema, a build flag)."
|
||||
|
||||
[[mechanism]]
|
||||
id = "unconstrained_protected_read"
|
||||
definition = "A read of a protected resource reaches the store without an identity constraint or ownership check bound from the authenticated context (authorization-obligations, protected_read)."
|
||||
|
||||
[[mechanism]]
|
||||
id = "unconstrained_protected_write"
|
||||
definition = "A write to a protected resource reaches the store without an identity constraint or ownership check bound from the authenticated context (authorization-obligations, protected_write)."
|
||||
|
||||
[[mechanism]]
|
||||
id = "unguarded_privileged_action"
|
||||
definition = "A privileged action is reached by a handler without a dominating path guard or ownership check (authorization-obligations, privileged_action)."
|
||||
|
||||
[[mechanism]]
|
||||
id = "other"
|
||||
definition = "Not yet classified. Present so bulk-mined pairs load; review should replace it."
|
||||
|
||||
Reference in New Issue
Block a user