Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 36 additions & 162 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,36 +137,36 @@ async function promptConfirmation(command: string, reason: string): Promise<bool

async function checkAndAuditCommand(command: string, config: any, source: "check" | "paste" | "stdin"): Promise<boolean> {
const result = checkDestructive(command);
if (!result.blocked) {
logAudit(command, result, { source, mode: config?.mode, threshold: config?.threshold, decision: "allowed" });
return true;
}

if (config?.mode === "permissive") {
const warningHeader = `⚠️ ShellShield WARNING: Command '${command}' would be blocked in enforce mode.`;
console.error(
`${warningHeader}\n` +
`Reason: ${result.reason}\n` +
`Suggestion: ${result.suggestion}`
);
logAudit(command, { ...result, blocked: false }, { source, mode: config?.mode, threshold: config?.threshold, decision: "warn" });
return true;
}

if (config?.mode === "interactive") {
const confirmed = await promptConfirmation(command, result.reason);
if (confirmed) {
logAudit(command, { ...result, blocked: false }, { source, mode: config?.mode, threshold: config?.threshold, decision: "approved" });
const msg = "Approved. Command will execute.";
const tty = process.stderr.isTTY;
console.error(tty ? `\x1b[32m${msg}\x1b[0m` : msg);
if (result.blocked) {
if (config?.mode === "permissive") {
const warningHeader = `⚠️ ShellShield WARNING: Command '${command}' would be blocked in enforce mode.`;
console.error(
`${warningHeader}\n` +
`Reason: ${result.reason}\n` +
`Suggestion: ${result.suggestion}`
);
logAudit(command, { ...result, blocked: false }, { source, mode: config?.mode, threshold: config?.threshold, decision: "warn" });
return true;
}

if (config?.mode === "interactive") {
const confirmed = await promptConfirmation(command, result.reason);
if (confirmed) {
logAudit(command, { ...result, blocked: false }, { source, mode: config?.mode, threshold: config?.threshold, decision: "approved" });
const msg = "Approved. Command will execute.";
const tty = process.stderr.isTTY;
console.error(tty ? `\x1b[32m${msg}\x1b[0m` : msg);
return true;
}
}

logAudit(command, result, { source, mode: config?.mode, threshold: config?.threshold, decision: "blocked" });
showBlockedMessage(result.reason, result.suggestion);
return false;
}

logAudit(command, result, { source, mode: config?.mode, threshold: config?.threshold, decision: "blocked" });
showBlockedMessage(result.reason, result.suggestion);
return false;
logAudit(command, result, { source, mode: config?.mode, threshold: config?.threshold, decision: "allowed" });
return true;
}

async function handleCheck(args: string[], config: any): Promise<void> {
Expand Down Expand Up @@ -271,148 +271,22 @@ function handleScore(args: string[], config: any): void {
process.exit(0);
}

import { SHELL_TEMPLATES } from "./integrations/templates";

function handleInit(): void {
const shellPath = process.env.SHELL || "";
const fallbackShell =
!shellPath && (process.env.PSModulePath || process.env.ComSpec) ? "powershell" : "bash";
const shellNameRaw = shellPath.split(/[\\/]/).pop() || fallbackShell;
const shellName = shellNameRaw.replace(/\.exe$/i, "").toLowerCase();
if (shellName === "zsh") {
console.log(`
# ShellShield Zsh Integration
_shellshield_accept_line() {
if [[ -n "$SHELLSHIELD_SKIP" ]]; then
zle .accept-line
return
fi
if command -v bun >/dev/null 2>&1; then
bun run "${process.argv[1]}" --check "$BUFFER" || return $?
fi
zle .accept-line
}
zle -N accept-line _shellshield_accept_line
autoload -Uz add-zsh-hook
add-zsh-hook -d preexec _shellshield_preexec 2>/dev/null
unfunction _shellshield_preexec 2>/dev/null

# Optional: auto-refresh alias/function context snapshot
# Enable by setting: export SHELLSHIELD_AUTO_SNAPSHOT=1
if [[ "$SHELLSHIELD_AUTO_SNAPSHOT" == "1" ]]; then
if [[ -z "$SHELLSHIELD_CONTEXT_PATH" ]]; then
export SHELLSHIELD_CONTEXT_PATH="$HOME/.shellshield/shell-context.json"
fi
if [[ -z "$_SHELLSHIELD_CONTEXT_SYNCED" ]]; then
export _SHELLSHIELD_CONTEXT_SYNCED=1
if command -v bun >/dev/null 2>&1; then
bun run "${process.argv[1]}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
fi
fi
fi

# Optional: bracketed paste safety (zsh only)
# Enable by setting: export SHELLSHIELD_PASTE_HOOK=1
if [[ "$SHELLSHIELD_PASTE_HOOK" == "1" ]]; then
_shellshield_bracketed_paste() {
local before_left="$LBUFFER"
local before_right="$RBUFFER"
zle .bracketed-paste
local pasted="\${LBUFFER#$before_left}"
if [[ -n "$pasted" ]]; then
if command -v bun >/dev/null 2>&1; then
printf "%s" "$pasted" | bun run "${process.argv[1]}" --paste || {
LBUFFER="$before_left"
RBUFFER="$before_right"
return 1
}
fi
fi
}
zle -N bracketed-paste _shellshield_bracketed_paste
fi
`);
} else if (shellName === "fish") {
console.log(`
# ShellShield Fish Integration
function __shellshield_preexec --on-event fish_preexec
if test -n "$SHELLSHIELD_SKIP"
return
end
if type -q bun
set -l cmd $argv
if test (count $cmd) -gt 1
set -l cmd (string join " " -- $cmd)
end
if test -n "$cmd"
bun run "${process.argv[1]}" --check "$cmd"; or return $status
end
end
end

# Optional: auto-refresh alias/function context snapshot
# Enable by setting: set -gx SHELLSHIELD_AUTO_SNAPSHOT 1
if test "$SHELLSHIELD_AUTO_SNAPSHOT" = "1"
if test -z "$SHELLSHIELD_CONTEXT_PATH"
set -gx SHELLSHIELD_CONTEXT_PATH "$HOME/.shellshield/shell-context.json"
end
if test -z "$_SHELLSHIELD_CONTEXT_SYNCED"
set -gx _SHELLSHIELD_CONTEXT_SYNCED 1
if type -q bun
bun run "${process.argv[1]}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
end
end
end
`);
} else if (shellName === "pwsh" || shellName === "powershell") {
console.log(`
# ShellShield PowerShell Integration
if (Get-Command Set-PSReadLineKeyHandler -ErrorAction SilentlyContinue) {
Set-PSReadLineKeyHandler -Key Enter -ScriptBlock {
param($key, $arg)
if ($env:SHELLSHIELD_SKIP) {
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
return
}
if (Get-Command bun -ErrorAction SilentlyContinue) {
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($line) {
bun run "${process.argv[1]}" --check $line
if ($LASTEXITCODE -ne 0) { return }
}
}
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
} else {
Write-Host "PSReadLine not available; cannot hook Enter key."
}
`);
} else {
console.log(`
# ShellShield Bash Integration
_shellshield_bash_preexec() {
if [[ -n "$SHELLSHIELD_SKIP" ]]; then return 0; fi
if command -v bun >/dev/null 2>&1; then
bun run "${process.argv[1]}" --check "$BASH_COMMAND" || return $?
fi
}
trap '_shellshield_bash_preexec' DEBUG

# Optional: auto-refresh alias/function context snapshot
# Enable by setting: export SHELLSHIELD_AUTO_SNAPSHOT=1
if [[ "$SHELLSHIELD_AUTO_SNAPSHOT" == "1" ]]; then
if [[ -z "$SHELLSHIELD_CONTEXT_PATH" ]]; then
export SHELLSHIELD_CONTEXT_PATH="$HOME/.shellshield/shell-context.json"
fi
if [[ -z "$_SHELLSHIELD_CONTEXT_SYNCED" ]]; then
export _SHELLSHIELD_CONTEXT_SYNCED=1
if command -v bun >/dev/null 2>&1; then
bun run "${process.argv[1]}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
fi
fi
fi
`);
}

let templateKey = "bash";
if (shellName === "zsh") templateKey = "zsh";
else if (shellName === "fish") templateKey = "fish";
else if (shellName === "pwsh" || shellName === "powershell") templateKey = "powershell";

const template = SHELL_TEMPLATES[templateKey] || SHELL_TEMPLATES.bash;
console.log(template.replaceAll("{{CLI_PATH}}", process.argv[1]));
process.exit(0);
}

Expand Down
129 changes: 129 additions & 0 deletions src/integrations/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
export const SHELL_TEMPLATES: Record<string, string> = {
zsh: `
# ShellShield Zsh Integration
_shellshield_accept_line() {
if [ -n "$SHELLSHIELD_SKIP" ]; then
zle .accept-line
return
fi
if command -v bun >/dev/null 2>&1; then
bun run "{{CLI_PATH}}" --check "$BUFFER" || return $?
fi
zle .accept-line
}
zle -N accept-line _shellshield_accept_line
autoload -Uz add-zsh-hook
add-zsh-hook -d preexec _shellshield_preexec 2>/dev/null
unfunction _shellshield_preexec 2>/dev/null

# Optional: auto-refresh alias/function context snapshot
if [ "$SHELLSHIELD_AUTO_SNAPSHOT" = "1" ]; then
if [ -z "$SHELLSHIELD_CONTEXT_PATH" ]; then
export SHELLSHIELD_CONTEXT_PATH="$HOME/.shellshield/shell-context.json"
fi
if [ -z "$_SHELLSHIELD_CONTEXT_SYNCED" ]; then
export _SHELLSHIELD_CONTEXT_SYNCED=1
if command -v bun >/dev/null 2>&1; then
bun run "{{CLI_PATH}}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
fi
fi
fi

# Optional: bracketed paste safety
if [ "$SHELLSHIELD_PASTE_HOOK" = "1" ]; then
_shellshield_bracketed_paste() {
local before_left="$LBUFFER"
local before_right="$RBUFFER"
zle .bracketed-paste
local pasted="\${LBUFFER#$before_left}"
if [ -n "$pasted" ]; then
if command -v bun >/dev/null 2>&1; then
printf "%s" "$pasted" | bun run "{{CLI_PATH}}" --paste || {
LBUFFER="$before_left"
RBUFFER="$before_right"
return 1
}
fi
fi
}
zle -N bracketed-paste _shellshield_bracketed_paste
fi
`,
fish: `
# ShellShield Fish Integration
function __shellshield_preexec --on-event fish_preexec
if test -n "$SHELLSHIELD_SKIP"
return
end
if type -q bun
set -l cmd $argv
if test (count $cmd) -gt 1
set -l cmd (string join " " -- $cmd)
end
if test -n "$cmd"
bun run "{{CLI_PATH}}" --check "$cmd"; or return $status
end
end
end

# Optional: auto-refresh alias/function context snapshot
if test "$SHELLSHIELD_AUTO_SNAPSHOT" = "1"
if test -z "$SHELLSHIELD_CONTEXT_PATH"
set -gx SHELLSHIELD_CONTEXT_PATH "$HOME/.shellshield/shell-context.json"
end
if test -z "$_SHELLSHIELD_CONTEXT_SYNCED"
set -gx _SHELLSHIELD_CONTEXT_SYNCED 1
if type -q bun
bun run "{{CLI_PATH}}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
end
end
end
`,
bash: `
# ShellShield Bash Integration
_shellshield_bash_preexec() {
if [ -n "$SHELLSHIELD_SKIP" ]; then return 0; fi
if command -v bun >/dev/null 2>&1; then
bun run "{{CLI_PATH}}" --check "$BASH_COMMAND" || return $?
fi
}
trap '_shellshield_bash_preexec' DEBUG

# Optional: auto-refresh alias/function context snapshot
if [ "$SHELLSHIELD_AUTO_SNAPSHOT" = "1" ]; then
if [ -z "$SHELLSHIELD_CONTEXT_PATH" ]; then
export SHELLSHIELD_CONTEXT_PATH="$HOME/.shellshield/shell-context.json"
fi
if [ -z "$_SHELLSHIELD_CONTEXT_SYNCED" ]; then
export _SHELLSHIELD_CONTEXT_SYNCED=1
if command -v bun >/dev/null 2>&1; then
bun run "{{CLI_PATH}}" --snapshot --out "$SHELLSHIELD_CONTEXT_PATH" >/dev/null 2>&1
fi
fi
fi
`,
powershell: `
# ShellShield PowerShell Integration
if (Get-Command Set-PSReadLineKeyHandler -ErrorAction SilentlyContinue) {
Set-PSReadLineKeyHandler -Key Enter -ScriptBlock {
param($key, $arg)
if ($env:SHELLSHIELD_SKIP) {
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
return
}
if (Get-Command bun -ErrorAction SilentlyContinue) {
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($line) {
bun run "{{CLI_PATH}}" --check $line
if ($LASTEXITCODE -ne 0) { return }
}
}
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
} else {
Write-Host "PSReadLine not available; cannot hook Enter key."
}
`,
};
14 changes: 8 additions & 6 deletions src/parser/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export function checkDestructive(
};

for (const rule of rules) {
if (rule.phase !== "pre") continue;
const result = annotateRule(rule.name, rule.check(stringContext));
if (result?.blocked) return result;
if (rule.phase === "pre") {
const result = annotateRule(rule.name, rule.check(stringContext));
if (result?.blocked) return result;
}
}

// 2. Parse Command
Expand All @@ -102,9 +103,10 @@ export function checkDestructive(
};

for (const rule of rules) {
if (rule.phase !== "post") continue;
const result = annotateRule(rule.name, rule.check(fullContext));
if (result?.blocked) return result;
if (rule.phase === "post") {
const result = annotateRule(rule.name, rule.check(fullContext));
if (result?.blocked) return result;
}
}

return { blocked: false };
Expand Down
Loading