Files
claude-skills-reference/engineering/skill-tester/tests/test_security_scorer.py
xingzihai 2f92a1dfcb feat(skill-tester): add Security dimension to quality scoring system
- 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.
2026-03-26 13:25:27 +00:00

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)