pal-code 0.1.0__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 (282) hide show
  1. pal/__init__.py +29 -0
  2. pal/__main__.py +3 -0
  3. pal/access_manager/__init__.py +16 -0
  4. pal/access_manager/components/__init__.py +14 -0
  5. pal/access_manager/components/access_control.py +41 -0
  6. pal/access_manager/components/base.py +39 -0
  7. pal/access_manager/components/intervention.py +31 -0
  8. pal/access_manager/components/privilege_registry.py +30 -0
  9. pal/access_manager/pipeline.py +68 -0
  10. pal/access_manager/utils/__init__.py +2 -0
  11. pal/agent_constraints/__init__.py +3 -0
  12. pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
  13. pal/agent_constraints/components/__init__.py +3 -0
  14. pal/agent_constraints/components/constraints.py +22 -0
  15. pal/agent_constraints/pipeline.py +22 -0
  16. pal/agent_constraints/utils/__init__.py +2 -0
  17. pal/agents/__init__.py +12 -0
  18. pal/agents/coding.py +14 -0
  19. pal/agents/docs.py +15 -0
  20. pal/chat/__init__.py +4 -0
  21. pal/chat/demo.py +231 -0
  22. pal/chat/providers.py +23 -0
  23. pal/chat/service.py +591 -0
  24. pal/chat/services/__init__.py +26 -0
  25. pal/chat/services/base.py +142 -0
  26. pal/chat/services/data.py +142 -0
  27. pal/chat/services/media.py +123 -0
  28. pal/chat/services/web.py +60 -0
  29. pal/chat/sessions.py +108 -0
  30. pal/chat/tools.py +446 -0
  31. pal/chat/uploads.py +81 -0
  32. pal/cli.py +502 -0
  33. pal/compression/__init__.py +12 -0
  34. pal/compression/components/__init__.py +64 -0
  35. pal/compression/components/adverbs.py +20 -0
  36. pal/compression/components/hedges.py +54 -0
  37. pal/compression/components/interjections.py +20 -0
  38. pal/compression/components/minifier.py +56 -0
  39. pal/compression/components/output_compressor.py +13 -0
  40. pal/compression/components/pleonasms.py +54 -0
  41. pal/compression/components/punctuations.py +36 -0
  42. pal/compression/components/stopwords.py +62 -0
  43. pal/compression/history.py +346 -0
  44. pal/compression/pipeline.py +58 -0
  45. pal/compression/utils/__init__.py +30 -0
  46. pal/compression/utils/negations.py +18 -0
  47. pal/compression/utils/nltk_setup.py +20 -0
  48. pal/compression/utils/pronouns.py +27 -0
  49. pal/compression/utils/proselint_setup.py +9 -0
  50. pal/compression/utils/spacy_setup.py +22 -0
  51. pal/config/default.yaml +163 -0
  52. pal/config.py +504 -0
  53. pal/context_manager/__init__.py +7 -0
  54. pal/context_manager/chat_context.py +117 -0
  55. pal/context_manager/components/__init__.py +5 -0
  56. pal/context_manager/components/assembler.py +75 -0
  57. pal/context_manager/components/context_cache.py +45 -0
  58. pal/context_manager/components/kv_bridge.py +16 -0
  59. pal/context_manager/components/pager.py +48 -0
  60. pal/context_manager/components/simple_context_manager.py +56 -0
  61. pal/context_manager/components/turns.py +88 -0
  62. pal/context_manager/pipeline.py +27 -0
  63. pal/context_manager/utils/__init__.py +18 -0
  64. pal/context_manager/utils/hashing.py +5 -0
  65. pal/context_manager/utils/relevance.py +59 -0
  66. pal/context_manager/utils/storage.py +37 -0
  67. pal/documents/__init__.py +4 -0
  68. pal/documents/components/__init__.py +0 -0
  69. pal/documents/components/chunker.py +79 -0
  70. pal/documents/components/index.py +155 -0
  71. pal/documents/utils/__init__.py +1 -0
  72. pal/errors.py +40 -0
  73. pal/input_converters/__init__.py +8 -0
  74. pal/input_converters/components/__init__.py +0 -0
  75. pal/input_converters/components/base.py +38 -0
  76. pal/input_converters/components/pandoc_converter.py +87 -0
  77. pal/input_converters/components/pdf_converter.py +46 -0
  78. pal/input_converters/components/pptx_converter.py +52 -0
  79. pal/input_converters/components/xlsx_converter.py +38 -0
  80. pal/input_converters/pipeline.py +72 -0
  81. pal/input_converters/utils/__init__.py +1 -0
  82. pal/input_converters/workspace.py +56 -0
  83. pal/input_savers/INPUT_RULES.md +2 -0
  84. pal/input_savers/__init__.py +5 -0
  85. pal/input_savers/components/__init__.py +0 -0
  86. pal/input_savers/components/base.py +40 -0
  87. pal/input_savers/components/methods.py +41 -0
  88. pal/input_savers/components/verifier.py +39 -0
  89. pal/input_savers/pipeline.py +154 -0
  90. pal/input_savers/utils/__init__.py +0 -0
  91. pal/input_savers/utils/guards.py +94 -0
  92. pal/knowledge_graph/__init__.py +10 -0
  93. pal/knowledge_graph/components/__init__.py +0 -0
  94. pal/knowledge_graph/components/base.py +50 -0
  95. pal/knowledge_graph/components/builder.py +54 -0
  96. pal/knowledge_graph/components/retriever.py +100 -0
  97. pal/knowledge_graph/components/workspace.py +116 -0
  98. pal/knowledge_graph/pipeline.py +215 -0
  99. pal/knowledge_graph/utils/__init__.py +1 -0
  100. pal/latency_tracker/__init__.py +3 -0
  101. pal/latency_tracker/pipeline.py +86 -0
  102. pal/llm_scheduler/__init__.py +11 -0
  103. pal/llm_scheduler/api.py +137 -0
  104. pal/llm_scheduler/code.py +28 -0
  105. pal/llm_scheduler/core.py +92 -0
  106. pal/llm_scheduler/local.py +215 -0
  107. pal/llm_scheduler/pipeline.py +42 -0
  108. pal/llm_scheduler/types.py +92 -0
  109. pal/llm_wrapper/__init__.py +34 -0
  110. pal/llm_wrapper/adapter.py +316 -0
  111. pal/llm_wrapper/components/__init__.py +23 -0
  112. pal/llm_wrapper/components/adapter_types.py +90 -0
  113. pal/llm_wrapper/components/base.py +195 -0
  114. pal/llm_wrapper/components/features.py +104 -0
  115. pal/llm_wrapper/components/litellm_wrapper.py +215 -0
  116. pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
  117. pal/llm_wrapper/modes.py +73 -0
  118. pal/llm_wrapper/pipeline.py +61 -0
  119. pal/llm_wrapper/utils/__init__.py +4 -0
  120. pal/llm_wrapper/utils/registry.py +49 -0
  121. pal/llm_wrapper/utils/token_counting.py +10 -0
  122. pal/local_llm/__init__.py +5 -0
  123. pal/local_llm/components/__init__.py +0 -0
  124. pal/local_llm/components/huggingface_local.py +217 -0
  125. pal/local_llm/components/local_llm.py +60 -0
  126. pal/logging_setup.py +18 -0
  127. pal/memory/__init__.py +7 -0
  128. pal/memory/components/__init__.py +6 -0
  129. pal/memory/components/lru_k.py +56 -0
  130. pal/memory/components/manager.py +98 -0
  131. pal/memory/components/note.py +54 -0
  132. pal/memory/components/storage_manager.py +60 -0
  133. pal/memory/pipeline.py +122 -0
  134. pal/memory/utils/__init__.py +3 -0
  135. pal/memory/utils/keyword_overlap.py +3 -0
  136. pal/memory_tracker/__init__.py +3 -0
  137. pal/memory_tracker/components/__init__.py +0 -0
  138. pal/memory_tracker/components/process.py +56 -0
  139. pal/memory_tracker/pipeline.py +44 -0
  140. pal/metrics/__init__.py +4 -0
  141. pal/metrics/components/__init__.py +4 -0
  142. pal/metrics/components/collector.py +140 -0
  143. pal/metrics/components/exporter.py +28 -0
  144. pal/metrics/pipeline.py +49 -0
  145. pal/metrics/utils/__init__.py +2 -0
  146. pal/notes_manager/__init__.py +5 -0
  147. pal/notes_manager/components/__init__.py +0 -0
  148. pal/notes_manager/components/privacy.py +14 -0
  149. pal/notes_manager/components/recall.py +31 -0
  150. pal/notes_manager/components/tidy.py +28 -0
  151. pal/notes_manager/pipeline.py +138 -0
  152. pal/orchestrator/__init__.py +26 -0
  153. pal/orchestrator/components/__init__.py +26 -0
  154. pal/orchestrator/components/agent.py +105 -0
  155. pal/orchestrator/components/cgroup_manager.py +209 -0
  156. pal/orchestrator/components/context_interrupt.py +220 -0
  157. pal/orchestrator/components/limits.py +9 -0
  158. pal/orchestrator/components/scheduler.py +69 -0
  159. pal/orchestrator/components/syscall_scheduler.py +200 -0
  160. pal/orchestrator/pipeline.py +228 -0
  161. pal/orchestrator/utils/__init__.py +4 -0
  162. pal/orchestrator/utils/device.py +164 -0
  163. pal/orchestrator/utils/protocol.py +32 -0
  164. pal/os_kernel/__init__.py +5 -0
  165. pal/os_kernel/filesystem.py +80 -0
  166. pal/os_kernel/network.py +146 -0
  167. pal/os_kernel/pipeline.py +175 -0
  168. pal/os_kernel/sysinfo.py +36 -0
  169. pal/paths.py +59 -0
  170. pal/pipeline.py +177 -0
  171. pal/py.typed +0 -0
  172. pal/redact.py +19 -0
  173. pal/sdk/__init__.py +33 -0
  174. pal/sdk/adapters.py +43 -0
  175. pal/sdk/agent.py +99 -0
  176. pal/sdk/components/__init__.py +14 -0
  177. pal/sdk/components/autogen_adapter.py +65 -0
  178. pal/sdk/components/autogen_client.py +98 -0
  179. pal/sdk/components/framework_adapter_base.py +69 -0
  180. pal/sdk/components/metagpt_adapter.py +46 -0
  181. pal/sdk/components/open_interpreter_adapter.py +47 -0
  182. pal/sdk/components/tool_catalog.py +44 -0
  183. pal/sdk/os_api.py +84 -0
  184. pal/sdk/patterns.py +129 -0
  185. pal/sdk/pipeline.py +240 -0
  186. pal/sdk/queries.py +68 -0
  187. pal/sdk/savers.py +88 -0
  188. pal/sdk/utils/__init__.py +2 -0
  189. pal/storage_manager/__init__.py +17 -0
  190. pal/storage_manager/components/__init__.py +19 -0
  191. pal/storage_manager/components/base.py +50 -0
  192. pal/storage_manager/components/local_file_backend.py +84 -0
  193. pal/storage_manager/components/storage_queue.py +91 -0
  194. pal/storage_manager/components/vector_store_backend.py +122 -0
  195. pal/storage_manager/pipeline.py +153 -0
  196. pal/storage_manager/utils/__init__.py +4 -0
  197. pal/storage_manager/utils/memory_adapter.py +60 -0
  198. pal/storage_manager/utils/sdk.py +93 -0
  199. pal/studio/__init__.py +0 -0
  200. pal/studio/builder.py +166 -0
  201. pal/studio/catalog.py +90 -0
  202. pal/studio/checks.py +129 -0
  203. pal/studio/clients.py +6 -0
  204. pal/studio/editor.py +137 -0
  205. pal/studio/files.py +6 -0
  206. pal/studio/models.py +6 -0
  207. pal/studio/plan.py +149 -0
  208. pal/studio/planner.py +205 -0
  209. pal/studio/policy.py +69 -0
  210. pal/studio/routes.py +254 -0
  211. pal/studio/runs.py +210 -0
  212. pal/task_router/__init__.py +11 -0
  213. pal/task_router/components/__init__.py +0 -0
  214. pal/task_router/components/answerer.py +37 -0
  215. pal/task_router/components/base.py +34 -0
  216. pal/task_router/components/classifier.py +40 -0
  217. pal/task_router/components/gates.py +23 -0
  218. pal/task_router/pipeline.py +61 -0
  219. pal/task_router/utils/__init__.py +1 -0
  220. pal/terminal/__init__.py +1 -0
  221. pal/terminal/backend.py +141 -0
  222. pal/terminal/bench.py +173 -0
  223. pal/terminal/cli.py +826 -0
  224. pal/terminal/clients.py +170 -0
  225. pal/terminal/commands.py +401 -0
  226. pal/terminal/decide.py +242 -0
  227. pal/terminal/files.py +55 -0
  228. pal/terminal/flow_cli.py +439 -0
  229. pal/terminal/hardware.py +199 -0
  230. pal/terminal/history.py +88 -0
  231. pal/terminal/keys.py +83 -0
  232. pal/terminal/local_models.py +261 -0
  233. pal/terminal/logo.py +117 -0
  234. pal/terminal/models.py +99 -0
  235. pal/terminal/policy.py +54 -0
  236. pal/terminal/project.py +413 -0
  237. pal/terminal/review.py +65 -0
  238. pal/terminal/security.py +239 -0
  239. pal/terminal/session.py +503 -0
  240. pal/terminal/setup.py +354 -0
  241. pal/terminal/syntax.py +98 -0
  242. pal/terminal/theme.py +37 -0
  243. pal/terminal/ui.py +647 -0
  244. pal/terminal/voice.py +137 -0
  245. pal/terminal/workflow.py +624 -0
  246. pal/token_tracker/__init__.py +3 -0
  247. pal/token_tracker/pipeline.py +54 -0
  248. pal/tokens.py +21 -0
  249. pal/tool_catalog.py +107 -0
  250. pal/tool_manager/__init__.py +17 -0
  251. pal/tool_manager/builtin.py +82 -0
  252. pal/tool_manager/components/__init__.py +19 -0
  253. pal/tool_manager/components/base.py +53 -0
  254. pal/tool_manager/components/conflict_resolver.py +27 -0
  255. pal/tool_manager/components/param_validator.py +41 -0
  256. pal/tool_manager/components/tool_call_queue.py +111 -0
  257. pal/tool_manager/components/tool_registry.py +60 -0
  258. pal/tool_manager/pipeline.py +80 -0
  259. pal/tool_manager/utils/__init__.py +3 -0
  260. pal/web/__init__.py +3 -0
  261. pal/web/app.py +276 -0
  262. pal/web/static/editor.js +219 -0
  263. pal/web/static/fonts/LICENSE-Inter.txt +3 -0
  264. pal/web/static/fonts/inter-400.woff2 +0 -0
  265. pal/web/static/fonts/inter-500.woff2 +0 -0
  266. pal/web/static/fonts/inter-600.woff2 +0 -0
  267. pal/web/static/fonts/inter-700.woff2 +0 -0
  268. pal/web/static/inter.css +5 -0
  269. pal/web/static/pal-logo-256.png +0 -0
  270. pal/web/static/pal-logo-32.png +0 -0
  271. pal/web/static/pal-logo-64.png +0 -0
  272. pal/web/static/studio.css +306 -0
  273. pal/web/static/studio.js +533 -0
  274. pal/web/static/voice.js +76 -0
  275. pal/web/templates/index.html +368 -0
  276. pal/web/templates/studio.html +120 -0
  277. pal_code-0.1.0.dist-info/METADATA +379 -0
  278. pal_code-0.1.0.dist-info/RECORD +282 -0
  279. pal_code-0.1.0.dist-info/WHEEL +5 -0
  280. pal_code-0.1.0.dist-info/entry_points.txt +2 -0
  281. pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. pal_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,239 @@
