- Add SecurityScorer module (605 lines) with comprehensive security assessment - Add 4 security scoring components: - Sensitive data exposure prevention (hardcoded credentials detection) - Safe file operations (path traversal prevention) - Command injection prevention (shell=True, eval, exec detection) - Input validation quality (argparse, error handling, type checking) - Add 53 unit tests with 850 lines of test code - Update quality_scorer.py to integrate Security dimension (20% weight) - Rebalance all dimensions from 25% to 20% (5 dimensions total) - Update tier requirements: - POWERFUL: Security ≥70 - STANDARD: Security ≥50 - BASIC: Security ≥40 - Update documentation (quality-scoring-rubric.md, tier-requirements-matrix.md) - Version bump to 2.0.0 This addresses the feedback from PR #420 by providing a focused, well-tested implementation of the Security dimension without bundling other changes.
851 lines
28 KiB
Python
851 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for security_scorer.py - Security Dimension Scoring Tests
|
|
|
|
This test module validates the security_scorer.py module's ability to:
|
|
- Detect hardcoded sensitive data (passwords, API keys, tokens, private keys)
|
|
- Detect path traversal vulnerabilities
|
|
- Detect command injection risks
|
|
- Score input validation quality
|
|
- Handle edge cases (empty files, environment variables, etc.)
|
|
|
|
Run with: python -m unittest test_security_scorer
|
|
"""
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
# Add the scripts directory to the path
|
|
import sys
|
|
SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
from security_scorer import (
|
|
SecurityScorer,
|
|
# Constants
|
|
MAX_COMPONENT_SCORE,
|
|
MIN_SCORE,
|
|
BASE_SCORE_SENSITIVE_DATA,
|
|
BASE_SCORE_FILE_OPS,
|
|
BASE_SCORE_COMMAND_INJECTION,
|
|
BASE_SCORE_INPUT_VALIDATION,
|
|
CRITICAL_VULNERABILITY_PENALTY,
|
|
HIGH_SEVERITY_PENALTY,
|
|
MEDIUM_SEVERITY_PENALTY,
|
|
LOW_SEVERITY_PENALTY,
|
|
SAFE_PATTERN_BONUS,
|
|
GOOD_PRACTICE_BONUS,
|
|
# Pre-compiled patterns
|
|
PATTERN_HARDCODED_PASSWORD,
|
|
PATTERN_HARDCODED_API_KEY,
|
|
PATTERN_HARDCODED_TOKEN,
|
|
PATTERN_HARDCODED_PRIVATE_KEY,
|
|
PATTERN_OS_SYSTEM,
|
|
PATTERN_EVAL,
|
|
PATTERN_EXEC,
|
|
PATTERN_SUBPROCESS_SHELL_TRUE,
|
|
PATTERN_SHLEX_QUOTE,
|
|
PATTERN_SAFE_ENV_VAR,
|
|
)
|
|
|
|
|
|
class TestSecurityScorerConstants(unittest.TestCase):
|
|
"""Tests for security scorer constants."""
|
|
|
|
def test_max_component_score_value(self):
|
|
"""Test that MAX_COMPONENT_SCORE is 25."""
|
|
self.assertEqual(MAX_COMPONENT_SCORE, 25)
|
|
|
|
def test_min_score_value(self):
|
|
"""Test that MIN_SCORE is 0."""
|
|
self.assertEqual(MIN_SCORE, 0)
|
|
|
|
def test_base_scores_are_reasonable(self):
|
|
"""Test that base scores are within valid range."""
|
|
self.assertGreaterEqual(BASE_SCORE_SENSITIVE_DATA, MIN_SCORE)
|
|
self.assertLessEqual(BASE_SCORE_SENSITIVE_DATA, MAX_COMPONENT_SCORE)
|
|
self.assertGreaterEqual(BASE_SCORE_FILE_OPS, MIN_SCORE)
|
|
self.assertLessEqual(BASE_SCORE_FILE_OPS, MAX_COMPONENT_SCORE)
|
|
self.assertGreaterEqual(BASE_SCORE_COMMAND_INJECTION, MIN_SCORE)
|
|
self.assertLessEqual(BASE_SCORE_COMMAND_INJECTION, MAX_COMPONENT_SCORE)
|
|
|
|
def test_penalty_values_are_negative(self):
|
|
"""Test that penalty values are negative."""
|
|
self.assertLess(CRITICAL_VULNERABILITY_PENALTY, 0)
|
|
self.assertLess(HIGH_SEVERITY_PENALTY, 0)
|
|
self.assertLess(MEDIUM_SEVERITY_PENALTY, 0)
|
|
self.assertLess(LOW_SEVERITY_PENALTY, 0)
|
|
|
|
def test_bonus_values_are_positive(self):
|
|
"""Test that bonus values are positive."""
|
|
self.assertGreater(SAFE_PATTERN_BONUS, 0)
|
|
self.assertGreater(GOOD_PRACTICE_BONUS, 0)
|
|
|
|
def test_severity_ordering(self):
|
|
"""Test that severity penalties are ordered correctly."""
|
|
# Critical should be most severe (most negative)
|
|
self.assertLess(CRITICAL_VULNERABILITY_PENALTY, HIGH_SEVERITY_PENALTY)
|
|
self.assertLess(HIGH_SEVERITY_PENALTY, MEDIUM_SEVERITY_PENALTY)
|
|
self.assertLess(MEDIUM_SEVERITY_PENALTY, LOW_SEVERITY_PENALTY)
|
|
|
|
|
|
class TestPrecompiledPatterns(unittest.TestCase):
|
|
"""Tests for pre-compiled regex patterns."""
|
|
|
|
def test_password_pattern_detects_hardcoded(self):
|
|
"""Test that password pattern detects hardcoded passwords."""
|
|
code = 'password = "my_secret_password_123"'
|
|
self.assertTrue(PATTERN_HARDCODED_PASSWORD.search(code))
|
|
|
|
def test_password_pattern_ignores_short_values(self):
|
|
"""Test that password pattern ignores very short values."""
|
|
code = 'password = "x"' # Too short
|
|
self.assertFalse(PATTERN_HARDCODED_PASSWORD.search(code))
|
|
|
|
def test_api_key_pattern_detects_hardcoded(self):
|
|
"""Test that API key pattern detects hardcoded keys."""
|
|
code = 'api_key = "sk-1234567890abcdef"'
|
|
self.assertTrue(PATTERN_HARDCODED_API_KEY.search(code))
|
|
|
|
def test_token_pattern_detects_hardcoded(self):
|
|
"""Test that token pattern detects hardcoded tokens."""
|
|
code = 'token = "ghp_1234567890abcdef"'
|
|
self.assertTrue(PATTERN_HARDCODED_TOKEN.search(code))
|
|
|
|
def test_private_key_pattern_detects_hardcoded(self):
|
|
"""Test that private key pattern detects hardcoded keys."""
|
|
code = 'private_key = "-----BEGIN RSA PRIVATE KEY-----"'
|
|
self.assertTrue(PATTERN_HARDCODED_PRIVATE_KEY.search(code))
|
|
|
|
def test_os_system_pattern_detects(self):
|
|
"""Test that os.system pattern is detected."""
|
|
code = 'os.system("ls -la")'
|
|
self.assertTrue(PATTERN_OS_SYSTEM.search(code))
|
|
|
|
def test_eval_pattern_detects(self):
|
|
"""Test that eval pattern is detected."""
|
|
code = 'result = eval(user_input)'
|
|
self.assertTrue(PATTERN_EVAL.search(code))
|
|
|
|
def test_exec_pattern_detects(self):
|
|
"""Test that exec pattern is detected."""
|
|
code = 'exec(user_code)'
|
|
self.assertTrue(PATTERN_EXEC.search(code))
|
|
|
|
def test_subprocess_shell_true_pattern_detects(self):
|
|
"""Test that subprocess shell=True pattern is detected."""
|
|
code = 'subprocess.run(cmd, shell=True)'
|
|
self.assertTrue(PATTERN_SUBPROCESS_SHELL_TRUE.search(code))
|
|
|
|
def test_shlex_quote_pattern_detects(self):
|
|
"""Test that shlex.quote pattern is detected."""
|
|
code = 'safe_cmd = shlex.quote(user_input)'
|
|
self.assertTrue(PATTERN_SHLEX_QUOTE.search(code))
|
|
|
|
def test_safe_env_var_pattern_detects(self):
|
|
"""Test that safe environment variable pattern is detected."""
|
|
code = 'password = os.getenv("DB_PASSWORD")'
|
|
self.assertTrue(PATTERN_SAFE_ENV_VAR.search(code))
|
|
|
|
|
|
class TestSecurityScorerInit(unittest.TestCase):
|
|
"""Tests for SecurityScorer initialization."""
|
|
|
|
def test_init_with_empty_list(self):
|
|
"""Test initialization with empty script list."""
|
|
scorer = SecurityScorer([])
|
|
self.assertEqual(scorer.scripts, [])
|
|
self.assertFalse(scorer.verbose)
|
|
|
|
def test_init_with_scripts(self):
|
|
"""Test initialization with script list."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "test.py"
|
|
script_path.write_text("# test")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
self.assertEqual(len(scorer.scripts), 1)
|
|
|
|
def test_init_with_verbose(self):
|
|
"""Test initialization with verbose mode."""
|
|
scorer = SecurityScorer([], verbose=True)
|
|
self.assertTrue(scorer.verbose)
|
|
|
|
|
|
class TestSensitiveDataExposure(unittest.TestCase):
|
|
"""Tests for sensitive data exposure scoring."""
|
|
|
|
def test_no_scripts_returns_max_score(self):
|
|
"""Test that empty script list returns max score."""
|
|
scorer = SecurityScorer([])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
self.assertEqual(score, float(MAX_COMPONENT_SCORE))
|
|
self.assertEqual(findings, [])
|
|
|
|
def test_clean_script_scores_high(self):
|
|
"""Test that clean script without sensitive data scores high."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "clean.py"
|
|
script_path.write_text("""
|
|
import os
|
|
|
|
def get_password():
|
|
return os.getenv("DB_PASSWORD")
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
self.assertGreaterEqual(score, 20)
|
|
self.assertEqual(len(findings), 0)
|
|
|
|
def test_hardcoded_password_detected(self):
|
|
"""Test that hardcoded password is detected and penalized."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "insecure.py"
|
|
script_path.write_text("""
|
|
password = "super_secret_password_123"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
self.assertTrue(any('password' in f.lower() for f in findings))
|
|
|
|
def test_hardcoded_api_key_detected(self):
|
|
"""Test that hardcoded API key is detected and penalized."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "insecure.py"
|
|
script_path.write_text("""
|
|
api_key = "sk-1234567890abcdef123456"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
# Check for 'api' or 'hardcoded' in findings (description is 'hardcoded API key')
|
|
self.assertTrue(any('api' in f.lower() or 'hardcoded' in f.lower() for f in findings))
|
|
|
|
def test_hardcoded_token_detected(self):
|
|
"""Test that hardcoded token is detected and penalized."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "insecure.py"
|
|
script_path.write_text("""
|
|
token = "ghp_1234567890abcdef"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
|
|
def test_hardcoded_private_key_detected(self):
|
|
"""Test that hardcoded private key is detected and penalized."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "insecure.py"
|
|
script_path.write_text("""
|
|
private_key = "-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAJCA..."
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
|
|
def test_environment_variable_not_flagged(self):
|
|
"""Test that environment variable usage is not flagged as sensitive."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "secure.py"
|
|
script_path.write_text("""
|
|
import os
|
|
|
|
def get_credentials():
|
|
password = os.getenv("DB_PASSWORD")
|
|
api_key = os.environ.get("API_KEY")
|
|
return password, api_key
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
# Should score well because using environment variables
|
|
self.assertGreaterEqual(score, 20)
|
|
|
|
def test_empty_file_handled(self):
|
|
"""Test that empty file is handled gracefully."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "empty.py"
|
|
script_path.write_text("")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
# Should return max score for empty file (no sensitive data)
|
|
self.assertGreaterEqual(score, 20)
|
|
|
|
def test_jwt_token_detected(self):
|
|
"""Test that JWT token in code is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "jwt.py"
|
|
script_path.write_text("""
|
|
# JWT token for testing
|
|
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
# Should be penalized for JWT token
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
|
|
|
|
class TestSafeFileOperations(unittest.TestCase):
|
|
"""Tests for safe file operations scoring."""
|
|
|
|
def test_no_scripts_returns_max_score(self):
|
|
"""Test that empty script list returns max score."""
|
|
scorer = SecurityScorer([])
|
|
score, findings = scorer.score_safe_file_operations()
|
|
self.assertEqual(score, float(MAX_COMPONENT_SCORE))
|
|
|
|
def test_safe_pathlib_usage_scores_high(self):
|
|
"""Test that safe pathlib usage scores high."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "safe.py"
|
|
script_path.write_text("""
|
|
from pathlib import Path
|
|
|
|
def read_file(filename):
|
|
path = Path(filename).resolve()
|
|
return path.read_text()
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_safe_file_operations()
|
|
|
|
self.assertGreaterEqual(score, 15)
|
|
|
|
def test_path_traversal_detected(self):
|
|
"""Test that path traversal pattern is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "risky.py"
|
|
script_path.write_text("""
|
|
def read_file(base_path, filename):
|
|
# Potential path traversal vulnerability
|
|
path = base_path + "/../../../etc/passwd"
|
|
return open(path).read()
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_safe_file_operations()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
|
|
def test_basename_usage_scores_bonus(self):
|
|
"""Test that basename usage gets bonus points."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "safe.py"
|
|
script_path.write_text("""
|
|
import os
|
|
|
|
def safe_filename(user_input):
|
|
return os.path.basename(user_input)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_safe_file_operations()
|
|
|
|
self.assertGreaterEqual(score, 15)
|
|
|
|
|
|
class TestCommandInjectionPrevention(unittest.TestCase):
|
|
"""Tests for command injection prevention scoring."""
|
|
|
|
def test_no_scripts_returns_max_score(self):
|
|
"""Test that empty script list returns max score."""
|
|
scorer = SecurityScorer([])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
self.assertEqual(score, float(MAX_COMPONENT_SCORE))
|
|
|
|
def test_os_system_detected(self):
|
|
"""Test that os.system usage is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "risky.py"
|
|
script_path.write_text("""
|
|
import os
|
|
|
|
def run_command(user_input):
|
|
os.system("echo " + user_input)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
self.assertTrue(any('os.system' in f.lower() for f in findings))
|
|
|
|
def test_subprocess_shell_true_detected(self):
|
|
"""Test that subprocess with shell=True is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "risky.py"
|
|
script_path.write_text("""
|
|
import subprocess
|
|
|
|
def run_command(cmd):
|
|
subprocess.run(cmd, shell=True)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
# Check for 'shell' or 'subprocess' in findings
|
|
self.assertTrue(any('shell' in f.lower() or 'subprocess' in f.lower() for f in findings))
|
|
|
|
def test_eval_detected(self):
|
|
"""Test that eval usage is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "risky.py"
|
|
script_path.write_text("""
|
|
def evaluate(user_input):
|
|
return eval(user_input)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
self.assertTrue(any('eval' in f.lower() for f in findings))
|
|
|
|
def test_exec_detected(self):
|
|
"""Test that exec usage is detected."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "risky.py"
|
|
script_path.write_text("""
|
|
def execute(user_code):
|
|
exec(user_code)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
self.assertTrue(any('exec' in f.lower() for f in findings))
|
|
|
|
def test_shlex_quote_gets_bonus(self):
|
|
"""Test that shlex.quote usage gets bonus points."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "safe.py"
|
|
script_path.write_text("""
|
|
import shlex
|
|
import subprocess
|
|
|
|
def run_command(user_input):
|
|
safe_cmd = shlex.quote(user_input)
|
|
subprocess.run(["echo", safe_cmd], shell=False)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertGreaterEqual(score, 20)
|
|
|
|
def test_safe_subprocess_scores_high(self):
|
|
"""Test that safe subprocess usage (shell=False) scores high."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "safe.py"
|
|
script_path.write_text("""
|
|
import subprocess
|
|
|
|
def run_command(cmd_parts):
|
|
subprocess.run(cmd_parts, shell=False)
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, findings = scorer.score_command_injection_prevention()
|
|
|
|
self.assertGreaterEqual(score, 20)
|
|
|
|
|
|
class TestInputValidation(unittest.TestCase):
|
|
"""Tests for input validation scoring."""
|
|
|
|
def test_no_scripts_returns_max_score(self):
|
|
"""Test that empty script list returns max score."""
|
|
scorer = SecurityScorer([])
|
|
score, suggestions = scorer.score_input_validation()
|
|
self.assertEqual(score, float(MAX_COMPONENT_SCORE))
|
|
|
|
def test_argparse_usage_scores_bonus(self):
|
|
"""Test that argparse usage gets bonus points."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "good.py"
|
|
script_path.write_text("""
|
|
import argparse
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("input")
|
|
args = parser.parse_args()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, suggestions = scorer.score_input_validation()
|
|
|
|
# Base score is 10, argparse gives +3 bonus, so score should be 13
|
|
self.assertGreaterEqual(score, 10)
|
|
self.assertLessEqual(score, MAX_COMPONENT_SCORE)
|
|
|
|
def test_isinstance_usage_scores_bonus(self):
|
|
"""Test that isinstance usage gets bonus points."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "good.py"
|
|
script_path.write_text("""
|
|
def process(value):
|
|
if isinstance(value, str):
|
|
return value.upper()
|
|
return value
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, suggestions = scorer.score_input_validation()
|
|
|
|
self.assertGreater(score, BASE_SCORE_INPUT_VALIDATION)
|
|
|
|
def test_try_except_scores_bonus(self):
|
|
"""Test that try/except usage gets bonus points."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "good.py"
|
|
script_path.write_text("""
|
|
def process(value):
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
return 0
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, suggestions = scorer.score_input_validation()
|
|
|
|
self.assertGreater(score, BASE_SCORE_INPUT_VALIDATION)
|
|
|
|
def test_minimal_validation_gets_suggestion(self):
|
|
"""Test that minimal validation triggers suggestion."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "minimal.py"
|
|
script_path.write_text("""
|
|
def main():
|
|
print("Hello")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
score, suggestions = scorer.score_input_validation()
|
|
|
|
self.assertLess(score, 15)
|
|
self.assertTrue(len(suggestions) > 0)
|
|
|
|
|
|
class TestOverallScore(unittest.TestCase):
|
|
"""Tests for overall security score calculation."""
|
|
|
|
def test_overall_score_components_present(self):
|
|
"""Test that overall score includes all components."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "test.py"
|
|
script_path.write_text("""
|
|
import os
|
|
import argparse
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.parse_args()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
results = scorer.get_overall_score()
|
|
|
|
self.assertIn('overall_score', results)
|
|
self.assertIn('components', results)
|
|
self.assertIn('findings', results)
|
|
self.assertIn('suggestions', results)
|
|
|
|
components = results['components']
|
|
self.assertIn('sensitive_data_exposure', components)
|
|
self.assertIn('safe_file_operations', components)
|
|
self.assertIn('command_injection_prevention', components)
|
|
self.assertIn('input_validation', components)
|
|
|
|
def test_overall_score_is_weighted_average(self):
|
|
"""Test that overall score is calculated as weighted average."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "test.py"
|
|
script_path.write_text("""
|
|
import argparse
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.parse_args()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
results = scorer.get_overall_score()
|
|
|
|
# Calculate expected weighted average
|
|
expected = (
|
|
results['components']['sensitive_data_exposure'] * 0.25 +
|
|
results['components']['safe_file_operations'] * 0.25 +
|
|
results['components']['command_injection_prevention'] * 0.25 +
|
|
results['components']['input_validation'] * 0.25
|
|
)
|
|
|
|
self.assertAlmostEqual(results['overall_score'], expected, places=0)
|
|
|
|
def test_critical_vulnerability_caps_score(self):
|
|
"""Test that critical vulnerabilities cap the overall score."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "critical.py"
|
|
script_path.write_text("""
|
|
password = "hardcoded_password_123"
|
|
api_key = "sk-1234567890abcdef"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
results = scorer.get_overall_score()
|
|
|
|
# Score should be capped at 30 for critical vulnerabilities
|
|
self.assertLessEqual(results['overall_score'], 30)
|
|
self.assertTrue(results['has_critical_vulnerabilities'])
|
|
|
|
def test_secure_script_scores_high(self):
|
|
"""Test that secure script scores high overall."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
script_path = Path(tmpdir) / "secure.py"
|
|
script_path.write_text("""
|
|
#!/usr/bin/env python3
|
|
import argparse
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
def validate_path(path_str):
|
|
path = Path(path_str).resolve()
|
|
if not path.exists():
|
|
raise FileNotFoundError("Path not found")
|
|
return path
|
|
|
|
def safe_command(args):
|
|
return subprocess.run(args, shell=False, capture_output=True)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("input")
|
|
args = parser.parse_args()
|
|
|
|
db_password = os.getenv("DB_PASSWORD")
|
|
path = validate_path(args.input)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([script_path])
|
|
results = scorer.get_overall_score()
|
|
|
|
# Secure script should score reasonably well
|
|
# Note: Score may vary based on pattern detection
|
|
self.assertGreater(results['overall_score'], 20)
|
|
|
|
|
|
class TestScoreClamping(unittest.TestCase):
|
|
"""Tests for score boundary clamping."""
|
|
|
|
def test_score_never_below_zero(self):
|
|
"""Test that score never goes below 0."""
|
|
scorer = SecurityScorer([])
|
|
# Test with extreme negative
|
|
result = scorer._clamp_score(-100)
|
|
self.assertEqual(result, MIN_SCORE)
|
|
|
|
def test_score_never_above_max(self):
|
|
"""Test that score never goes above max."""
|
|
scorer = SecurityScorer([])
|
|
# Test with extreme positive
|
|
result = scorer._clamp_score(1000)
|
|
self.assertEqual(result, MAX_COMPONENT_SCORE)
|
|
|
|
def test_score_unchanged_in_valid_range(self):
|
|
"""Test that score is unchanged in valid range."""
|
|
scorer = SecurityScorer([])
|
|
for test_score in [0, 5, 10, 15, 20, 25]:
|
|
result = scorer._clamp_score(test_score)
|
|
self.assertEqual(result, test_score)
|
|
|
|
|
|
class TestMultipleScripts(unittest.TestCase):
|
|
"""Tests for scoring multiple scripts."""
|
|
|
|
def test_multiple_scripts_averaged(self):
|
|
"""Test that scores are averaged across multiple scripts."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
secure_script = Path(tmpdir) / "secure.py"
|
|
secure_script.write_text("""
|
|
import os
|
|
|
|
def main():
|
|
password = os.getenv("PASSWORD")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
insecure_script = Path(tmpdir) / "insecure.py"
|
|
insecure_script.write_text("""
|
|
password = "hardcoded_secret_password"
|
|
|
|
def main():
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
""")
|
|
|
|
scorer = SecurityScorer([secure_script, insecure_script])
|
|
score, findings = scorer.score_sensitive_data_exposure()
|
|
|
|
# Score should be between secure and insecure
|
|
self.assertGreater(score, 0)
|
|
self.assertLess(score, MAX_COMPONENT_SCORE)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2) |