mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
learn: trace-enabled Joern run over labelled units (opt-in trace in taint.sc, trace parsing, resumable runner)
Implemented with Codex. Default scan queries do not request a trace. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
20777d8863
commit
19c936343b
@@ -56,7 +56,8 @@
|
||||
contextFilter: String = "",
|
||||
contextPaths: String = "",
|
||||
requests: String = "",
|
||||
requestsFile: String = ""
|
||||
requestsFile: String = "",
|
||||
trace: String = ""
|
||||
) = {
|
||||
importCpg(cpgFile)
|
||||
|
||||
@@ -1489,6 +1490,12 @@
|
||||
// collapsed the sizer to three sink visits per JVM start on the plugin.
|
||||
val started = System.nanoTime()
|
||||
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
|
||||
// Opt-in heartbeat for the trace runner's per-question process watchdog.
|
||||
// Killing the JVM preserves completed streamed rows without unsafe query threads.
|
||||
if (field("questionDeadline").nonEmpty) {
|
||||
println(ujson.write(ujson.Obj("__question__" -> id)))
|
||||
System.out.flush()
|
||||
}
|
||||
val paramSrc = req.obj.get("parameterSources").map(_.str).getOrElse("false")
|
||||
val fieldParamSrc = req.obj.get("fieldParameterSources").map(_.str).getOrElse(paramSrc)
|
||||
val fieldSrc = req.obj.get("fieldSources").map(_.str).getOrElse("true")
|
||||
@@ -1532,7 +1539,7 @@
|
||||
} else {
|
||||
println("---OUSAST-CPG-BEGIN---")
|
||||
println(
|
||||
ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, dispatchValue, function, parameterSources, parameterSources, fieldSources, evidenceOnly, file, callDepth, boundedSinks): _*))
|
||||
ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, dispatchValue, function, parameterSources, parameterSources, fieldSources, evidenceOnly, file, callDepth, boundedSinks, traceS = trace): _*))
|
||||
)
|
||||
}
|
||||
println("---OUSAST-CPG-END---")
|
||||
|
||||
@@ -28,6 +28,7 @@ def request_params(
|
||||
call_depth: int = 0,
|
||||
hook_callbacks: str = "",
|
||||
field_parameter_sources: bool | None = None,
|
||||
trace: bool = False,
|
||||
) -> dict[str, object]:
|
||||
"""The query parameters this arbiter sends. Shared with the batcher so there is one definition, not two.
|
||||
|
||||
@@ -40,6 +41,7 @@ def request_params(
|
||||
means unscoped, which is what the single-region pair path still uses.
|
||||
"""
|
||||
return {
|
||||
**({"trace": "true"} if trace else {}),
|
||||
"sources": spec.sources,
|
||||
"sinks": spec.sinks,
|
||||
"sanitizers": spec.sanitizers,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Lossless parsing of the optional shortest-path trace, with explicit evidence limits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
_STEP = re.compile(r"^(.*):(-?\d+) \[([^\]]+)\] ([\s\S]*)$")
|
||||
|
||||
|
||||
def parse_trace(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Preserve row-level discharge evidence; never guess which node sanitized a path.
|
||||
|
||||
The query emits basename-only files and truncated code. A bound can be off-path;
|
||||
it remains in ``guard`` even when no trace step exactly matches it. Invalid steps
|
||||
raise instead of turning an unreadable trace into an empty path.
|
||||
"""
|
||||
raw = row.get("trace", [])
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("trace must be an array")
|
||||
steps = []
|
||||
bound = str(row.get("bound") or "")
|
||||
for index, text in enumerate(raw):
|
||||
match = _STEP.fullmatch(text) if isinstance(text, str) else None
|
||||
if match is None:
|
||||
raise ValueError(f"invalid trace step: {text!r}")
|
||||
location, line, node_label, code = match.groups()
|
||||
file, separator, method = location.partition(":")
|
||||
roles = ["source"] if index == 0 else ["step"]
|
||||
if index == len(raw) - 1:
|
||||
roles = ["source", "sink"] if index == 0 else ["sink"]
|
||||
if bound and code == bound:
|
||||
roles.append("guard")
|
||||
steps.append(
|
||||
{
|
||||
"file": file,
|
||||
"method": method if separator else "?",
|
||||
"line": int(line),
|
||||
"node_label": node_label,
|
||||
"code": code,
|
||||
"roles": roles,
|
||||
"raw": text,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"steps": steps,
|
||||
"sanitized": bool(row.get("sanitized", False)),
|
||||
"sanitizer": {"on_path": bool(row.get("sanitized", False)), "step": None},
|
||||
"guard": {"text": bound, "bounded": bool(row.get("bounded", False))},
|
||||
}
|
||||
Reference in New Issue
Block a user