shield-agents / tests /test_sast.py
aisona-lab
fix: restaurar detectores de secretos y eliminar falsos positivos del SAST
fec1909
Raw
History Blame Contribute Delete
8.64 kB
"""Tests for the SAST scanner."""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from shield_agents.scanners.sast import SASTScanner, SAST_RULES
class TestSASTScanner(unittest.TestCase):
"""Test the SAST scanner."""
def setUp(self):
self.scanner = SASTScanner()
def test_sql_injection_string_concat(self):
code = '''cursor.execute("SELECT * FROM users WHERE id = " + user_id)'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("SQL" in f["title"] for f in findings))
def test_sql_injection_fstring(self):
code = '''cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("SQL" in f["title"] for f in findings))
def test_xss_innerhtml(self):
code = '''element.innerHTML = userInput;'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".js", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("XSS" in f["title"] for f in findings))
def test_command_injection(self):
code = '''os.system(f"ping {host}")'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Command" in f["title"] for f in findings))
def test_subprocess_shell_true(self):
code = '''subprocess.call(cmd, shell=True)'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Command" in f["title"] for f in findings))
def test_insecure_deserialization_pickle(self):
code = '''data = pickle.loads(user_input)'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Deserialization" in f["title"] for f in findings))
def test_weak_hash_md5(self):
code = '''hashlib.md5(data.encode()).hexdigest()'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Weak" in f["title"] or "Crypto" in f["title"] for f in findings))
def test_hardcoded_password(self):
code = '''password = "SuperSecret123!"'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Credential" in f["title"] or "Hardcoded" in f["title"] for f in findings))
def test_ssl_verify_false(self):
code = '''requests.get(url, verify=False)'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("SSL" in f["title"] for f in findings))
def test_weak_crypto_no_false_positive_on_words(self):
# Plain prose / identifiers containing "des", "modes", etc. must NOT
# be flagged as Weak Cryptography (regression for unbounded DES|RC4 regex).
code = '''# La consulta esta parametrizada, descrita en varios modes y nodes
def describe_nodes(candidates):
besides = candidates
return besides
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertFalse(
any("Crypto" in f["title"] for f in findings),
f"Unexpected weak-crypto finding(s): {[f['code_snippet'] for f in findings]}",
)
def test_weak_crypto_des_cipher_detected(self):
# Genuine DES cipher usage must still be detected.
code = '''cipher = DES.new(key, DES.MODE_ECB)'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(any("Crypto" in f["title"] for f in findings))
def test_login_required_not_flagged(self):
# @login_required is a SECURE pattern and must not be reported.
code = '''@login_required
def dashboard():
return render_dashboard()
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertFalse(any("Authentication" in f["title"] for f in findings))
def test_hardcoded_credentials_ignores_placeholder(self):
# Obvious placeholder / example credentials must not be flagged.
code = '''password = "example"
api_key = "your_api_key_here"
secret = "changeme"
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertFalse(
any("Credential" in f["title"] or "Hardcoded" in f["title"] for f in findings),
f"Placeholder creds were flagged: {[f['code_snippet'] for f in findings]}",
)
def test_no_cross_line_sql_false_positive(self):
# A safe f-string query on one line and an unrelated dict literal on
# another must NOT combine into a single SQL-injection match.
# (Regression for greedy '.*' crossing newlines under re.DOTALL.)
code = '''label = f"SELECT name FROM users WHERE active"
config = {"timeout": 30}
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertFalse(
any("SQL" in f["title"] for f in findings),
f"Cross-line false positive: {[f['code_snippet'] for f in findings]}",
)
def test_finding_not_suppressed_by_later_line(self):
# A weak-RNG call must be reported even when an unrelated comment
# containing the word 'security' appears later in the file.
# (Regression for negative lookahead scanning the whole file under DOTALL.)
code = '''token = random.randint(1000, 9999)
# Note: review the security model before release
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
self.assertTrue(
any(f["line"] == 1 for f in findings),
"Weak-RNG finding on line 1 was wrongly suppressed by a later line",
)
def test_ten_rules_exist(self):
self.assertEqual(len(SAST_RULES), 10)
def test_clean_code_minimal_findings(self):
code = '''import os
import hashlib
import secrets
def get_url():
return os.environ.get("DATABASE_URL")
def hash_data(data):
return hashlib.sha256(data.encode()).hexdigest()
def generate_token():
return secrets.token_urlsafe(32)
'''
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
f.flush()
findings = self.scanner.scan_file(f.name)
os.unlink(f.name)
# Should not find critical/high issues in clean code
critical_high = [f for f in findings if f["severity"] in ("CRITICAL", "HIGH")]
self.assertEqual(len(critical_high), 0)
if __name__ == "__main__":
unittest.main()