Files
zk-data-agent/tests/test_bash_security.py
T
Abdelrahman Abdallah aacf0a212a Created src/prompt_constants.py (550+ lines) porting all constants from npm src/constants/:
┌──────────────────┬───────────────────────────┬───────────────────────────────────────────────┐
│ Category         │ npm Source                │ Items                                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Product metadata │ product.ts                │ URLs, base URLs                               │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ System prefixes  │ system.ts                 │ 3 prompt prefixes                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Cyber risk       │ cyberRiskInstruction.ts   │ Safety instruction                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ API limits       │ apiLimits.ts              │ 10 image/PDF/media limits                     │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Tool limits      │ toolLimits.ts             │ 6 result size constants                       │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Spinner verbs    │ spinnerVerbs.ts           │ 187 whimsical gerunds                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Completion verbs │ turnCompletionVerbs.ts    │ 8 past-tense verbs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Figures/symbols  │ figures.ts                │ 25 Unicode UI symbols                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ XML tags         │ xml.ts                    │ 30+ tag constants                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Messages         │ messages.ts               │ NO_CONTENT_MESSAGE                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Date utilities   │ common.ts                 │ 4 functions                                   │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Section caching  │ systemPromptSections.ts   │ Memoized/volatile sections                    │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Output styles    │ outputStyles.ts           │ 3 built-in configs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Prompt helpers   │ prompts.ts                │ Knowledge cutoff, language, scratchpad, hooks │
└──────────────────┴───────────────────────────┴───────────────────────────────────────────────┘

91 new tests in tests/test_prompt_constants.py. All 17 SQL todos done.
2026-04-08 00:02:53 +02:00

879 lines
33 KiB
Python

