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,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
|