claude-dev-env 1.92.1 → 1.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
8
|
+
assert specification is not None and specification.loader is not None
|
|
9
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
10
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
11
|
+
|
|
12
|
+
PRODUCTION_FILE_PATH = "packages/app/services/report.py"
|
|
13
|
+
TEST_FILE_PATH = "packages/app/services/test_report.py"
|
|
14
|
+
UNDERSCORE_TEST_FILE_PATH = "packages/app/services/report_test.py"
|
|
15
|
+
CONFTEST_FILE_PATH = "packages/app/tests/conftest.py"
|
|
16
|
+
SHARED_TEST_MODULE_PATH = "packages/app/tests/expected_values.py"
|
|
17
|
+
TEST_CONSTANTS_MODULE_PATH = "packages/app/hooks_constants/test_layout_constants.py"
|
|
18
|
+
CONFIG_UNDER_TESTS_PATH = "packages/app/tests/config/timing.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dead_constant(source: str, file_path: str) -> list[str]:
|
|
22
|
+
return code_rules_enforcer.check_dead_test_module_constant(source, file_path)
|
|
23
|
+
|
|
24
|
+
def _unused_parameter(source: str, file_path: str) -> list[str]:
|
|
25
|
+
return code_rules_enforcer.check_unused_test_helper_parameter(source, file_path)
|
|
26
|
+
|
|
27
|
+
def test_should_flag_dead_private_constant_in_test_file() -> None:
|
|
28
|
+
source = (
|
|
29
|
+
"_ABSENT_DOTENV_FILENAME = 'absent.env'\n"
|
|
30
|
+
"\n"
|
|
31
|
+
"def test_reads_environment() -> None:\n"
|
|
32
|
+
" assert True\n"
|
|
33
|
+
)
|
|
34
|
+
issues = _dead_constant(source, TEST_FILE_PATH)
|
|
35
|
+
assert any("_ABSENT_DOTENV_FILENAME" in each_issue for each_issue in issues), issues
|
|
36
|
+
|
|
37
|
+
def test_should_flag_dead_constant_in_underscore_test_module() -> None:
|
|
38
|
+
source = (
|
|
39
|
+
"_UNREAD_CONSTANT = 'x'\n"
|
|
40
|
+
"\n"
|
|
41
|
+
"def test_something() -> None:\n"
|
|
42
|
+
" assert True\n"
|
|
43
|
+
)
|
|
44
|
+
issues = _dead_constant(source, UNDERSCORE_TEST_FILE_PATH)
|
|
45
|
+
assert any("_UNREAD_CONSTANT" in each_issue for each_issue in issues), issues
|
|
46
|
+
|
|
47
|
+
def test_should_not_flag_dead_public_constant_in_test_file() -> None:
|
|
48
|
+
source = (
|
|
49
|
+
"EXPECTED_TOTAL = 3\n"
|
|
50
|
+
"\n"
|
|
51
|
+
"def test_total() -> None:\n"
|
|
52
|
+
" assert True\n"
|
|
53
|
+
)
|
|
54
|
+
assert _dead_constant(source, TEST_FILE_PATH) == []
|
|
55
|
+
|
|
56
|
+
def test_should_not_flag_constant_read_by_a_later_line() -> None:
|
|
57
|
+
source = (
|
|
58
|
+
"_EXPECTED_TOTAL = 208\n"
|
|
59
|
+
"\n"
|
|
60
|
+
"def test_total_matches() -> None:\n"
|
|
61
|
+
" assert compute_total() == _EXPECTED_TOTAL\n"
|
|
62
|
+
)
|
|
63
|
+
assert _dead_constant(source, TEST_FILE_PATH) == []
|
|
64
|
+
|
|
65
|
+
def test_should_not_flag_dead_constant_in_production_file() -> None:
|
|
66
|
+
source = "_UNUSED_CONSTANT = 'x'\n"
|
|
67
|
+
assert _dead_constant(source, PRODUCTION_FILE_PATH) == []
|
|
68
|
+
|
|
69
|
+
def test_should_not_flag_shared_constant_in_conftest() -> None:
|
|
70
|
+
source = (
|
|
71
|
+
"SHARED_TIMEOUT_SECONDS = 30\n"
|
|
72
|
+
"\n"
|
|
73
|
+
"def _database() -> object:\n"
|
|
74
|
+
" return connect()\n"
|
|
75
|
+
)
|
|
76
|
+
assert _dead_constant(source, CONFTEST_FILE_PATH) == []
|
|
77
|
+
|
|
78
|
+
def test_should_not_flag_shared_constant_module_under_tests() -> None:
|
|
79
|
+
source = "EXPECTED_TOTAL = 208\n"
|
|
80
|
+
assert _dead_constant(source, SHARED_TEST_MODULE_PATH) == []
|
|
81
|
+
|
|
82
|
+
def test_should_not_flag_constant_in_a_constants_module() -> None:
|
|
83
|
+
source = "SHARED_LAYOUT_LIMIT = 50\n"
|
|
84
|
+
assert _dead_constant(source, TEST_CONSTANTS_MODULE_PATH) == []
|
|
85
|
+
|
|
86
|
+
def test_should_not_flag_constant_in_config_module_under_tests() -> None:
|
|
87
|
+
source = "REQUEST_TIMEOUT_SECONDS = 30\n"
|
|
88
|
+
assert _dead_constant(source, CONFIG_UNDER_TESTS_PATH) == []
|
|
89
|
+
|
|
90
|
+
def test_should_flag_unused_private_helper_parameter() -> None:
|
|
91
|
+
source = "def _configuration(monkeypatch, tmp_path):\n return build_config()\n"
|
|
92
|
+
issues = _unused_parameter(source, TEST_FILE_PATH)
|
|
93
|
+
flagged_parameters = {"monkeypatch", "tmp_path"}
|
|
94
|
+
assert all(
|
|
95
|
+
any(each_name in each_issue for each_issue in issues) for each_name in flagged_parameters
|
|
96
|
+
), issues
|
|
97
|
+
|
|
98
|
+
def test_should_not_flag_parameter_the_body_reads() -> None:
|
|
99
|
+
source = "def _configuration(database_url):\n return build_config(database_url)\n"
|
|
100
|
+
assert _unused_parameter(source, TEST_FILE_PATH) == []
|
|
101
|
+
|
|
102
|
+
def test_should_not_flag_fixture_parameters() -> None:
|
|
103
|
+
source = "import pytest\n\n@pytest.fixture\ndef _database(monkeypatch):\n return connect()\n"
|
|
104
|
+
assert _unused_parameter(source, TEST_FILE_PATH) == []
|
|
105
|
+
|
|
106
|
+
def test_should_not_flag_public_test_function_parameters() -> None:
|
|
107
|
+
source = (
|
|
108
|
+
"def test_writes_row(postgres_database_url) -> None:\n"
|
|
109
|
+
" assert postgres_database_url is not None\n"
|
|
110
|
+
)
|
|
111
|
+
assert _unused_parameter(source, TEST_FILE_PATH) == []
|
|
@@ -192,3 +192,79 @@ def test_should_skip_module_level_docstring_text() -> None:
|
|
|
192
192
|
)
|
|
193
193
|
issues = check_imports_at_top(content)
|
|
194
194
|
assert issues == []
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_should_flag_module_level_import_after_top_level_def() -> None:
|
|
198
|
+
"""A module-scope import placed below a top-level function must flag.
|
|
199
|
+
|
|
200
|
+
This is the "imports mid-file after test functions" drift: the imports sit
|
|
201
|
+
at indent zero (module scope), so the inside-function tracker never sees
|
|
202
|
+
them, yet they violate the imports-at-top rule.
|
|
203
|
+
"""
|
|
204
|
+
content = (
|
|
205
|
+
"import os\n"
|
|
206
|
+
"\n"
|
|
207
|
+
"def first_test():\n"
|
|
208
|
+
" return os.getcwd()\n"
|
|
209
|
+
"\n"
|
|
210
|
+
"from pathlib import Path\n"
|
|
211
|
+
"\n"
|
|
212
|
+
"def second_test():\n"
|
|
213
|
+
" return Path('.')\n"
|
|
214
|
+
)
|
|
215
|
+
issues = check_imports_at_top(content)
|
|
216
|
+
assert len(issues) == 1
|
|
217
|
+
assert issues[0].startswith("Line 6:")
|
|
218
|
+
assert "after top-level definition" in issues[0]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_should_flag_module_level_import_after_top_level_class() -> None:
|
|
222
|
+
content = (
|
|
223
|
+
"class Widget:\n"
|
|
224
|
+
" pass\n"
|
|
225
|
+
"\n"
|
|
226
|
+
"from collections import OrderedDict\n"
|
|
227
|
+
)
|
|
228
|
+
issues = check_imports_at_top(content)
|
|
229
|
+
assert len(issues) == 1
|
|
230
|
+
assert issues[0].startswith("Line 4:")
|
|
231
|
+
assert "after top-level definition" in issues[0]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_should_allow_type_checking_import_after_top_level_def() -> None:
|
|
235
|
+
content = (
|
|
236
|
+
"from typing import TYPE_CHECKING\n"
|
|
237
|
+
"\n"
|
|
238
|
+
"def helper():\n"
|
|
239
|
+
" return 1\n"
|
|
240
|
+
"\n"
|
|
241
|
+
"if TYPE_CHECKING:\n"
|
|
242
|
+
" from foo import Bar\n"
|
|
243
|
+
)
|
|
244
|
+
issues = check_imports_at_top(content)
|
|
245
|
+
assert issues == []
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_should_allow_indented_import_in_main_guard_after_def() -> None:
|
|
249
|
+
content = (
|
|
250
|
+
"def helper():\n"
|
|
251
|
+
" return 1\n"
|
|
252
|
+
"\n"
|
|
253
|
+
"if __name__ == '__main__':\n"
|
|
254
|
+
" import argparse\n"
|
|
255
|
+
" helper()\n"
|
|
256
|
+
)
|
|
257
|
+
issues = check_imports_at_top(content)
|
|
258
|
+
assert issues == []
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_should_not_flag_top_level_imports_before_any_definition() -> None:
|
|
262
|
+
content = (
|
|
263
|
+
"import os\n"
|
|
264
|
+
"from pathlib import Path\n"
|
|
265
|
+
"\n"
|
|
266
|
+
"def helper():\n"
|
|
267
|
+
" return Path(os.getcwd())\n"
|
|
268
|
+
)
|
|
269
|
+
issues = check_imports_at_top(content)
|
|
270
|
+
assert issues == []
|
|
@@ -14,6 +14,14 @@ GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
|
14
14
|
GH_REDIRECT_ACTIVE_VALUE = "1"
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _windows_style_system_temporary_root() -> str:
|
|
18
|
+
return tempfile.gettempdir().replace("/", "\\")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _posix_style_system_temporary_root() -> str:
|
|
22
|
+
return tempfile.gettempdir().replace("\\", "/")
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
def _run_hook(
|
|
18
26
|
payload: dict,
|
|
19
27
|
gh_redirect_active: bool = True,
|
|
@@ -42,7 +50,7 @@ def _make_bash_payload(command: str) -> dict:
|
|
|
42
50
|
|
|
43
51
|
def test_denies_gh_issue_comment_as_redirect_duplicate_guard() -> None:
|
|
44
52
|
payload = _make_bash_payload(
|
|
45
|
-
'gh issue comment 83 --repo jl-cmd/claude-
|
|
53
|
+
'gh issue comment 83 --repo jl-cmd/claude-dev-env --body "hello"'
|
|
46
54
|
)
|
|
47
55
|
result = _run_hook(payload)
|
|
48
56
|
response = json.loads(result.stdout)
|
|
@@ -298,7 +306,8 @@ def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_unquoted() ->
|
|
|
298
306
|
|
|
299
307
|
def test_rm_rf_allowed_when_leading_cd_into_windows_temp_worktree_subdirectory() -> None:
|
|
300
308
|
windows_style_temp_worktree = (
|
|
301
|
-
|
|
309
|
+
_windows_style_system_temporary_root()
|
|
310
|
+
+ r"\bugteam-pr-58-20260424071040\pr-58\worktree"
|
|
302
311
|
)
|
|
303
312
|
payload = _make_bash_payload(
|
|
304
313
|
f'cd "{windows_style_temp_worktree}" && rm -rf .bugteam-tmp'
|
|
@@ -563,7 +572,9 @@ def test_rm_rf_asks_when_leading_cd_target_is_relative_path() -> None:
|
|
|
563
572
|
|
|
564
573
|
def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_cleanup() -> None:
|
|
565
574
|
payload = _make_bash_payload(
|
|
566
|
-
|
|
575
|
+
'cd "'
|
|
576
|
+
+ _windows_style_system_temporary_root()
|
|
577
|
+
+ r'\bugteam-pr-58-20260424071040\pr-58\worktree" && rm -rf .bugteam-tmp'
|
|
567
578
|
)
|
|
568
579
|
|
|
569
580
|
result = _run_rm_hook(payload)
|
|
@@ -574,7 +585,9 @@ def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_clea
|
|
|
574
585
|
|
|
575
586
|
def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_cleanup() -> None:
|
|
576
587
|
payload = _make_bash_payload(
|
|
577
|
-
'cd "
|
|
588
|
+
'cd "'
|
|
589
|
+
+ _posix_style_system_temporary_root()
|
|
590
|
+
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf .bugteam-tmp'
|
|
578
591
|
)
|
|
579
592
|
|
|
580
593
|
result = _run_rm_hook(payload)
|
|
@@ -585,7 +598,9 @@ def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_
|
|
|
585
598
|
|
|
586
599
|
def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() -> None:
|
|
587
600
|
payload = _make_bash_payload(
|
|
588
|
-
'cd "
|
|
601
|
+
'cd "'
|
|
602
|
+
+ _posix_style_system_temporary_root()
|
|
603
|
+
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf tmp_reply_loop1-1.md'
|
|
589
604
|
)
|
|
590
605
|
|
|
591
606
|
result = _run_rm_hook(payload)
|
|
@@ -596,7 +611,9 @@ def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() ->
|
|
|
596
611
|
|
|
597
612
|
def test_rm_rf_allowed_for_chat_observed_bugfind_multiple_scratch_files_cleanup() -> None:
|
|
598
613
|
payload = _make_bash_payload(
|
|
599
|
-
'cd "
|
|
614
|
+
'cd "'
|
|
615
|
+
+ _posix_style_system_temporary_root()
|
|
616
|
+
+ '/bugteam-pr-58-20260424071040/pr-256/worktree" && rm -rf tmp_review_body.md tmp_finding_1.md'
|
|
600
617
|
)
|
|
601
618
|
|
|
602
619
|
result = _run_rm_hook(payload)
|
|
@@ -610,7 +627,8 @@ def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_work
|
|
|
610
627
|
"tool_name": "Bash",
|
|
611
628
|
"tool_input": {
|
|
612
629
|
"command": "rm -rf .bugteam-tmp",
|
|
613
|
-
"cwd":
|
|
630
|
+
"cwd": _posix_style_system_temporary_root()
|
|
631
|
+
+ "/bugteam-pr-58-20260424071040/pr-58/worktree",
|
|
614
632
|
},
|
|
615
633
|
}
|
|
616
634
|
|
|
@@ -622,7 +640,9 @@ def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_work
|
|
|
622
640
|
|
|
623
641
|
def test_rm_rf_allowed_for_chat_observed_absolute_path_in_bugteam_windows_worktree_scratch() -> None:
|
|
624
642
|
payload = _make_bash_payload(
|
|
625
|
-
'rm -rf "
|
|
643
|
+
'rm -rf "'
|
|
644
|
+
+ _posix_style_system_temporary_root()
|
|
645
|
+
+ '/bugteam-pr-58-20260424071040/pr-58/worktree/.bugteam-tmp"'
|
|
626
646
|
)
|
|
627
647
|
|
|
628
648
|
result = _run_rm_hook(payload)
|
|
@@ -839,7 +859,7 @@ def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees
|
|
|
839
859
|
|
|
840
860
|
def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target() -> None:
|
|
841
861
|
payload = _make_bash_payload(
|
|
842
|
-
|
|
862
|
+
"rm -rf " + _windows_style_system_temporary_root() + r"\scratch"
|
|
843
863
|
)
|
|
844
864
|
|
|
845
865
|
result = _run_rm_hook(payload)
|
|
@@ -138,7 +138,7 @@ def test_no_false_positive_body_as_title_value() -> None:
|
|
|
138
138
|
def test_no_false_positive_windows_path_in_body_file() -> None:
|
|
139
139
|
"""Unquoted Windows path with backslashes in --body-file must be approved without token corruption."""
|
|
140
140
|
assert not _uses_body_string_arg(
|
|
141
|
-
r'gh pr create --title "T" --body-file C:\Users\
|
|
141
|
+
r'gh pr create --title "T" --body-file C:\Users\example\tmp\body.md'
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
|
|
@@ -222,7 +222,7 @@ def test_blocks_body_file_followed_by_body_string() -> None:
|
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
def test_blocks_windows_path_with_trailing_backslash_continuation() -> None:
|
|
225
|
-
"""C1: prior line ending in `C:\\Users\\
|
|
225
|
+
"""C1: prior line ending in `C:\\Users\\example\\` must still continue lines.
|
|
226
226
|
|
|
227
227
|
Naive `count("\\\\") % 2 == 1` mis-classifies this (count=4, even -> no
|
|
228
228
|
continuation) and misses --body on the next line. Counting only the
|
|
@@ -230,7 +230,7 @@ def test_blocks_windows_path_with_trailing_backslash_continuation() -> None:
|
|
|
230
230
|
backslash as a continuation marker.
|
|
231
231
|
"""
|
|
232
232
|
command = (
|
|
233
|
-
'gh pr create --title C:\\Users\\
|
|
233
|
+
'gh pr create --title C:\\Users\\example\\ \\\n'
|
|
234
234
|
' --body "real body text"\n'
|
|
235
235
|
)
|
|
236
236
|
assert _uses_body_string_arg(command)
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
"""Unit tests for nas-ssh-binary-enforcer PreToolUse hook.
|
|
1
|
+
"""Unit tests for nas-ssh-binary-enforcer PreToolUse hook.
|
|
2
|
+
|
|
3
|
+
The real NAS host, ssh user, and port are private, so these tests set them via
|
|
4
|
+
the ``CLAUDE_NAS_*`` environment variables and drive the real hook, asserting
|
|
5
|
+
the runtime-built host pattern and deny messages.
|
|
6
|
+
"""
|
|
2
7
|
|
|
3
8
|
import importlib.util
|
|
9
|
+
import os
|
|
4
10
|
import pathlib
|
|
5
11
|
import sys
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
6
16
|
|
|
7
17
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
8
18
|
if str(_HOOK_DIR) not in sys.path:
|
|
9
19
|
sys.path.insert(0, str(_HOOK_DIR))
|
|
20
|
+
if str(_HOOK_DIR.parent) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_HOOK_DIR.parent))
|
|
10
22
|
|
|
11
23
|
hook_spec = importlib.util.spec_from_file_location(
|
|
12
24
|
"nas_ssh_binary_enforcer",
|
|
@@ -18,55 +30,88 @@ hook_module = importlib.util.module_from_spec(hook_spec)
|
|
|
18
30
|
hook_spec.loader.exec_module(hook_module)
|
|
19
31
|
_find_nas_ssh_violation = hook_module._find_nas_ssh_violation
|
|
20
32
|
|
|
21
|
-
from hooks_constants.
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
from hooks_constants.local_identity import ( # noqa: E402
|
|
34
|
+
bare_ssh_binary_deny_message,
|
|
35
|
+
missing_batch_mode_deny_message,
|
|
24
36
|
)
|
|
25
37
|
|
|
38
|
+
NAS_HOST = "test-nas.example.net"
|
|
39
|
+
NAS_SSH_USER = "tester"
|
|
40
|
+
NAS_SSH_PORT = "2222"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(autouse=True)
|
|
44
|
+
def _set_nas_identity_env() -> Iterator[None]:
|
|
45
|
+
with patch.dict(
|
|
46
|
+
os.environ,
|
|
47
|
+
{
|
|
48
|
+
"CLAUDE_NAS_HOST": NAS_HOST,
|
|
49
|
+
"CLAUDE_NAS_SSH_USER": NAS_SSH_USER,
|
|
50
|
+
"CLAUDE_NAS_SSH_PORT": NAS_SSH_PORT,
|
|
51
|
+
},
|
|
52
|
+
clear=False,
|
|
53
|
+
):
|
|
54
|
+
yield
|
|
55
|
+
|
|
26
56
|
|
|
27
57
|
def test_bare_ssh_to_nas_is_denied_with_binary_message() -> None:
|
|
28
58
|
assert (
|
|
29
|
-
_find_nas_ssh_violation(
|
|
30
|
-
|
|
59
|
+
_find_nas_ssh_violation(
|
|
60
|
+
f'ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "ls /volume1"'
|
|
61
|
+
)
|
|
62
|
+
== bare_ssh_binary_deny_message()
|
|
31
63
|
)
|
|
32
64
|
|
|
33
65
|
|
|
34
66
|
def test_bare_scp_to_nas_is_denied() -> None:
|
|
35
67
|
assert (
|
|
36
|
-
_find_nas_ssh_violation(
|
|
37
|
-
|
|
68
|
+
_find_nas_ssh_violation(
|
|
69
|
+
f"scp -P {NAS_SSH_PORT} file.txt {NAS_SSH_USER}@{NAS_HOST}:/volume1/"
|
|
70
|
+
)
|
|
71
|
+
== bare_ssh_binary_deny_message()
|
|
38
72
|
)
|
|
39
73
|
|
|
40
74
|
|
|
41
75
|
def test_bare_sftp_to_nas_is_denied() -> None:
|
|
42
|
-
assert
|
|
76
|
+
assert (
|
|
77
|
+
_find_nas_ssh_violation(f"sftp -P {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST}")
|
|
78
|
+
== bare_ssh_binary_deny_message()
|
|
79
|
+
)
|
|
43
80
|
|
|
44
81
|
|
|
45
82
|
def test_bare_ssh_to_nas_behind_launcher_wrapper_is_denied() -> None:
|
|
46
83
|
assert (
|
|
47
|
-
_find_nas_ssh_violation(
|
|
48
|
-
|
|
84
|
+
_find_nas_ssh_violation(
|
|
85
|
+
f'timeout 10 ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
86
|
+
)
|
|
87
|
+
== bare_ssh_binary_deny_message()
|
|
49
88
|
)
|
|
50
89
|
|
|
51
90
|
|
|
52
91
|
def test_bare_ssh_to_nas_after_shell_operator_is_denied() -> None:
|
|
53
92
|
assert (
|
|
54
|
-
_find_nas_ssh_violation(
|
|
55
|
-
|
|
93
|
+
_find_nas_ssh_violation(
|
|
94
|
+
f'cd /tmp && ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
95
|
+
)
|
|
96
|
+
== bare_ssh_binary_deny_message()
|
|
56
97
|
)
|
|
57
98
|
|
|
58
99
|
|
|
59
100
|
def test_bare_ssh_to_nas_after_glued_operator_is_denied() -> None:
|
|
60
101
|
assert (
|
|
61
|
-
_find_nas_ssh_violation(
|
|
62
|
-
|
|
102
|
+
_find_nas_ssh_violation(
|
|
103
|
+
f'true;ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
104
|
+
)
|
|
105
|
+
== bare_ssh_binary_deny_message()
|
|
63
106
|
)
|
|
64
107
|
|
|
65
108
|
|
|
66
109
|
def test_bare_ssh_to_nas_after_pipe_ampersand_operator_is_denied() -> None:
|
|
67
110
|
assert (
|
|
68
|
-
_find_nas_ssh_violation(
|
|
69
|
-
|
|
111
|
+
_find_nas_ssh_violation(
|
|
112
|
+
f'echo start |& ssh -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "echo hi"'
|
|
113
|
+
)
|
|
114
|
+
== bare_ssh_binary_deny_message()
|
|
70
115
|
)
|
|
71
116
|
|
|
72
117
|
|
|
@@ -74,18 +119,19 @@ def test_full_openssh_binary_to_nas_without_batchmode_is_denied() -> None:
|
|
|
74
119
|
assert (
|
|
75
120
|
_find_nas_ssh_violation(
|
|
76
121
|
'"/c/Windows/System32/OpenSSH/ssh.exe" -o ConnectTimeout=10 '
|
|
77
|
-
'-p
|
|
122
|
+
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
78
123
|
)
|
|
79
|
-
==
|
|
124
|
+
== missing_batch_mode_deny_message()
|
|
80
125
|
)
|
|
81
126
|
|
|
82
127
|
|
|
83
128
|
def test_full_openssh_scp_to_nas_without_batchmode_is_denied() -> None:
|
|
84
129
|
assert (
|
|
85
130
|
_find_nas_ssh_violation(
|
|
86
|
-
'"/c/Windows/System32/OpenSSH/scp.exe"
|
|
131
|
+
'"/c/Windows/System32/OpenSSH/scp.exe" '
|
|
132
|
+
f"-P {NAS_SSH_PORT} file.txt {NAS_SSH_USER}@{NAS_HOST}:/volume1/"
|
|
87
133
|
)
|
|
88
|
-
==
|
|
134
|
+
== missing_batch_mode_deny_message()
|
|
89
135
|
)
|
|
90
136
|
|
|
91
137
|
|
|
@@ -93,7 +139,7 @@ def test_full_openssh_binary_to_nas_with_batchmode_is_allowed() -> None:
|
|
|
93
139
|
assert (
|
|
94
140
|
_find_nas_ssh_violation(
|
|
95
141
|
'"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes '
|
|
96
|
-
'-o ConnectTimeout=10 -p
|
|
142
|
+
f'-o ConnectTimeout=10 -p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
97
143
|
)
|
|
98
144
|
is None
|
|
99
145
|
)
|
|
@@ -103,61 +149,72 @@ def test_full_openssh_binary_glued_batchmode_flag_is_allowed() -> None:
|
|
|
103
149
|
assert (
|
|
104
150
|
_find_nas_ssh_violation(
|
|
105
151
|
'"/c/Windows/System32/OpenSSH/ssh.exe" -oBatchMode=yes '
|
|
106
|
-
'-p
|
|
152
|
+
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "uptime"'
|
|
107
153
|
)
|
|
108
154
|
is None
|
|
109
155
|
)
|
|
110
156
|
|
|
111
157
|
|
|
112
158
|
def test_bare_ssh_to_other_host_is_allowed() -> None:
|
|
113
|
-
assert _find_nas_ssh_violation('ssh -p 22
|
|
159
|
+
assert _find_nas_ssh_violation(f'ssh -p 22 {NAS_SSH_USER}@example.com "uptime"') is None
|
|
114
160
|
|
|
115
161
|
|
|
116
162
|
def test_full_openssh_binary_to_other_host_without_batchmode_is_allowed() -> None:
|
|
117
163
|
assert (
|
|
118
164
|
_find_nas_ssh_violation(
|
|
119
|
-
'"/c/Windows/System32/OpenSSH/ssh.exe" -p 22
|
|
165
|
+
f'"/c/Windows/System32/OpenSSH/ssh.exe" -p 22 {NAS_SSH_USER}@example.com "uptime"'
|
|
120
166
|
)
|
|
121
167
|
is None
|
|
122
168
|
)
|
|
123
169
|
|
|
124
170
|
|
|
125
|
-
def
|
|
126
|
-
assert _find_nas_ssh_violation('echo "connect via ssh to
|
|
171
|
+
def test_nas_host_as_echoed_data_is_allowed() -> None:
|
|
172
|
+
assert _find_nas_ssh_violation(f'echo "connect via ssh to {NAS_HOST}"') is None
|
|
127
173
|
|
|
128
174
|
|
|
129
|
-
def
|
|
130
|
-
assert _find_nas_ssh_violation("ping -c 1
|
|
175
|
+
def test_nas_host_mentioned_without_ssh_command_word_is_allowed() -> None:
|
|
176
|
+
assert _find_nas_ssh_violation(f"ping -c 1 {NAS_HOST}") is None
|
|
131
177
|
|
|
132
178
|
|
|
133
179
|
def test_ssh_family_word_only_in_remote_command_argument_is_allowed() -> None:
|
|
134
180
|
assert (
|
|
135
181
|
_find_nas_ssh_violation(
|
|
136
182
|
'"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes '
|
|
137
|
-
'-p
|
|
183
|
+
f'-p {NAS_SSH_PORT} {NAS_SSH_USER}@{NAS_HOST} "grep ssh /etc/config"'
|
|
138
184
|
)
|
|
139
185
|
is None
|
|
140
186
|
)
|
|
141
187
|
|
|
142
188
|
|
|
143
|
-
def
|
|
144
|
-
assert
|
|
189
|
+
def test_nas_host_in_different_segment_than_ssh_is_allowed() -> None:
|
|
190
|
+
assert (
|
|
191
|
+
_find_nas_ssh_violation(
|
|
192
|
+
f'echo {NAS_HOST} && ssh -p 22 {NAS_SSH_USER}@example.com "id"'
|
|
193
|
+
)
|
|
194
|
+
is None
|
|
195
|
+
)
|
|
145
196
|
|
|
146
197
|
|
|
147
|
-
def
|
|
198
|
+
def test_ssh_to_other_host_with_nas_host_in_remote_command_is_allowed() -> None:
|
|
148
199
|
assert (
|
|
149
|
-
_find_nas_ssh_violation(
|
|
200
|
+
_find_nas_ssh_violation(
|
|
201
|
+
f'ssh -p 22 {NAS_SSH_USER}@example.com "ping -c1 {NAS_HOST}"'
|
|
202
|
+
)
|
|
203
|
+
is None
|
|
150
204
|
)
|
|
151
205
|
|
|
152
206
|
|
|
153
|
-
def
|
|
207
|
+
def test_scp_to_other_host_with_nas_host_in_remote_path_is_allowed() -> None:
|
|
154
208
|
assert (
|
|
155
|
-
_find_nas_ssh_violation(
|
|
209
|
+
_find_nas_ssh_violation(
|
|
210
|
+
f"scp -P 22 f.txt {NAS_SSH_USER}@example.com:/backup/{NAS_HOST}/"
|
|
211
|
+
)
|
|
212
|
+
is None
|
|
156
213
|
)
|
|
157
214
|
|
|
158
215
|
|
|
159
|
-
def
|
|
160
|
-
assert _find_nas_ssh_violation('ssh -p 22
|
|
216
|
+
def test_similar_host_suffix_is_not_matched() -> None:
|
|
217
|
+
assert _find_nas_ssh_violation(f'ssh -p 22 {NAS_SSH_USER}@{NAS_HOST}x "id"') is None
|
|
161
218
|
|
|
162
219
|
|
|
163
220
|
def test_empty_command_is_allowed() -> None:
|
|
@@ -165,4 +222,4 @@ def test_empty_command_is_allowed() -> None:
|
|
|
165
222
|
|
|
166
223
|
|
|
167
224
|
def test_unbalanced_quotes_command_is_allowed() -> None:
|
|
168
|
-
assert _find_nas_ssh_violation("ssh 'unterminated
|
|
225
|
+
assert _find_nas_ssh_violation(f"ssh 'unterminated {NAS_HOST}") is None
|