"""
Tests for bash_security module.
Tests are organized by validator function, matching the npm test structure.
"""
import pytest
from src.bash_security import (
SecurityBehavior,
SecurityResult,
ValidationContext,
bash_command_is_safe,
check_shell_security,
extract_quoted_content,
get_destructive_command_warning,
has_unescaped_char,
interpret_command_result,
is_command_read_only,
split_command,
strip_safe_redirections,
validate_backslash_escaped_operators,
validate_backslash_escaped_whitespace,
validate_brace_expansion,
validate_carriage_return,
validate_comment_quote_desync,
validate_control_characters,
validate_dangerous_patterns,
validate_dangerous_variables,
validate_empty,
validate_git_commit,
validate_ifs_injection,
validate_incomplete_commands,
validate_jq_command,
validate_mid_word_hash,
validate_newlines,
validate_obfuscated_flags,
validate_proc_environ_access,
validate_quoted_newline,
validate_redirections,
validate_shell_metacharacters,
validate_unicode_whitespace,
validate_zsh_dangerous_commands,
)
# ---- Helper to build a context ----
def _ctx(cmd: str) -> ValidationContext:
"""Build a ValidationContext for the given command."""
base = cmd.strip().split()[0] if cmd.strip() else ''
with_dq, fully_unq, keep_qc = extract_quoted_content(cmd)
return ValidationContext(
original_command=cmd,
base_command=base,
unquoted_content=with_dq,
fully_unquoted_content=strip_safe_redirections(fully_unq),
fully_unquoted_pre_strip=fully_unq,
unquoted_keep_quote_chars=keep_qc,
)
# ===========================================================================
# extract_quoted_content
# ===========================================================================
class TestExtractQuotedContent:
def test_no_quotes(self):
dq, full, kqc = extract_quoted_content('echo hello')
assert dq == 'echo hello'
assert full == 'echo hello'
def test_single_quotes_stripped(self):
dq, full, kqc = extract_quoted_content("echo 'hello world'")
assert 'hello world' not in full
assert 'echo' in full
def test_double_quotes_in_dq_output(self):
dq, full, kqc = extract_quoted_content('echo "hello world"')
assert 'hello world' in dq # double-quoted content preserved in dq
assert 'hello world' not in full # but stripped in fully_unquoted
def test_escape_handling(self):
dq, full, kqc = extract_quoted_content('echo \\$HOME')
assert '$HOME' in full
def test_keep_quote_chars(self):
_, _, kqc = extract_quoted_content("echo 'x'#")
assert "'" in kqc # quote chars preserved
# ===========================================================================
# strip_safe_redirections
# ===========================================================================
class TestStripSafeRedirections:
def test_dev_null_output(self):
assert '>/dev/null' not in strip_safe_redirections('cmd > /dev/null')
def test_stderr_redirect(self):
assert '2>&1' not in strip_safe_redirections('cmd 2>&1')
def test_dev_null_input(self):
assert '</dev/null' not in strip_safe_redirections('cmd < /dev/null')
def test_preserves_other_redirections(self):
result = strip_safe_redirections('cmd > output.txt')
assert '> output.txt' in result
# ===========================================================================
# has_unescaped_char
# ===========================================================================
class TestHasUnescapedChar:
def test_unescaped_backtick(self):
assert has_unescaped_char('echo `date`', '`') is True
def test_escaped_backtick(self):
assert has_unescaped_char('echo \\`safe\\`', '`') is False
def test_double_backslash_then_backtick(self):
# \\\` → \\ (literal backslash) + ` (unescaped)
assert has_unescaped_char('test\\\\`date`', '`') is True
def test_no_match(self):
assert has_unescaped_char('echo hello', '`') is False
# ===========================================================================
# split_command
# ===========================================================================
class TestSplitCommand:
def test_simple(self):
assert split_command('echo hello') == ['echo hello']
def test_semicolon(self):
assert split_command('echo a; echo b') == ['echo a', 'echo b']
def test_and_and(self):
assert split_command('cmd1 && cmd2') == ['cmd1', 'cmd2']
def test_pipe(self):
assert split_command('cat file | grep pattern') == ['cat file', 'grep pattern']
def test_or_or(self):
assert split_command('cmd1 || cmd2') == ['cmd1', 'cmd2']
def test_quotes_preserved(self):
result = split_command("echo 'a; b'")
assert len(result) == 1 # semicolon inside quotes not split
def test_complex(self):
result = split_command('cd /tmp && echo hi; ls | head')
assert len(result) == 4
# ===========================================================================
# validate_empty
# ===========================================================================
class TestValidateEmpty:
def test_empty(self):
assert validate_empty(_ctx('')).behavior == SecurityBehavior.ALLOW
def test_whitespace_only(self):
assert validate_empty(_ctx(' ')).behavior == SecurityBehavior.ALLOW
def test_non_empty(self):
assert validate_empty(_ctx('ls')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_control_characters
# ===========================================================================
class TestValidateControlCharacters:
def test_null_byte(self):
result = validate_control_characters(_ctx('echo\x00hello'))
assert result.behavior == SecurityBehavior.ASK
def test_bell(self):
result = validate_control_characters(_ctx('echo\x07hello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean_command(self):
result = validate_control_characters(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_tab_allowed(self):
result = validate_control_characters(_ctx('echo\thello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_newline_allowed(self):
result = validate_control_characters(_ctx('echo\nhello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_incomplete_commands
# ===========================================================================
class TestValidateIncompleteCommands:
def test_starts_with_tab(self):
result = validate_incomplete_commands(_ctx('\techo hello'))
assert result.behavior == SecurityBehavior.ASK
def test_starts_with_dash(self):
result = validate_incomplete_commands(_ctx('-rf /'))
assert result.behavior == SecurityBehavior.ASK
def test_starts_with_operator(self):
result = validate_incomplete_commands(_ctx('&& echo hello'))
assert result.behavior == SecurityBehavior.ASK
result = validate_incomplete_commands(_ctx('; echo hello'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
result = validate_incomplete_commands(_ctx('ls -la'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_git_commit
# ===========================================================================
class TestValidateGitCommit:
def test_not_git(self):
result = validate_git_commit(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_commit(self):
result = validate_git_commit(_ctx("git commit -m 'initial commit'"))
assert result.behavior == SecurityBehavior.ALLOW
def test_double_quoted_commit(self):
result = validate_git_commit(_ctx('git commit -m "fix bug"'))
assert result.behavior == SecurityBehavior.ALLOW
def test_commit_with_substitution(self):
result = validate_git_commit(_ctx('git commit -m "$(date)"'))
assert result.behavior == SecurityBehavior.ASK
def test_commit_with_backtick(self):
result = validate_git_commit(_ctx('git commit -m "`date`"'))
assert result.behavior == SecurityBehavior.ASK
def test_commit_with_chained_commands(self):
result = validate_git_commit(_ctx("git commit -m 'msg'; rm -rf /"))
# Should passthrough (not early-allow) due to ; in remainder
assert result.behavior != SecurityBehavior.ALLOW
def test_commit_with_backslash(self):
result = validate_git_commit(_ctx('git commit -m "test\\"msg"'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_jq_command
# ===========================================================================
class TestValidateJqCommand:
def test_not_jq(self):
assert validate_jq_command(_ctx('echo hi')).behavior == SecurityBehavior.PASSTHROUGH
def test_jq_system(self):
result = validate_jq_command(_ctx('jq "system(\"rm -rf /\")"'))
assert result.behavior == SecurityBehavior.ASK
def test_jq_from_file(self):
result = validate_jq_command(_ctx('jq -f evil.jq'))
assert result.behavior == SecurityBehavior.ASK
def test_jq_slurpfile(self):
result = validate_jq_command(_ctx('jq --slurpfile x data.json'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_jq(self):
result = validate_jq_command(_ctx('jq ".name" data.json'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_obfuscated_flags
# ===========================================================================
class TestValidateObfuscatedFlags:
def test_ansi_c_quoting(self):
result = validate_obfuscated_flags(_ctx("find . $'-exec' evil"))
assert result.behavior == SecurityBehavior.ASK
def test_locale_quoting(self):
result = validate_obfuscated_flags(_ctx('find . $"-exec" evil'))
assert result.behavior == SecurityBehavior.ASK
def test_echo_safe(self):
result = validate_obfuscated_flags(_ctx("echo $'hello'"))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_empty_quotes_before_dash(self):
result = validate_obfuscated_flags(_ctx("find . '' -exec evil"))
assert result.behavior == SecurityBehavior.ASK
def test_quoted_flag(self):
result = validate_obfuscated_flags(_ctx('find . "-exec" rm {} ;'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
result = validate_obfuscated_flags(_ctx('ls -la'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_three_consecutive_quotes(self):
result = validate_obfuscated_flags(_ctx("find . '''exec"))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# validate_shell_metacharacters
# ===========================================================================
class TestValidateShellMetacharacters:
def test_semicolon_in_quotes(self):
result = validate_shell_metacharacters(_ctx('echo "a;b"'))
# unquoted_content (with_double_quotes) has the ; inside
assert result.behavior == SecurityBehavior.PASSTHROUGH or result.behavior == SecurityBehavior.ASK
def test_find_name_with_pipe(self):
# Single-quoted pipe is stripped entirely from unquoted content → safe
ctx = _ctx("find . -name '|evil'")
result = validate_shell_metacharacters(ctx)
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_double_quoted_metachar(self):
# Check that we catch metacharacters in unquoted positions
# The npm version checks the double-quote-retained string for
# quoted metacharacters, but we strip quote chars. So we test
# the actual dangerous case: unquoted semicolon
ctx = _ctx('find . -name evil; rm -rf /')
# This won't be caught by this specific validator (it looks for
# metacharacters INSIDE quoted args, not command separators)
result = validate_shell_metacharacters(ctx)
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_clean_command(self):
assert validate_shell_metacharacters(_ctx('ls -la')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_dangerous_variables
# ===========================================================================
class TestValidateDangerousVariables:
def test_variable_in_pipe(self):
result = validate_dangerous_variables(_ctx('$CMD | grep x'))
assert result.behavior == SecurityBehavior.ASK
def test_variable_in_redirect(self):
result = validate_dangerous_variables(_ctx('echo x > $FILE'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_variable(self):
result = validate_dangerous_variables(_ctx('echo $HOME'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_dangerous_patterns
# ===========================================================================
class TestValidateDangerousPatterns:
def test_backtick(self):
result = validate_dangerous_patterns(_ctx('echo `date`'))
assert result.behavior == SecurityBehavior.ASK
def test_dollar_paren(self):
result = validate_dangerous_patterns(_ctx('echo $(date)'))
assert result.behavior == SecurityBehavior.ASK
def test_dollar_brace(self):
result = validate_dangerous_patterns(_ctx('echo ${PATH}'))
assert result.behavior == SecurityBehavior.ASK
def test_process_substitution(self):
result = validate_dangerous_patterns(_ctx('diff <(cmd1) <(cmd2)'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_echo(self):
result = validate_dangerous_patterns(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_escaped_backtick(self):
result = validate_dangerous_patterns(_ctx('echo \\`safe\\`'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_redirections
# ===========================================================================
class TestValidateRedirections:
def test_output_redirect(self):
result = validate_redirections(_ctx('echo x > file.txt'))
assert result.behavior == SecurityBehavior.ASK
def test_input_redirect(self):
result = validate_redirections(_ctx('cat < /etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_dev_null_stripped(self):
# >/dev/null is stripped by strip_safe_redirections
result = validate_redirections(_ctx('cmd > /dev/null'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_no_redirect(self):
result = validate_redirections(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_newlines
# ===========================================================================
class TestValidateNewlines:
def test_no_newlines(self):
assert validate_newlines(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_newline_with_command(self):
result = validate_newlines(_ctx('echo hello\nrm -rf /'))
assert result.behavior == SecurityBehavior.ASK
def test_backslash_continuation(self):
result = validate_newlines(_ctx('cmd \\\n--flag'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_carriage_return
# ===========================================================================
class TestValidateCarriageReturn:
def test_no_cr(self):
assert validate_carriage_return(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_cr_in_command(self):
result = validate_carriage_return(_ctx('echo hello\rworld'))
assert result.behavior == SecurityBehavior.ASK
assert result.is_misparsing is True
def test_cr_in_double_quotes_safe(self):
result = validate_carriage_return(_ctx('echo "hello\rworld"'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_ifs_injection
# ===========================================================================
class TestValidateIFSInjection:
def test_ifs_variable(self):
result = validate_ifs_injection(_ctx('echo$IFS/etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_ifs_expansion(self):
result = validate_ifs_injection(_ctx('echo ${IFS:0:1}'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_ifs_injection(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_proc_environ_access
# ===========================================================================
class TestValidateProcEnvironAccess:
def test_proc_environ(self):
result = validate_proc_environ_access(_ctx('cat /proc/self/environ'))
assert result.behavior == SecurityBehavior.ASK
def test_proc_pid_environ(self):
result = validate_proc_environ_access(_ctx('cat /proc/1/environ'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_proc_environ_access(_ctx('cat /etc/hosts')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_backslash_escaped_whitespace
# ===========================================================================
class TestValidateBackslashEscapedWhitespace:
def test_escaped_space(self):
result = validate_backslash_escaped_whitespace(_ctx('echo\\ hello'))
assert result.behavior == SecurityBehavior.ASK
def test_escaped_tab(self):
result = validate_backslash_escaped_whitespace(_ctx('echo\\\thello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_backslash_escaped_whitespace(
_ctx('echo hello')
).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_backslash_escaped_operators
# ===========================================================================
class TestValidateBackslashEscapedOperators:
def test_escaped_semicolon(self):
result = validate_backslash_escaped_operators(_ctx('cat safe.txt \\; echo /etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_escaped_pipe(self):
result = validate_backslash_escaped_operators(_ctx('cmd \\| evil'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_backslash_escaped_operators(
_ctx('ls -la')
).behavior == SecurityBehavior.PASSTHROUGH
def test_inside_quotes_safe(self):
result = validate_backslash_escaped_operators(_ctx("echo '\\;'"))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_brace_expansion
# ===========================================================================
class TestValidateBraceExpansion:
def test_comma_expansion(self):
result = validate_brace_expansion(_ctx('echo {a,b,c}'))
assert result.behavior == SecurityBehavior.ASK
def test_sequence_expansion(self):
result = validate_brace_expansion(_ctx('echo {1..5}'))
assert result.behavior == SecurityBehavior.ASK
def test_no_expansion(self):
result = validate_brace_expansion(_ctx('echo {hello}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_escaped_brace(self):
result = validate_brace_expansion(_ctx('echo \\{a,b\\}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_excess_closing_braces(self):
result = validate_brace_expansion(_ctx("git diff {@'{'0},--output=/tmp/pwned}"))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# validate_unicode_whitespace
# ===========================================================================
class TestValidateUnicodeWhitespace:
def test_nbsp(self):
result = validate_unicode_whitespace(_ctx('echo\u00a0hello'))
assert result.behavior == SecurityBehavior.ASK
def test_em_space(self):
result = validate_unicode_whitespace(_ctx('echo\u2003hello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_unicode_whitespace(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_mid_word_hash
# ===========================================================================
class TestValidateMidWordHash:
def test_mid_word_hash(self):
result = validate_mid_word_hash(_ctx('echotest#comment'))
assert result.behavior == SecurityBehavior.ASK
def test_word_start_hash(self):
result = validate_mid_word_hash(_ctx('echo # comment'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_dollar_brace_hash_safe(self):
# ${#var} is bash string length, should be safe
result = validate_mid_word_hash(_ctx('echo ${#var}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_comment_quote_desync
# ===========================================================================
class TestValidateCommentQuoteDesync:
def test_quote_in_comment(self):
result = validate_comment_quote_desync(_ctx("echo hello # it's a comment"))
assert result.behavior == SecurityBehavior.ASK
def test_clean_comment(self):
result = validate_comment_quote_desync(_ctx('echo hello # clean comment'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_no_comment(self):
assert validate_comment_quote_desync(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_quoted_newline
# ===========================================================================
class TestValidateQuotedNewline:
def test_quoted_newline_with_hash(self):
result = validate_quoted_newline(_ctx("echo 'hello\n# dangerous line'"))
assert result.behavior == SecurityBehavior.ASK
def test_no_newline(self):
assert validate_quoted_newline(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_newline_without_hash(self):
assert validate_quoted_newline(_ctx("echo 'hello\nworld'")).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_zsh_dangerous_commands
# ===========================================================================
class TestValidateZshDangerousCommands:
def test_zmodload(self):
result = validate_zsh_dangerous_commands(_ctx('zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
def test_zpty(self):
result = validate_zsh_dangerous_commands(_ctx('zpty cmd echo'))
assert result.behavior == SecurityBehavior.ASK
def test_emulate(self):
result = validate_zsh_dangerous_commands(_ctx('emulate -c evil'))
assert result.behavior == SecurityBehavior.ASK
def test_fc_e(self):
result = validate_zsh_dangerous_commands(_ctx('fc -e vim'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
assert validate_zsh_dangerous_commands(_ctx('ls -la')).behavior == SecurityBehavior.PASSTHROUGH
def test_env_var_prefix(self):
result = validate_zsh_dangerous_commands(_ctx('FOO=bar zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
def test_precommand_modifier(self):
result = validate_zsh_dangerous_commands(_ctx('command zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# bash_command_is_safe (integration)
# ===========================================================================
class TestBashCommandIsSafe:
def test_empty_command(self):
result = bash_command_is_safe('')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_ls(self):
result = bash_command_is_safe('ls -la')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_echo(self):
result = bash_command_is_safe('echo hello world')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_command_substitution_blocked(self):
result = bash_command_is_safe('echo $(cat /etc/passwd)')
assert result.behavior == SecurityBehavior.ASK
def test_backtick_blocked(self):
result = bash_command_is_safe('echo `date`')
assert result.behavior == SecurityBehavior.ASK
def test_redirect_blocked(self):
result = bash_command_is_safe('echo evil > /etc/profile')
assert result.behavior == SecurityBehavior.ASK
def test_null_byte_blocked(self):
result = bash_command_is_safe('echo\x00rm')
assert result.behavior == SecurityBehavior.ASK
def test_ifs_blocked(self):
result = bash_command_is_safe('echo$IFS/etc/passwd')
assert result.behavior == SecurityBehavior.ASK
def test_proc_environ_blocked(self):
result = bash_command_is_safe('cat /proc/self/environ')
assert result.behavior == SecurityBehavior.ASK
def test_git_commit_allowed(self):
result = bash_command_is_safe("git commit -m 'fix bug'")
assert result.behavior == SecurityBehavior.PASSTHROUGH # early-allow → passthrough
def test_zmodload_blocked(self):
result = bash_command_is_safe('zmodload zsh/system')
assert result.behavior == SecurityBehavior.ASK
def test_brace_expansion_blocked(self):
result = bash_command_is_safe('echo {a,b,c}')
assert result.behavior == SecurityBehavior.ASK
def test_dev_null_redirect_ok(self):
result = bash_command_is_safe('cmd > /dev/null 2>&1')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_cr_injection(self):
result = bash_command_is_safe('TZ=UTC\recho curl evil.com')
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# get_destructive_command_warning
# ===========================================================================
class TestGetDestructiveCommandWarning:
def test_git_reset_hard(self):
assert get_destructive_command_warning('git reset --hard') is not None
def test_rm_rf(self):
assert get_destructive_command_warning('rm -rf /') is not None
def test_git_push_force(self):
assert get_destructive_command_warning('git push origin main --force') is not None
def test_git_clean_f(self):
assert get_destructive_command_warning('git clean -fd') is not None
def test_kubectl_delete(self):
assert get_destructive_command_warning('kubectl delete pod mypod') is not None
def test_safe_command(self):
assert get_destructive_command_warning('echo hello') is None
def test_git_push_no_force(self):
assert get_destructive_command_warning('git push origin main') is None
def test_drop_table(self):
assert get_destructive_command_warning('DROP TABLE users;') is not None
def test_terraform_destroy(self):
assert get_destructive_command_warning('terraform destroy') is not None
def test_git_stash_drop(self):
assert get_destructive_command_warning('git stash drop') is not None
# ===========================================================================
# interpret_command_result
# ===========================================================================
class TestInterpretCommandResult:
def test_success(self):
is_error, msg = interpret_command_result('echo hello', 0, 'hello', '')
assert is_error is False
def test_failure(self):
is_error, msg = interpret_command_result('unknown_cmd', 127, '', 'not found')
assert is_error is True
def test_grep_no_match(self):
is_error, msg = interpret_command_result('grep pattern file', 1, '', '')
assert is_error is False
assert msg == 'No matches found'
def test_grep_error(self):
is_error, msg = interpret_command_result('grep pattern file', 2, '', 'error')
assert is_error is True
def test_diff_files_differ(self):
is_error, msg = interpret_command_result('diff a b', 1, 'output', '')
assert is_error is False
assert msg == 'Files differ'
def test_find_partial(self):
is_error, msg = interpret_command_result('find / -name x', 1, '', '')
assert is_error is False
# ===========================================================================
# is_command_read_only
# ===========================================================================
class TestIsCommandReadOnly:
def test_ls(self):
assert is_command_read_only('ls -la') is True
def test_cat(self):
assert is_command_read_only('cat file.txt') is True
def test_grep(self):
assert is_command_read_only('grep -r pattern .') is True
def test_git_status(self):
assert is_command_read_only('git status') is True
def test_git_log(self):
assert is_command_read_only('git log --oneline') is True
def test_git_push(self):
assert is_command_read_only('git push') is False
def test_rm(self):
assert is_command_read_only('rm file.txt') is False
def test_sed_read_only(self):
assert is_command_read_only("sed -n '1,5p' file") is True
def test_sed_in_place(self):
assert is_command_read_only("sed -i 's/old/new/' file") is False
def test_find_safe(self):
assert is_command_read_only('find . -name "*.py"') is True
def test_find_exec(self):
assert is_command_read_only('find . -exec rm {} ;') is False
def test_find_delete(self):
assert is_command_read_only('find . -name "*.tmp" -delete') is False
def test_echo(self):
assert is_command_read_only('echo hello') is True
def test_python_version(self):
assert is_command_read_only('python --version') is True
def test_unknown_command(self):
assert is_command_read_only('some_random_command') is False
# ===========================================================================
# check_shell_security (integration)
# ===========================================================================
class TestCheckShellSecurity:
def test_shell_disabled(self):
allowed, msg = check_shell_security('ls', allow_shell=False)
assert allowed is False
assert 'disabled' in msg.lower()
def test_safe_command_allowed(self):
allowed, msg = check_shell_security('ls -la')
assert allowed is True
def test_destructive_blocked(self):
allowed, msg = check_shell_security('rm -rf /', allow_destructive=False)
assert allowed is False
assert 'destructive' in msg.lower()
def test_destructive_allowed_when_enabled(self):
allowed, msg = check_shell_security('rm -rf /tmp/test', allow_destructive=True)
# rm -rf still triggers destructive check, but allow_destructive=True skips it
# However rm -rf may also trigger the security check for force-remove
# Let's check: the main security check should pass (no injection)
# and destructive should be allowed
assert allowed is True
def test_injection_blocked(self):
allowed, msg = check_shell_security('echo `evil`')
assert allowed is False
assert 'backtick' in msg.lower() or 'security' in msg.lower()
def test_misparsing_always_blocked(self):
allowed, msg = check_shell_security('echo\x00rm')
assert allowed is False
def test_safe_git_commit(self):
allowed, msg = check_shell_security("git commit -m 'fix'")
assert allowed is True