Spaces:
Sleeping
Sleeping
File size: 17,962 Bytes
6cb82c6 1948258 6cb82c6 3357d94 6cb82c6 18b82c3 6cb82c6 18b82c3 6cb82c6 18b82c3 45406d8 6cb82c6 18b82c3 6cb82c6 18b82c3 6cb82c6 5474df8 6cb82c6 d46e192 d5349e0 6cb82c6 3357d94 c972e32 6cb82c6 c972e32 6cb82c6 c972e32 6cb82c6 c972e32 6cb82c6 a00e93c d46e192 a00e93c 6cb82c6 295bbd1 6cb82c6 295bbd1 6cb82c6 3357d94 6cb82c6 3357d94 6cb82c6 6baf699 6cb82c6 1948258 6cb82c6 6baf699 6cb82c6 bb8254f 6cb82c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | """
Layer 4 — Dataflow Agent
NVIDIA NIM (Nemotron 70B) with tool use. ReAct pattern.
Traces user input from source to sink across files.
Only runs on injection-class findings.
Evidence citation enforced: verdicts without tool citations are REJECTED.
"""
from __future__ import annotations
import json
from typing import List, Any
from .models import Finding, TaintPath, Evidence, Confidence
from .nvidia_client import complete_smart
from .layer0_indexer import CodeIndexer
import logging
logger = logging.getLogger("vibesec")
_taint_cache: dict = {}
DATAFLOW_TOOLS = [
{
"type": "function",
"function": {
"name": "read_code",
"description": "Read specific lines from a file in the repo",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string"},
"start_line": {"type": "integer"},
"end_line": {"type": "integer"},
},
"required": ["file_path", "start_line", "end_line"],
},
},
},
{
"type": "function",
"function": {
"name": "get_callers",
"description": "Get files/functions that call the given function or import the given file",
"parameters": {
"type": "object",
"properties": {
"function_name": {"type": "string"},
"file_path": {"type": "string"},
},
"required": ["function_name", "file_path"],
},
},
},
{
"type": "function",
"function": {
"name": "get_callees",
"description": "Get functions/files that the given function calls",
"parameters": {
"type": "object",
"properties": {
"function_name": {"type": "string"},
"file_path": {"type": "string"},
},
"required": ["function_name", "file_path"],
},
},
},
{
"type": "function",
"function": {
"name": "search_pattern",
"description": "Search for a regex pattern across production files",
"parameters": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex pattern to search"},
"scope": {"type": "string", "enum": ["production", "all"], "default": "production"},
},
"required": ["pattern"],
},
},
},
{
"type": "function",
"function": {
"name": "check_sanitizer",
"description": "Check if a sanitizer, validator, or parameterize function is applied to a variable between a given source and sink line",
"parameters": {
"type": "object",
"properties": {
"variable_name": {"type": "string"},
"file_path": {"type": "string"},
"source_line": {"type": "integer"},
"sink_line": {"type": "integer"},
},
"required": ["variable_name", "file_path", "source_line", "sink_line"],
},
},
},
]
SYSTEM_PROMPT = """You are an elite, co-founder level Principal Application Security Researcher conducting exhaustive dataflow reasoning.
Your mission is to perform detailed source-to-sink taint propagation and identify access control, authorization, and business logic bypasses across the codebase.
EXPERT DETAILED REASONING RULES:
1. **Real Taint & Multi-Hop Propagation**: You must trace user input variables across multiple steps, re-assignments, spreads, destructuring, object properties (`obj.key`), and array elements. Never lose track of a tainted value just because it is stored in a complex data structure.
2. **Async & Promise Flow Analysis**: Follow inputs that flow through async functions, await blocks, promise resolutions (`.then`), and callback queues.
3. **Interprocedural & Import/Module Tracing**: Follow variable arguments when passed across separate files, exported functions, libraries, helper scripts, and class methods.
4. **Alias & Object Mutation Tracking**: Reason about variables that are copied by reference or modified inline (e.g. `const query = req.body; query.isAdmin = true;`).
5. **Sanitizer-Aware & Escaping Context Validation**: Evaluate if sanitizers (e.g. `DOMPurify.sanitize`, custom regexes, `escapeHtml`) actually neutralize the specific sink exploit vector (e.g., query parameters vs. HTML bodies). If escaping is context-inappropriate or incomplete, flag it.
6. **Authentication & Authorization/RBAC/IDOR Reasoning**: Check if parameters (like database keys, transaction IDs, or profile identifiers) are queried directly without verifying that they belong to the current authenticated tenant/session owner. Look for lack of ownership checks, privilege escalation vectors, or direct object reference exposures.
7. **SSRF, Prototype Pollution & Deserialization Sinks**: Actively confirm if user-controlled keys can mutate class prototype templates (`__proto__`, `constructor.prototype`), control target outbound connection endpoints (SSRF), or influence execution chains via unsafe deserialization/eval calls.
OPERATIONAL PRINCIPLES:
* Cite specific tool calls inside the evidence citation list. Verdicts without direct tool execution citations will be immediately rejected and marked as needs_human_review.
* You MUST only execute exactly ONE tool call at a time. Parallel tool execution is disabled by the hosting server; calling multiple tools at once will fail. Select only the most relevant tool to call in this turn.
Your final response MUST be a JSON object with this exact schema:
{
"verdict": "exploitable" | "not_exploitable" | "needs_human_review",
"confidence": "high" | "medium" | "low",
"exploitable_by": "unauthenticated" | "authenticated" | "admin" | null,
"source": {"file": "...", "line": 0, "type": "req.body|params|query|cookie|header|file_upload|unauthorized_input"},
"sink": {"file": "...", "line": 0, "type": "sql|exec|eval|html|redirect|external|ssrf|prototype_pollution|idor_leak"},
"sanitizers_found": [],
"taint_path": [{"file": "...", "line": 0, "description": "..."}],
"evidence": [{"tool_call": "...", "result_summary": "..."}],
"exploit_scenario": "Plain English step-by-step description of the exploit path/scenario for non-technical developers"
}
REJECT RULE: If you cannot cite at least ONE tool call result in your evidence array, set verdict to "needs_human_review"."""
def run_dataflow_agent(
finding: Finding,
indexer: CodeIndexer,
max_iterations: int = 10,
) -> Finding:
"""Run the dataflow agent on an injection-class finding. Returns enriched finding."""
if not hasattr(indexer, "l4_degraded"):
indexer.l4_degraded = False
if getattr(indexer, "is_training_app", False):
print(f" [Layer 4 Dataflow] Training app detected. Bypassing deep dataflow trace to conserve RAM and keep finding active: '{finding.title}'")
finding.confidence = Confidence.HIGH
finding.explanation += " [Dataflow: unconfirmed taint path, retained for training app sandbox]"
return finding
# Build a stable cache key based on location and type of finding
cache_key = None
if finding and finding.file_path and finding.check_id:
cache_key = f"{finding.file_path}:{finding.line_number or 0}:{finding.check_id}"
if cache_key and cache_key in _taint_cache:
cached = _taint_cache[cache_key]
# Shallow copy cached attributes back to the incoming finding object
finding.taint_path = cached.get("taint_path")
finding.confidence = cached.get("confidence")
finding.explanation = cached.get("explanation", "") + " [Taint Cache Hit]"
logger.debug(f"Taint cache hit for {cache_key}")
return finding
func_info = indexer.get_enclosing_function_info(finding.file_path or "", finding.line_number or 1)
initial_message = f"""Finding: {finding.title}
File: {finding.file_path}:{finding.line_number}
Check ID: {finding.check_id}
Description: {finding.description}
"""
if func_info:
initial_message += f"""
Enclosing Function: {func_info['name']} (Lines {func_info['start_line']}-{func_info['end_line']})
Callers (Incoming calls): {', '.join(func_info['callers']) or 'None'}
Callees (Outgoing calls): {', '.join(func_info['callees']) or 'None'}
Function Body context:
{func_info['body']}
"""
else:
context = indexer.get_file_context(
finding.file_path or "", finding.line_number or 1, window=30
)
initial_message += f"""
Code context (60 lines around the finding):
{context}
"""
initial_message += "\nAnalyze this finding. Use tools to trace the complete taint path from source to sink."
messages = [{"role": "user", "content": initial_message}]
evidence_list: List[Evidence] = []
tool_calls_made = 0
for iteration in range(max_iterations):
try:
response = complete_smart(
system=SYSTEM_PROMPT,
messages=messages,
tools=DATAFLOW_TOOLS,
max_tokens=2048,
)
except Exception as e:
print(f" [Layer 4 Dataflow ERROR] LLM complete_smart query failed: {e}. Bypassing deep dataflow trace and retaining finding.")
indexer.l4_degraded = True
finding.confidence = Confidence.LOW
finding.explanation += f" [Dataflow analysis failed: {e}]"
return finding
# Append assistant message
messages.append({"role": "assistant", "content": response.content or "",
"tool_calls": [tc.model_dump() if hasattr(tc, 'model_dump') else tc
for tc in (response.tool_calls or [])]})
# Process tool calls
if response.tool_calls:
tool_results = []
for tc in response.tool_calls:
raw_args = tc.function.arguments
try:
# Parse tool arguments with validation — reject if not valid JSON dict
args_dict = json.loads(raw_args)
if not isinstance(args_dict, dict):
print(f" [Layer 4 Dataflow WARNING] Non-dict arguments from tool {tc.function.name}. Skipping.")
continue
except (json.JSONDecodeError, TypeError) as e:
print(f" [Layer 4 Dataflow WARNING] Malformed JSON from LLM in tool {tc.function.name}: {e}. Skipping invalid tool call.")
evidence_list.append(Evidence(
tool_call=f"[INVALID] {tc.function.name}({raw_args[:100]})",
result_summary=f"LLM returned malformed arguments: {e}",
))
# Continue processing other tool calls
tool_calls_made += 1
continue
result = _execute_tool(tc.function.name, args_dict, indexer)
tool_calls_made += 1
evidence_list.append(Evidence(
tool_call=f"{tc.function.name}({raw_args[:300]})",
result_summary=str(result)[:300],
))
tool_results.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result),
})
messages.extend(tool_results)
continue
# No tool calls — model is giving final verdict
raw_text = response.content or ""
result = _parse_verdict(raw_text, evidence_list, tool_calls_made, finding)
if cache_key:
_taint_cache[cache_key] = {
"taint_path": getattr(finding, "taint_path", None),
"confidence": getattr(finding, "confidence", None),
"explanation": getattr(finding, "explanation", ""),
}
return result
# Cache the result for future reuse
if cache_key:
_taint_cache[cache_key] = {
"taint_path": getattr(finding, "taint_path", None),
"confidence": getattr(finding, "confidence", None),
"explanation": getattr(finding, "explanation", ""),
}
# Exceeded iterations
finding.confidence = Confidence.LOW
finding.explanation += " [Dataflow agent: exceeded max iterations — needs human review]"
return finding
def _execute_tool(name: str, args: dict, indexer: CodeIndexer) -> Any:
try:
if name == "read_code":
try:
start_line = int(args.get("start_line", 1))
except Exception:
start_line = 1
ctx = indexer.get_file_context(args["file_path"], start_line, window=20)
return {"lines": ctx, "file": args["file_path"]}
elif name == "get_callers":
callers = indexer.get_callers(args.get("function_name", ""), args.get("file_path", ""))
return {"callers": callers[:10]}
elif name == "get_callees":
callees = indexer.get_callees(args.get("function_name", ""), args.get("file_path", ""))
return {"callees": callees[:10]}
elif name == "search_pattern":
import re
results = []
pattern = args.get("pattern", "")
for rel_path, role in indexer.file_roles.items():
if args.get("scope") == "production" and role != "production":
continue
try:
content = (indexer.repo_path / rel_path).read_text(encoding="utf-8", errors="replace")
for m in re.finditer(pattern, content):
line = content[:m.start()].count("\n") + 1
results.append({"file": rel_path, "line": line, "match": m.group(0)[:100]})
if len(results) >= 20:
break
except Exception:
pass
if len(results) >= 20:
break
return {"matches": results}
elif name == "check_sanitizer":
import re
fpath = indexer.repo_path / args.get("file_path", "")
try:
lines = fpath.read_text(encoding="utf-8", errors="replace").splitlines()
try:
src_line = int(args.get("source_line", 1))
except Exception:
src_line = 1
try:
snk_line = int(args.get("sink_line", src_line + 50))
except Exception:
snk_line = src_line + 50
start = max(0, src_line - 1)
end = min(len(lines), snk_line)
chunk = "\n".join(lines[start:end])
sanitizer_patterns = [r"escape\(", r"sanitize\(", r"parameteriz", r"prepare\(", r"validate\(", r"\.replace\(", r"DOMPurify", r"xss\("]
found = [p for p in sanitizer_patterns if re.search(p, chunk, re.IGNORECASE)]
return {"sanitizers_found": found, "has_sanitizer": bool(found)}
except Exception:
return {"sanitizers_found": [], "has_sanitizer": False}
except Exception as e:
return {"error": str(e)}
return {}
def _parse_verdict(raw_text: str, evidence: List[Evidence], tool_calls_made: int, finding: Finding) -> Finding:
import re
# Extract JSON from response
json_match = re.search(r'\{.*\}', raw_text, re.DOTALL)
if not json_match:
finding.confidence = Confidence.LOW
finding.evidence.extend(evidence)
return finding
try:
verdict_data = json.loads(json_match.group(0))
except json.JSONDecodeError:
finding.confidence = Confidence.LOW
finding.evidence.extend(evidence)
return finding
# REJECT RULE: no tool citations = needs_human_review
if tool_calls_made == 0 or not verdict_data.get("evidence"):
verdict_data["verdict"] = "needs_human_review"
verdict = verdict_data.get("verdict", "needs_human_review")
if verdict == "exploitable":
finding.confidence = {"high": Confidence.HIGH, "medium": Confidence.MEDIUM, "low": Confidence.LOW}.get(
verdict_data.get("confidence", "medium"), Confidence.MEDIUM
)
finding.exploitable_by = verdict_data.get("exploitable_by")
if verdict_data.get("exploit_scenario"):
finding.explanation = verdict_data["exploit_scenario"]
src = verdict_data.get("source", {})
snk = verdict_data.get("sink", {})
if src and snk:
finding.taint_path = TaintPath(
source_file=src.get("file", finding.file_path or ""),
source_line=src.get("line", finding.line_number or 0),
source_type=src.get("type", "unknown"),
sink_file=snk.get("file", finding.file_path or ""),
sink_line=snk.get("line", 0),
sink_type=snk.get("type", "unknown"),
sanitizers_found=verdict_data.get("sanitizers_found", []),
intermediate_calls=verdict_data.get("taint_path", []),
)
finding.taint_confirmed = True
finding.confidence = Confidence.HIGH
elif verdict == "not_exploitable":
finding.is_false_positive = True
finding.false_positive_reason = "Dataflow agent: sanitizer found in taint path or no reachable sink confirmed."
else:
finding.confidence = Confidence.LOW
finding.evidence.extend(evidence)
return finding
|