codebuff-agent 2.1.74__py3-none-any.whl

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.
Files changed (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,657 @@
1
+ """
2
+ 보안 스캐너 래퍼 모듈.
3
+
4
+ bandit, semgrep, pip-audit, detect-secrets 등 보안 도구를 통합하여
5
+ SecurityFinding 데이터 모델로 결과를 반환합니다.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import time
13
+ import re
14
+ import subprocess
15
+ from dataclasses import asdict, dataclass
16
+ from pathlib import Path
17
+
18
+ from codebuff_core.directory_policy import (
19
+ EXCLUDED_RELATIVE_DIRS,
20
+ SEARCH_EXCLUDED_DIRS,
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class SecurityFinding:
26
+ """보안 스캔 결과 단일 항목."""
27
+
28
+ scanner: str # "bandit" | "semgrep" | "pip-audit" | "detect-secrets" | "custom"
29
+ severity: str # "critical" | "high" | "medium" | "low" | "info"
30
+ category: str # "sast" | "dependency" | "secret" | "injection"
31
+ title: str
32
+ description: str
33
+ file: str | None = None
34
+ line: int | None = None
35
+ cwe_id: str | None = None
36
+ owasp_id: str | None = None
37
+ confidence: str = "medium" # "high" | "medium" | "low"
38
+ fix_suggestion: str | None = None
39
+ raw_id: str | None = None
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Bandit CWE/OWASP 매핑
44
+ # ---------------------------------------------------------------------------
45
+ _BANDIT_CWE_MAP: dict[str, tuple[str, str]] = {
46
+ # (CWE, OWASP)
47
+ "B105": ("CWE-798", "A07:2021"), # hardcoded password
48
+ "B106": ("CWE-798", "A07:2021"), # hardcoded password func arg
49
+ "B107": ("CWE-798", "A07:2021"), # hardcoded password default
50
+ "B108": ("CWE-377", "A01:2021"), # hardcoded tmp dir
51
+ "B301": ("CWE-502", "A08:2021"), # pickle
52
+ "B302": ("CWE-502", "A08:2021"), # marshal
53
+ "B303": ("CWE-327", "A02:2021"), # insecure hash (MD5, SHA1)
54
+ "B304": ("CWE-327", "A02:2021"), # insecure cipher
55
+ "B305": ("CWE-327", "A02:2021"), # insecure cipher mode
56
+ "B306": ("CWE-327", "A02:2021"), # mktemp
57
+ "B307": ("CWE-78", "A03:2021"), # eval
58
+ "B308": ("CWE-95", "A03:2021"), # mark_safe
59
+ "B310": ("CWE-918", "A10:2021"), # urllib open
60
+ "B311": ("CWE-330", "A02:2021"), # random
61
+ "B312": ("CWE-295", "A07:2021"), # telnetlib
62
+ "B320": ("CWE-611", "A05:2021"), # xml parse
63
+ "B321": ("CWE-319", "A02:2021"), # FTP
64
+ "B323": ("CWE-295", "A07:2021"), # unverified SSL context
65
+ "B324": ("CWE-327", "A02:2021"), # insecure hash
66
+ "B501": ("CWE-295", "A07:2021"), # verify=False
67
+ "B502": ("CWE-327", "A02:2021"), # ssl no version
68
+ "B506": ("CWE-502", "A08:2021"), # yaml load
69
+ "B601": ("CWE-78", "A03:2021"), # paramiko shell
70
+ "B602": ("CWE-78", "A03:2021"), # subprocess shell=True
71
+ "B603": ("CWE-78", "A03:2021"), # subprocess without shell
72
+ "B604": ("CWE-78", "A03:2021"), # function call shell
73
+ "B605": ("CWE-78", "A03:2021"), # os.system
74
+ "B606": ("CWE-78", "A03:2021"), # os.popen
75
+ "B607": ("CWE-78", "A03:2021"), # partial executable path
76
+ "B608": ("CWE-89", "A03:2021"), # SQL injection
77
+ "B609": ("CWE-78", "A03:2021"), # wildcard injection
78
+ "B610": ("CWE-78", "A03:2021"), # django extra
79
+ "B611": ("CWE-78", "A03:2021"), # django raw SQL
80
+ "B701": ("CWE-94", "A03:2021"), # jinja2 autoescape
81
+ "B702": ("CWE-79", "A03:2021"), # mako template
82
+ "B703": ("CWE-79", "A03:2021"), # django mark_safe
83
+ }
84
+
85
+ _BANDIT_SEVERITY_MAP: dict[str, str] = {
86
+ "HIGH": "high",
87
+ "MEDIUM": "medium",
88
+ "LOW": "low",
89
+ }
90
+
91
+ _BANDIT_CONFIDENCE_MAP: dict[str, str] = {
92
+ "HIGH": "high",
93
+ "MEDIUM": "medium",
94
+ "LOW": "low",
95
+ }
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # 커스텀 우회 패턴
99
+ # ---------------------------------------------------------------------------
100
+ _EVASION_PATTERNS: list[dict] = [
101
+ {
102
+ "pattern": r"base64\.(b64decode|decodebytes)\s*\(",
103
+ "title": "Base64 Encoded Credential",
104
+ "cwe": "CWE-798",
105
+ "owasp": "A07:2021",
106
+ "description": "base64로 인코딩된 비밀번호/키 - 디코딩하면 평문 노출",
107
+ "confidence": "medium",
108
+ },
109
+ {
110
+ "pattern": r"os\.(?:environ|getenv)\s*\.?\s*(?:get)?\s*\([^)]+,\s*['\"][^'\"]{8,}['\"]",
111
+ "title": "Hardcoded Fallback in env var",
112
+ "cwe": "CWE-798",
113
+ "owasp": "A07:2021",
114
+ "description": "환경변수 미설정 시 하드코딩된 기본값 사용 - 프로덕션 유출 위험",
115
+ "confidence": "medium",
116
+ },
117
+ {
118
+ "pattern": r"(?:\[::-1\]|reversed\(|''.join\(|str\.join)",
119
+ "title": "String Obfuscation (potential credential hiding)",
120
+ "cwe": "CWE-798",
121
+ "owasp": "A07:2021",
122
+ "description": "문자열 역순/조합으로 크리덴셜 은닉 시도",
123
+ "confidence": "low",
124
+ },
125
+ {
126
+ "pattern": r"['\"](?:password|passwd|secret|api_key|token|auth)['\"]:\s*['\"][^'\"]+['\"]",
127
+ "title": "Credential in dict/config literal",
128
+ "cwe": "CWE-798",
129
+ "owasp": "A07:2021",
130
+ "description": "딕셔너리/설정 리터럴에 크리덴셜 하드코딩",
131
+ "confidence": "high",
132
+ },
133
+ {
134
+ "pattern": r"(?:pw|pswd|passwd|tkn|api_k|auth_t|secret_k)\s*=\s*['\"][^'\"]{6,}['\"]",
135
+ "title": "Obfuscated credential variable name",
136
+ "cwe": "CWE-798",
137
+ "owasp": "A07:2021",
138
+ "description": "약어 변수명으로 크리덴셜 은닉 (pw, tkn 등)",
139
+ "confidence": "medium",
140
+ },
141
+ {
142
+ "pattern": r"codecs\.decode\s*\([^)]+['\"]rot",
143
+ "title": "ROT13/codec-based credential obfuscation",
144
+ "cwe": "CWE-798",
145
+ "owasp": "A07:2021",
146
+ "description": "ROT13 등 간단한 인코딩으로 크리덴셜 은닉",
147
+ "confidence": "medium",
148
+ },
149
+ {
150
+ "pattern": r"(?:connect|login|authenticate|create_engine)\s*\([^)]*(?:password|passwd|secret)\s*=\s*['\"][^'\"]+['\"]",
151
+ "title": "Credential passed as function argument",
152
+ "cwe": "CWE-798",
153
+ "owasp": "A07:2021",
154
+ "description": "DB 연결/인증 함수에 크리덴셜 직접 전달",
155
+ "confidence": "high",
156
+ },
157
+ {
158
+ "pattern": r"(?:eval|exec)\s*\(\s*(?:\w+\s*\+|f['\"]|['\"].*\+)",
159
+ "title": "Dynamic code execution with string building",
160
+ "cwe": "CWE-95",
161
+ "owasp": "A03:2021",
162
+ "description": "문자열 조합 후 eval/exec - 코드 인젝션 우회 패턴",
163
+ "confidence": "high",
164
+ },
165
+ ]
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # 한국 금융권 개인정보(PII) 패턴
169
+ # ---------------------------------------------------------------------------
170
+ _PII_PATTERNS: list[dict] = [
171
+ {
172
+ "pattern": r"\d{6}\s*-?\s*[1-4]\d{6}",
173
+ "title": "주민등록번호 패턴",
174
+ "cwe": "CWE-359",
175
+ "owasp": "A01:2021",
176
+ "description": "주민등록번호(YYMMDD-GNNNNNN) 패턴 탐지 - 개인정보보호법 위반 가능",
177
+ "confidence": "high",
178
+ "severity": "high",
179
+ },
180
+ {
181
+ "pattern": r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}",
182
+ "title": "신용카드번호 패턴",
183
+ "cwe": "CWE-359",
184
+ "owasp": "A01:2021",
185
+ "description": "신용카드번호(16자리) 패턴 탐지 - 여신전문금융업법 위반 가능",
186
+ "confidence": "high",
187
+ "severity": "high",
188
+ },
189
+ {
190
+ "pattern": r"(?:\d{3,4}[\s-]?\d{2,4}[\s-]?\d{4,6}[\s-]?\d{2,3})",
191
+ "title": "은행 계좌번호 패턴",
192
+ "cwe": "CWE-359",
193
+ "owasp": "A01:2021",
194
+ "description": "은행 계좌번호 형식 탐지 (은행별 다양한 형식) - 금융실명법 관련",
195
+ "confidence": "low",
196
+ "severity": "medium",
197
+ },
198
+ {
199
+ "pattern": r"01[016789][\s-]?\d{3,4}[\s-]?\d{4}",
200
+ "title": "휴대전화번호 패턴",
201
+ "cwe": "CWE-359",
202
+ "owasp": "A01:2021",
203
+ "description": "한국 휴대전화번호(010-XXXX-XXXX) 패턴 탐지 - 개인정보보호법 위반 가능",
204
+ "confidence": "medium",
205
+ "severity": "medium",
206
+ },
207
+ {
208
+ "pattern": r"[MSms]\d{8}",
209
+ "title": "여권번호 패턴",
210
+ "cwe": "CWE-359",
211
+ "owasp": "A01:2021",
212
+ "description": "한국 여권번호(M12345678) 패턴 탐지 - 개인정보보호법 위반 가능",
213
+ "confidence": "medium",
214
+ "severity": "medium",
215
+ },
216
+ {
217
+ "pattern": r"\d{2}[\s-]?\d{2}[\s-]?\d{6}[\s-]?\d{2}",
218
+ "title": "운전면허번호 패턴",
219
+ "cwe": "CWE-359",
220
+ "owasp": "A01:2021",
221
+ "description": "운전면허번호(지역-년도-일련번호-검증) 패턴 탐지 - 개인정보보호법 위반 가능",
222
+ "confidence": "medium",
223
+ "severity": "medium",
224
+ },
225
+ {
226
+ "pattern": r"\d{10}[\s-]?\d{2}",
227
+ "title": "건강보험번호 패턴",
228
+ "cwe": "CWE-359",
229
+ "owasp": "A01:2021",
230
+ "description": "건강보험 자격득실확인서 번호(10+2자리) 패턴 탐지",
231
+ "confidence": "low",
232
+ "severity": "medium",
233
+ },
234
+ {
235
+ "pattern": r"(?:log(?:ging)?|logger)\s*[\.(]\s*(?:info|debug|warning|error|critical)\s*\([^)]*(?:주민|resident|ssn|card_?num|카드번호|계좌|account_?num|phone|전화|휴대폰)",
236
+ "title": "PII 로깅 anti-pattern",
237
+ "cwe": "CWE-532",
238
+ "owasp": "A09:2021",
239
+ "description": "개인정보가 포함된 변수를 로그에 기록 - 로그 유출 시 대량 정보 노출 위험",
240
+ "confidence": "high",
241
+ "severity": "high",
242
+ },
243
+ {
244
+ "pattern": r"requests\.(?:post|put|patch)\s*\([^)]*(?:주민|resident|ssn|card_?num|카드번호|계좌|account_?num|phone|전화|휴대폰)",
245
+ "title": "PII 외부 전송 anti-pattern",
246
+ "cwe": "CWE-319",
247
+ "owasp": "A02:2021",
248
+ "description": "개인정보를 외부 API에 평문 전송 - 전자금융거래법 위반 가능",
249
+ "confidence": "high",
250
+ "severity": "high",
251
+ },
252
+ {
253
+ "pattern": r"(?:print|sys\.stdout\.write)\s*\([^)]*(?:주민|resident|ssn|card_?num|카드번호|계좌|account_?num|phone|전화|휴대폰)",
254
+ "title": "PII 마스킹 없는 출력",
255
+ "cwe": "CWE-359",
256
+ "owasp": "A01:2021",
257
+ "description": "개인정보를 마스킹 없이 표준 출력에 노출 - 화면 캡처/공유 시 유출 위험",
258
+ "confidence": "medium",
259
+ "severity": "medium",
260
+ },
261
+ {
262
+ "pattern": r"(?:open|to_csv|to_excel|to_json)\s*\([^)]*(?:주민|resident|ssn|card_?num|카드번호|계좌|account_?num|phone|전화|휴대폰)",
263
+ "title": "PII 평문 파일 저장",
264
+ "cwe": "CWE-312",
265
+ "owasp": "A02:2021",
266
+ "description": "개인정보를 암호화 없이 파일에 저장 - 신용정보법/개인정보보호법 위반 가능",
267
+ "confidence": "high",
268
+ "severity": "high",
269
+ },
270
+ {
271
+ "pattern": r'df\s*\[\s*["\'](?:주민등록번호|주민번호|resident_id|ssn|카드번호|card_number|계좌번호|account_number)["\']',
272
+ "title": "DataFrame PII 미암호화",
273
+ "cwe": "CWE-311",
274
+ "owasp": "A02:2021",
275
+ "description": "DataFrame에 개인정보 컬럼이 평문으로 존재 - 암호화/마스킹 처리 필요",
276
+ "confidence": "medium",
277
+ "severity": "medium",
278
+ },
279
+ ]
280
+
281
+ _SCANNER_TIMEOUT = 120
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # 스캐너 래퍼 함수
286
+ # ---------------------------------------------------------------------------
287
+
288
+
289
+ def run_bandit(
290
+ target_path: str,
291
+ workspace_root: str,
292
+ severity_filter: str = "low",
293
+ ) -> list[SecurityFinding]:
294
+ """bandit SAST 스캐너 실행."""
295
+ severity_levels = {"low": "l", "medium": "m", "high": "h", "critical": "h"}
296
+ sev_flag = severity_levels.get(severity_filter, "l")
297
+
298
+ cmd = [
299
+ "bandit",
300
+ "-r",
301
+ target_path,
302
+ "-f",
303
+ "json",
304
+ "--exit-zero",
305
+ f"-{'l' * ('lmh'.index(sev_flag) + 1)}",
306
+ ]
307
+
308
+ try:
309
+ result = subprocess.run(
310
+ cmd,
311
+ capture_output=True,
312
+ text=True,
313
+ cwd=workspace_root,
314
+ timeout=_SCANNER_TIMEOUT,
315
+ )
316
+ except FileNotFoundError:
317
+ return []
318
+ except subprocess.TimeoutExpired:
319
+ return []
320
+
321
+ findings: list[SecurityFinding] = []
322
+ try:
323
+ data = json.loads(result.stdout)
324
+ except (json.JSONDecodeError, TypeError):
325
+ return []
326
+
327
+ for issue in data.get("results", []):
328
+ test_id = issue.get("test_id", "")
329
+ cwe, owasp = _BANDIT_CWE_MAP.get(test_id, (None, None))
330
+ sev = _BANDIT_SEVERITY_MAP.get(issue.get("issue_severity", ""), "medium")
331
+ conf = _BANDIT_CONFIDENCE_MAP.get(issue.get("issue_confidence", ""), "medium")
332
+
333
+ findings.append(
334
+ SecurityFinding(
335
+ scanner="bandit",
336
+ severity=sev,
337
+ category="sast",
338
+ title=issue.get("test_name", test_id),
339
+ description=issue.get("issue_text", "")[:200],
340
+ file=issue.get("filename"),
341
+ line=issue.get("line_number"),
342
+ cwe_id=cwe,
343
+ owasp_id=owasp,
344
+ confidence=conf,
345
+ fix_suggestion=issue.get("more_info"),
346
+ raw_id=test_id,
347
+ )
348
+ )
349
+
350
+ return findings
351
+
352
+
353
+ def run_semgrep(
354
+ target_path: str,
355
+ workspace_root: str,
356
+ ruleset: str = "p/owasp-top-ten",
357
+ ) -> list[SecurityFinding]:
358
+ """semgrep SAST 스캐너 실행."""
359
+ cmd = [
360
+ "semgrep",
361
+ "--config",
362
+ ruleset,
363
+ "--json",
364
+ "--quiet",
365
+ target_path,
366
+ ]
367
+
368
+ try:
369
+ result = subprocess.run(
370
+ cmd,
371
+ capture_output=True,
372
+ text=True,
373
+ cwd=workspace_root,
374
+ timeout=_SCANNER_TIMEOUT,
375
+ )
376
+ except FileNotFoundError:
377
+ return []
378
+ except subprocess.TimeoutExpired:
379
+ return []
380
+
381
+ findings: list[SecurityFinding] = []
382
+ try:
383
+ data = json.loads(result.stdout)
384
+ except (json.JSONDecodeError, TypeError):
385
+ return []
386
+
387
+ _semgrep_severity_map = {
388
+ "ERROR": "high",
389
+ "WARNING": "medium",
390
+ "INFO": "low",
391
+ }
392
+
393
+ for res in data.get("results", []):
394
+ extra = res.get("extra", {})
395
+ metadata = extra.get("metadata", {})
396
+
397
+ severity = _semgrep_severity_map.get(extra.get("severity", ""), "medium")
398
+
399
+ cwe_ids = metadata.get("cwe", [])
400
+ cwe_id = cwe_ids[0] if cwe_ids else None
401
+ if cwe_id and not cwe_id.startswith("CWE-"):
402
+ cwe_id = f"CWE-{cwe_id}"
403
+
404
+ owasp_ids = metadata.get("owasp", [])
405
+ owasp_id = owasp_ids[0] if owasp_ids else None
406
+
407
+ findings.append(
408
+ SecurityFinding(
409
+ scanner="semgrep",
410
+ severity=severity,
411
+ category="sast",
412
+ title=res.get("check_id", "unknown"),
413
+ description=extra.get("message", "")[:200],
414
+ file=res.get("path"),
415
+ line=res.get("start", {}).get("line"),
416
+ cwe_id=cwe_id,
417
+ owasp_id=owasp_id,
418
+ confidence=metadata.get("confidence", "medium").lower(),
419
+ fix_suggestion=extra.get("fix"),
420
+ raw_id=res.get("check_id"),
421
+ )
422
+ )
423
+
424
+ return findings
425
+
426
+
427
+ def run_pip_audit(workspace_root: str) -> list[SecurityFinding]:
428
+ """pip-audit 의존성 취약점 스캐너 실행."""
429
+ cmd = ["pip-audit", "--format", "json", "--output", "-"]
430
+
431
+ try:
432
+ result = subprocess.run(
433
+ cmd,
434
+ capture_output=True,
435
+ text=True,
436
+ cwd=workspace_root,
437
+ timeout=_SCANNER_TIMEOUT,
438
+ )
439
+ except FileNotFoundError:
440
+ return []
441
+ except subprocess.TimeoutExpired:
442
+ return []
443
+
444
+ findings: list[SecurityFinding] = []
445
+ try:
446
+ data = json.loads(result.stdout)
447
+ except (json.JSONDecodeError, TypeError):
448
+ return []
449
+
450
+ deps = data if isinstance(data, list) else data.get("dependencies", [])
451
+
452
+ for dep in deps:
453
+ for vuln in dep.get("vulns", []):
454
+ vuln_id = vuln.get("id", "")
455
+ desc = vuln.get("description", "")[:200]
456
+ fix_ver = vuln.get("fix_versions", [])
457
+ fix_str = f"업데이트: {', '.join(fix_ver)}" if fix_ver else None
458
+
459
+ findings.append(
460
+ SecurityFinding(
461
+ scanner="pip-audit",
462
+ severity="high",
463
+ category="dependency",
464
+ title=f"{dep.get('name', 'unknown')} - {vuln_id}",
465
+ description=desc or f"알려진 취약점: {vuln_id}",
466
+ cwe_id=None,
467
+ owasp_id="A06:2021",
468
+ confidence="high",
469
+ fix_suggestion=fix_str,
470
+ raw_id=vuln_id,
471
+ )
472
+ )
473
+
474
+ return findings
475
+
476
+
477
+ def run_detect_secrets(
478
+ target_path: str,
479
+ workspace_root: str,
480
+ ) -> list[SecurityFinding]:
481
+ """detect-secrets 시크릿 스캐너 실행."""
482
+ cmd = ["detect-secrets", "scan", target_path]
483
+
484
+ try:
485
+ result = subprocess.run(
486
+ cmd,
487
+ capture_output=True,
488
+ text=True,
489
+ cwd=workspace_root,
490
+ timeout=_SCANNER_TIMEOUT,
491
+ )
492
+ except FileNotFoundError:
493
+ return []
494
+ except subprocess.TimeoutExpired:
495
+ return []
496
+
497
+ findings: list[SecurityFinding] = []
498
+ try:
499
+ data = json.loads(result.stdout)
500
+ except (json.JSONDecodeError, TypeError):
501
+ return []
502
+
503
+ for filepath, secrets_list in data.get("results", {}).items():
504
+ for secret in secrets_list:
505
+ findings.append(
506
+ SecurityFinding(
507
+ scanner="detect-secrets",
508
+ severity="high",
509
+ category="secret",
510
+ title=f"Secret detected: {secret.get('type', 'unknown')}",
511
+ description=f"시크릿 탐지 ({secret.get('type', '')}) - 라인 {secret.get('line_number', '?')}",
512
+ file=filepath,
513
+ line=secret.get("line_number"),
514
+ cwe_id="CWE-798",
515
+ owasp_id="A07:2021",
516
+ confidence="high",
517
+ raw_id=secret.get("type"),
518
+ )
519
+ )
520
+
521
+ return findings
522
+
523
+
524
+ _SCAN_TIMEOUT = 10 # seconds per custom scanner
525
+ _SCAN_MAX_FINDINGS = 10_000
526
+ _SCAN_MAX_FILE_SIZE = 1_000_000
527
+ _SCAN_MAX_LINE_SIZE = 10_000
528
+
529
+
530
+ class PatternScanResult(list):
531
+ """List-compatible findings plus completion status for the tool response."""
532
+
533
+ def __init__(self):
534
+ super().__init__()
535
+ self.truncated = False
536
+ self.truncation_reason = None
537
+
538
+ def mark_partial(self, reason: str) -> None:
539
+ self.truncated = True
540
+ self.truncation_reason = reason
541
+
542
+
543
+ def _scan_files(target, suffixes, deadline, findings):
544
+ if time.monotonic() >= deadline:
545
+ findings.mark_partial("time_budget")
546
+ return
547
+ try:
548
+ if target.is_file():
549
+ yield target
550
+ return
551
+ if not target.is_dir():
552
+ findings.mark_partial("invalid_target")
553
+ return
554
+ except OSError:
555
+ findings.mark_partial("unreadable_target")
556
+ return
557
+
558
+ def on_error(error):
559
+ findings.mark_partial("unreadable_directory")
560
+
561
+ for directory, dirs, names in os.walk(target, onerror=on_error):
562
+ if time.monotonic() >= deadline:
563
+ findings.mark_partial("time_budget")
564
+ return
565
+ base = Path(directory)
566
+ dirs[:] = [
567
+ name
568
+ for name in dirs
569
+ if name not in SEARCH_EXCLUDED_DIRS
570
+ and (base / name).relative_to(target).parts not in EXCLUDED_RELATIVE_DIRS
571
+ ]
572
+ for name in names:
573
+ if time.monotonic() >= deadline:
574
+ findings.mark_partial("time_budget")
575
+ return
576
+ path = base / name
577
+ if path.suffix in suffixes:
578
+ yield path
579
+
580
+
581
+ def _run_pattern_scan(target_path, workspace_root, patterns, suffixes, category):
582
+ findings = PatternScanResult()
583
+ target = Path(target_path)
584
+ root = Path(workspace_root)
585
+ deadline = time.monotonic() + _SCAN_TIMEOUT
586
+ for fpath in _scan_files(target, suffixes, deadline, findings):
587
+ try:
588
+ if time.monotonic() >= deadline:
589
+ findings.mark_partial("time_budget")
590
+ break
591
+ if fpath.stat().st_size > _SCAN_MAX_FILE_SIZE:
592
+ findings.mark_partial("file_size_limit")
593
+ continue
594
+ # Do not follow a file symlink out of the explicitly requested scope.
595
+ fpath.resolve().relative_to(
596
+ target.resolve() if target.is_dir() else target.parent.resolve()
597
+ )
598
+ try:
599
+ rel = str(fpath.relative_to(root))
600
+ except ValueError:
601
+ rel = str(fpath)
602
+ with fpath.open(errors="ignore") as source:
603
+ for number, line in enumerate(source, 1):
604
+ if time.monotonic() >= deadline:
605
+ findings.mark_partial("time_budget")
606
+ return findings
607
+ if len(line) > _SCAN_MAX_LINE_SIZE:
608
+ findings.mark_partial("line_size_limit")
609
+ continue
610
+ for pattern in patterns:
611
+ if time.monotonic() >= deadline:
612
+ findings.mark_partial("time_budget")
613
+ return findings
614
+ if re.search(pattern["pattern"], line):
615
+ if len(findings) >= _SCAN_MAX_FINDINGS:
616
+ findings.mark_partial("result_limit")
617
+ return findings
618
+ findings.append(
619
+ SecurityFinding(
620
+ scanner="custom",
621
+ severity=pattern.get("severity", "high")
622
+ if category == "pii"
623
+ else "medium",
624
+ category=category,
625
+ title=pattern["title"],
626
+ description=pattern["description"][:200],
627
+ file=rel,
628
+ line=number,
629
+ cwe_id=pattern.get("cwe"),
630
+ owasp_id=pattern.get("owasp"),
631
+ confidence=pattern.get("confidence", "medium"),
632
+ )
633
+ )
634
+ except (OSError, ValueError):
635
+ findings.mark_partial("unreadable_file")
636
+ if time.monotonic() >= deadline:
637
+ findings.mark_partial("time_budget")
638
+ return findings
639
+
640
+
641
+ def run_custom_pattern_scan(target_path: str, workspace_root: str) -> PatternScanResult:
642
+ """Custom evasion patterns with pruning and cooperative scan budgets."""
643
+ return _run_pattern_scan(
644
+ target_path, workspace_root, _EVASION_PATTERNS, {".py"}, "sast"
645
+ )
646
+
647
+
648
+ def run_pii_scan(target_path: str, workspace_root: str) -> PatternScanResult:
649
+ """PII patterns with pruning and cooperative scan budgets."""
650
+ return _run_pattern_scan(
651
+ target_path, workspace_root, _PII_PATTERNS, {".py", ".ipynb"}, "pii"
652
+ )
653
+
654
+
655
+ def finding_to_dict(f: SecurityFinding) -> dict:
656
+ """SecurityFinding을 직렬화 가능한 dict로 변환."""
657
+ return asdict(f)