182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
"""Helper utilities for SEC1 config security scanner Robot integration tests.
|
|
|
|
Covers the security_scanner module: pattern detection, line-number reporting,
|
|
validation gate, and comment skipping.
|
|
|
|
Each command prints structured output lines that Robot Framework can
|
|
independently verify:
|
|
VIOLATIONS=<count>
|
|
TOKEN=<token>
|
|
SEVERITY=<severity>
|
|
LINE=<line_number>
|
|
STATUS=ok|fail
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from cleveragents.config.security_scanner import (
|
|
Severity,
|
|
scan_content,
|
|
validate_config_safety,
|
|
)
|
|
from cleveragents.core.exceptions import ConfigurationError
|
|
|
|
|
|
def _detect_eval() -> None:
|
|
"""Scanner should detect eval() in config content."""
|
|
content = 'transform: eval("1+1")\n'
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
print(f"SEVERITY={v.severity.name}")
|
|
assert any(v.token == "eval(" for v in violations), "eval( not detected"
|
|
assert all(
|
|
v.severity == Severity.CRITICAL for v in violations if v.token == "eval("
|
|
)
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _detect_exec() -> None:
|
|
"""Scanner should detect exec() in config content."""
|
|
content = 'code: exec("import os")\n'
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
assert any(v.token == "exec(" for v in violations), "exec( not detected"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _detect_import() -> None:
|
|
"""Scanner should detect __import__ in config content."""
|
|
content = "loader: __import__('os')\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
assert any(v.token == "__import__" for v in violations), "__import__ not detected"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _detect_os_system() -> None:
|
|
"""Scanner should detect os.system in config content."""
|
|
content = "run: os.system('rm -rf /')\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
assert any(v.token == "os.system" for v in violations), "os.system not detected"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _detect_subprocess() -> None:
|
|
"""Scanner should detect subprocess calls in config content."""
|
|
content = "cmd: subprocess.call(['ls'])\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
assert any(v.token == "subprocess.call" for v in violations), (
|
|
"subprocess.call not detected"
|
|
)
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _clean_config() -> None:
|
|
"""Scanner should return zero violations for safe content."""
|
|
content = "name: my-project\nversion: 1.0.0\ndescription: A safe config\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
assert len(violations) == 0, f"Expected 0 violations, got {len(violations)}"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _line_numbers() -> None:
|
|
"""Scanner should report correct line numbers."""
|
|
content = "safe: true\nunsafe: eval('boom')\nother: ok\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
eval_violations = [v for v in violations if v.token == "eval("]
|
|
print(f"VIOLATIONS={len(eval_violations)}")
|
|
for v in eval_violations:
|
|
print(f"LINE={v.line_number}")
|
|
assert len(eval_violations) == 1, (
|
|
f"Expected 1 eval violation, got {len(eval_violations)}"
|
|
)
|
|
assert eval_violations[0].line_number == 2, (
|
|
f"Expected line 2, got {eval_violations[0].line_number}"
|
|
)
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _validate_raises() -> None:
|
|
"""validate_config_safety should raise ConfigurationError on violations."""
|
|
content = 'action: exec("bad")\n'
|
|
try:
|
|
validate_config_safety(content, "test.yaml")
|
|
print("STATUS=fail")
|
|
print("REASON=Expected ConfigurationError")
|
|
return
|
|
except ConfigurationError:
|
|
pass
|
|
# Clean content should not raise
|
|
validate_config_safety("name: safe\n", "test.yaml")
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _skip_comments() -> None:
|
|
"""Scanner should skip pure-comment lines."""
|
|
content = "# eval('this is a comment')\nname: safe\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
assert len(violations) == 0, f"Expected 0 violations, got {len(violations)}"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _detect_template() -> None:
|
|
"""Scanner should detect template directives."""
|
|
content = "template: {{ user_input }}\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
for v in violations:
|
|
print(f"TOKEN={v.token}")
|
|
assert any(v.token == "{{" for v in violations), "{{ not detected"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def _inline_comment() -> None:
|
|
"""Scanner should skip patterns in inline comments."""
|
|
content = "name: safe-project # eval('just a note')\n"
|
|
violations = scan_content(content, "test.yaml")
|
|
print(f"VIOLATIONS={len(violations)}")
|
|
assert len(violations) == 0, f"Expected 0 violations, got {len(violations)}"
|
|
print("STATUS=ok")
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit("Expected command argument")
|
|
command = sys.argv[1]
|
|
commands = {
|
|
"detect-eval": _detect_eval,
|
|
"detect-exec": _detect_exec,
|
|
"detect-import": _detect_import,
|
|
"detect-os-system": _detect_os_system,
|
|
"detect-subprocess": _detect_subprocess,
|
|
"clean-config": _clean_config,
|
|
"line-numbers": _line_numbers,
|
|
"validate-raises": _validate_raises,
|
|
"skip-comments": _skip_comments,
|
|
"detect-template": _detect_template,
|
|
"inline-comment": _inline_comment,
|
|
}
|
|
if command not in commands:
|
|
raise SystemExit(f"Unknown command: {command}")
|
|
commands[command]()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|