1
+ """Pal's security layer. The checks follow the vibe-security checklist (Chris Raroque, MIT) where it applies to a coding CLI:
2
+
3
+ - keys never leave the machine: secret files are never sent to a model, key-shaped strings are hidden first (`guard_outgoing`);
4
+ - the model's output is untrusted: code it proposes is checked before anything is written (`review_code`), and text it prints
5
+ cannot drive the terminal (`clean_terminal`);
6
+ - the project can be audited on demand (`audit_folder`): secrets in files, `.env` outside `.gitignore`, and the usual
7
+ mistakes AI-written code makes (client-side keys, eval, shell and SQL built from strings, unsafe HTML, trusting a price
8
+ or role sent by the browser, open CORS, debug left on, TLS checks off).
9
+
10
+ Everything is pattern matching: it finds the common cases quickly and cannot prove code is safe. Findings say so.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import fnmatch
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ from dataclasses import dataclass
19
+ from pathlib import Path, PurePosixPath
20
+
21
+ SEVERITIES = ("critical", "high", "medium", "low")
22
+
23
+ # -- secrets ---------------------------------------------------------------------------------------------------------------
24
+ SECRET_PATTERNS: list[tuple[str, re.Pattern]] = [
25
+ ("private key", re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----")),
26
+ ("Stripe secret key", re.compile(r"\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}")),
27
+ ("Anthropic key", re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{20,}")),
28
+ ("OpenAI-style key", re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}")),
29
+ ("AWS access key", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
30
+ ("GitHub token", re.compile(r"\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})")),
31
+ ("GitLab token", re.compile(r"\bglpat-[A-Za-z0-9_\-]{20,}")),
32
+ ("Slack token", re.compile(r"\bxox[baprs]-[A-Za-z0-9\-]{10,}")),
33
+ ("Google API key", re.compile(r"\bAIza[0-9A-Za-z_\-]{35}\b")),
34
+ ("password in a connection string", re.compile(r"\b[a-z][a-z0-9+.\-]{1,20}://[^\s:/@'\"]+:[^\s@/'\"]{3,}@[^\s'\"]+", re.I)),
35
+ ("hardcoded secret", re.compile(r"(?i)\b[\w.\-]*(?:secret|password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|service[_-]?role)[\w\-]*"
36
+ r"\s*[:=]\s*[\"'](?![\"'])(?!\s*(?:your|xxx|changeme|example|placeholder|<|\$\{|process\.env|os\.environ))[^\"'\s]{8,}[\"']")),
37
+ ]
38
+ SECRET_FILE_GLOBS = (".env", ".env.*", "*.env", "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "*.pem", "*.key", "*.p12", "*.pfx", "*.keystore", "*.jks",
39
+ "credentials.json", "service-account*.json", "*serviceaccount*.json", ".npmrc", ".pypirc", ".netrc", "secrets.*", ".git-credentials")
40
+ PLACEHOLDER_FILES = (".env.example", ".env.sample", ".env.template")
41
+
42
+
43
+ def is_secret_file(path: str) -> bool:
44
+ """A file that holds keys by its very name: never sent to a model, never written by one."""
45
+ name = PurePosixPath(path.replace("\\", "/")).name.lower()
46
+ if name in PLACEHOLDER_FILES:
47
+ return False
48
+ return any(fnmatch.fnmatch(name, g) for g in SECRET_FILE_GLOBS)
49
+
50
+
51
+ def find_secrets(text: str) -> list[tuple[int, str]]:
52
+ """[(line number, what it looks like)] for every key-shaped string."""
53
+ out = []
54
+ for i, line in enumerate(text.splitlines(), 1):
55
+ for kind, rx in SECRET_PATTERNS:
56
+ if rx.search(line):
57
+ out.append((i, kind))
58
+ break
59
+ return out
60
+
61
+
62
+ def redact_secrets(text: str) -> tuple[str, int]:
63
+ """The text with every key-shaped string replaced by [hidden secret], and how many were hidden."""
64
+ count = 0
65
+
66
+ def hide(m: re.Match) -> str:
67
+ nonlocal count
68
+ count += 1
69
+ return "[hidden secret]"
70
+ for kind, rx in SECRET_PATTERNS:
71
+ if kind == "private key":
72
+ text, n = re.subn(r"-----BEGIN [A-Z ]*PRIVATE KEY[A-Z ]*-----.*?(?:-----END [A-Z ]*PRIVATE KEY[A-Z ]*-----|\Z)", "[hidden private key]", text, flags=re.S)
73
+ count += n
74
+ else:
75
+ text = rx.sub(hide, text)
76
+ return text, count
77
+
78
+
79
+ def guard_outgoing(name: str, text: str) -> tuple[str | None, str | None]:
80
+ """(the text to send, or None to send nothing; a warning for the person, or None)."""
81
+ if is_secret_file(name):
82
+ return None, f"@{name}: not sent: it looks like a secrets file (keys stay on this computer)"
83
+ clean, n = redact_secrets(text)
84
+ return clean, (f"@{name}: {n} secret{'s' if n != 1 else ''} hidden before sending" if n else None)
85
+
86
+
87
+ # -- terminal safety -------------------------------------------------------------------------------------------------------
88
+ _ANSI = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[PX^_][^\x1b]*\x1b\\|\x1b.")
89
+ _CONTROL = re.compile(r"[\x00-\x08\x0b-\x1f\x7f\x80-\x9f]")
90
+
91
+
92
+ def clean_terminal(text: str) -> str:
93
+ """Text a model wrote, made safe to print: no escape sequences (colours, cursor moves, window titles, links, clipboard writes)
94
+ and no control characters, so a reply or a file cannot rewrite what the screen shows or act on the terminal."""
95
+ return _CONTROL.sub("", _ANSI.sub("", text.replace("\r\n", "\n")))
96
+
97
+
98
+ # -- reviewing code ----------------------------------------------------------------------------------------------------------
99
+ @dataclass
100
+ class Finding:
101
+ severity: str
102
+ path: str
103
+ line: int
104
+ title: str
105
+ why: str
106
+
107
+
108
+ _CLIENT = (".js", ".jsx", ".ts", ".tsx", ".mjs", ".vue", ".svelte", ".html", ".htm")
109
+ _PY = (".py",)
110
+ RULES: list[tuple[str, str, re.Pattern, tuple[str, ...], str]] = [
111
+ # severity, title, pattern, file kinds ('' = any), what an attacker could do
112
+ ("critical", "Secret key in a client-side variable", re.compile(r"\b(?:NEXT_PUBLIC|VITE|EXPO_PUBLIC|REACT_APP)_\w*(?:SECRET|PRIVATE|SERVICE|PASSWORD|SK_|TOKEN)\w*"), (),
113
+ "these prefixes are copied into the browser or app bundle, so anyone can read the key and use it at your expense"),
114
+ ("critical", "Supabase service_role key in front-end code", re.compile(r"service_role", re.I), _CLIENT,
115
+ "the service_role key bypasses all row-level security: anyone who opens the page can read or delete every row"),
116
+ ("high", "AI or payment key used directly in front-end code", re.compile(r"(?:api\.openai\.com|api\.anthropic\.com|generativelanguage\.googleapis\.com|api\.stripe\.com)"), _CLIENT,
117
+ "calling the provider from the browser exposes the key; call it from your server instead"),
118
+ ("high", "eval of dynamic code", re.compile(r"(?<![\w.])(?:eval|exec)\s*\((?!\s*[\"')])|new\s+Function\s*\("), _PY + _CLIENT,
119
+ "if any part of the string comes from a user or a model, they can run their own code"),
120
+ ("high", "Shell command built from a string", re.compile(r"shell\s*=\s*True|os\.system\s*\(|os\.popen\s*\(|child_process[\s\S]{0,40}\bexec(?:Sync)?\s*\(\s*[`'\"].*?(?:\$\{|\"\s*\+|'\s*\+)"), (),
121
+ "a value with ; or && in it runs extra commands on your machine or server"),
122
+ ("high", "SQL built from strings", re.compile(r"(?i)(?:execute|query|raw|exec)\s*\(\s*(?:f[\"']|[\"'][^\"']*(?:select|insert|update|delete)\b[^\"']*[\"']\s*(?:%|\+|\.format))"
123
+ r"|`\s*(?:select|insert|update|delete)\b[^`]*\$\{"), (),
124
+ "SQL injection: a crafted value can read, change or delete any data; use parameters (?, %s, $1) instead"),
125
+ ("high", "HTML inserted from a variable", re.compile(r"\.innerHTML\s*[+]?=\s*(?![\"'`]\s*[;<]?[\"'`]?\s*;)|dangerouslySetInnerHTML|document\.write\s*\(|v-html\s*="), _CLIENT,
126
+ "cross-site scripting: if the text comes from a user or a model, it can run scripts in your users' browsers"),
127
+ ("high", "Price, amount or role taken from the request", re.compile(r"(?i)(?:req\.body|request\.(?:json|form|get_json\(\)|data)|body)\s*(?:\.|\[\s*[\"'])(?:price|amount|total|unit_amount|role|is_?admin|plan|credits)\b"), (),
128
+ "never trust the client: anyone can send price=0.01 or role=admin; look the value up on the server"),
129
+ ("high", "TLS certificate checks turned off", re.compile(r"verify\s*=\s*False|rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['\"]?0|InsecureSkipVerify\s*:\s*true"), (),
130
+ "anyone on the network can read or change the traffic"),
131
+ ("high", "Unsafe deserialisation", re.compile(r"pickle\.loads?\s*\(|yaml\.load\s*\((?![^)]*Loader\s*=\s*yaml\.(?:Safe|C?Safe)Loader)|marshal\.loads\s*\("), _PY,
132
+ "loading untrusted data this way runs code hidden in it"),
133
+ ("medium", "JWT read without checking its signature", re.compile(r"verify_signature[\"']?\s*:\s*False|jwt\.decode\([^)]*verify\s*=\s*False|algorithms\s*=\s*\[\s*[\"']none[\"']"), (),
134
+ "anyone can forge a token and log in as anyone"),
135
+ ("medium", "CORS open to every site", re.compile(r"Access-Control-Allow-Origin[\"']?\s*[,:]\s*[\"']\*|origin\s*:\s*[\"']\*[\"']|allow_origins\s*=\s*\[\s*[\"']\*[\"']\s*\]|CORS\([^)]*origins\s*=\s*[\"']\*"), (),
136
+ "any website can call your API from a visitor's browser; list the origins you trust"),
137
+ ("medium", "Debug mode left on", re.compile(r"\.run\([^)]*debug\s*=\s*True|^\s*DEBUG\s*=\s*True\b", re.M), _PY,
138
+ "debug pages show code and settings, and Flask's debugger can run code; switch it off outside development"),
139
+ ("low", "Random numbers that are not secure", re.compile(r"Math\.random\(\)|random\.(?:random|randint|choice)\("), (),
140
+ "fine for games; for tokens, codes or passwords use secrets (Python) or crypto.getRandomValues (JavaScript)"),
141
+ ]
142
+ _COMMENT = re.compile(r"^\s*(?:#|//|\*|/\*|<!--)")
143
+ _LOW_ONLY_IF = re.compile(r"(?i)token|password|secret|otp|code|session|salt|nonce")
144
+
145
+
146
+ def review_code(path: str, text: str) -> list[Finding]:
147
+ """Findings for one file's text: secrets first, then the code rules that apply to this kind of file."""
148
+ out: list[Finding] = []
149
+ suffix = PurePosixPath(path).suffix.lower()
150
+ is_placeholder = PurePosixPath(path).name.lower() in PLACEHOLDER_FILES
151
+ for at, kind in find_secrets(text):
152
+ out.append(Finding("high" if is_placeholder else "critical", path, at, f"Hardcoded {kind}",
153
+ "keep real keys out of files: read them from the environment, and rotate any key that was ever committed"
154
+ + (" (an example file must hold placeholders only)" if is_placeholder else "")))
155
+ lines = text.splitlines()
156
+ for sev, title, rx, kinds, why in RULES:
157
+ if kinds and suffix not in kinds:
158
+ continue
159
+ for i, line in enumerate(lines, 1):
160
+ if _COMMENT.match(line): # a comment that mentions eval() is not a call
161
+ continue
162
+ if rx.search(line):
163
+ if sev == "low" and not _LOW_ONLY_IF.search(line):
164
+ continue
165
+ out.append(Finding(sev, path, i, title, why))
166
+ break # one finding per rule per file is enough to act on
167
+ return out
168
+
169
+
170
+ def worst(findings: list[Finding]) -> str:
171
+ return min((f.severity for f in findings), key=SEVERITIES.index, default="")
172
+
173
+
174
+ # -- auditing a folder -------------------------------------------------------------------------------------------------------
175
+ SKIP_DIRS = frozenset({".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build", ".next", ".idea", ".vscode", "coverage"})
176
+ TEXT_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte", ".html", ".htm", ".json", ".yml", ".yaml", ".toml", ".ini", ".cfg",
177
+ ".env", ".sh", ".rb", ".php", ".go", ".rs", ".java", ".kt", ".cs", ".sql", ".md", ".txt", ".example", ".sample"})
178
+
179
+
180
+ def _ignored_by_gitignore(root: Path, name: str) -> bool:
181
+ gi = root / ".gitignore"
182
+ if not gi.is_file():
183
+ return False
184
+ for raw in gi.read_text(encoding="utf-8", errors="replace").splitlines():
185
+ pat = raw.strip().lstrip("/")
186
+ if pat and not pat.startswith(("#", "!")) and fnmatch.fnmatch(name, pat.rstrip("/")):
187
+ return True
188
+ return False
189
+
190
+
191
+ def _tracked_by_git(root: Path, rel: str) -> bool:
192
+ git = shutil.which("git")
193
+ if not git or not (root / ".git").exists():
194
+ return False
195
+ try:
196
+ return subprocess.run([git, "-C", str(root), "ls-files", "--error-unmatch", rel], capture_output=True, timeout=5).returncode == 0
197
+ except (OSError, subprocess.SubprocessError):
198
+ return False
199
+
200
+
201
+ def audit_folder(root: Path, max_files: int = 400, max_bytes: int = 300_000) -> tuple[list[Finding], int]:
202
+ """(findings, files checked) for the folder. Reads files only; runs nothing except an optional `git ls-files` to see if a secrets file is committed."""
203
+ root = Path(root).resolve()
204
+ findings: list[Finding] = []
205
+ checked = 0
206
+ for p in sorted(root.rglob("*")):
207
+ rel = p.relative_to(root)
208
+ if any(part in SKIP_DIRS for part in rel.parts) or not p.is_file() or p.is_symlink():
209
+ continue
210
+ name, rel_s = p.name, rel.as_posix()
211
+ if is_secret_file(name):
212
+ if _tracked_by_git(root, rel_s):
213
+ findings.append(Finding("critical", rel_s, 0, "Secrets file committed to git", "everyone with the repository has these keys; rotate them, then remove the file from git history"))
214
+ elif not _ignored_by_gitignore(root, name):
215
+ findings.append(Finding("high", rel_s, 0, "Secrets file not in .gitignore", f"add {name} to .gitignore before your next commit, or the keys go to the repository"))
216
+ continue # its content is not read: nothing in it is shown or sent
217
+ if p.suffix.lower() not in TEXT_SUFFIXES and name not in PLACEHOLDER_FILES:
218
+ continue
219
+ try:
220
+ if p.stat().st_size > max_bytes:
221
+ continue
222
+ text = p.read_text(encoding="utf-8")
223
+ except (OSError, UnicodeDecodeError):
224
+ continue
225
+ findings.extend(review_code(rel_s, text))
226
+ checked += 1
227
+ if checked >= max_files:
228
+ break
229
+ findings.sort(key=lambda f: (SEVERITIES.index(f.severity), f.path, f.line))
230
+ return findings, checked
231
+
232
+
233
+ AUDIT_PROMPT = """Review this project for security problems, the way a careful reviewer of AI-written code would. Report only real, exploitable issues, worst first
234
+ (Critical, High, Medium, Low). For each: the file and line, the problem, what an attacker could actually do, and the fix in a few lines.
235
+ Check: secrets in code or in client-side variables; database access without per-user rules (Supabase RLS, Firebase rules); routes and actions
236
+ that do not check who is logged in; prices, roles or plans trusted from the client; payment webhooks without signature checks; missing rate
237
+ limits on login and AI endpoints; AI keys in front-end code and no usage caps; model output rendered as HTML or run as code; SQL or shell
238
+ built from strings; open CORS; debug or source maps left on in production.
239
+ The automatic scanner already found the items below; confirm or dismiss them, then add what it cannot see. Do not write files."""