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,74 @@
1
+ #!/bin/bash
2
+ #
3
+ # audit-logger.sh — records every tool call to .llm-av/audit.jsonl
4
+ #
5
+ # PostToolUse hook. Purely observational, never blocks (always exits 0).
6
+ # Rotates the log at 10MB.
7
+ #
8
+
9
+ set -o pipefail
10
+
11
+ if [[ "${LLMAV_SKIP:-}" == "1" ]]; then
12
+ # Log bypass even when skipping — audit trail must be complete
13
+ AUDIT_DIR=".llm-av"
14
+ AUDIT_FILE="${AUDIT_DIR}/audit.jsonl"
15
+ mkdir -p "$AUDIT_DIR"
16
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
17
+ jq -cn \
18
+ --arg timestamp "$TIMESTAMP" \
19
+ --arg severity "warn" \
20
+ --arg category "audit" \
21
+ --arg pattern "" \
22
+ --arg tool "audit-logger" \
23
+ --arg content "LLMAV_SKIP bypass — audit logging skipped" \
24
+ '{timestamp:$timestamp,severity:$severity,category:$category,pattern:$pattern,tool:$tool,content:$content}' \
25
+ >> "$AUDIT_FILE" 2>/dev/null
26
+ exit 0
27
+ fi
28
+
29
+ # silently skip if jq missing — logging isn't worth breaking the workflow
30
+ if ! command -v jq &> /dev/null; then
31
+ exit 0
32
+ fi
33
+
34
+ AUDIT_DIR=".llm-av"
35
+ AUDIT_FILE="${AUDIT_DIR}/audit.jsonl"
36
+
37
+ mkdir -p "$AUDIT_DIR"
38
+
39
+ INPUT=$(cat)
40
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
41
+ HOOK_TYPE=$(echo "$INPUT" | jq -r '.hook_type // "post"')
42
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
43
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
44
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
45
+
46
+ SUMMARY=""
47
+ if [[ -n "$COMMAND" ]]; then
48
+ SUMMARY="${COMMAND:0:100}"
49
+ elif [[ -n "$FILE_PATH" ]]; then
50
+ SUMMARY="file: $FILE_PATH"
51
+ else
52
+ SUMMARY="(no command or file)"
53
+ fi
54
+
55
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
56
+ jq -cn \
57
+ --arg timestamp "$TIMESTAMP" \
58
+ --arg severity "info" \
59
+ --arg category "audit" \
60
+ --arg pattern "" \
61
+ --arg tool "$TOOL_NAME" \
62
+ --arg content "$SUMMARY" \
63
+ '{timestamp:$timestamp,severity:$severity,category:$category,pattern:$pattern,tool:$tool,content:$content}' \
64
+ >> "$AUDIT_FILE" 2>/dev/null
65
+
66
+ # rotate at 10MB
67
+ if [[ -f "$AUDIT_FILE" ]]; then
68
+ SIZE=$(wc -c < "$AUDIT_FILE" 2>/dev/null | tr -d ' ')
69
+ if [[ "$SIZE" -gt 10485760 ]]; then
70
+ mv "$AUDIT_FILE" "${AUDIT_FILE}.$(date +%s)" 2>/dev/null
71
+ fi
72
+ fi
73
+
74
+ exit 0
@@ -0,0 +1,301 @@
1
+ #!/bin/bash
2
+ #
3
+ # detection-lib.sh — shared pattern library for ChainWall git hooks
4
+ #
5
+ # Provides credential, private key, PII, and dangerous command detection.
6
+ # Sourced by git-pre-commit.sh and git-pre-push.sh.
7
+ #
8
+ # Bash 3.2 compatible. Requires jq.
9
+ #
10
+
11
+ # --- colors ---
12
+ RED_BOLD=$'\e[1;31m'
13
+ YELLOW=$'\e[33m'
14
+ GREEN=$'\e[32m'
15
+ RESET=$'\e[0m'
16
+
17
+ # --- config ---
18
+ CONFIG_GLOBAL="${HOME}/.llm-av/config.json"
19
+ CONFIG_PROJECT=".llm-av/config.json"
20
+
21
+ ALLOW_PATHS=""
22
+ ALLOW_PATTERNS=""
23
+ BLOCK_PATHS=""
24
+ BLOCK_PATTERNS=""
25
+
26
+ # --- protection toggle ---
27
+ # Returns 0 if protection is disabled (caller should exit 0).
28
+ # Returns 1 if protection is enabled (caller should continue).
29
+ check_protection_enabled() {
30
+ if [[ -f "$CONFIG_GLOBAL" ]]; then
31
+ local enabled
32
+ enabled=$(jq -r 'if .enabled == false then "false" else "true" end' "$CONFIG_GLOBAL" 2>/dev/null)
33
+ if [[ "$enabled" == "false" ]]; then
34
+ log_event "info" "protection_disabled" "Real-time protection disabled"
35
+ return 0
36
+ fi
37
+ fi
38
+ return 1
39
+ }
40
+
41
+ # --- config loading ---
42
+ load_config() {
43
+ if [[ -f "$CONFIG_GLOBAL" ]]; then
44
+ ALLOW_PATHS=$(jq -r '.allowlist.paths[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
45
+ ALLOW_PATTERNS=$(jq -r '.allowlist.patterns[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
46
+ BLOCK_PATHS=$(jq -r '.blocklist.paths[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
47
+ BLOCK_PATTERNS=$(jq -r '.blocklist.patterns[]? // empty' "$CONFIG_GLOBAL" 2>/dev/null | tr '\n' '|')
48
+ fi
49
+
50
+ if [[ -f "$CONFIG_PROJECT" ]]; then
51
+ local proj_ap proj_apatt proj_bp proj_bpatt
52
+ proj_ap=$(jq -r '.allowlist.paths[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
53
+ proj_apatt=$(jq -r '.allowlist.patterns[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
54
+ proj_bp=$(jq -r '.blocklist.paths[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
55
+ proj_bpatt=$(jq -r '.blocklist.patterns[]? // empty' "$CONFIG_PROJECT" 2>/dev/null | tr '\n' '|')
56
+
57
+ if [[ -n "$proj_ap" ]]; then
58
+ [[ -n "$ALLOW_PATHS" ]] && ALLOW_PATHS="${ALLOW_PATHS}|${proj_ap}" || ALLOW_PATHS="$proj_ap"
59
+ fi
60
+ if [[ -n "$proj_apatt" ]]; then
61
+ [[ -n "$ALLOW_PATTERNS" ]] && ALLOW_PATTERNS="${ALLOW_PATTERNS}|${proj_apatt}" || ALLOW_PATTERNS="$proj_apatt"
62
+ fi
63
+ if [[ -n "$proj_bp" ]]; then
64
+ [[ -n "$BLOCK_PATHS" ]] && BLOCK_PATHS="${BLOCK_PATHS}|${proj_bp}" || BLOCK_PATHS="$proj_bp"
65
+ fi
66
+ if [[ -n "$proj_bpatt" ]]; then
67
+ [[ -n "$BLOCK_PATTERNS" ]] && BLOCK_PATTERNS="${BLOCK_PATTERNS}|${proj_bpatt}" || BLOCK_PATTERNS="$proj_bpatt"
68
+ fi
69
+ fi
70
+
71
+ ALLOW_PATHS="${ALLOW_PATHS#|}"; ALLOW_PATHS="${ALLOW_PATHS%|}"
72
+ ALLOW_PATTERNS="${ALLOW_PATTERNS#|}"; ALLOW_PATTERNS="${ALLOW_PATTERNS%|}"
73
+ BLOCK_PATHS="${BLOCK_PATHS#|}"; BLOCK_PATHS="${BLOCK_PATHS%|}"
74
+ BLOCK_PATTERNS="${BLOCK_PATTERNS#|}"; BLOCK_PATTERNS="${BLOCK_PATTERNS%|}"
75
+ }
76
+
77
+ # --- allowlist check ---
78
+ check_allowlist() {
79
+ local value="$1"
80
+ local patterns="$2"
81
+
82
+ [[ -z "$patterns" ]] && return 1
83
+
84
+ local IFS='|'
85
+ for pattern in $patterns; do
86
+ [[ -n "$pattern" ]] && [[ "$value" =~ $pattern ]] && return 0
87
+ done
88
+
89
+ return 1
90
+ }
91
+
92
+ # --- audit logging ---
93
+ log_event() {
94
+ local severity="$1"
95
+ local category="$2"
96
+ local detail="$3"
97
+
98
+ local audit_dir=".llm-av"
99
+ local audit_file="${audit_dir}/audit.jsonl"
100
+ mkdir -p "$audit_dir"
101
+
102
+ local timestamp
103
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
104
+
105
+ jq -cn \
106
+ --arg timestamp "$timestamp" \
107
+ --arg severity "$severity" \
108
+ --arg category "$category" \
109
+ --arg pattern "" \
110
+ --arg tool "git-hook" \
111
+ --arg content "${detail:0:200}" \
112
+ '{timestamp:$timestamp,severity:$severity,category:$category,pattern:$pattern,tool:$tool,content:$content}' \
113
+ >> "$audit_file" 2>/dev/null
114
+ }
115
+
116
+ # --- redact match for display ---
117
+ redact_match() {
118
+ local text="$1"
119
+ local len=${#text}
120
+
121
+ if (( len <= 8 )); then
122
+ printf '%*s' "$len" '' | tr ' ' '*'
123
+ return
124
+ fi
125
+
126
+ local show=4
127
+ local masked=$((len - show * 2))
128
+ local prefix="${text:0:$show}"
129
+ local suffix="${text:$((len - show)):$show}"
130
+ local stars
131
+ stars=$(printf '%*s' "$masked" '' | tr ' ' '*')
132
+ printf '%s%s%s' "$prefix" "$stars" "$suffix"
133
+ }
134
+
135
+ # --- credential detection ---
136
+ # Returns 0 (found) or 1 (clean). Sets DETECT_RULE and DETECT_MATCH on hit.
137
+ detect_credentials() {
138
+ local text="$1"
139
+ DETECT_RULE=""
140
+ DETECT_MATCH=""
141
+
142
+ if [[ "$text" =~ AKIA[0-9A-Z]{16} ]]; then
143
+ DETECT_RULE="AWS Access Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
144
+ fi
145
+ if [[ "$text" =~ ghp_[a-zA-Z0-9]{36} ]]; then
146
+ DETECT_RULE="GitHub Personal Access Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
147
+ fi
148
+ if [[ "$text" =~ github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59} ]]; then
149
+ DETECT_RULE="GitHub Fine-Grained Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
150
+ fi
151
+ if [[ "$text" =~ gh[ours]_[a-zA-Z0-9]{36} ]]; then
152
+ DETECT_RULE="GitHub OAuth/App Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
153
+ fi
154
+ if [[ "$text" =~ sk-ant-[a-zA-Z0-9_-]{40,} ]]; then
155
+ DETECT_RULE="Anthropic API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
156
+ elif [[ "$text" =~ sk-proj-[a-zA-Z0-9_-]{48,} ]]; then
157
+ DETECT_RULE="OpenAI Project API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
158
+ elif [[ "$text" =~ sk-[a-zA-Z0-9_-]{48} ]]; then
159
+ DETECT_RULE="OpenAI API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
160
+ fi
161
+ if [[ "$text" =~ xox[pboa]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,} ]]; then
162
+ DETECT_RULE="Slack Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
163
+ fi
164
+ if [[ "$text" =~ sk_(live|test)_[a-zA-Z0-9]{24,} ]]; then
165
+ DETECT_RULE="Stripe API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
166
+ fi
167
+ if [[ "$text" =~ SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43} ]]; then
168
+ DETECT_RULE="SendGrid API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
169
+ fi
170
+ if [[ "$text" =~ SK[a-f0-9]{32} ]]; then
171
+ DETECT_RULE="Twilio API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
172
+ fi
173
+ if [[ "$text" =~ AIza[0-9A-Za-z_-]{35} ]]; then
174
+ DETECT_RULE="Google API Key"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
175
+ fi
176
+ if [[ "$text" =~ glpat-[a-zA-Z0-9_-]{20,} ]]; then
177
+ DETECT_RULE="GitLab Personal Access Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
178
+ fi
179
+ if [[ "$text" =~ npm_[a-zA-Z0-9]{36} ]]; then
180
+ DETECT_RULE="npm Access Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
181
+ fi
182
+ if [[ "$text" =~ hvs\.[a-zA-Z0-9_-]{24,} ]]; then
183
+ DETECT_RULE="Vault Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
184
+ fi
185
+ if [[ "$text" =~ dapi[a-f0-9]{32} ]]; then
186
+ DETECT_RULE="Databricks Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
187
+ fi
188
+ if [[ "$text" =~ shp(at|ca|pa)_[a-f0-9]{32} ]]; then
189
+ DETECT_RULE="Shopify Token"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
190
+ fi
191
+
192
+ return 1
193
+ }
194
+
195
+ # --- private key detection ---
196
+ detect_private_keys() {
197
+ local text="$1"
198
+ DETECT_RULE=""
199
+ DETECT_MATCH=""
200
+
201
+ if [[ "$text" == *"-----BEGIN RSA PRIVATE KEY-----"* ]]; then
202
+ DETECT_RULE="RSA private key"; DETECT_MATCH="-----BEGIN RSA PRIVATE KEY-----"; return 0
203
+ fi
204
+ if [[ "$text" == *"-----BEGIN DSA PRIVATE KEY-----"* ]]; then
205
+ DETECT_RULE="DSA private key"; DETECT_MATCH="-----BEGIN DSA PRIVATE KEY-----"; return 0
206
+ fi
207
+ if [[ "$text" == *"-----BEGIN EC PRIVATE KEY-----"* ]]; then
208
+ DETECT_RULE="EC private key"; DETECT_MATCH="-----BEGIN EC PRIVATE KEY-----"; return 0
209
+ fi
210
+ if [[ "$text" == *"-----BEGIN OPENSSH PRIVATE KEY-----"* ]]; then
211
+ DETECT_RULE="OpenSSH private key"; DETECT_MATCH="-----BEGIN OPENSSH PRIVATE KEY-----"; return 0
212
+ fi
213
+ if [[ "$text" == *"-----BEGIN PGP PRIVATE KEY BLOCK-----"* ]]; then
214
+ DETECT_RULE="PGP private key"; DETECT_MATCH="-----BEGIN PGP PRIVATE KEY BLOCK-----"; return 0
215
+ fi
216
+
217
+ return 1
218
+ }
219
+
220
+ # --- PII detection ---
221
+ detect_pii() {
222
+ local text="$1"
223
+ DETECT_RULE=""
224
+ DETECT_MATCH=""
225
+
226
+ # SSN
227
+ if [[ "$text" =~ ([0-9]{3})-([0-9]{2})-([0-9]{4}) ]]; then
228
+ local ssn_first="${BASH_REMATCH[1]}"
229
+ local ssn_middle="${BASH_REMATCH[2]}"
230
+ local ssn_last="${BASH_REMATCH[3]}"
231
+ if [[ "$ssn_first" != "000" && "$ssn_first" != "666" && "$ssn_middle" != "00" && "$ssn_last" != "0000" ]]; then
232
+ local ssn_num=$((10#$ssn_first))
233
+ if (( ssn_num < 900 )); then
234
+ DETECT_RULE="Potential SSN"; DETECT_MATCH="${BASH_REMATCH[0]}"; return 0
235
+ fi
236
+ fi
237
+ fi
238
+
239
+ # Credit card (15-16 digits with BIN validation)
240
+ if [[ "$text" =~ ([0-9]{4})[-[:space:]]?[0-9]{4}[-[:space:]]?[0-9]{4}[-[:space:]]?[0-9]{3,4} ]]; then
241
+ local cc_first4="${BASH_REMATCH[1]}"
242
+ local cc_first2="${cc_first4:0:2}"
243
+ local cc_first1="${cc_first4:0:1}"
244
+ if [[ "$cc_first1" == "4" ]] || \
245
+ [[ "$cc_first2" == "51" || "$cc_first2" == "52" || "$cc_first2" == "53" || "$cc_first2" == "54" || "$cc_first2" == "55" ]] || \
246
+ [[ "$cc_first2" == "34" || "$cc_first2" == "37" ]] || \
247
+ [[ "$cc_first2" == "60" || "$cc_first2" == "65" ]]; then
248
+ DETECT_RULE="Potential credit card number"; DETECT_MATCH="XXXX-XXXX-XXXX-XXXX"; return 0
249
+ fi
250
+ fi
251
+
252
+ return 1
253
+ }
254
+
255
+ # --- sensitive filename detection ---
256
+ detect_sensitive_filename() {
257
+ local filename="$1"
258
+ DETECT_RULE=""
259
+
260
+ local basename_f
261
+ basename_f=$(basename "$filename")
262
+
263
+ case "$basename_f" in
264
+ .env|.env.local|.env.production|.env.development|\
265
+ id_rsa|id_dsa|id_ed25519|\
266
+ .npmrc|.pypirc|credentials.json|secrets.json)
267
+ DETECT_RULE="Sensitive file: $basename_f"
268
+ return 0
269
+ ;;
270
+ esac
271
+
272
+ return 1
273
+ }
274
+
275
+ # --- allowlist mutation (shared with git hooks) ---
276
+ add_to_allowlist() {
277
+ local filepath="$1"
278
+ local config_file="${HOME}/.llm-av/config.json"
279
+ mkdir -p "${HOME}/.llm-av"
280
+ if [[ -f "$config_file" ]]; then
281
+ local tmp
282
+ tmp=$(jq --arg p "$filepath" \
283
+ '.allowlist.paths = ((.allowlist.paths // []) + [$p] | unique)' \
284
+ "$config_file") && echo "$tmp" > "$config_file"
285
+ else
286
+ jq -n --arg p "$filepath" \
287
+ '{"allowlist":{"paths":[$p],"patterns":[],"rules":[]}}' > "$config_file"
288
+ fi
289
+ }
290
+
291
+ # --- run all content detections on text ---
292
+ # Returns 0 on first hit, 1 if clean. Sets DETECT_RULE and DETECT_MATCH.
293
+ detect_in_content() {
294
+ local text="$1"
295
+
296
+ detect_credentials "$text" && return 0
297
+ detect_private_keys "$text" && return 0
298
+ detect_pii "$text" && return 0
299
+
300
+ return 1
301
+ }
@@ -0,0 +1,195 @@
1
+ #!/bin/bash
2
+ #
3
+ # git-pre-commit.sh — ChainWall pre-commit hook
4
+ #
5
+ # Scans staged files for credentials, private keys, PII, and sensitive filenames.
6
+ # Respects .llm-av/config.json allowlist/blocklist.
7
+ #
8
+ # Exit 0 = allow commit, exit 1 = block commit.
9
+ # Set LLMAV_SKIP=1 to bypass (logged).
10
+ #
11
+
12
+ set -o pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+
16
+ # Source shared detection library
17
+ if [[ -f "${SCRIPT_DIR}/detection-lib.sh" ]]; then
18
+ source "${SCRIPT_DIR}/detection-lib.sh"
19
+ else
20
+ echo "WARNING: detection-lib.sh not found, skipping security scan" >&2
21
+ exit 0
22
+ fi
23
+
24
+ # jq required
25
+ if ! command -v jq &> /dev/null; then
26
+ echo "WARNING: jq not installed, skipping pre-commit scan" >&2
27
+ exit 0
28
+ fi
29
+
30
+ # escape hatch
31
+ if [[ "${LLMAV_SKIP:-}" == "1" ]]; then
32
+ echo -e "${YELLOW}WARNING: Pre-commit security checks bypassed via LLMAV_SKIP${RESET}" >&2
33
+ log_event "warn" "git_pre_commit" "LLMAV_SKIP bypass — pre-commit checks skipped"
34
+ exit 0
35
+ fi
36
+
37
+ # protection toggle
38
+ if check_protection_enabled; then
39
+ exit 0
40
+ fi
41
+
42
+ load_config
43
+
44
+ # Get staged files (Added, Copied, Modified only — not Deleted)
45
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null)
46
+
47
+ if [[ -z "$STAGED_FILES" ]]; then
48
+ exit 0
49
+ fi
50
+
51
+ BLOCKED=0
52
+ BLOCKED_FILES=()
53
+ BLOCKED_RULES=()
54
+ BLOCKED_MATCHES=()
55
+
56
+ MAX_FILE_SIZE=10485760 # 10MB
57
+
58
+ while IFS= read -r file; do
59
+ [[ -z "$file" ]] && continue
60
+
61
+ # Skip test directories and test files (contain intentional fake credentials)
62
+ # Exception: .env files in test dirs are still checked (real secrets leak risk)
63
+ case "$file" in
64
+ tests/*|test/*|__tests__/*|spec/*|specs/*|e2e/*|cypress/*|playwright/*|__mocks__/*|__fixtures__/*)
65
+ case "$file" in
66
+ *.env|*.env.*) ;; # still check .env files in test dirs
67
+ *) continue ;;
68
+ esac
69
+ ;;
70
+ *.test.ts|*.test.js|*.test.tsx|*.test.jsx|*.spec.ts|*.spec.js|*.spec.tsx|*.spec.jsx)
71
+ continue
72
+ ;;
73
+ esac
74
+
75
+ # Check path allowlist
76
+ if check_allowlist "$file" "$ALLOW_PATHS"; then
77
+ continue
78
+ fi
79
+
80
+ # Check path blocklist
81
+ if [[ -n "$BLOCK_PATHS" ]]; then
82
+ IFS_BAK="$IFS"
83
+ IFS='|'
84
+ for pattern in $BLOCK_PATHS; do
85
+ if [[ -n "$pattern" ]] && [[ "$file" =~ $pattern ]]; then
86
+ BLOCKED_FILES+=("$file")
87
+ BLOCKED_RULES+=("Custom blocklist path: $pattern")
88
+ BLOCKED_MATCHES+=("$file")
89
+ BLOCKED=1
90
+ fi
91
+ done
92
+ IFS="$IFS_BAK"
93
+ fi
94
+
95
+ # Check sensitive filename
96
+ if detect_sensitive_filename "$file"; then
97
+ BLOCKED_FILES+=("$file")
98
+ BLOCKED_RULES+=("$DETECT_RULE")
99
+ BLOCKED_MATCHES+=("$file")
100
+ BLOCKED=1
101
+ continue
102
+ fi
103
+
104
+ # Skip binary files
105
+ if git diff --cached --numstat -- "$file" 2>/dev/null | grep -q '^-'; then
106
+ continue
107
+ fi
108
+
109
+ # Check file size (from index)
110
+ FILE_SIZE=$(git cat-file -s ":$file" 2>/dev/null || echo 0)
111
+ if (( FILE_SIZE > MAX_FILE_SIZE )); then
112
+ continue
113
+ fi
114
+
115
+ # Get staged content
116
+ CONTENT=$(git show ":$file" 2>/dev/null) || continue
117
+
118
+ # Content allowlist check
119
+ if check_allowlist "$CONTENT" "$ALLOW_PATTERNS"; then
120
+ continue
121
+ fi
122
+
123
+ # Run content detection
124
+ if detect_in_content "$CONTENT"; then
125
+ REDACTED=$(redact_match "$DETECT_MATCH")
126
+ BLOCKED_FILES+=("$file")
127
+ BLOCKED_RULES+=("$DETECT_RULE")
128
+ BLOCKED_MATCHES+=("$REDACTED")
129
+ BLOCKED=1
130
+ fi
131
+
132
+ done <<< "$STAGED_FILES"
133
+
134
+ if (( BLOCKED > 0 )); then
135
+ echo "" >&2
136
+ echo -e "${RED_BOLD}╔══════════════════════════════════════════════╗${RESET}" >&2
137
+ echo -e "${RED_BOLD}║ COMMIT BLOCKED — Security findings detected ║${RESET}" >&2
138
+ echo -e "${RED_BOLD}╚══════════════════════════════════════════════╝${RESET}" >&2
139
+ echo "" >&2
140
+
141
+ IDX=0
142
+ while (( IDX < ${#BLOCKED_FILES[@]} )); do
143
+ echo -e "${RED_BOLD} File: ${BLOCKED_FILES[$IDX]}${RESET}" >&2
144
+ echo -e "${RED_BOLD} Rule: ${BLOCKED_RULES[$IDX]}${RESET}" >&2
145
+ echo -e "${RED_BOLD} Match: ${BLOCKED_MATCHES[$IDX]}${RESET}" >&2
146
+ echo "" >&2
147
+ log_event "block" "git_pre_commit" "${BLOCKED_RULES[$IDX]}: ${BLOCKED_FILES[$IDX]}"
148
+ IDX=$((IDX + 1))
149
+ done
150
+
151
+ echo -e "${YELLOW}Remediation:${RESET}" >&2
152
+ echo -e " - Move secrets to environment variables or a vault" >&2
153
+ echo -e " - Use git reset HEAD <file> to unstage" >&2
154
+ echo -e " - Set LLMAV_SKIP=1 to bypass (logged)" >&2
155
+ echo "" >&2
156
+
157
+ # Interactive prompt if TTY available
158
+ if [[ -t 0 ]] || [[ -t 2 ]]; then
159
+ echo -e "${YELLOW}[a]llow once [p]ermanently allowlist [b]lock (default)${RESET}" >&2
160
+ printf "Choice: " >&2
161
+ CHOICE=""
162
+ read -r -n 1 CHOICE < /dev/tty 2>/dev/null || true
163
+ echo "" >&2
164
+
165
+ case "$CHOICE" in
166
+ a|A)
167
+ log_event "warn" "git_pre_commit" "User allowed once — commit proceeding"
168
+ exit 0
169
+ ;;
170
+ p|P)
171
+ IDX=0
172
+ while (( IDX < ${#BLOCKED_FILES[@]} )); do
173
+ add_to_allowlist "${BLOCKED_FILES[$IDX]}"
174
+ echo -e "${GREEN} Allowlisted: ${BLOCKED_FILES[$IDX]}${RESET}" >&2
175
+ log_event "info" "git_pre_commit" "User permanently allowlisted: ${BLOCKED_FILES[$IDX]}"
176
+ IDX=$((IDX + 1))
177
+ done
178
+ exit 0
179
+ ;;
180
+ esac
181
+ else
182
+ # Non-TTY (CI, piped) — suggest allowlist commands
183
+ echo -e "${YELLOW}To allowlist blocked files:${RESET}" >&2
184
+ IDX=0
185
+ while (( IDX < ${#BLOCKED_FILES[@]} )); do
186
+ echo -e " chainwall allow ${BLOCKED_FILES[$IDX]}" >&2
187
+ IDX=$((IDX + 1))
188
+ done
189
+ echo "" >&2
190
+ fi
191
+
192
+ exit 1
193
+ fi
194
+
195
+ exit 0
@@ -0,0 +1,125 @@
1
+ #!/bin/bash
2
+ #
3
+ # git-pre-push.sh — ChainWall pre-push hook
4
+ #
5
+ # Blocks force-push to protected branches (main/master).
6
+ # Optional deep scan with LLMAV_SCAN_PUSH=1.
7
+ #
8
+ # Exit 0 = allow push, exit 1 = block push.
9
+ # Set LLMAV_SKIP=1 to bypass (logged).
10
+ #
11
+
12
+ set -o pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+
16
+ # Source shared detection library
17
+ if [[ -f "${SCRIPT_DIR}/detection-lib.sh" ]]; then
18
+ source "${SCRIPT_DIR}/detection-lib.sh"
19
+ else
20
+ echo "WARNING: detection-lib.sh not found, skipping push checks" >&2
21
+ exit 0
22
+ fi
23
+
24
+ # jq required
25
+ if ! command -v jq &> /dev/null; then
26
+ echo "WARNING: jq not installed, skipping pre-push checks" >&2
27
+ exit 0
28
+ fi
29
+
30
+ # escape hatch
31
+ if [[ "${LLMAV_SKIP:-}" == "1" ]]; then
32
+ echo -e "${YELLOW}WARNING: Pre-push security checks bypassed via LLMAV_SKIP${RESET}" >&2
33
+ log_event "warn" "git_pre_push" "LLMAV_SKIP bypass — pre-push checks skipped"
34
+ exit 0
35
+ fi
36
+
37
+ # protection toggle
38
+ if check_protection_enabled; then
39
+ exit 0
40
+ fi
41
+
42
+ load_config
43
+
44
+ PROTECTED_BRANCHES="main|master"
45
+ REMOTE="$1"
46
+ URL="$2"
47
+
48
+ # Read push info from stdin (format: local_ref local_sha remote_ref remote_sha)
49
+ BLOCKED=0
50
+ while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
51
+ [[ -z "$REMOTE_REF" ]] && continue
52
+
53
+ # Extract branch name from ref
54
+ BRANCH="${REMOTE_REF#refs/heads/}"
55
+
56
+ # Check if pushing to protected branch
57
+ if [[ "$BRANCH" =~ ^($PROTECTED_BRANCHES)$ ]]; then
58
+ # Detect force-push: check if remote_sha is ancestor of local_sha
59
+ if [[ "$REMOTE_SHA" != "0000000000000000000000000000000000000000" ]]; then
60
+ if ! git merge-base --is-ancestor "$REMOTE_SHA" "$LOCAL_SHA" 2>/dev/null; then
61
+ echo "" >&2
62
+ echo -e "${RED_BOLD}╔══════════════════════════════════════════════╗${RESET}" >&2
63
+ echo -e "${RED_BOLD}║ PUSH BLOCKED — Force-push to $BRANCH ║${RESET}" >&2
64
+ echo -e "${RED_BOLD}╚══════════════════════════════════════════════╝${RESET}" >&2
65
+ echo "" >&2
66
+ echo -e "${YELLOW}Force-pushing to ${BRANCH} is blocked.${RESET}" >&2
67
+ echo -e "${YELLOW}Use a feature branch or set LLMAV_SKIP=1 to bypass (logged).${RESET}" >&2
68
+ echo "" >&2
69
+ log_event "block" "git_pre_push" "Force-push to protected branch: ${BRANCH}"
70
+
71
+ # Interactive prompt if TTY available
72
+ if [[ -t 0 ]] || [[ -t 2 ]]; then
73
+ echo -e "${YELLOW}[a]llow once [b]lock (default)${RESET}" >&2
74
+ printf "Choice: " >&2
75
+ CHOICE=""
76
+ read -r -n 1 CHOICE < /dev/tty 2>/dev/null || true
77
+ echo "" >&2
78
+
79
+ case "$CHOICE" in
80
+ a|A)
81
+ log_event "warn" "git_pre_push" "User allowed force-push once to ${BRANCH}"
82
+ continue
83
+ ;;
84
+ esac
85
+ fi
86
+
87
+ BLOCKED=1
88
+ fi
89
+ fi
90
+ fi
91
+
92
+ # Optional deep scan of commits being pushed
93
+ if [[ "${LLMAV_SCAN_PUSH:-}" == "1" ]]; then
94
+ if [[ "$REMOTE_SHA" == "0000000000000000000000000000000000000000" ]]; then
95
+ # New branch — scan all commits
96
+ COMMITS=$(git rev-list "$LOCAL_SHA" --not --remotes 2>/dev/null)
97
+ else
98
+ COMMITS=$(git rev-list "${REMOTE_SHA}..${LOCAL_SHA}" 2>/dev/null)
99
+ fi
100
+
101
+ while IFS= read -r commit; do
102
+ [[ -z "$commit" ]] && continue
103
+ FILES=$(git diff-tree --no-commit-id --name-only -r "$commit" 2>/dev/null)
104
+ while IFS= read -r file; do
105
+ [[ -z "$file" ]] && continue
106
+ CONTENT=$(git show "${commit}:${file}" 2>/dev/null) || continue
107
+ if detect_in_content "$CONTENT"; then
108
+ REDACTED=$(redact_match "$DETECT_MATCH")
109
+ echo -e "${RED_BOLD} Finding in ${commit:0:8}:${file}${RESET}" >&2
110
+ echo -e "${RED_BOLD} Rule: ${DETECT_RULE}${RESET}" >&2
111
+ echo -e "${RED_BOLD} Match: ${REDACTED}${RESET}" >&2
112
+ echo "" >&2
113
+ log_event "block" "git_pre_push" "${DETECT_RULE}: ${commit:0:8}:${file}"
114
+ BLOCKED=1
115
+ fi
116
+ done <<< "$FILES"
117
+ done <<< "$COMMITS"
118
+ fi
119
+ done
120
+
121
+ if (( BLOCKED > 0 )); then
122
+ exit 1
123
+ fi
124
+
125
+ exit 0