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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/commands/security-scan.md +35 -0
- package/dist/auditor/access-mapper.d.ts +3 -0
- package/dist/auditor/access-mapper.d.ts.map +1 -0
- package/dist/auditor/access-mapper.js +15 -0
- package/dist/auditor/access-mapper.js.map +1 -0
- package/dist/auditor/cli-detector.d.ts +7 -0
- package/dist/auditor/cli-detector.d.ts.map +1 -0
- package/dist/auditor/cli-detector.js +63 -0
- package/dist/auditor/cli-detector.js.map +1 -0
- package/dist/auditor/cross-reference.d.ts +4 -0
- package/dist/auditor/cross-reference.d.ts.map +1 -0
- package/dist/auditor/cross-reference.js +16 -0
- package/dist/auditor/cross-reference.js.map +1 -0
- package/dist/auditor/env-auditor.d.ts +9 -0
- package/dist/auditor/env-auditor.d.ts.map +1 -0
- package/dist/auditor/env-auditor.js +83 -0
- package/dist/auditor/env-auditor.js.map +1 -0
- package/dist/auditor/mcp-analyzer.d.ts +11 -0
- package/dist/auditor/mcp-analyzer.d.ts.map +1 -0
- package/dist/auditor/mcp-analyzer.js +145 -0
- package/dist/auditor/mcp-analyzer.js.map +1 -0
- package/dist/auditor/mcp-detector.d.ts +17 -0
- package/dist/auditor/mcp-detector.d.ts.map +1 -0
- package/dist/auditor/mcp-detector.js +86 -0
- package/dist/auditor/mcp-detector.js.map +1 -0
- package/dist/auditor/remediation.d.ts +26 -0
- package/dist/auditor/remediation.d.ts.map +1 -0
- package/dist/auditor/remediation.js +222 -0
- package/dist/auditor/remediation.js.map +1 -0
- package/dist/auditor/tool-detector.d.ts +15 -0
- package/dist/auditor/tool-detector.d.ts.map +1 -0
- package/dist/auditor/tool-detector.js +241 -0
- package/dist/auditor/tool-detector.js.map +1 -0
- package/dist/auditor/types.d.ts +31 -0
- package/dist/auditor/types.d.ts.map +1 -0
- package/dist/auditor/types.js +2 -0
- package/dist/auditor/types.js.map +1 -0
- package/dist/auditor/vscode-extension-scanner.d.ts +8 -0
- package/dist/auditor/vscode-extension-scanner.d.ts.map +1 -0
- package/dist/auditor/vscode-extension-scanner.js +51 -0
- package/dist/auditor/vscode-extension-scanner.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +159 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audit.d.ts +8 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +151 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +34 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/remediate-cli.d.ts +3 -0
- package/dist/commands/remediate-cli.d.ts.map +1 -0
- package/dist/commands/remediate-cli.js +96 -0
- package/dist/commands/remediate-cli.js.map +1 -0
- package/dist/commands/scan.d.ts +11 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +138 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +203 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +235 -0
- package/dist/config.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +69 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/mcp-server/schemas.d.ts +13 -0
- package/dist/mcp-server/schemas.d.ts.map +1 -0
- package/dist/mcp-server/schemas.js +13 -0
- package/dist/mcp-server/schemas.js.map +1 -0
- package/dist/mcp-server/tools/audit-status.d.ts +3 -0
- package/dist/mcp-server/tools/audit-status.d.ts.map +1 -0
- package/dist/mcp-server/tools/audit-status.js +46 -0
- package/dist/mcp-server/tools/audit-status.js.map +1 -0
- package/dist/mcp-server/tools/check-command.d.ts +4 -0
- package/dist/mcp-server/tools/check-command.d.ts.map +1 -0
- package/dist/mcp-server/tools/check-command.js +30 -0
- package/dist/mcp-server/tools/check-command.js.map +1 -0
- package/dist/mcp-server/tools/scan-content.d.ts +4 -0
- package/dist/mcp-server/tools/scan-content.d.ts.map +1 -0
- package/dist/mcp-server/tools/scan-content.js +18 -0
- package/dist/mcp-server/tools/scan-content.js.map +1 -0
- package/dist/mcp-server/tools/scan-file.d.ts +4 -0
- package/dist/mcp-server/tools/scan-file.d.ts.map +1 -0
- package/dist/mcp-server/tools/scan-file.js +48 -0
- package/dist/mcp-server/tools/scan-file.js.map +1 -0
- package/dist/mcp-server/types.d.ts +15 -0
- package/dist/mcp-server/types.d.ts.map +1 -0
- package/dist/mcp-server/types.js +2 -0
- package/dist/mcp-server/types.js.map +1 -0
- package/dist/reporter/audit-report.d.ts +4 -0
- package/dist/reporter/audit-report.d.ts.map +1 -0
- package/dist/reporter/audit-report.js +186 -0
- package/dist/reporter/audit-report.js.map +1 -0
- package/dist/reporter/json-report.d.ts +3 -0
- package/dist/reporter/json-report.d.ts.map +1 -0
- package/dist/reporter/json-report.js +4 -0
- package/dist/reporter/json-report.js.map +1 -0
- package/dist/reporter/remediation-text.d.ts +3 -0
- package/dist/reporter/remediation-text.d.ts.map +1 -0
- package/dist/reporter/remediation-text.js +12 -0
- package/dist/reporter/remediation-text.js.map +1 -0
- package/dist/reporter/risk-scorer.d.ts +8 -0
- package/dist/reporter/risk-scorer.d.ts.map +1 -0
- package/dist/reporter/risk-scorer.js +40 -0
- package/dist/reporter/risk-scorer.js.map +1 -0
- package/dist/reporter/sarif-report.d.ts +3 -0
- package/dist/reporter/sarif-report.d.ts.map +1 -0
- package/dist/reporter/sarif-report.js +80 -0
- package/dist/reporter/sarif-report.js.map +1 -0
- package/dist/reporter/shared.d.ts +11 -0
- package/dist/reporter/shared.d.ts.map +1 -0
- package/dist/reporter/shared.js +85 -0
- package/dist/reporter/shared.js.map +1 -0
- package/dist/reporter/summary-generator.d.ts +16 -0
- package/dist/reporter/summary-generator.d.ts.map +1 -0
- package/dist/reporter/summary-generator.js +89 -0
- package/dist/reporter/summary-generator.js.map +1 -0
- package/dist/reporter/terminal-report.d.ts +4 -0
- package/dist/reporter/terminal-report.d.ts.map +1 -0
- package/dist/reporter/terminal-report.js +135 -0
- package/dist/reporter/terminal-report.js.map +1 -0
- package/dist/rules/crypto-rules.d.ts +3 -0
- package/dist/rules/crypto-rules.d.ts.map +1 -0
- package/dist/rules/crypto-rules.js +252 -0
- package/dist/rules/crypto-rules.js.map +1 -0
- package/dist/rules/default-rules.d.ts +9 -0
- package/dist/rules/default-rules.d.ts.map +1 -0
- package/dist/rules/default-rules.js +1319 -0
- package/dist/rules/default-rules.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +7 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/injection-rules.d.ts +8 -0
- package/dist/rules/injection-rules.d.ts.map +1 -0
- package/dist/rules/injection-rules.js +108 -0
- package/dist/rules/injection-rules.js.map +1 -0
- package/dist/rules/types.d.ts +52 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/scanner/filesystem-scanner.d.ts +26 -0
- package/dist/scanner/filesystem-scanner.d.ts.map +1 -0
- package/dist/scanner/filesystem-scanner.js +369 -0
- package/dist/scanner/filesystem-scanner.js.map +1 -0
- package/dist/scanner/injection-scanner.d.ts +12 -0
- package/dist/scanner/injection-scanner.d.ts.map +1 -0
- package/dist/scanner/injection-scanner.js +136 -0
- package/dist/scanner/injection-scanner.js.map +1 -0
- package/dist/scanner/permission-checker.d.ts +4 -0
- package/dist/scanner/permission-checker.d.ts.map +1 -0
- package/dist/scanner/permission-checker.js +37 -0
- package/dist/scanner/permission-checker.js.map +1 -0
- package/dist/scanner/redact.d.ts +3 -0
- package/dist/scanner/redact.d.ts.map +1 -0
- package/dist/scanner/redact.js +17 -0
- package/dist/scanner/redact.js.map +1 -0
- package/dist/scanner/rule-engine.d.ts +9 -0
- package/dist/scanner/rule-engine.d.ts.map +1 -0
- package/dist/scanner/rule-engine.js +129 -0
- package/dist/scanner/rule-engine.js.map +1 -0
- package/dist/scanner/system-targets.d.ts +17 -0
- package/dist/scanner/system-targets.d.ts.map +1 -0
- package/dist/scanner/system-targets.js +81 -0
- package/dist/scanner/system-targets.js.map +1 -0
- package/dist/tui/App.d.ts +6 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +224 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/components/BootSequence.d.ts +6 -0
- package/dist/tui/components/BootSequence.d.ts.map +1 -0
- package/dist/tui/components/BootSequence.js +40 -0
- package/dist/tui/components/BootSequence.js.map +1 -0
- package/dist/tui/components/BorderedSection.d.ts +12 -0
- package/dist/tui/components/BorderedSection.d.ts.map +1 -0
- package/dist/tui/components/BorderedSection.js +7 -0
- package/dist/tui/components/BorderedSection.js.map +1 -0
- package/dist/tui/components/ErrorBoundary.d.ts +18 -0
- package/dist/tui/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/tui/components/ErrorBoundary.js +36 -0
- package/dist/tui/components/ErrorBoundary.js.map +1 -0
- package/dist/tui/components/FirstUseHint.d.ts +7 -0
- package/dist/tui/components/FirstUseHint.d.ts.map +1 -0
- package/dist/tui/components/FirstUseHint.js +20 -0
- package/dist/tui/components/FirstUseHint.js.map +1 -0
- package/dist/tui/components/Footer.d.ts +10 -0
- package/dist/tui/components/Footer.d.ts.map +1 -0
- package/dist/tui/components/Footer.js +51 -0
- package/dist/tui/components/Footer.js.map +1 -0
- package/dist/tui/components/MetricCard.d.ts +11 -0
- package/dist/tui/components/MetricCard.d.ts.map +1 -0
- package/dist/tui/components/MetricCard.js +8 -0
- package/dist/tui/components/MetricCard.js.map +1 -0
- package/dist/tui/components/Panel.d.ts +15 -0
- package/dist/tui/components/Panel.d.ts.map +1 -0
- package/dist/tui/components/Panel.js +25 -0
- package/dist/tui/components/Panel.js.map +1 -0
- package/dist/tui/components/RemediationMenu.d.ts +10 -0
- package/dist/tui/components/RemediationMenu.d.ts.map +1 -0
- package/dist/tui/components/RemediationMenu.js +84 -0
- package/dist/tui/components/RemediationMenu.js.map +1 -0
- package/dist/tui/components/RiskGauge.d.ts +7 -0
- package/dist/tui/components/RiskGauge.d.ts.map +1 -0
- package/dist/tui/components/RiskGauge.js +55 -0
- package/dist/tui/components/RiskGauge.js.map +1 -0
- package/dist/tui/components/ScrollableList.d.ts +11 -0
- package/dist/tui/components/ScrollableList.d.ts.map +1 -0
- package/dist/tui/components/ScrollableList.js +14 -0
- package/dist/tui/components/ScrollableList.js.map +1 -0
- package/dist/tui/components/Section.d.ts +9 -0
- package/dist/tui/components/Section.d.ts.map +1 -0
- package/dist/tui/components/Section.js +7 -0
- package/dist/tui/components/Section.js.map +1 -0
- package/dist/tui/components/SectionHeader.d.ts +8 -0
- package/dist/tui/components/SectionHeader.d.ts.map +1 -0
- package/dist/tui/components/SectionHeader.js +15 -0
- package/dist/tui/components/SectionHeader.js.map +1 -0
- package/dist/tui/components/SeverityBadge.d.ts +5 -0
- package/dist/tui/components/SeverityBadge.d.ts.map +1 -0
- package/dist/tui/components/SeverityBadge.js +7 -0
- package/dist/tui/components/SeverityBadge.js.map +1 -0
- package/dist/tui/components/Sidebar.d.ts +2 -0
- package/dist/tui/components/Sidebar.d.ts.map +1 -0
- package/dist/tui/components/Sidebar.js +40 -0
- package/dist/tui/components/Sidebar.js.map +1 -0
- package/dist/tui/components/StatusIndicator.d.ts +8 -0
- package/dist/tui/components/StatusIndicator.d.ts.map +1 -0
- package/dist/tui/components/StatusIndicator.js +15 -0
- package/dist/tui/components/StatusIndicator.js.map +1 -0
- package/dist/tui/components/Table.d.ts +21 -0
- package/dist/tui/components/Table.d.ts.map +1 -0
- package/dist/tui/components/Table.js +38 -0
- package/dist/tui/components/Table.js.map +1 -0
- package/dist/tui/components/Transition.d.ts +8 -0
- package/dist/tui/components/Transition.d.ts.map +1 -0
- package/dist/tui/components/Transition.js +38 -0
- package/dist/tui/components/Transition.js.map +1 -0
- package/dist/tui/components/WelcomeScreen.d.ts +6 -0
- package/dist/tui/components/WelcomeScreen.d.ts.map +1 -0
- package/dist/tui/components/WelcomeScreen.js +14 -0
- package/dist/tui/components/WelcomeScreen.js.map +1 -0
- package/dist/tui/educational.d.ts +32 -0
- package/dist/tui/educational.d.ts.map +1 -0
- package/dist/tui/educational.js +117 -0
- package/dist/tui/educational.js.map +1 -0
- package/dist/tui/hooks/useAudit.d.ts +24 -0
- package/dist/tui/hooks/useAudit.d.ts.map +1 -0
- package/dist/tui/hooks/useAudit.js +263 -0
- package/dist/tui/hooks/useAudit.js.map +1 -0
- package/dist/tui/hooks/useConfig.d.ts +18 -0
- package/dist/tui/hooks/useConfig.d.ts.map +1 -0
- package/dist/tui/hooks/useConfig.js +85 -0
- package/dist/tui/hooks/useConfig.js.map +1 -0
- package/dist/tui/hooks/useHookStatus.d.ts +10 -0
- package/dist/tui/hooks/useHookStatus.d.ts.map +1 -0
- package/dist/tui/hooks/useHookStatus.js +59 -0
- package/dist/tui/hooks/useHookStatus.js.map +1 -0
- package/dist/tui/hooks/useLogs.d.ts +42 -0
- package/dist/tui/hooks/useLogs.d.ts.map +1 -0
- package/dist/tui/hooks/useLogs.js +105 -0
- package/dist/tui/hooks/useLogs.js.map +1 -0
- package/dist/tui/hooks/useScan.d.ts +39 -0
- package/dist/tui/hooks/useScan.d.ts.map +1 -0
- package/dist/tui/hooks/useScan.js +255 -0
- package/dist/tui/hooks/useScan.js.map +1 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +10 -0
- package/dist/tui/hooks/useTerminalSize.d.ts.map +1 -0
- package/dist/tui/hooks/useTerminalSize.js +27 -0
- package/dist/tui/hooks/useTerminalSize.js.map +1 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/screens/AuditPanel.d.ts +7 -0
- package/dist/tui/screens/AuditPanel.d.ts.map +1 -0
- package/dist/tui/screens/AuditPanel.js +467 -0
- package/dist/tui/screens/AuditPanel.js.map +1 -0
- package/dist/tui/screens/LogsPanel.d.ts +2 -0
- package/dist/tui/screens/LogsPanel.d.ts.map +1 -0
- package/dist/tui/screens/LogsPanel.js +127 -0
- package/dist/tui/screens/LogsPanel.js.map +1 -0
- package/dist/tui/screens/OverviewPanel.d.ts +2 -0
- package/dist/tui/screens/OverviewPanel.d.ts.map +1 -0
- package/dist/tui/screens/OverviewPanel.js +84 -0
- package/dist/tui/screens/OverviewPanel.js.map +1 -0
- package/dist/tui/screens/ScanPanel.d.ts +2 -0
- package/dist/tui/screens/ScanPanel.d.ts.map +1 -0
- package/dist/tui/screens/ScanPanel.js +188 -0
- package/dist/tui/screens/ScanPanel.js.map +1 -0
- package/dist/tui/screens/ScanResultsPanel.d.ts +2 -0
- package/dist/tui/screens/ScanResultsPanel.d.ts.map +1 -0
- package/dist/tui/screens/ScanResultsPanel.js +394 -0
- package/dist/tui/screens/ScanResultsPanel.js.map +1 -0
- package/dist/tui/screens/SettingsPanel.d.ts +2 -0
- package/dist/tui/screens/SettingsPanel.d.ts.map +1 -0
- package/dist/tui/screens/SettingsPanel.js +353 -0
- package/dist/tui/screens/SettingsPanel.js.map +1 -0
- package/dist/tui/state.d.ts +35 -0
- package/dist/tui/state.d.ts.map +1 -0
- package/dist/tui/state.js +13 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/theme.d.ts +58 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +80 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/hooks/audit-logger.sh +74 -0
- package/hooks/detection-lib.sh +301 -0
- package/hooks/git-pre-commit.sh +195 -0
- package/hooks/git-pre-push.sh +125 -0
- package/hooks/git-safety.sh +152 -0
- package/hooks/security-scanner.sh +527 -0
- package/install.sh +543 -0
- package/package.json +67 -0
- package/patterns/credentials.yaml +317 -0
- package/patterns/dangerous-commands.yaml +167 -0
- package/patterns/pii.yaml +95 -0
- package/patterns/prompt-injection.yaml +131 -0
- package/patterns/supply-chain.yaml +119 -0
- package/rules/AGENTS.md +60 -0
- package/rules/SECURITY-RULES.md +177 -0
- package/rules/claude.md +9 -0
- package/rules/clinerules +29 -0
- package/rules/continuerules +29 -0
- package/rules/copilot-instructions.md +9 -0
- package/rules/cursor-security.mdc +14 -0
- package/rules/gemini.md +9 -0
- package/rules/kiro-security.md +29 -0
- package/rules/roocode-security.md +29 -0
- package/rules/trae-security.md +29 -0
- package/rules/windsurfrules +9 -0
- package/skill/llm-antivirus/SKILL.md +73 -0
- package/skill/llm-antivirus/references/threat-patterns.yaml +82 -0
- package/skill/llm-antivirus/scripts/security-audit.sh +244 -0
- 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
|