mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Refactor command execution and security handling
- Removed security warning detection from CommandExecution component and related tests. - Simplified command parsing logic by consolidating command extraction and validation. - Updated command pattern extraction to handle duplicate patterns gracefully. - Enhanced command parsing to limit extracted patterns to a maximum of three levels. - Removed unused security issue detection functions and related tests. - Improved test coverage for command pattern extraction and validation.
This commit is contained in:
parent
0a5cf463a1
commit
8b4150e017
8 changed files with 221 additions and 714 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useState, memo, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ChevronDown, Skull, AlertTriangle } from "lucide-react"
|
||||
import { ChevronDown, Skull } from "lucide-react"
|
||||
|
||||
import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/types"
|
||||
|
||||
|
|
@ -18,7 +18,6 @@ import {
|
|||
getPatternDescription,
|
||||
parseCommandAndOutput,
|
||||
CommandPattern,
|
||||
detectSecurityIssues,
|
||||
} from "../../utils/commandPatterns"
|
||||
|
||||
interface CommandExecutionProps {
|
||||
|
|
@ -71,11 +70,6 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
return patterns
|
||||
}, [command])
|
||||
|
||||
// Detect security issues in the command
|
||||
const securityWarnings = useMemo(() => {
|
||||
return detectSecurityIssues(command)
|
||||
}, [command])
|
||||
|
||||
// Handle pattern changes
|
||||
const handleAllowPatternChange = (pattern: string) => {
|
||||
const isAllowed = allowedCommands.includes(pattern)
|
||||
|
|
@ -186,21 +180,6 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs">
|
||||
<div className="p-2">
|
||||
<CodeBlock source={command} language="shell" />
|
||||
{securityWarnings.length > 0 && (
|
||||
<div className="mt-2 p-2 bg-yellow-500/10 border border-yellow-500/20 rounded-xs">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="size-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-yellow-500 mb-1">Security Warning</div>
|
||||
{securityWarnings.map((warning, index) => (
|
||||
<div key={index} className="text-vscode-descriptionForeground">
|
||||
{warning.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<OutputContainer isExpanded={isExpanded} output={output} />
|
||||
</div>
|
||||
{commandPatterns.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -365,18 +365,6 @@ Other output here`
|
|||
expect(screen.queryByText("whoami")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should display security warning for commands with subshells", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-security" text="echo $(malicious)" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
// Should show security warning
|
||||
expect(screen.getByText("Security Warning")).toBeInTheDocument()
|
||||
expect(screen.getByText(/subshell execution/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle commands with backtick subshells", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
|
|
|
|||
|
|
@ -1,50 +1,29 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { CommandPatternSelector } from "../CommandPatternSelector"
|
||||
import { CommandPattern } from "../../../utils/commandPatterns"
|
||||
import { TooltipProvider } from "../../../components/ui/tooltip"
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: any) => {
|
||||
if (i18nKey === "chat:commandExecution.commandManagementDescription") {
|
||||
return (
|
||||
<span>
|
||||
Manage command permissions: Click ✓ to allow auto-execution, ✗ to deny execution. Patterns can be
|
||||
toggled on/off or removed from lists. {components.settingsLink}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span>{i18nKey}</span>
|
||||
},
|
||||
Trans: ({ i18nKey, children }: any) => <span>{i18nKey || children}</span>,
|
||||
}))
|
||||
|
||||
// Mock VSCodeLink
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeLink: ({ children, onClick }: any) => (
|
||||
<a href="#" onClick={onClick}>
|
||||
{children || "View all settings"}
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock StandardTooltip
|
||||
vi.mock("../../ui/standard-tooltip", () => ({
|
||||
StandardTooltip: ({ children, content }: any) => (
|
||||
<div title={typeof content === "string" ? content : "tooltip"}>
|
||||
{children}
|
||||
{/* Render the content to make it testable */}
|
||||
<div style={{ display: "none" }}>{content}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock window.postMessage
|
||||
const mockPostMessage = vi.fn()
|
||||
window.postMessage = mockPostMessage
|
||||
// Wrapper component with TooltipProvider
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => <TooltipProvider>{children}</TooltipProvider>
|
||||
|
||||
describe("CommandPatternSelector", () => {
|
||||
const mockPatterns: CommandPattern[] = [
|
||||
|
|
@ -61,192 +40,53 @@ describe("CommandPatternSelector", () => {
|
|||
onDenyPatternChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
it("should render with unique pattern keys", () => {
|
||||
const { container } = render(
|
||||
<TestWrapper>
|
||||
<CommandPatternSelector {...defaultProps} />
|
||||
</TestWrapper>,
|
||||
)
|
||||
|
||||
it("should render collapsed by default", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
// The component should render without errors
|
||||
expect(container).toBeTruthy()
|
||||
|
||||
expect(screen.getByText("chat:commandExecution.manageCommands")).toBeInTheDocument()
|
||||
expect(screen.queryByText("npm commands")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should expand when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
// Click to expand the component
|
||||
const expandButton = screen.getByRole("button", { name: /chat:commandExecution.expandManagement/i })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Check for the patterns themselves
|
||||
// Check that patterns are rendered
|
||||
expect(screen.getByText("npm")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm install")).toBeInTheDocument()
|
||||
expect(screen.getByText("git")).toBeInTheDocument()
|
||||
|
||||
// Check for the descriptions
|
||||
expect(screen.getByText("- npm commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("- npm install commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("- git commands")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should collapse when clicked again", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
it("should handle duplicate patterns gracefully", () => {
|
||||
// Test with duplicate patterns to ensure keys are still unique
|
||||
const duplicatePatterns: CommandPattern[] = [
|
||||
{ pattern: "npm", description: "npm commands" },
|
||||
{ pattern: "npm", description: "duplicate npm commands" }, // Duplicate pattern
|
||||
{ pattern: "git", description: "git commands" },
|
||||
]
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
const collapseButton = screen.getByRole("button", { name: "chat:commandExecution.collapseManagement" })
|
||||
fireEvent.click(collapseButton)
|
||||
|
||||
expect(screen.queryByText("npm commands")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show correct status for patterns", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Check that npm has allowed styling (green)
|
||||
const npmAllowButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromAllowed" })[0]
|
||||
expect(npmAllowButton).toHaveClass("bg-green-500/20")
|
||||
|
||||
// Check that git has denied styling (red)
|
||||
const gitDenyButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromDenied" })[0]
|
||||
expect(gitDenyButton).toHaveClass("bg-red-500/20")
|
||||
})
|
||||
|
||||
it("should call onAllowPatternChange when allow button is clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find all allow buttons with the "add to allowed" label
|
||||
const allowButtons = screen.getAllByRole("button", { name: "chat:commandExecution.addToAllowed" })
|
||||
|
||||
// The second one should be for npm install (first is npm which is already allowed)
|
||||
fireEvent.click(allowButtons[0])
|
||||
|
||||
expect(defaultProps.onAllowPatternChange).toHaveBeenCalledWith("npm install")
|
||||
})
|
||||
|
||||
it("should call onDenyPatternChange when deny button is clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find all deny buttons with the "add to denied" label
|
||||
const denyButtons = screen.getAllByRole("button", { name: "chat:commandExecution.addToDenied" })
|
||||
|
||||
// The second one should be for npm install (first is npm, third is git which is already denied)
|
||||
fireEvent.click(denyButtons[1])
|
||||
|
||||
expect(defaultProps.onDenyPatternChange).toHaveBeenCalledWith("npm install")
|
||||
})
|
||||
|
||||
it("should toggle allowed pattern when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find the allow button for npm (which is already allowed)
|
||||
const npmAllowButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromAllowed" })[0]
|
||||
fireEvent.click(npmAllowButton)
|
||||
|
||||
expect(defaultProps.onAllowPatternChange).toHaveBeenCalledWith("npm")
|
||||
})
|
||||
|
||||
it("should toggle denied pattern when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find the deny button for git (which is already denied)
|
||||
const gitDenyButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromDenied" })[0]
|
||||
fireEvent.click(gitDenyButton)
|
||||
|
||||
expect(defaultProps.onDenyPatternChange).toHaveBeenCalledWith("git")
|
||||
})
|
||||
|
||||
it("should have tooltip with settings link", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// The info icon should have a tooltip
|
||||
const tooltipWrapper = container.querySelector('[title="tooltip"]')
|
||||
expect(tooltipWrapper).toBeTruthy()
|
||||
|
||||
// The tooltip content includes a settings link (mocked as VSCodeLink)
|
||||
// It's rendered in a hidden div for testing purposes
|
||||
const settingsLink = container.querySelector('a[href="#"]')
|
||||
expect(settingsLink).toBeTruthy()
|
||||
expect(settingsLink?.textContent).toBe("View all settings")
|
||||
|
||||
// Test that clicking the link posts the correct message
|
||||
if (settingsLink) {
|
||||
fireEvent.click(settingsLink)
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
values: { section: "autoApprove" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
const props = {
|
||||
...defaultProps,
|
||||
patterns: duplicatePatterns,
|
||||
}
|
||||
})
|
||||
|
||||
it("should render with empty patterns", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} patterns={[]} />)
|
||||
// This should not throw an error even with duplicate patterns
|
||||
const { container } = render(
|
||||
<TestWrapper>
|
||||
<CommandPatternSelector {...props} />
|
||||
</TestWrapper>,
|
||||
)
|
||||
expect(container).toBeTruthy()
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
// Click to expand the component
|
||||
const expandButton = screen.getByRole("button", { name: /chat:commandExecution.expandManagement/i })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// The expanded view should exist but be empty since there are no patterns
|
||||
const expandedContent = screen
|
||||
.getByRole("button", { name: "chat:commandExecution.collapseManagement" })
|
||||
.parentElement?.querySelector(".px-3.pb-3")
|
||||
expect(expandedContent).toBeInTheDocument()
|
||||
expect(expandedContent?.children.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should render patterns without descriptions", () => {
|
||||
const patternsWithoutDesc: CommandPattern[] = [{ pattern: "custom-command" }]
|
||||
|
||||
render(<CommandPatternSelector {...defaultProps} patterns={patternsWithoutDesc} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
expect(screen.getByText("custom-command")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should always show info icon with tooltip", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// Info icon should always be visible (not just when expanded)
|
||||
// Look for the Info icon which is wrapped in StandardTooltip
|
||||
const infoIcon = container.querySelector(".ml-1")
|
||||
expect(infoIcon).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should apply correct classes for chevron rotation", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// Initially collapsed - chevron should be rotated
|
||||
let chevron = container.querySelector(".size-3.transition-transform")
|
||||
expect(chevron).toHaveClass("-rotate-90")
|
||||
|
||||
// Click to expand
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// When expanded - chevron should not be rotated
|
||||
chevron = container.querySelector(".size-3.transition-transform")
|
||||
expect(chevron).toHaveClass("rotate-0")
|
||||
// Both instances of "npm" should be rendered
|
||||
const npmElements = screen.getAllByText("npm")
|
||||
expect(npmElements).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,77 +1,5 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { parseCommandString, extractPatternsFromCommand, detectCommandSecurityIssues } from "../command-parser"
|
||||
|
||||
describe("parseCommandString", () => {
|
||||
it("should parse simple command", () => {
|
||||
const result = parseCommandString("ls -la")
|
||||
expect(result.subCommands).toEqual(["ls -la"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
expect(result.subshellCommands).toEqual([])
|
||||
})
|
||||
|
||||
it("should parse command with && operator", () => {
|
||||
const result = parseCommandString("npm install && npm test")
|
||||
expect(result.subCommands).toEqual(["npm install", "npm test"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should parse command with || operator", () => {
|
||||
const result = parseCommandString("npm test || npm run test:ci")
|
||||
expect(result.subCommands).toEqual(["npm test", "npm run test:ci"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should parse command with pipe", () => {
|
||||
const result = parseCommandString("ls -la | grep test")
|
||||
expect(result.subCommands).toEqual(["ls -la", "grep test"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect and extract subshells with $()", () => {
|
||||
const result = parseCommandString("echo $(date)")
|
||||
expect(result.subCommands).toEqual(["echo", "date"])
|
||||
expect(result.hasSubshells).toBe(true)
|
||||
expect(result.subshellCommands).toEqual(["date"])
|
||||
})
|
||||
|
||||
it("should detect and extract subshells with backticks", () => {
|
||||
const result = parseCommandString("echo `whoami`")
|
||||
expect(result.subCommands).toEqual(["echo", "whoami"])
|
||||
expect(result.hasSubshells).toBe(true)
|
||||
expect(result.subshellCommands).toEqual(["whoami"])
|
||||
})
|
||||
|
||||
it("should handle PowerShell redirections", () => {
|
||||
const result = parseCommandString("command 2>&1")
|
||||
expect(result.subCommands).toEqual(["command 2>&1"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle quoted strings", () => {
|
||||
const result = parseCommandString('echo "hello world"')
|
||||
expect(result.subCommands).toEqual(['echo "hello world"'])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle array indexing expressions", () => {
|
||||
const result = parseCommandString("echo ${array[0]}")
|
||||
expect(result.subCommands).toEqual(["echo ${array[0]}"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle empty command", () => {
|
||||
const result = parseCommandString("")
|
||||
expect(result.subCommands).toEqual([])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
expect(result.subshellCommands).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle complex command with multiple operators", () => {
|
||||
const result = parseCommandString("npm install && npm test | grep success || echo 'failed'")
|
||||
expect(result.subCommands).toEqual(["npm install", "npm test", "grep success", "echo failed"])
|
||||
expect(result.hasSubshells).toBe(false)
|
||||
})
|
||||
})
|
||||
import { extractPatternsFromCommand } from "../command-parser"
|
||||
|
||||
describe("extractPatternsFromCommand", () => {
|
||||
it("should extract simple command pattern", () => {
|
||||
|
|
@ -79,9 +7,9 @@ describe("extractPatternsFromCommand", () => {
|
|||
expect(patterns).toEqual(["ls"])
|
||||
})
|
||||
|
||||
it("should extract command with arguments", () => {
|
||||
const patterns = extractPatternsFromCommand("npm install express")
|
||||
expect(patterns).toEqual(["npm", "npm install", "npm install express"])
|
||||
it("should extract command with subcommand", () => {
|
||||
const patterns = extractPatternsFromCommand("git push origin main")
|
||||
expect(patterns).toEqual(["git", "git push", "git push origin"])
|
||||
})
|
||||
|
||||
it("should stop at flags", () => {
|
||||
|
|
@ -94,17 +22,24 @@ describe("extractPatternsFromCommand", () => {
|
|||
expect(patterns).toEqual(["cd"])
|
||||
})
|
||||
|
||||
it("should handle piped commands", () => {
|
||||
it("should handle pipes", () => {
|
||||
const patterns = extractPatternsFromCommand("ls -la | grep test")
|
||||
expect(patterns).toContain("ls")
|
||||
expect(patterns).toContain("grep")
|
||||
expect(patterns).toContain("grep test")
|
||||
expect(patterns).toEqual(["grep", "grep test", "ls"])
|
||||
})
|
||||
|
||||
it("should remove subshells before extracting patterns", () => {
|
||||
const patterns = extractPatternsFromCommand("echo $(malicious)")
|
||||
expect(patterns).toEqual(["echo"])
|
||||
expect(patterns).not.toContain("malicious")
|
||||
it("should handle && operator", () => {
|
||||
const patterns = extractPatternsFromCommand("npm install && git push origin main")
|
||||
expect(patterns).toEqual(["git", "git push", "git push origin", "npm", "npm install"])
|
||||
})
|
||||
|
||||
it("should handle || operator", () => {
|
||||
const patterns = extractPatternsFromCommand("npm test || npm run test:ci")
|
||||
expect(patterns).toEqual(["npm", "npm run", "npm test"])
|
||||
})
|
||||
|
||||
it("should handle semicolon separator", () => {
|
||||
const patterns = extractPatternsFromCommand("cd src; npm install")
|
||||
expect(patterns).toEqual(["cd", "cd src", "npm", "npm install"])
|
||||
})
|
||||
|
||||
it("should skip numeric commands", () => {
|
||||
|
|
@ -112,50 +47,91 @@ describe("extractPatternsFromCommand", () => {
|
|||
expect(patterns).toEqual([])
|
||||
})
|
||||
|
||||
it("should skip common output words", () => {
|
||||
const patterns = extractPatternsFromCommand("error")
|
||||
expect(patterns).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle empty command", () => {
|
||||
const patterns = extractPatternsFromCommand("")
|
||||
expect(patterns).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle null/undefined", () => {
|
||||
expect(extractPatternsFromCommand(null as any)).toEqual([])
|
||||
expect(extractPatternsFromCommand(undefined as any)).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle scripts", () => {
|
||||
const patterns = extractPatternsFromCommand("./script.sh --verbose")
|
||||
expect(patterns).toEqual(["./script.sh"])
|
||||
})
|
||||
|
||||
it("should handle paths with dots", () => {
|
||||
const patterns = extractPatternsFromCommand("git add .")
|
||||
expect(patterns).toEqual(["git", "git add"])
|
||||
})
|
||||
|
||||
it("should handle paths with tilde", () => {
|
||||
const patterns = extractPatternsFromCommand("cd ~/projects")
|
||||
expect(patterns).toEqual(["cd"])
|
||||
})
|
||||
|
||||
it("should handle colons in arguments", () => {
|
||||
const patterns = extractPatternsFromCommand("docker run image:tag")
|
||||
expect(patterns).toEqual(["docker", "docker run"])
|
||||
})
|
||||
|
||||
it("should return sorted patterns", () => {
|
||||
const patterns = extractPatternsFromCommand("npm run build && git push")
|
||||
expect(patterns).toEqual(["git", "git push", "npm", "npm run", "npm run build"])
|
||||
})
|
||||
|
||||
it("should handle complex command with multiple operators", () => {
|
||||
const patterns = extractPatternsFromCommand("npm install && npm test | grep success || echo 'failed'")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
expect(patterns).toContain("npm test")
|
||||
expect(patterns).toContain("grep")
|
||||
expect(patterns).toContain("echo")
|
||||
})
|
||||
|
||||
it("should handle malformed commands gracefully", () => {
|
||||
const patterns = extractPatternsFromCommand("echo 'unclosed quote")
|
||||
expect(patterns).toContain("echo")
|
||||
})
|
||||
|
||||
it("should not treat package managers specially", () => {
|
||||
const patterns = extractPatternsFromCommand("npm run build")
|
||||
expect(patterns).toEqual(["npm", "npm run", "npm run build"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectCommandSecurityIssues", () => {
|
||||
it("should detect subshell with $()", () => {
|
||||
const warnings = detectCommandSecurityIssues("echo $(malicious)")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
expect(warnings[0].message).toContain("subshell execution")
|
||||
})
|
||||
|
||||
it("should detect subshell with backticks", () => {
|
||||
const warnings = detectCommandSecurityIssues("echo `malicious`")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
expect(warnings[0].message).toContain("subshell execution")
|
||||
})
|
||||
|
||||
it("should detect multiple subshell patterns", () => {
|
||||
const warnings = detectCommandSecurityIssues("echo $(date) && echo `whoami`")
|
||||
expect(warnings).toHaveLength(1) // Still one warning for subshell presence
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
})
|
||||
|
||||
it("should not detect issues in safe commands", () => {
|
||||
const warnings = detectCommandSecurityIssues("npm install express")
|
||||
expect(warnings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle empty command", () => {
|
||||
const warnings = detectCommandSecurityIssues("")
|
||||
expect(warnings).toHaveLength(0)
|
||||
// Now includes "npm run build" with 3-level extraction
|
||||
})
|
||||
|
||||
it("should extract at most 3 levels", () => {
|
||||
const patterns = extractPatternsFromCommand("git push origin main --force")
|
||||
expect(patterns).toEqual(["git", "git push", "git push origin"])
|
||||
// Should NOT include deeper levels beyond 3
|
||||
})
|
||||
|
||||
it("should handle multi-level commands like gh pr", () => {
|
||||
const patterns = extractPatternsFromCommand("gh pr checkout 123")
|
||||
expect(patterns).toEqual(["gh", "gh pr", "gh pr checkout"])
|
||||
})
|
||||
|
||||
it("should extract 3 levels for git remote add", () => {
|
||||
const patterns = extractPatternsFromCommand("git remote add origin https://github.com/user/repo.git")
|
||||
expect(patterns).toEqual(["git", "git remote", "git remote add"])
|
||||
})
|
||||
|
||||
it("should extract 3 levels for npm run build", () => {
|
||||
const patterns = extractPatternsFromCommand("npm run build --production")
|
||||
expect(patterns).toEqual(["npm", "npm run", "npm run build"])
|
||||
})
|
||||
|
||||
it("should stop at file extensions even at third level", () => {
|
||||
const patterns = extractPatternsFromCommand("node scripts test.js")
|
||||
expect(patterns).toEqual(["node", "node scripts"])
|
||||
// Should NOT include "node scripts test.js" because of .js
|
||||
})
|
||||
|
||||
it("should stop at flags at any level", () => {
|
||||
const patterns = extractPatternsFromCommand("docker run -it ubuntu")
|
||||
expect(patterns).toEqual(["docker", "docker run"])
|
||||
// Stops at -it flag
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
extractCommandPatterns,
|
||||
getPatternDescription,
|
||||
parseCommandAndOutput,
|
||||
detectSecurityIssues,
|
||||
} from "../commandPatterns"
|
||||
import { extractCommandPatterns, getPatternDescription, parseCommandAndOutput } from "../commandPatterns"
|
||||
|
||||
describe("extractCommandPatterns", () => {
|
||||
it("should extract simple command", () => {
|
||||
|
|
@ -37,7 +32,7 @@ describe("extractCommandPatterns", () => {
|
|||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm test")
|
||||
expect(patterns).toContain("npm run")
|
||||
expect(patterns).toContain("npm run test:ci")
|
||||
expect(patterns).not.toContain("npm run test:ci")
|
||||
})
|
||||
|
||||
it("should handle semicolon separated commands", () => {
|
||||
|
|
@ -94,12 +89,12 @@ describe("extractCommandPatterns", () => {
|
|||
const patterns = extractCommandPatterns("git add .")
|
||||
expect(patterns).toContain("git")
|
||||
expect(patterns).toContain("git add")
|
||||
expect(patterns).not.toContain("git add .")
|
||||
expect(patterns).not.toContain("git add .") // dot is excluded
|
||||
})
|
||||
|
||||
it("should return sorted patterns", () => {
|
||||
const patterns = extractCommandPatterns("npm run build && git push")
|
||||
expect(patterns).toEqual([...patterns].sort())
|
||||
expect(patterns).toEqual(["git", "git push", "npm", "npm run", "npm run build"])
|
||||
})
|
||||
|
||||
it("should handle numeric input like '0 total'", () => {
|
||||
|
|
@ -354,80 +349,6 @@ drwxr-xr-x 20 user staff 640 Jan 22 11:00 ..`
|
|||
})
|
||||
})
|
||||
|
||||
describe("detectSecurityIssues", () => {
|
||||
it("should detect subshell execution with $()", () => {
|
||||
const warnings = detectSecurityIssues("echo $(malicious)")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
expect(warnings[0].message).toContain("subshell execution")
|
||||
})
|
||||
|
||||
it("should detect subshell execution with backticks", () => {
|
||||
const warnings = detectSecurityIssues("echo `malicious`")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
expect(warnings[0].message).toContain("subshell execution")
|
||||
})
|
||||
|
||||
it("should detect nested subshells", () => {
|
||||
const warnings = detectSecurityIssues("echo $(echo $(date))")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
})
|
||||
|
||||
it("should detect subshells in complex commands", () => {
|
||||
const warnings = detectSecurityIssues("npm install && echo $(whoami) || git push")
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
})
|
||||
|
||||
it("should not detect issues in safe commands", () => {
|
||||
const warnings = detectSecurityIssues("npm install express")
|
||||
expect(warnings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle empty commands", () => {
|
||||
const warnings = detectSecurityIssues("")
|
||||
expect(warnings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect multiple subshell patterns", () => {
|
||||
const warnings = detectSecurityIssues("echo $(date) && echo `whoami`")
|
||||
expect(warnings).toHaveLength(1) // Should still be 1 warning for subshell presence
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
})
|
||||
|
||||
it("should detect subshells in quoted strings", () => {
|
||||
const warnings = detectSecurityIssues('echo "Current user: $(whoami)"')
|
||||
expect(warnings).toHaveLength(1)
|
||||
expect(warnings[0].type).toBe("subshell")
|
||||
})
|
||||
})
|
||||
|
||||
describe("security integration with extractCommandPatterns", () => {
|
||||
it("should not include subshell content in patterns", () => {
|
||||
const patterns = extractCommandPatterns("echo $(malicious)")
|
||||
expect(patterns).toContain("echo")
|
||||
expect(patterns).not.toContain("$(malicious)")
|
||||
expect(patterns).not.toContain("malicious")
|
||||
})
|
||||
|
||||
it("should handle commands with subshells properly", () => {
|
||||
const patterns = extractCommandPatterns("npm install && echo $(whoami)")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
expect(patterns).toContain("echo")
|
||||
expect(patterns).not.toContain("whoami")
|
||||
})
|
||||
|
||||
it("should extract patterns from commands with backtick subshells", () => {
|
||||
const patterns = extractCommandPatterns("git commit -m `date`")
|
||||
expect(patterns).toContain("git")
|
||||
expect(patterns).toContain("git commit")
|
||||
expect(patterns).not.toContain("date")
|
||||
})
|
||||
})
|
||||
|
||||
describe("integration: parseCommandAndOutput with extractCommandPatterns", () => {
|
||||
it("should not extract patterns from output text", () => {
|
||||
const text = `wc -l *.go *.java
|
||||
|
|
|
|||
|
|
@ -1,289 +1,67 @@
|
|||
import { parse } from "shell-quote"
|
||||
|
||||
type ShellToken = string | { op: string } | { command: string }
|
||||
|
||||
/**
|
||||
* Shared command parsing utility that consolidates parsing logic
|
||||
* from both command-validation.ts and commandPatterns.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a command string and handle special cases like subshells,
|
||||
* redirections, and quoted strings.
|
||||
*
|
||||
* @param command - The command string to parse
|
||||
* @returns Object containing parsed information
|
||||
*/
|
||||
export function parseCommandString(command: string): {
|
||||
subCommands: string[]
|
||||
hasSubshells: boolean
|
||||
subshellCommands: string[]
|
||||
} {
|
||||
if (!command?.trim()) {
|
||||
return {
|
||||
subCommands: [],
|
||||
hasSubshells: false,
|
||||
subshellCommands: [],
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// First split by newlines (including all types: \n, \r\n, \r) to handle multi-line commands
|
||||
const lines = command.split(/\r\n|\r|\n/)
|
||||
const allCommands: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (!trimmedLine) continue // Skip empty lines
|
||||
|
||||
// Storage for replaced content
|
||||
const redirections: string[] = []
|
||||
const subshells: string[] = []
|
||||
const quotes: string[] = []
|
||||
const arrayIndexing: string[] = []
|
||||
const arithmeticExpressions: string[] = []
|
||||
const variables: string[] = []
|
||||
|
||||
// First handle PowerShell redirections by temporarily replacing them
|
||||
let processedCommand = trimmedLine.replace(/\d*>&\d*/g, (match) => {
|
||||
redirections.push(match)
|
||||
return `__REDIR_${redirections.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle arithmetic expressions: $((...)) pattern
|
||||
// Match the entire arithmetic expression including nested parentheses
|
||||
processedCommand = processedCommand.replace(/\$\(\([^)]*(?:\)[^)]*)*\)\)/g, (match) => {
|
||||
arithmeticExpressions.push(match)
|
||||
return `__ARITH_${arithmeticExpressions.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle array indexing expressions: ${array[...]} pattern and partial expressions
|
||||
processedCommand = processedCommand.replace(/\$\{[^}]*\[[^\]]*(\]([^}]*\})?)?/g, (match) => {
|
||||
arrayIndexing.push(match)
|
||||
return `__ARRAY_${arrayIndexing.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle simple variable references: $varname pattern
|
||||
// This prevents shell-quote from splitting $count into separate tokens
|
||||
processedCommand = processedCommand.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, (match) => {
|
||||
variables.push(match)
|
||||
return `__VAR_${variables.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle special bash variables: $?, $!, $#, $$, $@, $*, $-, $0-$9
|
||||
processedCommand = processedCommand.replace(/\$[?!#$@*\-0-9]/g, (match) => {
|
||||
variables.push(match)
|
||||
return `__VAR_${variables.length - 1}__`
|
||||
})
|
||||
|
||||
// Then handle subshell commands - store them for security analysis
|
||||
const _hasSubshells = trimmedLine.includes("$(") || trimmedLine.includes("`")
|
||||
|
||||
processedCommand = processedCommand
|
||||
.replace(/\$\(((?!\().*?)\)/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
.replace(/`(.*?)`/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
|
||||
// Then handle quoted strings
|
||||
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
|
||||
quotes.push(match)
|
||||
return `__QUOTE_${quotes.length - 1}__`
|
||||
})
|
||||
|
||||
const tokens = parse(processedCommand) as ShellToken[]
|
||||
const commands: string[] = []
|
||||
let currentCommand: string[] = []
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
|
||||
if (typeof token === "object" && "op" in token) {
|
||||
// Chain operator - split command
|
||||
if (["&&", "||", ";", "|"].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
} else {
|
||||
// Other operators (>, &) are part of the command
|
||||
currentCommand.push(token.op)
|
||||
}
|
||||
} else if (typeof token === "string") {
|
||||
// Check if it's a subshell placeholder
|
||||
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
|
||||
if (subshellMatch) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
commands.push(subshells[parseInt(subshellMatch[1])])
|
||||
} else {
|
||||
currentCommand.push(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining command
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
}
|
||||
|
||||
// Restore quotes, redirections, arithmetic expressions, variables, and array indexing
|
||||
const restoredCommands = commands.map((cmd) => {
|
||||
let result = cmd
|
||||
// Restore quotes
|
||||
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
|
||||
// Restore redirections
|
||||
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
|
||||
// Restore arithmetic expressions
|
||||
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
|
||||
// Restore variables
|
||||
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
|
||||
// Restore array indexing expressions
|
||||
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
|
||||
return result
|
||||
})
|
||||
|
||||
allCommands.push(...restoredCommands)
|
||||
}
|
||||
|
||||
// Check if any line has subshells
|
||||
const hasSubshells = command.includes("$(") || command.includes("`")
|
||||
const subshellCommands: string[] = []
|
||||
|
||||
// Extract subshell commands for security analysis
|
||||
let match: RegExpExecArray | null
|
||||
const subshellRegex1 = /\$\(((?!\().*?)\)/g
|
||||
const subshellRegex2 = /`(.*?)`/g
|
||||
|
||||
while ((match = subshellRegex1.exec(command)) !== null) {
|
||||
if (match[1]) {
|
||||
subshellCommands.push(match[1].trim())
|
||||
}
|
||||
}
|
||||
|
||||
while ((match = subshellRegex2.exec(command)) !== null) {
|
||||
if (match[1]) {
|
||||
subshellCommands.push(match[1].trim())
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subCommands: allCommands,
|
||||
hasSubshells,
|
||||
subshellCommands,
|
||||
}
|
||||
} catch (_error) {
|
||||
// If shell-quote fails, fall back to simple splitting
|
||||
const fallbackCommands = command
|
||||
.split(/\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
return {
|
||||
subCommands: fallbackCommands.length > 0 ? fallbackCommands : [command],
|
||||
hasSubshells: command.includes("$(") || command.includes("`"),
|
||||
subshellCommands: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract command patterns for permission management.
|
||||
* This is a simplified version that focuses on extracting
|
||||
* the main command and its subcommands for pattern matching.
|
||||
*
|
||||
* @param command - The command string to extract patterns from
|
||||
* @returns Array of command patterns
|
||||
* Extract command patterns from a command string.
|
||||
* Returns at most 3 levels: base command, command + first argument, and command + first two arguments.
|
||||
* Stops at flags (-), paths (/\~), file extensions (.ext), or special characters (:).
|
||||
*/
|
||||
export function extractPatternsFromCommand(command: string): string[] {
|
||||
if (!command?.trim()) return []
|
||||
|
||||
// First, remove subshells for security - we don't want to extract patterns from subshell contents
|
||||
const cleanedCommand = command
|
||||
.replace(/\$\([^)]*\)/g, "") // Remove $() subshells
|
||||
.replace(/`[^`]*`/g, "") // Remove backtick subshells
|
||||
|
||||
const patterns = new Set<string>()
|
||||
const parsed = parse(cleanedCommand) as ShellToken[]
|
||||
|
||||
const commandSeparators = new Set(["|", "&&", "||", ";"])
|
||||
let current: string[] = []
|
||||
try {
|
||||
const parsed = parse(command)
|
||||
const commandSeparators = new Set(["|", "&&", "||", ";"])
|
||||
let currentTokens: string[] = []
|
||||
|
||||
for (const token of parsed) {
|
||||
if (typeof token === "object" && "op" in token && commandSeparators.has(token.op)) {
|
||||
if (current.length) processCommandForPatterns(current, patterns)
|
||||
current = []
|
||||
} else {
|
||||
current.push(String(token))
|
||||
for (const token of parsed) {
|
||||
if (typeof token === "object" && "op" in token && commandSeparators.has(token.op)) {
|
||||
// Process accumulated tokens as a command
|
||||
if (currentTokens.length > 0) {
|
||||
extractFromTokens(currentTokens, patterns)
|
||||
currentTokens = []
|
||||
}
|
||||
} else if (typeof token === "string") {
|
||||
currentTokens.push(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length) processCommandForPatterns(current, patterns)
|
||||
// Process any remaining tokens
|
||||
if (currentTokens.length > 0) {
|
||||
extractFromTokens(currentTokens, patterns)
|
||||
}
|
||||
} catch (_error) {
|
||||
// Fallback: just extract the first word
|
||||
const firstWord = command.trim().split(/\s+/)[0]
|
||||
if (firstWord) patterns.add(firstWord)
|
||||
}
|
||||
|
||||
return Array.from(patterns).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single command to extract patterns
|
||||
*/
|
||||
function processCommandForPatterns(cmd: string[], patterns: Set<string>): void {
|
||||
if (!cmd.length || typeof cmd[0] !== "string") return
|
||||
function isValidToken(token: string): boolean {
|
||||
return !!token && !token.match(/[/\\~:]/) && token !== "." && !token.match(/\.\w+$/)
|
||||
}
|
||||
|
||||
const mainCmd = cmd[0]
|
||||
function extractFromTokens(tokens: string[], patterns: Set<string>): void {
|
||||
if (tokens.length === 0) return
|
||||
|
||||
// Skip if it's just a number (like "0" from "0 total")
|
||||
const mainCmd = tokens[0]
|
||||
|
||||
// Skip numeric commands like "0" from "0 total"
|
||||
if (/^\d+$/.test(mainCmd)) return
|
||||
|
||||
// Skip common output patterns that aren't commands
|
||||
const skipWords = ["total", "error", "warning", "failed", "success", "done"]
|
||||
if (skipWords.includes(mainCmd.toLowerCase())) return
|
||||
// Build patterns progressively up to 3 levels
|
||||
let pattern = mainCmd
|
||||
patterns.add(pattern)
|
||||
|
||||
patterns.add(mainCmd)
|
||||
|
||||
const breakingExps = [/^-/, /[\\/.~]/]
|
||||
|
||||
for (let i = 1; i < cmd.length; i++) {
|
||||
const arg = cmd[i]
|
||||
|
||||
if (typeof arg !== "string" || breakingExps.some((re) => re.test(arg))) break
|
||||
|
||||
const pattern = cmd.slice(0, i + 1).join(" ")
|
||||
patterns.add(pattern)
|
||||
for (let i = 1; i < Math.min(tokens.length, 3); i++) {
|
||||
if (isValidToken(tokens[i])) {
|
||||
pattern += ` ${tokens[i]}`
|
||||
patterns.add(pattern)
|
||||
} else {
|
||||
break // Stop at first invalid token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Security analysis for commands
|
||||
*/
|
||||
export interface SecurityWarning {
|
||||
type: "subshell" | "injection"
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect security issues in a command
|
||||
*
|
||||
* @param command - The command to analyze
|
||||
* @returns Array of security warnings
|
||||
*/
|
||||
export function detectCommandSecurityIssues(command: string): SecurityWarning[] {
|
||||
const warnings: SecurityWarning[] = []
|
||||
|
||||
// Check for subshell execution attempts
|
||||
if (command.includes("$(") || command.includes("`")) {
|
||||
warnings.push({
|
||||
type: "subshell",
|
||||
message: "Command contains subshell execution which could bypass restrictions",
|
||||
})
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseCommandString } from "./command-parser"
|
||||
import { parse } from "shell-quote"
|
||||
|
||||
/**
|
||||
* # Command Denylist Feature - Longest Prefix Match Strategy
|
||||
|
|
@ -68,8 +68,40 @@ import { parseCommandString } from "./command-parser"
|
|||
* - Newlines as command separators
|
||||
*/
|
||||
export function parseCommand(command: string): string[] {
|
||||
const { subCommands } = parseCommandString(command)
|
||||
return subCommands
|
||||
if (!command?.trim()) return []
|
||||
|
||||
try {
|
||||
const parsed = parse(command)
|
||||
const commands: string[] = []
|
||||
let currentCommand: string[] = []
|
||||
|
||||
for (const token of parsed) {
|
||||
if (typeof token === "object" && "op" in token) {
|
||||
// Chain operator - split command
|
||||
if (["&&", "||", ";", "|"].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
} else {
|
||||
// Other operators are part of the command
|
||||
currentCommand.push(token.op)
|
||||
}
|
||||
} else if (typeof token === "string") {
|
||||
currentCommand.push(token)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining command
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
}
|
||||
|
||||
return commands
|
||||
} catch (_error) {
|
||||
// If shell-quote fails, fall back to simple splitting
|
||||
return [command]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
import { extractPatternsFromCommand, detectCommandSecurityIssues, SecurityWarning } from "./command-parser"
|
||||
import { extractPatternsFromCommand } from "./command-parser"
|
||||
|
||||
export interface CommandPattern {
|
||||
pattern: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// Re-export SecurityWarning type from command-parser
|
||||
export type { SecurityWarning }
|
||||
|
||||
export function extractCommandPatterns(command: string): string[] {
|
||||
return extractPatternsFromCommand(command)
|
||||
}
|
||||
|
||||
export function detectSecurityIssues(command: string): SecurityWarning[] {
|
||||
return detectCommandSecurityIssues(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable description for a command pattern.
|
||||
* Simply returns the pattern followed by "commands".
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue