conftest.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import http.server
  2. import json
  3. import os
  4. import subprocess
  5. import sys
  6. import threading
  7. from pathlib import Path
  8. import pytest
  9. HOOKS_DIR = Path(__file__).resolve().parent.parent / "hooks"
  10. HOOK_SCRIPT = HOOKS_DIR / "security_reminder_hook.py"
  11. sys.path.insert(0, str(HOOKS_DIR))
  12. GIT_ENV = {
  13. "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.com",
  14. "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.com",
  15. "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull,
  16. }
  17. def git(cwd, *args):
  18. r = subprocess.run(
  19. ["git", *args], cwd=cwd, capture_output=True, text=True,
  20. env={**os.environ, **GIT_ENV},
  21. )
  22. assert r.returncode == 0, r.stderr
  23. return r.stdout
  24. def make_repo(path, files=None):
  25. path.mkdir(parents=True, exist_ok=True)
  26. git(path, "init", "-q", "-b", "main")
  27. git(path, "config", "commit.gpgsign", "false")
  28. for name, content in (files or {"README.md": "init\n"}).items():
  29. p = path / name
  30. p.parent.mkdir(parents=True, exist_ok=True)
  31. p.write_text(content)
  32. git(path, "add", "-A")
  33. git(path, "commit", "-q", "-m", "init")
  34. return path
  35. def commit_file(repo, name, content, msg="change"):
  36. p = repo / name
  37. p.parent.mkdir(parents=True, exist_ok=True)
  38. p.write_text(content)
  39. git(repo, "add", "-A")
  40. out = subprocess.run(
  41. ["git", "commit", "-m", msg], cwd=repo, capture_output=True, text=True,
  42. env={**os.environ, **GIT_ENV},
  43. )
  44. assert out.returncode == 0, out.stderr
  45. sha = git(repo, "rev-parse", "HEAD").strip()
  46. return sha, out.stdout + out.stderr
  47. VULN_PY = (
  48. "import subprocess\n"
  49. "def run(user):\n"
  50. " subprocess.call('ls ' + user, shell=True)\n"
  51. )
  52. @pytest.fixture
  53. def workspace(tmp_path):
  54. ws = tmp_path / "ws"
  55. ws.mkdir()
  56. repo = make_repo(ws / "sub", {"app.py": "print('hi')\n"})
  57. (ws / "node_modules" / "junk").mkdir(parents=True)
  58. return ws, repo
  59. class _Stub(http.server.BaseHTTPRequestHandler):
  60. def do_POST(self):
  61. n = int(self.headers.get("Content-Length") or 0)
  62. if n:
  63. self.rfile.read(n)
  64. self.server.calls.append(self.path)
  65. if self.server.status == 200:
  66. body = json.dumps({
  67. "id": "msg_stub", "type": "message", "role": "assistant",
  68. "model": "stub", "stop_reason": "end_turn",
  69. "content": [{"type": "text", "text": json.dumps(
  70. {"hasVulnerabilities": False, "vulnerabilities": []})}],
  71. "usage": {"input_tokens": 1, "output_tokens": 1},
  72. }).encode()
  73. else:
  74. body = b'{"type":"error","error":{"type":"invalid_request_error","message":"stub"}}'
  75. self.send_response(self.server.status)
  76. self.send_header("Content-Type", "application/json")
  77. self.send_header("Content-Length", str(len(body)))
  78. self.end_headers()
  79. self.wfile.write(body)
  80. do_HEAD = do_GET = do_POST
  81. def log_message(self, *a):
  82. pass
  83. @pytest.fixture
  84. def stub_api():
  85. srv = http.server.HTTPServer(("127.0.0.1", 0), _Stub)
  86. srv.calls = []
  87. srv.status = 200
  88. t = threading.Thread(target=srv.serve_forever, daemon=True)
  89. t.start()
  90. try:
  91. yield srv
  92. finally:
  93. srv.shutdown()
  94. @pytest.fixture
  95. def hook_env(tmp_path, stub_api):
  96. state = tmp_path / "state"
  97. state.mkdir()
  98. env = {k: v for k, v in os.environ.items()
  99. if k not in ("ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_REMOTE",
  100. "CLAUDE_PROJECT_DIR", "HTTP_PROXY", "HTTPS_PROXY",
  101. "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy",
  102. "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX",
  103. "CLAUDE_CODE_USE_FOUNDRY")}
  104. env.update(GIT_ENV)
  105. env.update({
  106. "SECURITY_WARNINGS_STATE_DIR": str(state),
  107. "ANTHROPIC_API_KEY": "test-key",
  108. "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{stub_api.server_port}",
  109. "NO_PROXY": "*", "no_proxy": "*",
  110. "SG_AGENTIC_COMMIT_REVIEW": "0",
  111. "SECURITY_GUIDANCE_COMMIT_REVIEW": "on",
  112. "SG_PUSH_SWEEP": "on",
  113. "PYTHONDONTWRITEBYTECODE": "1",
  114. })
  115. return env
  116. def run_hook(payload, env, python=sys.executable):
  117. r = subprocess.run(
  118. [python, str(HOOK_SCRIPT)], input=json.dumps(payload),
  119. capture_output=True, text=True, env=env, timeout=120,
  120. )
  121. return r.returncode, r.stdout, r.stderr
  122. def metrics_of(stdout):
  123. for line in stdout.splitlines():
  124. line = line.strip()
  125. if line.startswith("{"):
  126. try:
  127. m = json.loads(line).get("metrics")
  128. except json.JSONDecodeError:
  129. continue
  130. if m is not None:
  131. return m
  132. return None
  133. def bash_payload(cwd, command, stdout="", stderr="", session_id="s1", tool_use_id=None):
  134. p = {
  135. "session_id": session_id,
  136. "hook_event_name": "PostToolUse",
  137. "tool_name": "Bash",
  138. "tool_input": {"command": command},
  139. "tool_response": {"stdout": stdout, "stderr": stderr, "interrupted": False},
  140. "cwd": str(cwd),
  141. }
  142. if tool_use_id:
  143. p["tool_use_id"] = tool_use_id
  144. return p
  145. def edit_payload(cwd, file_path, new_string="x", session_id="s1"):
  146. return {
  147. "session_id": session_id,
  148. "hook_event_name": "PostToolUse",
  149. "tool_name": "Edit",
  150. "tool_input": {"file_path": str(file_path), "old_string": "", "new_string": new_string},
  151. "tool_response": {},
  152. "cwd": str(cwd),
  153. }
  154. def stop_payload(cwd, event="Stop", session_id="s1"):
  155. return {
  156. "session_id": session_id,
  157. "hook_event_name": event,
  158. "stop_hook_active": False,
  159. "cwd": str(cwd),
  160. }
  161. def ups_payload(cwd, session_id="s1"):
  162. return {
  163. "session_id": session_id,
  164. "hook_event_name": "UserPromptSubmit",
  165. "prompt": "hi",
  166. "cwd": str(cwd),
  167. }