|
4 | 4 | Some tests are skipped pending full API alignment. |
5 | 5 | """ |
6 | 6 | import ast |
7 | | -import pytest |
| 7 | +import os |
| 8 | +import textwrap |
8 | 9 |
|
9 | 10 | from pyaegis.core.call_graph import GlobalSymbolTable, InterproceduralAnalyzer |
| 11 | +from pyaegis.core.parser import PyASTParser |
| 12 | +from pyaegis.core.taint import TaintTracker |
| 13 | + |
| 14 | + |
| 15 | +def _write(tmp_path, rel: str, code: str): |
| 16 | + p = tmp_path / rel |
| 17 | + p.parent.mkdir(parents=True, exist_ok=True) |
| 18 | + p.write_text(textwrap.dedent(code).lstrip("\n"), encoding="utf-8") |
| 19 | + return p |
| 20 | + |
| 21 | + |
| 22 | +def _first_call(tree: ast.AST) -> ast.Call: |
| 23 | + for node in ast.walk(tree): |
| 24 | + if isinstance(node, ast.Call): |
| 25 | + return node |
| 26 | + raise AssertionError("No ast.Call found") |
| 27 | + |
| 28 | + |
| 29 | +# --- existing symbol table tests --- |
10 | 30 |
|
11 | 31 |
|
12 | 32 | def test_global_symbol_table_build_empty(): |
@@ -45,13 +65,91 @@ def test_symbol_table_registers_imports(): |
45 | 65 | assert "helper" in st.imports["app.py"] |
46 | 66 |
|
47 | 67 |
|
48 | | -@pytest.mark.skip(reason="API alignment pending - tracked in ROADMAP P0") |
49 | | -def test_interprocedural_basic(): |
50 | | - pass |
| 68 | +# --- inter-procedural taint tests --- |
| 69 | + |
| 70 | + |
| 71 | +def test_interprocedural_basic(tmp_path): |
| 72 | + """Taint should flow across modules into a sink inside the callee.""" |
| 73 | + utils = _write( |
| 74 | + tmp_path, |
| 75 | + "utils.py", |
| 76 | + """ |
| 77 | + def run_cmd(cmd): |
| 78 | + import os |
| 79 | + os.system(cmd) |
| 80 | + """, |
| 81 | + ) |
| 82 | + app = _write( |
| 83 | + tmp_path, |
| 84 | + "app.py", |
| 85 | + """ |
| 86 | + from utils import run_cmd |
| 87 | +
|
| 88 | + def handler(request): |
| 89 | + cmd = request.args.get("cmd") |
| 90 | + run_cmd(cmd) |
| 91 | + """, |
| 92 | + ) |
| 93 | + |
| 94 | + gst = GlobalSymbolTable.build([str(utils), str(app)], root_dir=str(tmp_path)) |
| 95 | + parser = PyASTParser(str(app)) |
| 96 | + parser.parse() |
| 97 | + cfg = parser.extract_cfg() |
| 98 | + |
| 99 | + tracker = TaintTracker( |
| 100 | + sources=["request", "request.args", "input"], |
| 101 | + sinks=["os.system", "eval"], |
| 102 | + sanitizers=[], |
| 103 | + symbol_table=gst, |
| 104 | + ) |
| 105 | + tracker.analyze_cfg(cfg, filepath=str(app)) |
| 106 | + |
| 107 | + findings = tracker.get_findings() |
| 108 | + assert any( |
| 109 | + f.sink_name == "os.system" |
| 110 | + and os.path.abspath(f.file_path) == os.path.abspath(str(utils)) |
| 111 | + for f in findings |
| 112 | + ) |
51 | 113 |
|
52 | 114 |
|
53 | | -@pytest.mark.skip( |
54 | | - reason="requires full inter-procedural taint integration - ROADMAP P0" |
55 | | -) |
56 | 115 | def test_interprocedural_propagates_internal_source_return(tmp_path): |
57 | | - pass |
| 116 | + """Taint should propagate when the callee sources data internally.""" |
| 117 | + utils = _write( |
| 118 | + tmp_path, |
| 119 | + "utils.py", |
| 120 | + """ |
| 121 | + def get_cmd(): |
| 122 | + return input("cmd") |
| 123 | + """, |
| 124 | + ) |
| 125 | + app = _write( |
| 126 | + tmp_path, |
| 127 | + "app.py", |
| 128 | + """ |
| 129 | + from utils import get_cmd |
| 130 | +
|
| 131 | + def handler(): |
| 132 | + cmd = get_cmd() |
| 133 | + eval(cmd) |
| 134 | + """, |
| 135 | + ) |
| 136 | + |
| 137 | + gst = GlobalSymbolTable.build([str(utils), str(app)], root_dir=str(tmp_path)) |
| 138 | + parser = PyASTParser(str(app)) |
| 139 | + parser.parse() |
| 140 | + cfg = parser.extract_cfg() |
| 141 | + |
| 142 | + tracker = TaintTracker( |
| 143 | + sources=["input", "request"], |
| 144 | + sinks=["eval", "os.system"], |
| 145 | + sanitizers=[], |
| 146 | + symbol_table=gst, |
| 147 | + ) |
| 148 | + tracker.analyze_cfg(cfg, filepath=str(app)) |
| 149 | + |
| 150 | + findings = tracker.get_findings() |
| 151 | + assert any( |
| 152 | + f.sink_name == "eval" |
| 153 | + and os.path.abspath(f.file_path) == os.path.abspath(str(app)) |
| 154 | + for f in findings |
| 155 | + ) |
0 commit comments