Files
gyxx-flow/tests/test_security_scanner.py
T

134 lines
4.5 KiB
Python

from __future__ import annotations
from dataclasses import asdict
import pytest
def test_scan_repository_reports_only_location_and_rule(tmp_path):
from gyxx_flow.security.scanner import scan_repository
first_value = "not-a-real-" + "password-7f31"
second_value = "not-a-real-" + "url-secret-91ac"
private_key_header = "-----BEGIN " + "PRIVATE KEY-----"
source = tmp_path / "src" / "settings.py"
source.parent.mkdir()
source.write_text(
"DEBUG = True\n"
f'DATABASE_PASSWORD = "{first_value}"\n'
f'endpoint = "https://service-user:{second_value}@example.invalid/api"\n'
f'key = "{private_key_header}"\n',
encoding="utf-8",
)
findings = scan_repository(tmp_path)
assert [asdict(finding) for finding in findings] == [
{"file": "src/settings.py", "line": 2, "rule": "plaintext-credential"},
{"file": "src/settings.py", "line": 3, "rule": "credential-in-url"},
{"file": "src/settings.py", "line": 4, "rule": "private-key-material"},
]
rendered = repr(findings)
assert first_value not in rendered
assert second_value not in rendered
assert set(asdict(findings[0])) == {"file", "line", "rule"}
@pytest.mark.parametrize("directory", ["var", ".git", ".venv", ".learnings"])
def test_scan_repository_excludes_runtime_and_tool_directories(tmp_path, directory):
from gyxx_flow.security.scanner import scan_repository
value = "not-a-real-" + "excluded-secret-18d2"
source = tmp_path / directory / "nested" / "settings.py"
source.parent.mkdir(parents=True)
source.write_text(f'API_KEY = "{value}"\n', encoding="utf-8")
assert scan_repository(tmp_path) == []
def test_scan_repository_ignores_empty_and_externalized_values(tmp_path):
from gyxx_flow.security.scanner import scan_repository
password_name = "DATABASE_" + "PASSWORD"
api_key_name = "API_" + "KEY"
client_secret_name = "CLIENT_" + "SECRET"
access_token_name = "ACCESS_" + "TOKEN"
source = tmp_path / "config" / "settings.env"
source.parent.mkdir()
source.write_text(
f"{password_name}=\n"
f"{api_key_name}=${{GYXX_API_KEY}}\n"
f"{client_secret_name}=$GYXX_CLIENT_SECRET\n"
f"{access_token_name}=os.getenv('GYXX_ACCESS_TOKEN')\n",
encoding="utf-8",
)
assert scan_repository(tmp_path) == []
def test_scan_repository_detects_common_application_secret_names(tmp_path):
from gyxx_flow.security.scanner import scan_repository
app_secret_name = "FEISHU_APP_" + "SECRET"
secret_key_name = "DJANGO_" + "SECRET_KEY"
first_value = "not-a-real-" + "application-secret-20f9"
second_value = "not-a-real-" + "signing-secret-52bb"
source = tmp_path / "config" / "application.env"
source.parent.mkdir()
source.write_text(
f"{app_secret_name}={first_value}\n"
f"{secret_key_name}={second_value}\n",
encoding="utf-8",
)
assert [asdict(finding) for finding in scan_repository(tmp_path)] == [
{"file": "config/application.env", "line": 1, "rule": "plaintext-credential"},
{"file": "config/application.env", "line": 2, "rule": "plaintext-credential"},
]
def test_scan_repository_ignores_declarations_references_and_comparisons(tmp_path):
from gyxx_flow.security.scanner import scan_repository
password_name = "DATABASE_" + "PASSWORD"
api_key_name = "API_" + "KEY"
client_secret_name = "CLIENT_" + "SECRET"
source = tmp_path / "src" / "model.py"
source.parent.mkdir()
source.write_text(
f"{password_name}: str\n"
f"{api_key_name}: SecretStr\n"
f"{client_secret_name} = settings.client_secret\n"
f"if {password_name} == candidate:\n pass\n",
encoding="utf-8",
)
assert scan_repository(tmp_path) == []
def test_scan_repository_rejects_a_non_directory_root(tmp_path):
from gyxx_flow.security.scanner import scan_repository
target = tmp_path / "settings.py"
target.write_text("DEBUG = True\n", encoding="utf-8")
with pytest.raises(NotADirectoryError):
scan_repository(target)
def test_scan_repository_detects_multiline_environment_secret_default(tmp_path):
from gyxx_flow.security.scanner import scan_repository
source = tmp_path / "collector.py"
source.write_text(
'API_KEY = os.getenv(\n "SERVICE_API_KEY",\n'
' "0123456789abcdef0123456789abcdef",\n)\n',
encoding="utf-8",
)
findings = scan_repository(tmp_path)
assert [(item.line, item.rule) for item in findings] == [
(1, "hardcoded-long-hex-credential")
]