mirror of
https://github.com/q3alique/codeflow
synced 2026-06-21 14:06:02 +00:00
acc1b4f3e7
Static taint-analysis and visualization tool for source-code security review. Supports deep analysis for Python, JavaScript/TypeScript, Java, Go, and C#, with structural support for all other languages via the generic extractor. Outputs: interactive HTML report, LLM-ready Markdown review document, and optional Burp Suite JSON export. Self-bootstrapping launcher (run.py) requires no virtual environment.
154 lines
5.9 KiB
Python
154 lines
5.9 KiB
Python
"""Tasks 006-008 tests: Python extractor (functions, sources, sinks)."""
|
|
import pytest
|
|
from pathlib import Path
|
|
from codeflow.discovery.file_scanner import FileInfo
|
|
from codeflow.analysis.python_extractor import extract, PythonExtractor
|
|
from codeflow.models.node import NodeType
|
|
from tests.fixtures import FLASK_FIXTURE_DIR
|
|
|
|
|
|
@pytest.fixture
|
|
def helpers_fixture():
|
|
return FileInfo(
|
|
path=str(FLASK_FIXTURE_DIR / "helpers.py"),
|
|
language="python",
|
|
size_bytes=0,
|
|
relative_path="flask_app/helpers.py",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def flask_app_fixture():
|
|
return FileInfo(
|
|
path=str(FLASK_FIXTURE_DIR / "app.py"),
|
|
language="python",
|
|
size_bytes=0,
|
|
relative_path="flask_app/app.py",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_fixture():
|
|
return FileInfo(
|
|
path=str(FLASK_FIXTURE_DIR / "auth.py"),
|
|
language="python",
|
|
size_bytes=0,
|
|
relative_path="flask_app/auth.py",
|
|
)
|
|
|
|
|
|
class TestPythonExtractor:
|
|
def test_module_node_created(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
mods = [n for n in result.nodes if n.node_type == NodeType.MODULE]
|
|
assert len(mods) == 1
|
|
assert mods[0].label == "app.py"
|
|
|
|
def test_entry_points_count(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
eps = [n for n in result.nodes if n.node_type == NodeType.ENTRY_POINT]
|
|
assert len(eps) >= 3
|
|
|
|
def test_login_entry_point(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
login = None
|
|
for n in result.nodes:
|
|
if n.node_type == NodeType.ENTRY_POINT and "/login" in n.label:
|
|
login = n
|
|
break
|
|
assert login is not None
|
|
assert login.metadata.get("http_method") == "POST"
|
|
assert login.metadata.get("route_path") == "/login"
|
|
|
|
def test_username_source_node(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
sources = [n for n in result.nodes if n.node_type == NodeType.SOURCE]
|
|
usernames = [s for s in sources if "username" in s.label]
|
|
assert len(usernames) >= 1
|
|
|
|
def test_username_input_type(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
sources = [n for n in result.nodes if n.node_type == NodeType.SOURCE]
|
|
for s in sources:
|
|
if "username" in s.label:
|
|
assert s.metadata.get("input_type") == "form_field"
|
|
return
|
|
pytest.fail("No username source found")
|
|
|
|
def test_database_sink(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
sinks = [n for n in result.nodes if n.node_type == NodeType.SINK]
|
|
db_sinks = [s for s in sinks if s.metadata.get("sink_type") == "database"]
|
|
assert len(db_sinks) >= 1
|
|
|
|
def test_command_execution_sink(self, helpers_fixture):
|
|
result = extract(helpers_fixture)
|
|
sinks = [n for n in result.nodes if n.node_type == NodeType.SINK]
|
|
cmd_sinks = [s for s in sinks if s.metadata.get("sink_type") == "command_execution"]
|
|
assert len(cmd_sinks) >= 1
|
|
|
|
def test_function_node_exists(self, auth_fixture):
|
|
result = extract(auth_fixture)
|
|
funcs = [n for n in result.nodes if n.node_type == NodeType.FUNCTION]
|
|
names = [f.label for f in funcs]
|
|
assert "authenticate" in names
|
|
|
|
def test_calls_edge(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
from codeflow.models.edge import EdgeType
|
|
calls = [e for e in result.edges if e.edge_type == EdgeType.CALLS]
|
|
assert len(calls) >= 1
|
|
|
|
def test_data_flow_edge(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
from codeflow.models.edge import EdgeType
|
|
data_flows = [e for e in result.edges if e.edge_type == EdgeType.DATA_FLOW]
|
|
assert len(data_flows) >= 1
|
|
|
|
def test_flask_import_detected(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
mods = [n for n in result.nodes if n.node_type == NodeType.MODULE]
|
|
assert len(mods) == 1
|
|
imports = mods[0].metadata.get("imports", [])
|
|
has_flask = any("flask" in imp.lower() for imp in imports)
|
|
assert has_flask
|
|
|
|
def test_no_functions_file(self, tmp_path):
|
|
f = tmp_path / "empty_mod.py"
|
|
f.write_text("x = 1\ny = 2")
|
|
fi = FileInfo(path=str(f), language="python", size_bytes=0, relative_path="empty_mod.py")
|
|
result = extract(fi)
|
|
mods = [n for n in result.nodes if n.node_type == NodeType.MODULE]
|
|
assert len(mods) == 1
|
|
funcs = [n for n in result.nodes if n.node_type == NodeType.FUNCTION]
|
|
assert len(funcs) == 0
|
|
|
|
def test_syntax_error_file(self, tmp_path):
|
|
f = tmp_path / "broken.py"
|
|
f.write_text("def broken(")
|
|
fi = FileInfo(path=str(f), language="python", size_bytes=0, relative_path="broken.py")
|
|
result = extract(fi)
|
|
assert len(result.errors) >= 0
|
|
|
|
def test_unique_node_ids(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
ids = [n.id for n in result.nodes]
|
|
assert len(ids) == len(set(ids))
|
|
|
|
def test_env_var_node(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
env_vars = [n for n in result.nodes if n.node_type == NodeType.ENV_VAR]
|
|
assert len(env_vars) >= 1
|
|
|
|
def test_run_code_sink(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
sinks = [n for n in result.nodes if n.node_type == NodeType.SINK]
|
|
eval_sinks = [s for s in sinks if s.metadata.get("sink_type") == "code_execution"]
|
|
assert len(eval_sinks) >= 1
|
|
|
|
def test_file_write_sink(self, flask_app_fixture):
|
|
result = extract(flask_app_fixture)
|
|
sinks = [n for n in result.nodes if n.node_type == NodeType.SINK]
|
|
fw_sinks = [s for s in sinks if s.metadata.get("sink_type") == "file_write"]
|
|
assert len(fw_sinks) >= 1
|