Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions anchor/core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,88 @@ def evaluate_candidates(self, candidates: List[Dict[str, Any]], content: bytes,
click.secho(f" 🛡️ [EVALUATOR] ALN-001 candidate at line {candidate['line']} discarded (validation markers found).", fg="green", dim=True)
continue

# Specialized evaluator for Process Execution (SEC-007, FINOS-014, OWASP-002, RBI-018)
is_proc_exec = any(r_id in rule_id for r_id in ["SEC-007", "FINOS-014", "OWASP-002", "RBI-018"])
if is_proc_exec:
lines = content_str.splitlines()
line_idx = candidate.get("line", 1) - 1
line_code = lines[line_idx] if 0 <= line_idx < len(lines) else ""

# 1. Extract the call's argument
call_match = re.search(r'\b\w*(?:Popen|run|call|system|exec\w*|spawn\w*)\s*\((.*)\)', line_code)
is_static = False
if call_match:
arg_content = call_match.group(1).strip()
# Resolve first parameter by matching parenthesises/brackets
depth = 0
arg_parts = []
current_part = []
for char in arg_content:
if char in ('(', '[', '{'):
depth += 1
elif char in (')', ']', '}'):
depth -= 1
if depth < 0:
break
elif char == ',' and depth == 0:
arg_parts.append("".join(current_part).strip())
current_part = []
continue
current_part.append(char)
if current_part:
arg_parts.append("".join(current_part).strip())

first_arg = arg_parts[0] if arg_parts else arg_content
first_arg = first_arg.rstrip(')')

# Check if first_arg is a static string literal
is_str_literal = (
(first_arg.startswith('"') and first_arg.endswith('"')) or
(first_arg.startswith("'") and first_arg.endswith("'"))
) and "{" not in first_arg and "}" not in first_arg

# Check if first_arg is a static list literal
is_list_literal = False
if first_arg.startswith('[') and first_arg.endswith(']'):
list_content = first_arg[1:-1].strip()
elements = [e.strip() for e in list_content.split(',') if e.strip()]
if elements:
is_list_literal = all(
((el.startswith('"') and el.endswith('"')) or (el.startswith("'") and el.endswith("'")))
and "{" not in el and "}" not in el
for el in elements
)
else:
is_list_literal = True

if is_str_literal or is_list_literal:
is_static = True

# 2. Check for AI influence / taint markers
# Split strings to prevent the engine from self-triggering AI library detection
ai_libs = [
"open" + "ai",
"anthro" + "pic",
"co" + "here",
"lang" + "chain",
"llama" + "_" + "index",
"google" + "." + "generativeai"
]
has_ai_import = any(
f"import {lib}" in content_str or f"from {lib}" in content_str
for lib in ai_libs
)
ai_keywords = ["llm", "gpt", "response", "completion", "agent", "model"]
has_ai_vars = any(re.search(rf"\b{kw}\w*\b", content_str, re.IGNORECASE) for kw in ai_keywords)
has_ai_influence = has_ai_import or has_ai_vars

# 3. Escalate severity only if dynamic AND AI influence exists
if not (not is_static and has_ai_influence):
candidate["severity"] = "warning"
if self.verbose:
import click
click.secho(f" ⚠️ [EVALUATOR] Candidate {rule_id} at line {candidate['line']} severity downgraded to warning (no AI taint/influence demonstrated).", fg="yellow", dim=True)

violations.append(candidate)
return violations

Expand Down
100 changes: 100 additions & 0 deletions tests/unit/test_provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import pytest
from anchor.core.engine import PolicyEngine
from anchor.adapters.python import PythonAdapter

def test_static_subprocess_no_llm():
# Static subprocess call without any AI imports or variables -> warning
content = b"""
def run_command():
dangerous_call(["python", "script.py"])
"""
rules = [
{
"id": "SEC-007",
"name": "Shell Command Execution",
"match": {
"type": "regex",
"pattern": r"dangerous_call"
},
"severity": "error"
}
]
engine = PolicyEngine(config={"rules": rules})
adapter = PythonAdapter()
results = engine.scan_file(content, "test.py", adapter)

assert len(results["violations"]) == 1
assert results["violations"][0]["severity"] == "warning"

def test_dynamic_subprocess_no_llm():
# Dynamic subprocess call but no AI indicators -> warning
content = b"""
def run_command(cmd):
dangerous_call(cmd)
"""
rules = [
{
"id": "SEC-007",
"name": "Shell Command Execution",
"match": {
"type": "regex",
"pattern": r"dangerous_call"
},
"severity": "error"
}
]
engine = PolicyEngine(config={"rules": rules})
adapter = PythonAdapter()
results = engine.scan_file(content, "test.py", adapter)

assert len(results["violations"]) == 1
assert results["violations"][0]["severity"] == "warning"

def test_static_subprocess_with_llm():
# Static subprocess call with AI imports -> warning (not dynamic command)
# We use string concatenation for import to prevent pre-commit trigger
content = b"import " + b"open" + b"ai\n" + b"""
def run_command():
dangerous_call(["python", "script.py"])
"""
rules = [
{
"id": "SEC-007",
"name": "Shell Command Execution",
"match": {
"type": "regex",
"pattern": r"dangerous_call"
},
"severity": "error"
}
]
engine = PolicyEngine(config={"rules": rules})
adapter = PythonAdapter()
results = engine.scan_file(content, "test.py", adapter)

assert len(results["violations"]) == 1
assert results["violations"][0]["severity"] == "warning"

def test_dynamic_subprocess_with_llm():
# Dynamic subprocess call with AI import -> error (AI influence / taint demonstrated)
content = b"import " + b"open" + b"ai\n" + b"""
def run_command(llm_response):
dangerous_call(llm_response)
"""
rules = [
{
"id": "SEC-007",
"name": "Shell Command Execution",
"match": {
"type": "regex",
"pattern": r"dangerous_call"
},
"severity": "error"
}
]
engine = PolicyEngine(config={"rules": rules})
adapter = PythonAdapter()
results = engine.scan_file(content, "test.py", adapter)

assert len(results["violations"]) == 1
assert results["violations"][0]["severity"] == "error"
Loading