chainwall 0.1.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.
Files changed (348) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/commands/security-scan.md +35 -0
  4. package/dist/auditor/access-mapper.d.ts +3 -0
  5. package/dist/auditor/access-mapper.d.ts.map +1 -0
  6. package/dist/auditor/access-mapper.js +15 -0
  7. package/dist/auditor/access-mapper.js.map +1 -0
  8. package/dist/auditor/cli-detector.d.ts +7 -0
  9. package/dist/auditor/cli-detector.d.ts.map +1 -0
  10. package/dist/auditor/cli-detector.js +63 -0
  11. package/dist/auditor/cli-detector.js.map +1 -0
  12. package/dist/auditor/cross-reference.d.ts +4 -0
  13. package/dist/auditor/cross-reference.d.ts.map +1 -0
  14. package/dist/auditor/cross-reference.js +16 -0
  15. package/dist/auditor/cross-reference.js.map +1 -0
  16. package/dist/auditor/env-auditor.d.ts +9 -0
  17. package/dist/auditor/env-auditor.d.ts.map +1 -0
  18. package/dist/auditor/env-auditor.js +83 -0
  19. package/dist/auditor/env-auditor.js.map +1 -0
  20. package/dist/auditor/mcp-analyzer.d.ts +11 -0
  21. package/dist/auditor/mcp-analyzer.d.ts.map +1 -0
  22. package/dist/auditor/mcp-analyzer.js +145 -0
  23. package/dist/auditor/mcp-analyzer.js.map +1 -0
  24. package/dist/auditor/mcp-detector.d.ts +17 -0
  25. package/dist/auditor/mcp-detector.d.ts.map +1 -0
  26. package/dist/auditor/mcp-detector.js +86 -0
  27. package/dist/auditor/mcp-detector.js.map +1 -0
  28. package/dist/auditor/remediation.d.ts +26 -0
  29. package/dist/auditor/remediation.d.ts.map +1 -0
  30. package/dist/auditor/remediation.js +222 -0
  31. package/dist/auditor/remediation.js.map +1 -0
  32. package/dist/auditor/tool-detector.d.ts +15 -0
  33. package/dist/auditor/tool-detector.d.ts.map +1 -0
  34. package/dist/auditor/tool-detector.js +241 -0
  35. package/dist/auditor/tool-detector.js.map +1 -0
  36. package/dist/auditor/types.d.ts +31 -0
  37. package/dist/auditor/types.d.ts.map +1 -0
  38. package/dist/auditor/types.js +2 -0
  39. package/dist/auditor/types.js.map +1 -0
  40. package/dist/auditor/vscode-extension-scanner.d.ts +8 -0
  41. package/dist/auditor/vscode-extension-scanner.d.ts.map +1 -0
  42. package/dist/auditor/vscode-extension-scanner.js +51 -0
  43. package/dist/auditor/vscode-extension-scanner.js.map +1 -0
  44. package/dist/cli.d.ts +3 -0
  45. package/dist/cli.d.ts.map +1 -0
  46. package/dist/cli.js +159 -0
  47. package/dist/cli.js.map +1 -0
  48. package/dist/commands/audit.d.ts +8 -0
  49. package/dist/commands/audit.d.ts.map +1 -0
  50. package/dist/commands/audit.js +151 -0
  51. package/dist/commands/audit.js.map +1 -0
  52. package/dist/commands/init.d.ts +2 -0
  53. package/dist/commands/init.d.ts.map +1 -0
  54. package/dist/commands/init.js +34 -0
  55. package/dist/commands/init.js.map +1 -0
  56. package/dist/commands/remediate-cli.d.ts +3 -0
  57. package/dist/commands/remediate-cli.d.ts.map +1 -0
  58. package/dist/commands/remediate-cli.js +96 -0
  59. package/dist/commands/remediate-cli.js.map +1 -0
  60. package/dist/commands/scan.d.ts +11 -0
  61. package/dist/commands/scan.d.ts.map +1 -0
  62. package/dist/commands/scan.js +138 -0
  63. package/dist/commands/scan.js.map +1 -0
  64. package/dist/commands/watch.d.ts +6 -0
  65. package/dist/commands/watch.d.ts.map +1 -0
  66. package/dist/commands/watch.js +203 -0
  67. package/dist/commands/watch.js.map +1 -0
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.d.ts.map +1 -0
  70. package/dist/config.js +235 -0
  71. package/dist/config.js.map +1 -0
  72. package/dist/mcp-server/index.d.ts +3 -0
  73. package/dist/mcp-server/index.d.ts.map +1 -0
  74. package/dist/mcp-server/index.js +69 -0
  75. package/dist/mcp-server/index.js.map +1 -0
  76. package/dist/mcp-server/schemas.d.ts +13 -0
  77. package/dist/mcp-server/schemas.d.ts.map +1 -0
  78. package/dist/mcp-server/schemas.js +13 -0
  79. package/dist/mcp-server/schemas.js.map +1 -0
  80. package/dist/mcp-server/tools/audit-status.d.ts +3 -0
  81. package/dist/mcp-server/tools/audit-status.d.ts.map +1 -0
  82. package/dist/mcp-server/tools/audit-status.js +46 -0
  83. package/dist/mcp-server/tools/audit-status.js.map +1 -0
  84. package/dist/mcp-server/tools/check-command.d.ts +4 -0
  85. package/dist/mcp-server/tools/check-command.d.ts.map +1 -0
  86. package/dist/mcp-server/tools/check-command.js +30 -0
  87. package/dist/mcp-server/tools/check-command.js.map +1 -0
  88. package/dist/mcp-server/tools/scan-content.d.ts +4 -0
  89. package/dist/mcp-server/tools/scan-content.d.ts.map +1 -0
  90. package/dist/mcp-server/tools/scan-content.js +18 -0
  91. package/dist/mcp-server/tools/scan-content.js.map +1 -0
  92. package/dist/mcp-server/tools/scan-file.d.ts +4 -0
  93. package/dist/mcp-server/tools/scan-file.d.ts.map +1 -0
  94. package/dist/mcp-server/tools/scan-file.js +48 -0
  95. package/dist/mcp-server/tools/scan-file.js.map +1 -0
  96. package/dist/mcp-server/types.d.ts +15 -0
  97. package/dist/mcp-server/types.d.ts.map +1 -0
  98. package/dist/mcp-server/types.js +2 -0
  99. package/dist/mcp-server/types.js.map +1 -0
  100. package/dist/reporter/audit-report.d.ts +4 -0
  101. package/dist/reporter/audit-report.d.ts.map +1 -0
  102. package/dist/reporter/audit-report.js +186 -0
  103. package/dist/reporter/audit-report.js.map +1 -0
  104. package/dist/reporter/json-report.d.ts +3 -0
  105. package/dist/reporter/json-report.d.ts.map +1 -0
  106. package/dist/reporter/json-report.js +4 -0
  107. package/dist/reporter/json-report.js.map +1 -0
  108. package/dist/reporter/remediation-text.d.ts +3 -0
  109. package/dist/reporter/remediation-text.d.ts.map +1 -0
  110. package/dist/reporter/remediation-text.js +12 -0
  111. package/dist/reporter/remediation-text.js.map +1 -0
  112. package/dist/reporter/risk-scorer.d.ts +8 -0
  113. package/dist/reporter/risk-scorer.d.ts.map +1 -0
  114. package/dist/reporter/risk-scorer.js +40 -0
  115. package/dist/reporter/risk-scorer.js.map +1 -0
  116. package/dist/reporter/sarif-report.d.ts +3 -0
  117. package/dist/reporter/sarif-report.d.ts.map +1 -0
  118. package/dist/reporter/sarif-report.js +80 -0
  119. package/dist/reporter/sarif-report.js.map +1 -0
  120. package/dist/reporter/shared.d.ts +11 -0
  121. package/dist/reporter/shared.d.ts.map +1 -0
  122. package/dist/reporter/shared.js +85 -0
  123. package/dist/reporter/shared.js.map +1 -0
  124. package/dist/reporter/summary-generator.d.ts +16 -0
  125. package/dist/reporter/summary-generator.d.ts.map +1 -0
  126. package/dist/reporter/summary-generator.js +89 -0
  127. package/dist/reporter/summary-generator.js.map +1 -0
  128. package/dist/reporter/terminal-report.d.ts +4 -0
  129. package/dist/reporter/terminal-report.d.ts.map +1 -0
  130. package/dist/reporter/terminal-report.js +135 -0
  131. package/dist/reporter/terminal-report.js.map +1 -0
  132. package/dist/rules/crypto-rules.d.ts +3 -0
  133. package/dist/rules/crypto-rules.d.ts.map +1 -0
  134. package/dist/rules/crypto-rules.js +252 -0
  135. package/dist/rules/crypto-rules.js.map +1 -0
  136. package/dist/rules/default-rules.d.ts +9 -0
  137. package/dist/rules/default-rules.d.ts.map +1 -0
  138. package/dist/rules/default-rules.js +1319 -0
  139. package/dist/rules/default-rules.js.map +1 -0
  140. package/dist/rules/index.d.ts +7 -0
  141. package/dist/rules/index.d.ts.map +1 -0
  142. package/dist/rules/index.js +7 -0
  143. package/dist/rules/index.js.map +1 -0
  144. package/dist/rules/injection-rules.d.ts +8 -0
  145. package/dist/rules/injection-rules.d.ts.map +1 -0
  146. package/dist/rules/injection-rules.js +108 -0
  147. package/dist/rules/injection-rules.js.map +1 -0
  148. package/dist/rules/types.d.ts +52 -0
  149. package/dist/rules/types.d.ts.map +1 -0
  150. package/dist/rules/types.js +2 -0
  151. package/dist/rules/types.js.map +1 -0
  152. package/dist/scanner/filesystem-scanner.d.ts +26 -0
  153. package/dist/scanner/filesystem-scanner.d.ts.map +1 -0
  154. package/dist/scanner/filesystem-scanner.js +369 -0
  155. package/dist/scanner/filesystem-scanner.js.map +1 -0
  156. package/dist/scanner/injection-scanner.d.ts +12 -0
  157. package/dist/scanner/injection-scanner.d.ts.map +1 -0
  158. package/dist/scanner/injection-scanner.js +136 -0
  159. package/dist/scanner/injection-scanner.js.map +1 -0
  160. package/dist/scanner/permission-checker.d.ts +4 -0
  161. package/dist/scanner/permission-checker.d.ts.map +1 -0
  162. package/dist/scanner/permission-checker.js +37 -0
  163. package/dist/scanner/permission-checker.js.map +1 -0
  164. package/dist/scanner/redact.d.ts +3 -0
  165. package/dist/scanner/redact.d.ts.map +1 -0
  166. package/dist/scanner/redact.js +17 -0
  167. package/dist/scanner/redact.js.map +1 -0
  168. package/dist/scanner/rule-engine.d.ts +9 -0
  169. package/dist/scanner/rule-engine.d.ts.map +1 -0
  170. package/dist/scanner/rule-engine.js +129 -0
  171. package/dist/scanner/rule-engine.js.map +1 -0
  172. package/dist/scanner/system-targets.d.ts +17 -0
  173. package/dist/scanner/system-targets.d.ts.map +1 -0
  174. package/dist/scanner/system-targets.js +81 -0
  175. package/dist/scanner/system-targets.js.map +1 -0
  176. package/dist/tui/App.d.ts +6 -0
  177. package/dist/tui/App.d.ts.map +1 -0
  178. package/dist/tui/App.js +224 -0
  179. package/dist/tui/App.js.map +1 -0
  180. package/dist/tui/components/BootSequence.d.ts +6 -0
  181. package/dist/tui/components/BootSequence.d.ts.map +1 -0
  182. package/dist/tui/components/BootSequence.js +40 -0
  183. package/dist/tui/components/BootSequence.js.map +1 -0
  184. package/dist/tui/components/BorderedSection.d.ts +12 -0
  185. package/dist/tui/components/BorderedSection.d.ts.map +1 -0
  186. package/dist/tui/components/BorderedSection.js +7 -0
  187. package/dist/tui/components/BorderedSection.js.map +1 -0
  188. package/dist/tui/components/ErrorBoundary.d.ts +18 -0
  189. package/dist/tui/components/ErrorBoundary.d.ts.map +1 -0
  190. package/dist/tui/components/ErrorBoundary.js +36 -0
  191. package/dist/tui/components/ErrorBoundary.js.map +1 -0
  192. package/dist/tui/components/FirstUseHint.d.ts +7 -0
  193. package/dist/tui/components/FirstUseHint.d.ts.map +1 -0
  194. package/dist/tui/components/FirstUseHint.js +20 -0
  195. package/dist/tui/components/FirstUseHint.js.map +1 -0
  196. package/dist/tui/components/Footer.d.ts +10 -0
  197. package/dist/tui/components/Footer.d.ts.map +1 -0
  198. package/dist/tui/components/Footer.js +51 -0
  199. package/dist/tui/components/Footer.js.map +1 -0
  200. package/dist/tui/components/MetricCard.d.ts +11 -0
  201. package/dist/tui/components/MetricCard.d.ts.map +1 -0
  202. package/dist/tui/components/MetricCard.js +8 -0
  203. package/dist/tui/components/MetricCard.js.map +1 -0
  204. package/dist/tui/components/Panel.d.ts +15 -0
  205. package/dist/tui/components/Panel.d.ts.map +1 -0
  206. package/dist/tui/components/Panel.js +25 -0
  207. package/dist/tui/components/Panel.js.map +1 -0
  208. package/dist/tui/components/RemediationMenu.d.ts +10 -0
  209. package/dist/tui/components/RemediationMenu.d.ts.map +1 -0
  210. package/dist/tui/components/RemediationMenu.js +84 -0
  211. package/dist/tui/components/RemediationMenu.js.map +1 -0
  212. package/dist/tui/components/RiskGauge.d.ts +7 -0
  213. package/dist/tui/components/RiskGauge.d.ts.map +1 -0
  214. package/dist/tui/components/RiskGauge.js +55 -0
  215. package/dist/tui/components/RiskGauge.js.map +1 -0
  216. package/dist/tui/components/ScrollableList.d.ts +11 -0
  217. package/dist/tui/components/ScrollableList.d.ts.map +1 -0
  218. package/dist/tui/components/ScrollableList.js +14 -0
  219. package/dist/tui/components/ScrollableList.js.map +1 -0
  220. package/dist/tui/components/Section.d.ts +9 -0
  221. package/dist/tui/components/Section.d.ts.map +1 -0
  222. package/dist/tui/components/Section.js +7 -0
  223. package/dist/tui/components/Section.js.map +1 -0
  224. package/dist/tui/components/SectionHeader.d.ts +8 -0
  225. package/dist/tui/components/SectionHeader.d.ts.map +1 -0
  226. package/dist/tui/components/SectionHeader.js +15 -0
  227. package/dist/tui/components/SectionHeader.js.map +1 -0
  228. package/dist/tui/components/SeverityBadge.d.ts +5 -0
  229. package/dist/tui/components/SeverityBadge.d.ts.map +1 -0
  230. package/dist/tui/components/SeverityBadge.js +7 -0
  231. package/dist/tui/components/SeverityBadge.js.map +1 -0
  232. package/dist/tui/components/Sidebar.d.ts +2 -0
  233. package/dist/tui/components/Sidebar.d.ts.map +1 -0
  234. package/dist/tui/components/Sidebar.js +40 -0
  235. package/dist/tui/components/Sidebar.js.map +1 -0
  236. package/dist/tui/components/StatusIndicator.d.ts +8 -0
  237. package/dist/tui/components/StatusIndicator.d.ts.map +1 -0
  238. package/dist/tui/components/StatusIndicator.js +15 -0
  239. package/dist/tui/components/StatusIndicator.js.map +1 -0
  240. package/dist/tui/components/Table.d.ts +21 -0
  241. package/dist/tui/components/Table.d.ts.map +1 -0
  242. package/dist/tui/components/Table.js +38 -0
  243. package/dist/tui/components/Table.js.map +1 -0
  244. package/dist/tui/components/Transition.d.ts +8 -0
  245. package/dist/tui/components/Transition.d.ts.map +1 -0
  246. package/dist/tui/components/Transition.js +38 -0
  247. package/dist/tui/components/Transition.js.map +1 -0
  248. package/dist/tui/components/WelcomeScreen.d.ts +6 -0
  249. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -0
  250. package/dist/tui/components/WelcomeScreen.js +14 -0
  251. package/dist/tui/components/WelcomeScreen.js.map +1 -0
  252. package/dist/tui/educational.d.ts +32 -0
  253. package/dist/tui/educational.d.ts.map +1 -0
  254. package/dist/tui/educational.js +117 -0
  255. package/dist/tui/educational.js.map +1 -0
  256. package/dist/tui/hooks/useAudit.d.ts +24 -0
  257. package/dist/tui/hooks/useAudit.d.ts.map +1 -0
  258. package/dist/tui/hooks/useAudit.js +263 -0
  259. package/dist/tui/hooks/useAudit.js.map +1 -0
  260. package/dist/tui/hooks/useConfig.d.ts +18 -0
  261. package/dist/tui/hooks/useConfig.d.ts.map +1 -0
  262. package/dist/tui/hooks/useConfig.js +85 -0
  263. package/dist/tui/hooks/useConfig.js.map +1 -0
  264. package/dist/tui/hooks/useHookStatus.d.ts +10 -0
  265. package/dist/tui/hooks/useHookStatus.d.ts.map +1 -0
  266. package/dist/tui/hooks/useHookStatus.js +59 -0
  267. package/dist/tui/hooks/useHookStatus.js.map +1 -0
  268. package/dist/tui/hooks/useLogs.d.ts +42 -0
  269. package/dist/tui/hooks/useLogs.d.ts.map +1 -0
  270. package/dist/tui/hooks/useLogs.js +105 -0
  271. package/dist/tui/hooks/useLogs.js.map +1 -0
  272. package/dist/tui/hooks/useScan.d.ts +39 -0
  273. package/dist/tui/hooks/useScan.d.ts.map +1 -0
  274. package/dist/tui/hooks/useScan.js +255 -0
  275. package/dist/tui/hooks/useScan.js.map +1 -0
  276. package/dist/tui/hooks/useTerminalSize.d.ts +10 -0
  277. package/dist/tui/hooks/useTerminalSize.d.ts.map +1 -0
  278. package/dist/tui/hooks/useTerminalSize.js +27 -0
  279. package/dist/tui/hooks/useTerminalSize.js.map +1 -0
  280. package/dist/tui/index.d.ts +2 -0
  281. package/dist/tui/index.d.ts.map +1 -0
  282. package/dist/tui/index.js +8 -0
  283. package/dist/tui/index.js.map +1 -0
  284. package/dist/tui/screens/AuditPanel.d.ts +7 -0
  285. package/dist/tui/screens/AuditPanel.d.ts.map +1 -0
  286. package/dist/tui/screens/AuditPanel.js +467 -0
  287. package/dist/tui/screens/AuditPanel.js.map +1 -0
  288. package/dist/tui/screens/LogsPanel.d.ts +2 -0
  289. package/dist/tui/screens/LogsPanel.d.ts.map +1 -0
  290. package/dist/tui/screens/LogsPanel.js +127 -0
  291. package/dist/tui/screens/LogsPanel.js.map +1 -0
  292. package/dist/tui/screens/OverviewPanel.d.ts +2 -0
  293. package/dist/tui/screens/OverviewPanel.d.ts.map +1 -0
  294. package/dist/tui/screens/OverviewPanel.js +84 -0
  295. package/dist/tui/screens/OverviewPanel.js.map +1 -0
  296. package/dist/tui/screens/ScanPanel.d.ts +2 -0
  297. package/dist/tui/screens/ScanPanel.d.ts.map +1 -0
  298. package/dist/tui/screens/ScanPanel.js +188 -0
  299. package/dist/tui/screens/ScanPanel.js.map +1 -0
  300. package/dist/tui/screens/ScanResultsPanel.d.ts +2 -0
  301. package/dist/tui/screens/ScanResultsPanel.d.ts.map +1 -0
  302. package/dist/tui/screens/ScanResultsPanel.js +394 -0
  303. package/dist/tui/screens/ScanResultsPanel.js.map +1 -0
  304. package/dist/tui/screens/SettingsPanel.d.ts +2 -0
  305. package/dist/tui/screens/SettingsPanel.d.ts.map +1 -0
  306. package/dist/tui/screens/SettingsPanel.js +353 -0
  307. package/dist/tui/screens/SettingsPanel.js.map +1 -0
  308. package/dist/tui/state.d.ts +35 -0
  309. package/dist/tui/state.d.ts.map +1 -0
  310. package/dist/tui/state.js +13 -0
  311. package/dist/tui/state.js.map +1 -0
  312. package/dist/tui/theme.d.ts +58 -0
  313. package/dist/tui/theme.d.ts.map +1 -0
  314. package/dist/tui/theme.js +80 -0
  315. package/dist/tui/theme.js.map +1 -0
  316. package/dist/version.d.ts +2 -0
  317. package/dist/version.d.ts.map +1 -0
  318. package/dist/version.js +5 -0
  319. package/dist/version.js.map +1 -0
  320. package/hooks/audit-logger.sh +74 -0
  321. package/hooks/detection-lib.sh +301 -0
  322. package/hooks/git-pre-commit.sh +195 -0
  323. package/hooks/git-pre-push.sh +125 -0
  324. package/hooks/git-safety.sh +152 -0
  325. package/hooks/security-scanner.sh +527 -0
  326. package/install.sh +543 -0
  327. package/package.json +67 -0
  328. package/patterns/credentials.yaml +317 -0
  329. package/patterns/dangerous-commands.yaml +167 -0
  330. package/patterns/pii.yaml +95 -0
  331. package/patterns/prompt-injection.yaml +131 -0
  332. package/patterns/supply-chain.yaml +119 -0
  333. package/rules/AGENTS.md +60 -0
  334. package/rules/SECURITY-RULES.md +177 -0
  335. package/rules/claude.md +9 -0
  336. package/rules/clinerules +29 -0
  337. package/rules/continuerules +29 -0
  338. package/rules/copilot-instructions.md +9 -0
  339. package/rules/cursor-security.mdc +14 -0
  340. package/rules/gemini.md +9 -0
  341. package/rules/kiro-security.md +29 -0
  342. package/rules/roocode-security.md +29 -0
  343. package/rules/trae-security.md +29 -0
  344. package/rules/windsurfrules +9 -0
  345. package/skill/llm-antivirus/SKILL.md +73 -0
  346. package/skill/llm-antivirus/references/threat-patterns.yaml +82 -0
  347. package/skill/llm-antivirus/scripts/security-audit.sh +244 -0
  348. package/uninstall.sh +215 -0
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ #
3
+ # git-safety.sh — prevents dangerous git operations
4
+ #
5
+ # Blocks force-pushes to main/master, checks for secrets in staged files,
6
+ # warns on hard resets and direct commits to protected branches.
7
+ #
8
+ # Exit 0 = allow, exit 2 = block.
9
+ #
10
+
11
+ set -o pipefail
12
+
13
+ RED_BOLD=$'\e[1;31m'
14
+ YELLOW=$'\e[33m'
15
+ RESET=$'\e[0m'
16
+
17
+ if ! command -v jq &> /dev/null; then
18
+ echo "ERROR: jq is required for git-safety hook." >&2
19
+ exit 0
20
+ fi
21
+
22
+ log_event() {
23
+ local severity="$1"
24
+ local category="$2"
25
+ local detail="$3"
26
+
27
+ local audit_dir=".llm-av"
28
+ local audit_file="${audit_dir}/audit.jsonl"
29
+ mkdir -p "$audit_dir"
30
+
31
+ local timestamp
32
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
33
+
34
+ jq -cn \
35
+ --arg timestamp "$timestamp" \
36
+ --arg severity "$severity" \
37
+ --arg category "$category" \
38
+ --arg pattern "" \
39
+ --arg tool "${TOOL_NAME:-unknown}" \
40
+ --arg detail "${detail:0:200}" \
41
+ '{timestamp:$timestamp,severity:$severity,category:$category,pattern:$pattern,tool:$tool,content:$detail}' \
42
+ >> "$audit_file" 2>/dev/null
43
+ }
44
+
45
+ block() {
46
+ local reason="$1"
47
+ local detail="$2"
48
+
49
+ echo -e "${RED_BOLD}BLOCKED: ${reason}${RESET}" >&2
50
+ echo -e "${RED_BOLD}Detail: ${detail}${RESET}" >&2
51
+ log_event "block" "git_safety" "$reason: $detail"
52
+ exit 2
53
+ }
54
+
55
+ warn() {
56
+ local reason="$1"
57
+ local detail="$2"
58
+
59
+ echo -e "${YELLOW}WARNING: ${reason}${RESET}" >&2
60
+ echo -e "${YELLOW}Detail: ${detail}${RESET}" >&2
61
+ log_event "warn" "git_safety" "$reason: $detail"
62
+ }
63
+
64
+ if [[ "${LLMAV_SKIP:-}" == "1" ]]; then
65
+ log_event "warn" "git_safety" "LLMAV_SKIP bypass — all git safety checks skipped"
66
+ exit 0
67
+ fi
68
+
69
+ # --- protection toggle ---
70
+ CONFIG_GLOBAL="${HOME}/.llm-av/config.json"
71
+ if [[ -f "$CONFIG_GLOBAL" ]]; then
72
+ PROTECTION_ENABLED=$(jq -r 'if .enabled == false then "false" else "true" end' "$CONFIG_GLOBAL" 2>/dev/null)
73
+ if [[ "$PROTECTION_ENABLED" == "false" ]]; then
74
+ log_event "info" "git_safety" "Real-time protection disabled"
75
+ exit 0
76
+ fi
77
+ fi
78
+
79
+ INPUT=$(cat)
80
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
81
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
82
+
83
+ # bail early on non-git commands
84
+ if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
85
+ exit 0
86
+ fi
87
+ if [[ "$COMMAND" != *"git "* ]]; then
88
+ exit 0
89
+ fi
90
+
91
+ PROTECTED_BRANCHES="main|master"
92
+ if [[ "$COMMAND" =~ git[[:space:]]+push[[:space:]].*--force ]] || \
93
+ [[ "$COMMAND" =~ git[[:space:]]+push[[:space:]].*-f([[:space:]]|$) ]]; then
94
+ # block if targeting a protected branch (word-boundary match) or no branch specified
95
+ if [[ "$COMMAND" =~ [[:space:]](main|master)[[:space:]] ]] || \
96
+ [[ "$COMMAND" =~ [[:space:]](main|master)$ ]] || \
97
+ [[ "$COMMAND" =~ git[[:space:]]+push[[:space:]]+-f$ ]] || \
98
+ [[ "$COMMAND" =~ git[[:space:]]+push[[:space:]]+--force$ ]]; then
99
+ block "Force-push to protected branch" "$COMMAND"
100
+ fi
101
+ fi
102
+
103
+ if [[ "$COMMAND" =~ git[[:space:]]+reset[[:space:]]+--hard ]]; then
104
+ if command -v git &> /dev/null; then
105
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
106
+ if [[ "$CURRENT_BRANCH" =~ ^($PROTECTED_BRANCHES)$ ]]; then
107
+ block "Hard reset on protected branch" "Current branch: $CURRENT_BRANCH — $COMMAND"
108
+ fi
109
+ fi
110
+ warn "Hard reset detected" "Verify this is intentional: $COMMAND"
111
+ fi
112
+
113
+ if [[ "$COMMAND" =~ git[[:space:]]+commit ]]; then
114
+ if command -v git &> /dev/null; then
115
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
116
+ if [[ "$CURRENT_BRANCH" =~ ^($PROTECTED_BRANCHES)$ ]]; then
117
+ warn "Direct commit to protected branch" "Current branch: $CURRENT_BRANCH — consider using a feature branch"
118
+ fi
119
+ fi
120
+ fi
121
+
122
+ # check staged files for sensitive filenames before commit
123
+ if [[ "$COMMAND" =~ git[[:space:]]+commit ]]; then
124
+ if command -v git &> /dev/null; then
125
+ STAGED_FILES=$(git diff --cached --name-only 2>/dev/null)
126
+ if [[ -n "$STAGED_FILES" ]]; then
127
+ while IFS= read -r staged_file; do
128
+ staged_basename=$(basename "$staged_file")
129
+ case "$staged_basename" in
130
+ .env|.env.local|.env.production|.env.development|\
131
+ id_rsa|id_dsa|id_ed25519|\
132
+ .npmrc|.pypirc)
133
+ block "Sensitive file in staged changes" "File: $staged_file"
134
+ ;;
135
+ esac
136
+ done <<< "$STAGED_FILES"
137
+ fi
138
+ fi
139
+ fi
140
+
141
+ # warn if .gitignore is missing common sensitive patterns
142
+ if [[ "$COMMAND" =~ git[[:space:]]+init ]] || [[ "$COMMAND" =~ git[[:space:]]+add[[:space:]]+-A ]] || [[ "$COMMAND" =~ git[[:space:]]+add[[:space:]]+\. ]]; then
143
+ if [[ -f ".gitignore" ]]; then
144
+ if ! grep -q '\.env' .gitignore 2>/dev/null; then
145
+ warn ".gitignore missing .env" "Add .env to .gitignore to prevent accidental commits"
146
+ fi
147
+ else
148
+ warn "No .gitignore found" "Create a .gitignore before adding all files"
149
+ fi
150
+ fi
151
+
152
+ exit 0
@@ -0,0 +1,527 @@
1
+ #!/bin/bash
2
+ #
3
+ # security-scanner.sh — main detection engine for ChainWall
4
+ #
5
+ # Runs as a Claude Code hook on PreToolUse (blocking, layers 1-5) and
6
+ # PostToolUse (warning only, layer 6). Scans tool inputs for credentials,
7
+ # private keys, dangerous commands, PII, and prompt injection.
8
+ #
9
+ # Exit 0 = allow, exit 2 = block. Must stay under 50ms.
10
+ #
11
+
12
+ set -o pipefail
13
+
14
+ # --- paths and config ---
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ PATTERNS_DIR="${REPO_ROOT}/patterns"
19
+
20
+ RED_BOLD=$'\e[1;31m'
21
+ YELLOW=$'\e[33m'
22
+ RESET=$'\e[0m'
23
+
24
+ CONFIG_GLOBAL="${HOME}/.llm-av/config.json"
25
+ CONFIG_PROJECT=".llm-av/config.json"
26
+
27
+ ALLOW_PATHS=""
28
+ ALLOW_PATTERNS=""
29
+ BLOCK_PATHS=""
30
+ BLOCK_PATTERNS=""
31
+
32
+ # jq is required but we can't break the user's workflow if it's missing
33
+ if ! command -v jq &> /dev/null; then
34
+ echo "ERROR: jq is required for security hooks. Install: brew install jq (macOS) or apt install jq (Linux)" >&2
35
+ exit 0
36
+ fi
37
+
38
+ # --- config loading (global ~/.llm-av/ + project .llm-av/) ---
39
+
40
+ load_config() {
41
+ if [[ -f "$CONFIG_GLOBAL" ]]; then
42
+ ALLOW_PATHS=$(jq -r '.allowlist.paths[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
43
+ ALLOW_PATTERNS=$(jq -r '.allowlist.patterns[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
44
+ BLOCK_PATHS=$(jq -r '.blocklist.paths[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
45
+ BLOCK_PATTERNS=$(jq -r '.blocklist.patterns[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
46
+ fi
47
+
48
+ # project config extends global (additive, not override)
49
+ if [[ -f "$CONFIG_PROJECT" ]]; then
50
+ local proj_allow_paths proj_allow_patterns proj_block_paths proj_block_patterns
51
+ proj_allow_paths=$(jq -r '.allowlist.paths[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
52
+ proj_allow_patterns=$(jq -r '.allowlist.patterns[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
53
+ proj_block_paths=$(jq -r '.blocklist.paths[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
54
+ proj_block_patterns=$(jq -r '.blocklist.patterns[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
55
+
56
+ # merge: avoid double-pipe when base is empty
57
+ if [[ -n "$proj_allow_paths" ]]; then
58
+ [[ -n "$ALLOW_PATHS" ]] && ALLOW_PATHS="${ALLOW_PATHS}|${proj_allow_paths}" || ALLOW_PATHS="$proj_allow_paths"
59
+ fi
60
+ if [[ -n "$proj_allow_patterns" ]]; then
61
+ [[ -n "$ALLOW_PATTERNS" ]] && ALLOW_PATTERNS="${ALLOW_PATTERNS}|${proj_allow_patterns}" || ALLOW_PATTERNS="$proj_allow_patterns"
62
+ fi
63
+ if [[ -n "$proj_block_paths" ]]; then
64
+ [[ -n "$BLOCK_PATHS" ]] && BLOCK_PATHS="${BLOCK_PATHS}|${proj_block_paths}" || BLOCK_PATHS="$proj_block_paths"
65
+ fi
66
+ if [[ -n "$proj_block_patterns" ]]; then
67
+ [[ -n "$BLOCK_PATTERNS" ]] && BLOCK_PATTERNS="${BLOCK_PATTERNS}|${proj_block_patterns}" || BLOCK_PATTERNS="$proj_block_patterns"
68
+ fi
69
+ fi
70
+
71
+ # trim stray pipe chars
72
+ ALLOW_PATHS="${ALLOW_PATHS#|}"; ALLOW_PATHS="${ALLOW_PATHS%|}"
73
+ ALLOW_PATTERNS="${ALLOW_PATTERNS#|}"; ALLOW_PATTERNS="${ALLOW_PATTERNS%|}"
74
+ BLOCK_PATHS="${BLOCK_PATHS#|}"; BLOCK_PATHS="${BLOCK_PATHS%|}"
75
+ BLOCK_PATTERNS="${BLOCK_PATTERNS#|}"; BLOCK_PATTERNS="${BLOCK_PATTERNS%|}"
76
+ }
77
+
78
+ # --- YAML pattern loading (used by security-audit.sh, not the hot path) ---
79
+ load_patterns() {
80
+ local yaml_file="$1"
81
+ local severity_filter="${2:-}"
82
+
83
+ [[ ! -f "$yaml_file" ]] && return
84
+
85
+ if [[ -n "$severity_filter" ]]; then
86
+ grep -A1 "severity: $severity_filter" "$yaml_file" 2>/dev/null \
87
+ | grep 'regex:' \
88
+ | sed 's/.*regex: *"//' \
89
+ | sed 's/"$//' \
90
+ | tr '\n' '|' \
91
+ | sed 's/|$//'
92
+ else
93
+ grep '^\s*regex:' "$yaml_file" 2>/dev/null \
94
+ | sed 's/.*regex: *"//' \
95
+ | sed 's/"$//' \
96
+ | tr '\n' '|' \
97
+ | sed 's/|$//'
98
+ fi
99
+ }
100
+
101
+ # --- allowlist ---
102
+ check_allowlist() {
103
+ local value="$1"
104
+ local patterns="$2"
105
+
106
+ [[ -z "$patterns" ]] && return 1
107
+
108
+ local IFS='|'
109
+ for pattern in $patterns; do
110
+ [[ -n "$pattern" ]] && [[ "$value" =~ $pattern ]] && return 0
111
+ done
112
+
113
+ return 1
114
+ }
115
+
116
+ # --- audit + blocking helpers ---
117
+
118
+ log_event() {
119
+ local severity="$1"
120
+ local category="$2"
121
+ local pattern="$3"
122
+ local content="$4"
123
+
124
+ local audit_dir=".llm-av"
125
+ local audit_file="${audit_dir}/audit.jsonl"
126
+
127
+ mkdir -p "$audit_dir"
128
+
129
+ local timestamp
130
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
131
+
132
+ # keep audit entries reasonable
133
+ local truncated_content="${content:0:200}"
134
+
135
+ jq -cn \
136
+ --arg timestamp "$timestamp" \
137
+ --arg severity "$severity" \
138
+ --arg category "$category" \
139
+ --arg pattern "$pattern" \
140
+ --arg tool "${TOOL_NAME:-unknown}" \
141
+ --arg content "$truncated_content" \
142
+ '{timestamp:$timestamp,severity:$severity,category:$category,pattern:$pattern,tool:$tool,content:$content}' \
143
+ >> "$audit_file" 2>/dev/null
144
+
145
+ # rotate at 10MB
146
+ if [[ -f "$audit_file" ]]; then
147
+ local size
148
+ size=$(wc -c < "$audit_file" 2>/dev/null | tr -d ' ')
149
+ if [[ "$size" -gt 10485760 ]]; then
150
+ mv "$audit_file" "${audit_file}.$(date +%s)" 2>/dev/null
151
+ fi
152
+ fi
153
+ }
154
+
155
+ block() {
156
+ local category="$1"
157
+ local pattern_name="$2"
158
+ local detail="$3"
159
+
160
+ echo -e "${RED_BOLD}BLOCKED: ${pattern_name}${RESET}" >&2
161
+ echo -e "${RED_BOLD}Detail: ${detail}${RESET}" >&2
162
+ echo -e "${RED_BOLD}Config: ~/.llm-av/config.json or .llm-av/config.json${RESET}" >&2
163
+ if [[ -n "$FILE_PATH" ]]; then
164
+ echo -e "${YELLOW}To allow: chainwall allow ${FILE_PATH}${RESET}" >&2
165
+ fi
166
+
167
+ log_event "block" "$category" "$pattern_name" "$detail"
168
+ exit 2
169
+ }
170
+
171
+ warn() {
172
+ local category="$1"
173
+ local pattern_name="$2"
174
+ local detail="$3"
175
+
176
+ echo -e "${YELLOW}WARNING: ${pattern_name}${RESET}" >&2
177
+ echo -e "${YELLOW}Detail: ${detail}${RESET}" >&2
178
+
179
+ log_event "warn" "$category" "$pattern_name" "$detail"
180
+ # Never exit — warnings don't block
181
+ }
182
+
183
+ # --- escape hatch ---
184
+
185
+ if [[ "${LLMAV_SKIP:-}" == "1" ]]; then
186
+ echo -e "${YELLOW}WARNING: Security checks bypassed via LLMAV_SKIP${RESET}" >&2
187
+ log_event "bypass" "escape_hatch" "LLMAV_SKIP=1" "All checks skipped"
188
+ exit 0
189
+ fi
190
+
191
+ # --- protection toggle ---
192
+ if [[ -f "$CONFIG_GLOBAL" ]]; then
193
+ PROTECTION_ENABLED=$(jq -r 'if .enabled == false then "false" else "true" end' "$CONFIG_GLOBAL" 2>/dev/null)
194
+ if [[ "$PROTECTION_ENABLED" == "false" ]]; then
195
+ log_event "info" "protection_disabled" "protection_disabled" "Real-time protection disabled"
196
+ exit 0
197
+ fi
198
+ fi
199
+
200
+ # --- main: parse input and run detection layers ---
201
+
202
+ load_config
203
+
204
+ INPUT=$(cat)
205
+
206
+ # Extract fields from JSON input. Using individual jq calls instead of
207
+ # eval+@sh — slightly slower but avoids eval in a security-critical path.
208
+ HOOK_TYPE=$(echo "$INPUT" | jq -r '.hook_type // "pre"')
209
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
210
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
211
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
212
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
213
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
214
+ TOOL_OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty')
215
+
216
+ # strip anything that could be used for path traversal
217
+ SESSION_ID=$(printf '%s' "$SESSION_ID" | tr -cd 'a-zA-Z0-9_-')
218
+
219
+ # session duration tracking (show how long the agent has been running)
220
+ if [[ -n "$SESSION_ID" ]]; then
221
+ TMPDIR_PATH="${TMPDIR:-/tmp}"
222
+ TMPDIR_PATH="${TMPDIR_PATH%/}"
223
+ STATE_FILE="${TMPDIR_PATH}/llm-av-session-${SESSION_ID}.json"
224
+
225
+ if [[ ! -f "$STATE_FILE" ]]; then
226
+ START_TIME=$(date +%s)
227
+ TMP_STATE="${STATE_FILE}.tmp.$$"
228
+ jq -n --arg start "$START_TIME" '{start_time: $start}' > "$TMP_STATE"
229
+ mv "$TMP_STATE" "$STATE_FILE" 2>/dev/null || true
230
+ fi
231
+
232
+ if [[ -f "$STATE_FILE" ]]; then
233
+ START_TIME=$(jq -r '.start_time' "$STATE_FILE" 2>/dev/null)
234
+ if [[ -n "$START_TIME" ]] && [[ "$START_TIME" =~ ^[0-9]+$ ]]; then
235
+ CURRENT_TIME=$(date +%s)
236
+ ELAPSED=$((CURRENT_TIME - START_TIME))
237
+ HOURS=$((ELAPSED / 3600))
238
+ MINUTES=$(((ELAPSED % 3600) / 60))
239
+ SECS=$((ELAPSED % 60))
240
+
241
+ if (( HOURS >= 24 )); then
242
+ DAYS=$((HOURS / 24)); HOURS=$((HOURS % 24))
243
+ DURATION="${DAYS}d ${HOURS}h ${MINUTES}m"
244
+ elif (( HOURS > 0 )); then
245
+ DURATION="${HOURS}h ${MINUTES}m ${SECS}s"
246
+ elif (( MINUTES > 0 )); then
247
+ DURATION="${MINUTES}m ${SECS}s"
248
+ else
249
+ DURATION="${SECS}s"
250
+ fi
251
+ echo -e "${YELLOW}Session duration: ${DURATION}${RESET}" >&2
252
+ fi
253
+ fi
254
+ fi
255
+
256
+ # combine all text fields for scanning
257
+ TEXT_TO_CHECK=""
258
+ [[ -n "$CONTENT" ]] && TEXT_TO_CHECK="$CONTENT"
259
+ [[ -n "$COMMAND" ]] && TEXT_TO_CHECK="$TEXT_TO_CHECK $COMMAND"
260
+ [[ -n "$TOOL_OUTPUT" ]] && TEXT_TO_CHECK="$TEXT_TO_CHECK $TOOL_OUTPUT"
261
+
262
+ # ======================================================================
263
+ # BLOCKING LAYERS (1-5) — PreToolUse only
264
+ # ======================================================================
265
+
266
+ if [[ "$HOOK_TYPE" != "post" ]]; then
267
+
268
+ # -- layer 1: file blocklist --
269
+ if [[ -n "$FILE_PATH" ]]; then
270
+ if ! check_allowlist "$FILE_PATH" "$ALLOW_PATHS"; then
271
+ BASENAME=$(basename "$FILE_PATH")
272
+
273
+ case "$BASENAME" in
274
+ config.json)
275
+ if [[ "$FILE_PATH" == *".llm-av/"* ]] || [[ "$FILE_PATH" == *"/.llm-av/"* ]]; then
276
+ block "file_access" "Security config modification prevented" "$FILE_PATH"
277
+ else
278
+ block "file_access" "Sensitive file access prevented" "$FILE_PATH"
279
+ fi
280
+ ;;
281
+ .env|.env.local|.env.production|.env.development|\
282
+ credentials|id_rsa|id_dsa|id_ed25519|\
283
+ secrets.json|credentials.json|.npmrc|.pypirc)
284
+ block "file_access" "Sensitive file access prevented" "$FILE_PATH"
285
+ ;;
286
+ esac
287
+ fi
288
+ fi
289
+
290
+ # -- layer 1.5: custom blocklist paths --
291
+ check_block_paths() {
292
+ local IFS='|'
293
+ for pattern in $BLOCK_PATHS; do
294
+ [[ -n "$pattern" ]] && [[ "$FILE_PATH" =~ $pattern ]] && \
295
+ block "custom_blocklist" "Custom blocklist path matched" "$FILE_PATH (pattern: $pattern)"
296
+ done
297
+ }
298
+ if [[ -n "$BLOCK_PATHS" ]] && [[ -n "$FILE_PATH" ]]; then
299
+ check_block_paths
300
+ fi
301
+
302
+ # content-based checks (layers 2-5)
303
+ if [[ -n "$TEXT_TO_CHECK" ]]; then
304
+
305
+ # -- layer 2.5: custom blocklist patterns --
306
+ if ! check_allowlist "$TEXT_TO_CHECK" "$ALLOW_PATTERNS"; then
307
+ check_block_patterns() {
308
+ local IFS='|'
309
+ for pattern in $BLOCK_PATTERNS; do
310
+ [[ -n "$pattern" ]] && [[ "$TEXT_TO_CHECK" =~ $pattern ]] && \
311
+ block "custom_blocklist" "Custom blocklist pattern matched" "pattern: $pattern"
312
+ done
313
+ }
314
+ if [[ -n "$BLOCK_PATTERNS" ]]; then
315
+ check_block_patterns
316
+ fi
317
+ fi
318
+
319
+ # -- layer 2: credentials (hardcoded for speed, not skippable via allowlist) --
320
+ if [[ "$TEXT_TO_CHECK" =~ AKIA[0-9A-Z]{16} ]]; then
321
+ block "credential" "AWS Access Key detected" "AKIA[0-9A-Z]{16}"
322
+ fi
323
+ if [[ "$TEXT_TO_CHECK" =~ ghp_[a-zA-Z0-9]{36} ]]; then
324
+ block "credential" "GitHub Personal Access Token detected" "ghp_[a-zA-Z0-9]{36}"
325
+ fi
326
+ if [[ "$TEXT_TO_CHECK" =~ github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59} ]]; then
327
+ block "credential" "GitHub Fine-Grained Token detected" "github_pat_*"
328
+ fi
329
+ if [[ "$TEXT_TO_CHECK" =~ gh[ours]_[a-zA-Z0-9]{36} ]]; then
330
+ block "credential" "GitHub OAuth/App Token detected" "gh[ours]_*"
331
+ fi
332
+ if [[ "$TEXT_TO_CHECK" =~ sk-ant-[a-zA-Z0-9_-]{40,} ]]; then
333
+ block "credential" "Anthropic API Key detected" "sk-ant-*"
334
+ elif [[ "$TEXT_TO_CHECK" =~ sk-proj-[a-zA-Z0-9_-]{48,} ]]; then
335
+ block "credential" "OpenAI Project API Key detected" "sk-proj-*"
336
+ elif [[ "$TEXT_TO_CHECK" =~ sk-[a-zA-Z0-9_-]{48} ]]; then
337
+ block "credential" "OpenAI API Key detected" "sk-[a-zA-Z0-9_-]{48}"
338
+ fi
339
+ if [[ "$TEXT_TO_CHECK" =~ xox[pboa]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,} ]]; then
340
+ block "credential" "Slack Token detected" "xox[pboa]-*"
341
+ fi
342
+ if [[ "$TEXT_TO_CHECK" =~ sk_(live|test)_[a-zA-Z0-9]{24,} ]]; then
343
+ block "credential" "Stripe API Key detected" "sk_(live|test)_*"
344
+ fi
345
+ if [[ "$TEXT_TO_CHECK" =~ SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43} ]]; then
346
+ block "credential" "SendGrid API Key detected" "SG.*"
347
+ fi
348
+ if [[ "$TEXT_TO_CHECK" =~ SK[a-f0-9]{32} ]]; then
349
+ block "credential" "Twilio API Key detected" "SK[hex]{32}"
350
+ fi
351
+ if [[ "$TEXT_TO_CHECK" =~ AIza[0-9A-Za-z_-]{35} ]]; then
352
+ block "credential" "Google API Key detected" "AIza*"
353
+ fi
354
+ if [[ "$TEXT_TO_CHECK" =~ Authorization:[[:space:]]*Bearer[[:space:]]+[a-zA-Z0-9_-]{20,} ]] || \
355
+ [[ "$TEXT_TO_CHECK" =~ Bearer[[:space:]]+ey[a-zA-Z0-9_-]{20,} ]]; then
356
+ block "credential" "Bearer token detected" "Bearer [token]"
357
+ fi
358
+ # gitlab
359
+ if [[ "$TEXT_TO_CHECK" =~ glpat-[a-zA-Z0-9_-]{20,} ]]; then
360
+ block "credential" "GitLab Personal Access Token detected" "glpat-*"
361
+ fi
362
+ # npm
363
+ if [[ "$TEXT_TO_CHECK" =~ npm_[a-zA-Z0-9]{36} ]]; then
364
+ block "credential" "npm Access Token detected" "npm_*"
365
+ fi
366
+ # hashicorp vault
367
+ if [[ "$TEXT_TO_CHECK" =~ hvs\.[a-zA-Z0-9_-]{24,} ]]; then
368
+ block "credential" "Vault Token detected" "hvs.*"
369
+ fi
370
+ # databricks
371
+ if [[ "$TEXT_TO_CHECK" =~ dapi[a-f0-9]{32} ]]; then
372
+ block "credential" "Databricks Access Token detected" "dapi*"
373
+ fi
374
+ # shopify
375
+ if [[ "$TEXT_TO_CHECK" =~ shp(at|ca|pa)_[a-f0-9]{32} ]]; then
376
+ block "credential" "Shopify Token detected" "shp*_*"
377
+ fi
378
+
379
+ # -- layer 3: private keys (PEM headers) --
380
+ if [[ "$TEXT_TO_CHECK" == *"-----BEGIN RSA PRIVATE KEY-----"* ]]; then
381
+ block "private_key" "RSA private key detected" "-----BEGIN RSA PRIVATE KEY-----"
382
+ fi
383
+ if [[ "$TEXT_TO_CHECK" == *"-----BEGIN DSA PRIVATE KEY-----"* ]]; then
384
+ block "private_key" "DSA private key detected" "-----BEGIN DSA PRIVATE KEY-----"
385
+ fi
386
+ if [[ "$TEXT_TO_CHECK" == *"-----BEGIN EC PRIVATE KEY-----"* ]]; then
387
+ block "private_key" "EC private key detected" "-----BEGIN EC PRIVATE KEY-----"
388
+ fi
389
+ if [[ "$TEXT_TO_CHECK" == *"-----BEGIN OPENSSH PRIVATE KEY-----"* ]]; then
390
+ block "private_key" "OpenSSH private key detected" "-----BEGIN OPENSSH PRIVATE KEY-----"
391
+ fi
392
+ if [[ "$TEXT_TO_CHECK" == *"-----BEGIN PGP PRIVATE KEY BLOCK-----"* ]]; then
393
+ block "private_key" "PGP private key detected" "-----BEGIN PGP PRIVATE KEY BLOCK-----"
394
+ fi
395
+ fi
396
+
397
+ # -- layer 4: dangerous commands (not skippable via allowlist) --
398
+ if [[ -n "$COMMAND" ]]; then
399
+ if [[ "$COMMAND" =~ rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f ]] || \
400
+ [[ "$COMMAND" =~ rm[[:space:]]+-[^[:space:]]*f[^[:space:]]*r ]]; then
401
+ block "dangerous_command" "rm -rf (recursive force delete)" "$COMMAND"
402
+ fi
403
+ # catch separated flags: rm -r -f, rm --recursive --force, etc.
404
+ if [[ "$COMMAND" =~ rm[[:space:]] ]]; then
405
+ if { [[ "$COMMAND" =~ [[:space:]]-[^[:space:]]*r ]] || [[ "$COMMAND" =~ --recursive ]]; } && \
406
+ { [[ "$COMMAND" =~ [[:space:]]-f([[:space:]]|$) ]] || [[ "$COMMAND" =~ --force ]]; }; then
407
+ block "dangerous_command" "rm -rf (recursive force delete)" "$COMMAND"
408
+ fi
409
+ fi
410
+
411
+ # download-and-execute
412
+ if [[ "$COMMAND" =~ (curl|wget)[[:space:]].+\|[[:space:]]*(bash|sh|zsh|ksh|dash|fish|python|perl|ruby|node) ]]; then
413
+ block "dangerous_command" "Download piped to shell interpreter" "$COMMAND"
414
+ fi
415
+
416
+ if [[ "$COMMAND" =~ chmod[[:space:]]+777 ]]; then
417
+ block "dangerous_command" "chmod 777 (world-writable permissions)" "$COMMAND"
418
+ fi
419
+
420
+ if [[ "$COMMAND" =~ dd[[:space:]].+of=/dev/ ]]; then
421
+ block "dangerous_command" "dd to device (potential disk destruction)" "$COMMAND"
422
+ fi
423
+
424
+ if [[ "$COMMAND" =~ mkfs ]]; then
425
+ block "dangerous_command" "mkfs (filesystem format)" "$COMMAND"
426
+ fi
427
+
428
+ if [[ "$COMMAND" =~ \>/dev/(sd|hd|nvme|vd|xvd) ]]; then
429
+ block "dangerous_command" "Direct device write" "$COMMAND"
430
+ fi
431
+
432
+ # reverse shells
433
+ if [[ "$COMMAND" =~ /dev/tcp/ ]] || \
434
+ [[ "$COMMAND" =~ bash[[:space:]]+-i[[:space:]]+\>\& ]]; then
435
+ block "dangerous_command" "Reverse shell pattern detected" "$COMMAND"
436
+ fi
437
+
438
+ if [[ "$COMMAND" =~ LD_PRELOAD= ]]; then
439
+ block "dangerous_command" "LD_PRELOAD injection" "$COMMAND"
440
+ fi
441
+
442
+ if [[ "$COMMAND" =~ base64[[:space:]]+(-d|--decode).*\|[[:space:]]*(bash|sh|eval) ]]; then
443
+ block "dangerous_command" "Base64 decode piped to shell" "$COMMAND"
444
+ fi
445
+
446
+ # -- layer 4.5: supply chain --
447
+ if [[ "$COMMAND" =~ pip3?[[:space:]]+install[[:space:]]+https?:// ]]; then
448
+ block "supply_chain" "pip install from URL (bypass PyPI vetting)" "$COMMAND"
449
+ fi
450
+ if [[ "$COMMAND" =~ npm[[:space:]]+(config[[:space:]]+set|install).*registry[[:space:]]*= ]]; then
451
+ block "supply_chain" "npm registry override (dependency confusion risk)" "$COMMAND"
452
+ fi
453
+ if [[ "$COMMAND" =~ rm[[:space:]]+(-f[[:space:]]+)?(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Gemfile\.lock|poetry\.lock) ]]; then
454
+ block "supply_chain" "Lock file deletion (dependency substitution risk)" "$COMMAND"
455
+ fi
456
+ fi
457
+
458
+ # -- layer 5: PII (SSN with validation, credit cards with BIN check) --
459
+ if [[ -n "$TEXT_TO_CHECK" ]]; then
460
+ if [[ "$TEXT_TO_CHECK" =~ ([0-9]{3})-([0-9]{2})-([0-9]{4}) ]]; then
461
+ SSN_FIRST="${BASH_REMATCH[1]}"
462
+ SSN_MIDDLE="${BASH_REMATCH[2]}"
463
+ SSN_LAST="${BASH_REMATCH[3]}"
464
+ if [[ "$SSN_FIRST" != "000" && "$SSN_FIRST" != "666" && "$SSN_MIDDLE" != "00" && "$SSN_LAST" != "0000" ]]; then
465
+ SSN_NUM=$((10#$SSN_FIRST))
466
+ if (( SSN_NUM < 900 )); then
467
+ block "pii" "Potential SSN detected (XXX-XX-XXXX)" "${BASH_REMATCH[0]}"
468
+ fi
469
+ fi
470
+ fi
471
+
472
+ # credit cards: match 15-16 digits, then verify BIN range (4=Visa, 51-55=MC, 34/37=Amex 15-digit, 60/65=Discover)
473
+ if [[ "$TEXT_TO_CHECK" =~ ([0-9]{4})[-[:space:]]?[0-9]{4}[-[:space:]]?[0-9]{4}[-[:space:]]?[0-9]{3,4} ]]; then
474
+ CC_FIRST4="${BASH_REMATCH[1]}"
475
+ CC_FIRST2="${CC_FIRST4:0:2}"
476
+ CC_FIRST1="${CC_FIRST4:0:1}"
477
+ # known BIN prefixes
478
+ if [[ "$CC_FIRST1" == "4" ]] || \
479
+ [[ "$CC_FIRST2" == "51" || "$CC_FIRST2" == "52" || "$CC_FIRST2" == "53" || "$CC_FIRST2" == "54" || "$CC_FIRST2" == "55" ]] || \
480
+ [[ "$CC_FIRST2" == "34" || "$CC_FIRST2" == "37" ]] || \
481
+ [[ "$CC_FIRST2" == "60" || "$CC_FIRST2" == "65" ]]; then
482
+ block "pii" "Potential credit card number detected" "XXXX-XXXX-XXXX-XXXX pattern"
483
+ fi
484
+ fi
485
+ fi
486
+
487
+ fi # end blocking layers
488
+
489
+ # ======================================================================
490
+ # WARNING LAYER (6) — runs on both pre and post, never blocks
491
+ # ======================================================================
492
+
493
+ if [[ -n "$TEXT_TO_CHECK" ]]; then
494
+ TEXT_LOWER=$(echo "$TEXT_TO_CHECK" | tr '[:upper:]' '[:lower:]')
495
+
496
+ # instruction override attempts
497
+ if [[ "$TEXT_LOWER" == *"ignore previous instructions"* ]] || \
498
+ [[ "$TEXT_LOWER" == *"ignore all previous"* ]] || \
499
+ [[ "$TEXT_LOWER" == *"disregard all prior"* ]] || \
500
+ [[ "$TEXT_LOWER" == *"disregard previous"* ]]; then
501
+ warn "prompt_injection" "Potential prompt injection detected" "instruction override phrase"
502
+ fi
503
+
504
+ # role confusion / privilege escalation claims
505
+ if [[ "$TEXT_LOWER" =~ (i\ am|acting\ as|my\ role\ is).*(system|admin|root|developer\ mode|superuser) ]]; then
506
+ warn "prompt_injection" "Potential role confusion detected" "role assumption phrase"
507
+ fi
508
+
509
+ # system prompt leakage
510
+ if [[ "$TEXT_LOWER" == *"you are a helpful"* ]] || \
511
+ [[ "$TEXT_LOWER" == *"your instructions are"* ]] || \
512
+ [[ "$TEXT_LOWER" == *"your system prompt"* ]] || \
513
+ [[ "$TEXT_LOWER" == *"reveal your prompt"* ]] || \
514
+ [[ "$TEXT_LOWER" == *"show system prompt"* ]]; then
515
+ warn "prompt_injection" "Potential system prompt leakage" "system prompt fragment"
516
+ fi
517
+
518
+ # known jailbreak keywords
519
+ if [[ "$TEXT_LOWER" == *"developer mode"* ]] || \
520
+ [[ "$TEXT_LOWER" == *"jailbreak"* ]] || \
521
+ [[ "$TEXT_LOWER" == *"dan mode"* ]]; then
522
+ warn "prompt_injection" "Potential jailbreak attempt detected" "jailbreak keyword"
523
+ fi
524
+ fi
525
+
526
+ # nothing caught — let it through
527
+ exit 0