@butlerw/vellum 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 +411 -0
- package/__fixtures__/responses/code-generation.json +42 -0
- package/__fixtures__/responses/error-response.json +20 -0
- package/__fixtures__/responses/hello-world.json +32 -0
- package/dist/auth-6MCXESOH.js +26 -0
- package/dist/chunk-SECXJGWA.js +597 -0
- package/dist/index.js +34023 -0
- package/package.json +67 -0
- package/src/__tests__/commands.e2e.test.ts +728 -0
- package/src/__tests__/credentials.test.ts +713 -0
- package/src/__tests__/mode-e2e.test.ts +391 -0
- package/src/__tests__/tui-integration.test.tsx +1271 -0
- package/src/agents/__tests__/task-persistence.test.ts +235 -0
- package/src/agents/commands/delegate.ts +240 -0
- package/src/agents/commands/index.ts +10 -0
- package/src/agents/commands/resume.ts +335 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/task-persistence.ts +272 -0
- package/src/agents/task-resumption.ts +242 -0
- package/src/app.tsx +4737 -0
- package/src/commands/__tests__/.gitkeep +1 -0
- package/src/commands/__tests__/agents.test.ts +606 -0
- package/src/commands/__tests__/auth.test.ts +626 -0
- package/src/commands/__tests__/autocomplete.test.ts +683 -0
- package/src/commands/__tests__/batch.test.ts +287 -0
- package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
- package/src/commands/__tests__/completion.test.ts +238 -0
- package/src/commands/__tests__/core.test.ts +363 -0
- package/src/commands/__tests__/executor.test.ts +496 -0
- package/src/commands/__tests__/exit-codes.test.ts +220 -0
- package/src/commands/__tests__/init.test.ts +243 -0
- package/src/commands/__tests__/language.test.ts +353 -0
- package/src/commands/__tests__/mode-cli.test.ts +667 -0
- package/src/commands/__tests__/model.test.ts +277 -0
- package/src/commands/__tests__/parser.test.ts +493 -0
- package/src/commands/__tests__/performance.bench.ts +380 -0
- package/src/commands/__tests__/registry.test.ts +534 -0
- package/src/commands/__tests__/resume.test.ts +449 -0
- package/src/commands/__tests__/security.test.ts +845 -0
- package/src/commands/__tests__/stream-json.test.ts +372 -0
- package/src/commands/__tests__/user-commands.test.ts +597 -0
- package/src/commands/adapters.ts +267 -0
- package/src/commands/agent.ts +395 -0
- package/src/commands/agents/generate.ts +506 -0
- package/src/commands/agents/index.ts +272 -0
- package/src/commands/agents/show.ts +271 -0
- package/src/commands/agents/validate.ts +387 -0
- package/src/commands/auth.ts +883 -0
- package/src/commands/autocomplete.ts +480 -0
- package/src/commands/batch/command.ts +388 -0
- package/src/commands/batch/executor.ts +361 -0
- package/src/commands/batch/index.ts +12 -0
- package/src/commands/commit.ts +235 -0
- package/src/commands/completion/index.ts +371 -0
- package/src/commands/condense.ts +191 -0
- package/src/commands/config.ts +344 -0
- package/src/commands/context-provider.ts +173 -0
- package/src/commands/copy.ts +329 -0
- package/src/commands/core/clear.ts +38 -0
- package/src/commands/core/exit.ts +43 -0
- package/src/commands/core/help.ts +354 -0
- package/src/commands/core/index.ts +15 -0
- package/src/commands/cost.ts +179 -0
- package/src/commands/credentials.tsx +618 -0
- package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
- package/src/commands/custom-agents/create.ts +377 -0
- package/src/commands/custom-agents/export.ts +135 -0
- package/src/commands/custom-agents/import.ts +199 -0
- package/src/commands/custom-agents/index.ts +372 -0
- package/src/commands/custom-agents/info.ts +318 -0
- package/src/commands/custom-agents/list.ts +267 -0
- package/src/commands/custom-agents/validate.ts +388 -0
- package/src/commands/diff-mode.ts +241 -0
- package/src/commands/env.ts +53 -0
- package/src/commands/executor.ts +579 -0
- package/src/commands/exit-codes.ts +202 -0
- package/src/commands/index.ts +701 -0
- package/src/commands/init/index.ts +15 -0
- package/src/commands/init/prompts.ts +366 -0
- package/src/commands/init/templates/commands-readme.md +80 -0
- package/src/commands/init/templates/example-command.md +79 -0
- package/src/commands/init/templates/example-skill.md +168 -0
- package/src/commands/init/templates/example-workflow.md +101 -0
- package/src/commands/init/templates/prompts-readme.md +52 -0
- package/src/commands/init/templates/rules-readme.md +63 -0
- package/src/commands/init/templates/skills-readme.md +83 -0
- package/src/commands/init/templates/workflows-readme.md +94 -0
- package/src/commands/init.ts +391 -0
- package/src/commands/install.ts +90 -0
- package/src/commands/language.ts +191 -0
- package/src/commands/loaders/.gitkeep +1 -0
- package/src/commands/lsp.ts +199 -0
- package/src/commands/markdown-commands.ts +253 -0
- package/src/commands/mcp.ts +588 -0
- package/src/commands/memory/export.ts +341 -0
- package/src/commands/memory/index.ts +148 -0
- package/src/commands/memory/list.ts +261 -0
- package/src/commands/memory/search.ts +346 -0
- package/src/commands/memory/utils.ts +15 -0
- package/src/commands/metrics.ts +75 -0
- package/src/commands/migrate/index.ts +16 -0
- package/src/commands/migrate/prompts.ts +477 -0
- package/src/commands/mode.ts +331 -0
- package/src/commands/model.ts +298 -0
- package/src/commands/onboard.ts +205 -0
- package/src/commands/open.ts +169 -0
- package/src/commands/output/stream-json.ts +373 -0
- package/src/commands/parser/chain-parser.ts +370 -0
- package/src/commands/parser/index.ts +29 -0
- package/src/commands/parser/pipe-parser.ts +480 -0
- package/src/commands/parser.ts +588 -0
- package/src/commands/persistence.ts +355 -0
- package/src/commands/progress.ts +18 -0
- package/src/commands/prompt/index.ts +17 -0
- package/src/commands/prompt/validate.ts +621 -0
- package/src/commands/prompt-priority.ts +401 -0
- package/src/commands/registry.ts +374 -0
- package/src/commands/sandbox/index.ts +131 -0
- package/src/commands/security/index.ts +21 -0
- package/src/commands/security/input-sanitizer.ts +168 -0
- package/src/commands/security/permission-checker.ts +456 -0
- package/src/commands/security/sensitive-data.ts +350 -0
- package/src/commands/session/delete.ts +38 -0
- package/src/commands/session/export.ts +39 -0
- package/src/commands/session/index.ts +26 -0
- package/src/commands/session/list.ts +26 -0
- package/src/commands/session/resume.ts +562 -0
- package/src/commands/session/search.ts +434 -0
- package/src/commands/session/show.ts +26 -0
- package/src/commands/settings.ts +368 -0
- package/src/commands/setup.ts +23 -0
- package/src/commands/shell/index.ts +16 -0
- package/src/commands/shell/setup.ts +422 -0
- package/src/commands/shell-init.ts +50 -0
- package/src/commands/shell-integration/index.ts +194 -0
- package/src/commands/skill.ts +1220 -0
- package/src/commands/spec.ts +558 -0
- package/src/commands/status.ts +246 -0
- package/src/commands/theme.ts +211 -0
- package/src/commands/think.ts +551 -0
- package/src/commands/trust.ts +211 -0
- package/src/commands/tutorial.ts +522 -0
- package/src/commands/types.ts +512 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/usage.ts +213 -0
- package/src/commands/user-commands.ts +630 -0
- package/src/commands/utils.ts +142 -0
- package/src/commands/vim.ts +152 -0
- package/src/commands/workflow.ts +257 -0
- package/src/components/header.tsx +25 -0
- package/src/components/input.tsx +25 -0
- package/src/components/message-list.tsx +32 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/index.tsx +614 -0
- package/src/onboarding/__tests__/tutorial.test.ts +740 -0
- package/src/onboarding/index.ts +69 -0
- package/src/onboarding/tips/index.ts +9 -0
- package/src/onboarding/tips/tip-engine.ts +459 -0
- package/src/onboarding/tutorial/index.ts +88 -0
- package/src/onboarding/tutorial/lessons/basics.ts +151 -0
- package/src/onboarding/tutorial/lessons/index.ts +151 -0
- package/src/onboarding/tutorial/lessons/modes.ts +230 -0
- package/src/onboarding/tutorial/lessons/tools.ts +172 -0
- package/src/onboarding/tutorial/progress-tracker.ts +350 -0
- package/src/onboarding/tutorial/storage.ts +249 -0
- package/src/onboarding/tutorial/tutorial-system.ts +462 -0
- package/src/onboarding/tutorial/types.ts +310 -0
- package/src/orchestrator-singleton.ts +129 -0
- package/src/shutdown.ts +33 -0
- package/src/test/e2e/assertions.ts +267 -0
- package/src/test/e2e/fixtures.ts +204 -0
- package/src/test/e2e/harness.ts +575 -0
- package/src/test/e2e/index.ts +57 -0
- package/src/test/e2e/types.ts +228 -0
- package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
- package/src/test/fixtures/fake-response-loader.ts +314 -0
- package/src/test/fixtures/index.ts +20 -0
- package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
- package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
- package/src/tui/__tests__/mode-components.test.tsx +395 -0
- package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
- package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
- package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
- package/src/tui/adapters/agent-adapter.ts +1008 -0
- package/src/tui/adapters/index.ts +48 -0
- package/src/tui/adapters/message-adapter.ts +315 -0
- package/src/tui/adapters/persistence-bridge.ts +331 -0
- package/src/tui/adapters/session-adapter.ts +419 -0
- package/src/tui/buffered-stdout.ts +223 -0
- package/src/tui/components/AgentProgress.tsx +424 -0
- package/src/tui/components/Banner/AsciiArt.ts +160 -0
- package/src/tui/components/Banner/Banner.tsx +355 -0
- package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
- package/src/tui/components/Banner/ShimmerText.tsx +193 -0
- package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
- package/src/tui/components/Banner/index.ts +61 -0
- package/src/tui/components/Banner/useShimmer.ts +241 -0
- package/src/tui/components/ChatView.tsx +11 -0
- package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
- package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
- package/src/tui/components/Checkpoint/index.ts +19 -0
- package/src/tui/components/CostDisplay.tsx +226 -0
- package/src/tui/components/InitErrorBanner.tsx +122 -0
- package/src/tui/components/Input/Autocomplete.tsx +603 -0
- package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
- package/src/tui/components/Input/HighlightedText.tsx +236 -0
- package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
- package/src/tui/components/Input/TextInput.tsx +1002 -0
- package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
- package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
- package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
- package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
- package/src/tui/components/Input/highlight.ts +362 -0
- package/src/tui/components/Input/index.ts +36 -0
- package/src/tui/components/Input/slash-command-utils.ts +135 -0
- package/src/tui/components/Layout.tsx +432 -0
- package/src/tui/components/McpPanel.tsx +137 -0
- package/src/tui/components/MemoryPanel.tsx +448 -0
- package/src/tui/components/Messages/CodeBlock.tsx +527 -0
- package/src/tui/components/Messages/DiffView.tsx +679 -0
- package/src/tui/components/Messages/ImageReference.tsx +89 -0
- package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
- package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
- package/src/tui/components/Messages/MessageBubble.tsx +270 -0
- package/src/tui/components/Messages/MessageList.tsx +1719 -0
- package/src/tui/components/Messages/StreamingText.tsx +216 -0
- package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
- package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
- package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
- package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
- package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
- package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
- package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
- package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
- package/src/tui/components/Messages/index.ts +25 -0
- package/src/tui/components/ModeIndicator.tsx +177 -0
- package/src/tui/components/ModeSelector.tsx +216 -0
- package/src/tui/components/ModelSelector.tsx +339 -0
- package/src/tui/components/OnboardingWizard.tsx +670 -0
- package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
- package/src/tui/components/RateLimitIndicator.tsx +82 -0
- package/src/tui/components/ScreenReaderLayout.tsx +295 -0
- package/src/tui/components/SettingsPanel.tsx +643 -0
- package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
- package/src/tui/components/Sidebar/index.ts +9 -0
- package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
- package/src/tui/components/Status/index.ts +12 -0
- package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
- package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
- package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
- package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
- package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
- package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
- package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
- package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
- package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
- package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
- package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
- package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
- package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
- package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
- package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
- package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
- package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
- package/src/tui/components/StatusBar/index.ts +60 -0
- package/src/tui/components/TipBanner.tsx +115 -0
- package/src/tui/components/TodoItem.tsx +208 -0
- package/src/tui/components/TodoPanel.tsx +455 -0
- package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
- package/src/tui/components/Tools/OptionSelector.tsx +160 -0
- package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
- package/src/tui/components/Tools/ToolParams.tsx +483 -0
- package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
- package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
- package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
- package/src/tui/components/Tools/index.ts +21 -0
- package/src/tui/components/TrustPrompt.tsx +279 -0
- package/src/tui/components/UpdateBanner.tsx +166 -0
- package/src/tui/components/VimModeIndicator.tsx +112 -0
- package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
- package/src/tui/components/backtrack/index.ts +13 -0
- package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
- package/src/tui/components/common/CostWarning.tsx +294 -0
- package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
- package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
- package/src/tui/components/common/ErrorBoundary.tsx +140 -0
- package/src/tui/components/common/GradientText.tsx +224 -0
- package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
- package/src/tui/components/common/HotkeyHints.tsx +70 -0
- package/src/tui/components/common/MaxSizedBox.tsx +354 -0
- package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
- package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
- package/src/tui/components/common/ScrollIndicator.tsx +160 -0
- package/src/tui/components/common/Spinner.tsx +342 -0
- package/src/tui/components/common/StreamingIndicator.tsx +316 -0
- package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
- package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
- package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
- package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
- package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
- package/src/tui/components/common/VirtualizedList/index.ts +30 -0
- package/src/tui/components/common/VirtualizedList/types.ts +107 -0
- package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
- package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
- package/src/tui/components/common/index.ts +110 -0
- package/src/tui/components/index.ts +79 -0
- package/src/tui/components/session/CheckpointPanel.tsx +323 -0
- package/src/tui/components/session/RollbackDialog.tsx +169 -0
- package/src/tui/components/session/SessionItem.tsx +136 -0
- package/src/tui/components/session/SessionListPanel.tsx +252 -0
- package/src/tui/components/session/SessionPicker.tsx +449 -0
- package/src/tui/components/session/SessionPreview.tsx +240 -0
- package/src/tui/components/session/__tests__/session.test.tsx +408 -0
- package/src/tui/components/session/index.ts +28 -0
- package/src/tui/components/session/types.ts +116 -0
- package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
- package/src/tui/components/theme/index.ts +227 -0
- package/src/tui/components/theme/tokens.ts +484 -0
- package/src/tui/config/defaults.ts +134 -0
- package/src/tui/config/index.ts +17 -0
- package/src/tui/context/AnimationContext.tsx +284 -0
- package/src/tui/context/AppContext.tsx +349 -0
- package/src/tui/context/BracketedPasteContext.tsx +372 -0
- package/src/tui/context/LspContext.tsx +192 -0
- package/src/tui/context/McpContext.tsx +325 -0
- package/src/tui/context/MessagesContext.tsx +870 -0
- package/src/tui/context/OverflowContext.tsx +213 -0
- package/src/tui/context/RateLimitContext.tsx +108 -0
- package/src/tui/context/ResilienceContext.tsx +275 -0
- package/src/tui/context/RootProvider.tsx +136 -0
- package/src/tui/context/ScrollContext.tsx +331 -0
- package/src/tui/context/ToolsContext.tsx +702 -0
- package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
- package/src/tui/context/index.ts +140 -0
- package/src/tui/enterprise-integration.ts +282 -0
- package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
- package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
- package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
- package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
- package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
- package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
- package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
- package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
- package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
- package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
- package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
- package/src/tui/hooks/index.ts +252 -0
- package/src/tui/hooks/useAgentLoop.ts +712 -0
- package/src/tui/hooks/useAlternateBuffer.ts +398 -0
- package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
- package/src/tui/hooks/useBacktrack.ts +443 -0
- package/src/tui/hooks/useBracketedPaste.ts +104 -0
- package/src/tui/hooks/useCollapsible.ts +240 -0
- package/src/tui/hooks/useCopyMode.ts +382 -0
- package/src/tui/hooks/useCostSummary.ts +75 -0
- package/src/tui/hooks/useDesktopNotification.ts +414 -0
- package/src/tui/hooks/useDiffMode.ts +44 -0
- package/src/tui/hooks/useFileChangeStats.ts +110 -0
- package/src/tui/hooks/useFileSuggestions.ts +284 -0
- package/src/tui/hooks/useFlickerDetector.ts +250 -0
- package/src/tui/hooks/useGitStatus.ts +200 -0
- package/src/tui/hooks/useHotkeys.ts +579 -0
- package/src/tui/hooks/useImagePaste.ts +114 -0
- package/src/tui/hooks/useInputHighlight.ts +145 -0
- package/src/tui/hooks/useInputHistory.ts +246 -0
- package/src/tui/hooks/useKeyboardScroll.ts +209 -0
- package/src/tui/hooks/useLineBuffer.ts +356 -0
- package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
- package/src/tui/hooks/useModeController.ts +167 -0
- package/src/tui/hooks/useModeShortcuts.ts +196 -0
- package/src/tui/hooks/usePermissionHandler.ts +146 -0
- package/src/tui/hooks/usePersistence.ts +480 -0
- package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
- package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
- package/src/tui/hooks/useProviderStatus.ts +270 -0
- package/src/tui/hooks/useRateLimitStatus.ts +90 -0
- package/src/tui/hooks/useScreenReader.ts +315 -0
- package/src/tui/hooks/useScrollController.ts +450 -0
- package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
- package/src/tui/hooks/useSidebarPanelData.ts +115 -0
- package/src/tui/hooks/useSmoothScroll.ts +202 -0
- package/src/tui/hooks/useSnapshots.ts +300 -0
- package/src/tui/hooks/useStateAndRef.ts +50 -0
- package/src/tui/hooks/useTerminalSize.ts +206 -0
- package/src/tui/hooks/useToolApprovalController.ts +91 -0
- package/src/tui/hooks/useVim.ts +334 -0
- package/src/tui/hooks/useWorkspace.ts +56 -0
- package/src/tui/i18n/__tests__/init.test.ts +278 -0
- package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
- package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
- package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
- package/src/tui/i18n/index.ts +72 -0
- package/src/tui/i18n/init.ts +131 -0
- package/src/tui/i18n/language-config.ts +106 -0
- package/src/tui/i18n/locale-detection.ts +173 -0
- package/src/tui/i18n/settings-integration.ts +557 -0
- package/src/tui/i18n/tui-namespace.ts +538 -0
- package/src/tui/i18n/types.ts +312 -0
- package/src/tui/index.ts +43 -0
- package/src/tui/lsp-integration.ts +409 -0
- package/src/tui/metrics-integration.ts +366 -0
- package/src/tui/plugins.ts +383 -0
- package/src/tui/resilience.ts +342 -0
- package/src/tui/sandbox-integration.ts +317 -0
- package/src/tui/services/clipboard.ts +348 -0
- package/src/tui/services/fuzzy-search.ts +441 -0
- package/src/tui/services/index.ts +72 -0
- package/src/tui/services/markdown-renderer.ts +565 -0
- package/src/tui/services/open-external.ts +247 -0
- package/src/tui/services/syntax-highlighter.ts +483 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/theme/index.ts +15 -0
- package/src/tui/theme/provider.tsx +206 -0
- package/src/tui/tip-integration.ts +300 -0
- package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
- package/src/tui/types/ink-extended.ts +87 -0
- package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
- package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
- package/src/tui/utils/__tests__/text-width.test.ts +158 -0
- package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
- package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
- package/src/tui/utils/bracketedPaste.ts +107 -0
- package/src/tui/utils/cursor-manager.ts +131 -0
- package/src/tui/utils/detectTerminal.ts +596 -0
- package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
- package/src/tui/utils/heightEstimator.ts +198 -0
- package/src/tui/utils/index.ts +91 -0
- package/src/tui/utils/isNarrowWidth.ts +52 -0
- package/src/tui/utils/stdoutGuard.ts +90 -0
- package/src/tui/utils/synchronized-update.ts +70 -0
- package/src/tui/utils/text-width.ts +225 -0
- package/src/tui/utils/textSanitizer.ts +225 -0
- package/src/tui/utils/textUtils.ts +114 -0
- package/src/tui/utils/ui-sizing.ts +192 -0
- package/src/tui-blessed/app.ts +160 -0
- package/src/tui-blessed/index.ts +2 -0
- package/src/tui-blessed/neo-blessed.d.ts +6 -0
- package/src/tui-blessed/test.ts +21 -0
- package/src/tui-blessed/types.ts +14 -0
- package/src/utils/icons.ts +130 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/resume-hint.ts +86 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Module Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the InputSanitizer class including:
|
|
5
|
+
* - Input sanitization
|
|
6
|
+
* - Path validation
|
|
7
|
+
* - Shell metacharacter escaping
|
|
8
|
+
*
|
|
9
|
+
* Tests for the SensitiveDataHandler class including:
|
|
10
|
+
* - Sensitive data detection
|
|
11
|
+
* - Sensitive data masking
|
|
12
|
+
* - Custom pattern registration
|
|
13
|
+
*
|
|
14
|
+
* @module cli/commands/__tests__/security
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type CommandSecurityPolicy,
|
|
21
|
+
createDefaultHandler,
|
|
22
|
+
createPermissionChecker,
|
|
23
|
+
InputSanitizer,
|
|
24
|
+
PermissionChecker,
|
|
25
|
+
SensitiveDataHandler,
|
|
26
|
+
} from "../security/index.js";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// T050: InputSanitizer Tests
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
describe("InputSanitizer", () => {
|
|
33
|
+
const sanitizer = new InputSanitizer();
|
|
34
|
+
|
|
35
|
+
// ===========================================================================
|
|
36
|
+
// sanitize() Tests
|
|
37
|
+
// ===========================================================================
|
|
38
|
+
|
|
39
|
+
describe("sanitize", () => {
|
|
40
|
+
it("should return empty string for empty input", () => {
|
|
41
|
+
expect(sanitizer.sanitize("")).toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return empty string for null/undefined input", () => {
|
|
45
|
+
expect(sanitizer.sanitize(null as unknown as string)).toBe("");
|
|
46
|
+
expect(sanitizer.sanitize(undefined as unknown as string)).toBe("");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should pass through safe input unchanged", () => {
|
|
50
|
+
expect(sanitizer.sanitize("hello world")).toBe("hello world");
|
|
51
|
+
expect(sanitizer.sanitize("file.txt")).toBe("file.txt");
|
|
52
|
+
expect(sanitizer.sanitize("user@example.com")).toBe("user@example.com");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should remove pipe character", () => {
|
|
56
|
+
expect(sanitizer.sanitize("cat file | grep test")).toBe("cat file grep test");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should remove ampersand", () => {
|
|
60
|
+
expect(sanitizer.sanitize("cmd1 && cmd2")).toBe("cmd1 cmd2");
|
|
61
|
+
expect(sanitizer.sanitize("background &")).toBe("background ");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should remove semicolon", () => {
|
|
65
|
+
expect(sanitizer.sanitize("cmd1; cmd2")).toBe("cmd1 cmd2");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should remove dollar sign", () => {
|
|
69
|
+
expect(sanitizer.sanitize("echo $HOME")).toBe("echo HOME");
|
|
70
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: testing literal string sanitization, not template
|
|
71
|
+
expect(sanitizer.sanitize("${PATH}")).toBe("PATH");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should remove backticks", () => {
|
|
75
|
+
expect(sanitizer.sanitize("`whoami`")).toBe("whoami");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should remove backslash", () => {
|
|
79
|
+
expect(sanitizer.sanitize("path\\to\\file")).toBe("pathtofile");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should remove exclamation mark", () => {
|
|
83
|
+
expect(sanitizer.sanitize("history!")).toBe("history");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should remove parentheses", () => {
|
|
87
|
+
expect(sanitizer.sanitize("$(cmd)")).toBe("cmd");
|
|
88
|
+
expect(sanitizer.sanitize("(subshell)")).toBe("subshell");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should remove curly braces", () => {
|
|
92
|
+
expect(sanitizer.sanitize("{a,b,c}")).toBe("a,b,c");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should remove square brackets", () => {
|
|
96
|
+
expect(sanitizer.sanitize("file[0-9]")).toBe("file0-9");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should remove angle brackets", () => {
|
|
100
|
+
expect(sanitizer.sanitize("cmd > file")).toBe("cmd file");
|
|
101
|
+
expect(sanitizer.sanitize("cmd < input")).toBe("cmd input");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should remove asterisk and question mark", () => {
|
|
105
|
+
expect(sanitizer.sanitize("*.txt")).toBe(".txt");
|
|
106
|
+
expect(sanitizer.sanitize("file?.log")).toBe("file.log");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should remove hash", () => {
|
|
110
|
+
expect(sanitizer.sanitize("# comment")).toBe(" comment");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should remove tilde", () => {
|
|
114
|
+
expect(sanitizer.sanitize("~/home")).toBe("/home");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should remove control characters", () => {
|
|
118
|
+
expect(sanitizer.sanitize("hello\x00world")).toBe("helloworld");
|
|
119
|
+
expect(sanitizer.sanitize("test\x1b[31m")).toBe("test31m");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should handle complex injection attempts", () => {
|
|
123
|
+
expect(sanitizer.sanitize("hello; rm -rf /")).toBe("hello rm -rf /");
|
|
124
|
+
expect(sanitizer.sanitize("$(cat /etc/passwd)")).toBe("cat /etc/passwd");
|
|
125
|
+
expect(sanitizer.sanitize("`cat /etc/passwd`")).toBe("cat /etc/passwd");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
// validatePath() Tests
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
|
|
133
|
+
describe("validatePath", () => {
|
|
134
|
+
const allowedRoot = "/app/data";
|
|
135
|
+
|
|
136
|
+
it("should return false for empty inputs", () => {
|
|
137
|
+
expect(sanitizer.validatePath("", allowedRoot)).toBe(false);
|
|
138
|
+
expect(sanitizer.validatePath("file.txt", "")).toBe(false);
|
|
139
|
+
expect(sanitizer.validatePath("", "")).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should return false for null/undefined inputs", () => {
|
|
143
|
+
expect(sanitizer.validatePath(null as unknown as string, allowedRoot)).toBe(false);
|
|
144
|
+
expect(sanitizer.validatePath("file.txt", null as unknown as string)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should allow simple relative paths", () => {
|
|
148
|
+
expect(sanitizer.validatePath("file.txt", allowedRoot)).toBe(true);
|
|
149
|
+
expect(sanitizer.validatePath("subdir/file.txt", allowedRoot)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should reject path traversal with ../", () => {
|
|
153
|
+
expect(sanitizer.validatePath("../secret", allowedRoot)).toBe(false);
|
|
154
|
+
expect(sanitizer.validatePath("subdir/../../../etc/passwd", allowedRoot)).toBe(false);
|
|
155
|
+
expect(sanitizer.validatePath("../", allowedRoot)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should reject path traversal with ..\\", () => {
|
|
159
|
+
expect(sanitizer.validatePath("..\\secret", allowedRoot)).toBe(false);
|
|
160
|
+
expect(sanitizer.validatePath("subdir\\..\\..\\secret", allowedRoot)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should reject paths starting with ~", () => {
|
|
164
|
+
expect(sanitizer.validatePath("~/secret", allowedRoot)).toBe(false);
|
|
165
|
+
expect(sanitizer.validatePath("~user/secret", allowedRoot)).toBe(false);
|
|
166
|
+
expect(sanitizer.validatePath("~", allowedRoot)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should reject Unix absolute paths", () => {
|
|
170
|
+
expect(sanitizer.validatePath("/etc/passwd", allowedRoot)).toBe(false);
|
|
171
|
+
expect(sanitizer.validatePath("/", allowedRoot)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should reject Windows absolute paths", () => {
|
|
175
|
+
expect(sanitizer.validatePath("C:\\Windows\\System32", allowedRoot)).toBe(false);
|
|
176
|
+
expect(sanitizer.validatePath("D:/data/file.txt", allowedRoot)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should handle nested directories correctly", () => {
|
|
180
|
+
expect(sanitizer.validatePath("a/b/c/file.txt", allowedRoot)).toBe(true);
|
|
181
|
+
expect(sanitizer.validatePath("deep/nested/path/file.txt", allowedRoot)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should use actual filesystem for real path validation", () => {
|
|
185
|
+
// Use process.cwd() as a real path that exists
|
|
186
|
+
const realRoot = process.cwd();
|
|
187
|
+
expect(sanitizer.validatePath(".", realRoot)).toBe(true);
|
|
188
|
+
expect(sanitizer.validatePath("subdir", realRoot)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ===========================================================================
|
|
193
|
+
// escapeShellMeta() Tests
|
|
194
|
+
// ===========================================================================
|
|
195
|
+
|
|
196
|
+
describe("escapeShellMeta", () => {
|
|
197
|
+
it("should return empty string for empty input", () => {
|
|
198
|
+
expect(sanitizer.escapeShellMeta("")).toBe("");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should return empty string for null/undefined input", () => {
|
|
202
|
+
expect(sanitizer.escapeShellMeta(null as unknown as string)).toBe("");
|
|
203
|
+
expect(sanitizer.escapeShellMeta(undefined as unknown as string)).toBe("");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should pass through safe input unchanged", () => {
|
|
207
|
+
expect(sanitizer.escapeShellMeta("hello world")).toBe("hello world");
|
|
208
|
+
expect(sanitizer.escapeShellMeta("file.txt")).toBe("file.txt");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should escape pipe character", () => {
|
|
212
|
+
expect(sanitizer.escapeShellMeta("cat file | grep test")).toBe("cat file \\| grep test");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should escape ampersand", () => {
|
|
216
|
+
expect(sanitizer.escapeShellMeta("cmd1 && cmd2")).toBe("cmd1 \\&\\& cmd2");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should escape semicolon", () => {
|
|
220
|
+
expect(sanitizer.escapeShellMeta("cmd1; cmd2")).toBe("cmd1\\; cmd2");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should escape dollar sign", () => {
|
|
224
|
+
expect(sanitizer.escapeShellMeta("echo $HOME")).toBe("echo \\$HOME");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should escape backticks", () => {
|
|
228
|
+
expect(sanitizer.escapeShellMeta("`whoami`")).toBe("\\`whoami\\`");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should escape backslash", () => {
|
|
232
|
+
expect(sanitizer.escapeShellMeta("path\\file")).toBe("path\\\\file");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should escape exclamation mark", () => {
|
|
236
|
+
expect(sanitizer.escapeShellMeta("history!")).toBe("history\\!");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should escape parentheses", () => {
|
|
240
|
+
expect(sanitizer.escapeShellMeta("(subshell)")).toBe("\\(subshell\\)");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should escape curly braces", () => {
|
|
244
|
+
expect(sanitizer.escapeShellMeta("{a,b}")).toBe("\\{a,b\\}");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should escape square brackets", () => {
|
|
248
|
+
expect(sanitizer.escapeShellMeta("file[0-9]")).toBe("file\\[0-9\\]");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should escape angle brackets", () => {
|
|
252
|
+
expect(sanitizer.escapeShellMeta("cmd > file")).toBe("cmd \\> file");
|
|
253
|
+
expect(sanitizer.escapeShellMeta("cmd < input")).toBe("cmd \\< input");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should escape asterisk and question mark", () => {
|
|
257
|
+
expect(sanitizer.escapeShellMeta("*.txt")).toBe("\\*.txt");
|
|
258
|
+
expect(sanitizer.escapeShellMeta("file?.log")).toBe("file\\?.log");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should escape hash", () => {
|
|
262
|
+
expect(sanitizer.escapeShellMeta("# comment")).toBe("\\# comment");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should escape tilde", () => {
|
|
266
|
+
expect(sanitizer.escapeShellMeta("~/home")).toBe("\\~/home");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should escape multiple metacharacters", () => {
|
|
270
|
+
expect(sanitizer.escapeShellMeta("$(cmd) | grep *")).toBe("\\$\\(cmd\\) \\| grep \\*");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ===========================================================================
|
|
275
|
+
// containsShellMeta() Tests
|
|
276
|
+
// ===========================================================================
|
|
277
|
+
|
|
278
|
+
describe("containsShellMeta", () => {
|
|
279
|
+
it("should return false for empty input", () => {
|
|
280
|
+
expect(sanitizer.containsShellMeta("")).toBe(false);
|
|
281
|
+
expect(sanitizer.containsShellMeta(null as unknown as string)).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should return false for safe input", () => {
|
|
285
|
+
expect(sanitizer.containsShellMeta("hello world")).toBe(false);
|
|
286
|
+
expect(sanitizer.containsShellMeta("file.txt")).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should return true for input with metacharacters", () => {
|
|
290
|
+
expect(sanitizer.containsShellMeta("cmd | grep")).toBe(true);
|
|
291
|
+
expect(sanitizer.containsShellMeta("$HOME")).toBe(true);
|
|
292
|
+
expect(sanitizer.containsShellMeta("`cmd`")).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
// containsPathTraversal() Tests
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
|
|
300
|
+
describe("containsPathTraversal", () => {
|
|
301
|
+
it("should return false for empty input", () => {
|
|
302
|
+
expect(sanitizer.containsPathTraversal("")).toBe(false);
|
|
303
|
+
expect(sanitizer.containsPathTraversal(null as unknown as string)).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should return false for safe paths", () => {
|
|
307
|
+
expect(sanitizer.containsPathTraversal("file.txt")).toBe(false);
|
|
308
|
+
expect(sanitizer.containsPathTraversal("subdir/file.txt")).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should return true for paths with traversal patterns", () => {
|
|
312
|
+
expect(sanitizer.containsPathTraversal("../secret")).toBe(true);
|
|
313
|
+
expect(sanitizer.containsPathTraversal("..\\secret")).toBe(true);
|
|
314
|
+
expect(sanitizer.containsPathTraversal("~/home")).toBe(true);
|
|
315
|
+
expect(sanitizer.containsPathTraversal("/etc/passwd")).toBe(true);
|
|
316
|
+
expect(sanitizer.containsPathTraversal("C:\\Windows")).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// =============================================================================
|
|
322
|
+
// T051: SensitiveDataHandler Tests
|
|
323
|
+
// =============================================================================
|
|
324
|
+
|
|
325
|
+
describe("SensitiveDataHandler", () => {
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
// Constructor and Pattern Management Tests
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
|
|
330
|
+
describe("constructor and pattern management", () => {
|
|
331
|
+
it("should create handler with no patterns", () => {
|
|
332
|
+
const handler = new SensitiveDataHandler();
|
|
333
|
+
expect(handler.getPatternNames()).toEqual([]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should create handler with initial patterns", () => {
|
|
337
|
+
const handler = new SensitiveDataHandler([{ name: "test", regex: /test-[a-z]+/g }]);
|
|
338
|
+
expect(handler.getPatternNames()).toContain("test");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should add custom pattern", () => {
|
|
342
|
+
const handler = new SensitiveDataHandler();
|
|
343
|
+
handler.addPattern("custom", /custom-[0-9]+/);
|
|
344
|
+
expect(handler.getPatternNames()).toContain("custom");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should ensure global flag on added patterns", () => {
|
|
348
|
+
const handler = new SensitiveDataHandler();
|
|
349
|
+
handler.addPattern("local", /local-[0-9]+/i);
|
|
350
|
+
// Should be able to mask multiple occurrences
|
|
351
|
+
const text = "local-123 and local-456";
|
|
352
|
+
expect(handler.mask(text)).toBe("**** and ****");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should remove pattern", () => {
|
|
356
|
+
const handler = new SensitiveDataHandler([{ name: "test", regex: /test/g }]);
|
|
357
|
+
expect(handler.removePattern("test")).toBe(true);
|
|
358
|
+
expect(handler.getPatternNames()).not.toContain("test");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should return false when removing non-existent pattern", () => {
|
|
362
|
+
const handler = new SensitiveDataHandler();
|
|
363
|
+
expect(handler.removePattern("nonexistent")).toBe(false);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ===========================================================================
|
|
368
|
+
// isSensitive() Tests
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
|
|
371
|
+
describe("isSensitive", () => {
|
|
372
|
+
const handler = createDefaultHandler();
|
|
373
|
+
|
|
374
|
+
it("should return false for empty/null input", () => {
|
|
375
|
+
expect(handler.isSensitive("")).toBe(false);
|
|
376
|
+
expect(handler.isSensitive(null as unknown as string)).toBe(false);
|
|
377
|
+
expect(handler.isSensitive(undefined as unknown as string)).toBe(false);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should return false for safe text", () => {
|
|
381
|
+
expect(handler.isSensitive("Hello world")).toBe(false);
|
|
382
|
+
expect(handler.isSensitive("Just some normal text")).toBe(false);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should detect OpenAI keys", () => {
|
|
386
|
+
expect(handler.isSensitive("sk-abcdefghijklmnopqrstuvwxyz1234567890")).toBe(true);
|
|
387
|
+
expect(handler.isSensitive("My key is sk-proj-abc123def456ghi789jkl012")).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should detect GitHub tokens (fine-grained)", () => {
|
|
391
|
+
expect(handler.isSensitive("ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should detect GitHub tokens (classic)", () => {
|
|
395
|
+
expect(handler.isSensitive("github_pat_abcdefghij1234567890_ABCD")).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should detect Anthropic keys", () => {
|
|
399
|
+
expect(handler.isSensitive("sk-ant-api03-abc123def456ghi789")).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should detect Bearer tokens", () => {
|
|
403
|
+
expect(handler.isSensitive("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should detect password assignments", () => {
|
|
407
|
+
expect(handler.isSensitive("password=mysecret123")).toBe(true);
|
|
408
|
+
expect(handler.isSensitive("pwd: secretvalue")).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should detect generic API keys", () => {
|
|
412
|
+
expect(handler.isSensitive('api_key="abc123def456ghi789jkl0"')).toBe(true);
|
|
413
|
+
expect(handler.isSensitive("apikey: xxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should detect connection string passwords", () => {
|
|
417
|
+
expect(handler.isSensitive("mongodb://user:secretpass@localhost")).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ===========================================================================
|
|
422
|
+
// mask() Tests
|
|
423
|
+
// ===========================================================================
|
|
424
|
+
|
|
425
|
+
describe("mask", () => {
|
|
426
|
+
const handler = createDefaultHandler();
|
|
427
|
+
|
|
428
|
+
it("should return empty string for null/undefined", () => {
|
|
429
|
+
expect(handler.mask(null as unknown as string)).toBe("");
|
|
430
|
+
expect(handler.mask(undefined as unknown as string)).toBe("");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should return empty string unchanged", () => {
|
|
434
|
+
expect(handler.mask("")).toBe("");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("should not modify safe text", () => {
|
|
438
|
+
const text = "Hello, this is safe text!";
|
|
439
|
+
expect(handler.mask(text)).toBe(text);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("should mask OpenAI keys with first 4 and last 4 chars", () => {
|
|
443
|
+
const result = handler.mask("Key: sk-proj-abcdefghij1234567890xyz");
|
|
444
|
+
expect(result).toBe("Key: sk-p...0xyz");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("should mask GitHub tokens", () => {
|
|
448
|
+
const result = handler.mask("Token: ghp_abcdefghijklmnopqrstuvwxyz1234567890");
|
|
449
|
+
expect(result).toBe("Token: ghp_...7890");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should mask Anthropic keys", () => {
|
|
453
|
+
const result = handler.mask("Key: sk-ant-api03-abc123def456789");
|
|
454
|
+
expect(result).toBe("Key: sk-a...6789");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should mask Bearer tokens", () => {
|
|
458
|
+
const result = handler.mask("Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
|
|
459
|
+
expect(result).toBe("Authorization: Bear...VCJ9");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("should mask multiple sensitive values", () => {
|
|
463
|
+
const text =
|
|
464
|
+
"OpenAI: sk-abcd1234567890efghijk GitHub: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
|
465
|
+
const masked = handler.mask(text);
|
|
466
|
+
expect(masked).not.toContain("sk-abcd1234567890efghijk");
|
|
467
|
+
expect(masked).not.toContain("ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
|
|
468
|
+
expect(masked).toContain("...");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("should mask short sensitive values with ****", () => {
|
|
472
|
+
const handler = new SensitiveDataHandler([{ name: "short", regex: /short-[a-z]{4}/g }]);
|
|
473
|
+
expect(handler.mask("value: short-abcd")).toBe("value: ****");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("should preserve surrounding text", () => {
|
|
477
|
+
const result = handler.mask("Before sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx After");
|
|
478
|
+
expect(result).toMatch(/^Before .+\.\.\..+ After$/);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ===========================================================================
|
|
483
|
+
// Default Patterns Coverage Tests
|
|
484
|
+
// ===========================================================================
|
|
485
|
+
|
|
486
|
+
describe("default patterns", () => {
|
|
487
|
+
const handler = createDefaultHandler();
|
|
488
|
+
|
|
489
|
+
it("should detect and mask AWS access keys", () => {
|
|
490
|
+
expect(handler.isSensitive("AKIAIOSFODNN7EXAMPLE")).toBe(true);
|
|
491
|
+
expect(handler.mask("Key: AKIAIOSFODNN7EXAMPLE")).toBe("Key: AKIA...MPLE");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("should detect and mask Stripe keys", () => {
|
|
495
|
+
expect(handler.isSensitive("sk_test_abcdefghijklmnopqrstuvwxyz")).toBe(true);
|
|
496
|
+
expect(handler.isSensitive("pk_live_abcdefghijklmnopqrstuvwxyz")).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should detect and mask Slack tokens", () => {
|
|
500
|
+
expect(handler.isSensitive("xoxb-123456789012-1234567890123-abcdefghij")).toBe(true);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("should detect and mask NPM tokens", () => {
|
|
504
|
+
expect(handler.isSensitive("npm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should detect and mask Google AI keys", () => {
|
|
508
|
+
expect(handler.isSensitive("AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe")).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("should detect GitHub OAuth tokens", () => {
|
|
512
|
+
expect(handler.isSensitive("gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should detect GitHub App tokens", () => {
|
|
516
|
+
expect(handler.isSensitive("ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
517
|
+
expect(handler.isSensitive("ghu_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("should detect private keys", () => {
|
|
521
|
+
const privateKey = `-----BEGIN PRIVATE KEY-----
|
|
522
|
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7...
|
|
523
|
+
-----END PRIVATE KEY-----`;
|
|
524
|
+
expect(handler.isSensitive(privateKey)).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("should detect Twilio keys", () => {
|
|
528
|
+
expect(handler.isSensitive("SKabcdef0123456789abcdef0123456789")).toBe(true);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ===========================================================================
|
|
533
|
+
// createDefaultHandler() Tests
|
|
534
|
+
// ===========================================================================
|
|
535
|
+
|
|
536
|
+
describe("createDefaultHandler", () => {
|
|
537
|
+
it("should create handler with default patterns", () => {
|
|
538
|
+
const handler = createDefaultHandler();
|
|
539
|
+
const names = handler.getPatternNames();
|
|
540
|
+
|
|
541
|
+
expect(names).toContain("openai-key");
|
|
542
|
+
expect(names).toContain("github-token-fine");
|
|
543
|
+
expect(names).toContain("github-token-classic");
|
|
544
|
+
expect(names).toContain("anthropic-key");
|
|
545
|
+
expect(names).toContain("bearer-token");
|
|
546
|
+
expect(names).toContain("password-assign");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("should allow adding custom patterns to default handler", () => {
|
|
550
|
+
const handler = createDefaultHandler();
|
|
551
|
+
handler.addPattern("custom-service", /custom-svc-[a-z0-9]{20}/gi);
|
|
552
|
+
|
|
553
|
+
expect(handler.isSensitive("custom-svc-abcdefghij1234567890")).toBe(true);
|
|
554
|
+
expect(handler.mask("Key: custom-svc-abcdefghij1234567890")).toBe("Key: cust...7890");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// =============================================================================
|
|
560
|
+
// T052: PermissionChecker Tests
|
|
561
|
+
// =============================================================================
|
|
562
|
+
|
|
563
|
+
describe("PermissionChecker", () => {
|
|
564
|
+
// ===========================================================================
|
|
565
|
+
// Constructor Tests
|
|
566
|
+
// ===========================================================================
|
|
567
|
+
|
|
568
|
+
describe("constructor", () => {
|
|
569
|
+
it("should create checker with default base directory", () => {
|
|
570
|
+
const checker = new PermissionChecker();
|
|
571
|
+
expect(checker).toBeInstanceOf(PermissionChecker);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("should create checker with custom base directory", () => {
|
|
575
|
+
const checker = new PermissionChecker("/custom/base");
|
|
576
|
+
expect(checker).toBeInstanceOf(PermissionChecker);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ===========================================================================
|
|
581
|
+
// checkFileAccess() Tests
|
|
582
|
+
// ===========================================================================
|
|
583
|
+
|
|
584
|
+
describe("checkFileAccess", () => {
|
|
585
|
+
const checker = new PermissionChecker("/app");
|
|
586
|
+
|
|
587
|
+
it("should deny access for empty file path", () => {
|
|
588
|
+
const result = checker.checkFileAccess("", {});
|
|
589
|
+
expect(result.allowed).toBe(false);
|
|
590
|
+
if (!result.allowed) {
|
|
591
|
+
expect(result.reason).toBe("File path is required");
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("should allow access when no policy restrictions", () => {
|
|
596
|
+
const result = checker.checkFileAccess("/app/src/file.ts", {});
|
|
597
|
+
expect(result.allowed).toBe(true);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("should allow access for path matching allowed patterns", () => {
|
|
601
|
+
const policy: CommandSecurityPolicy = {
|
|
602
|
+
allowedPaths: ["/app/src/**", "/app/config/**"],
|
|
603
|
+
};
|
|
604
|
+
const result = checker.checkFileAccess("/app/src/file.ts", policy);
|
|
605
|
+
expect(result.allowed).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("should deny access for path not matching allowed patterns", () => {
|
|
609
|
+
const policy: CommandSecurityPolicy = {
|
|
610
|
+
allowedPaths: ["/app/src/**"],
|
|
611
|
+
};
|
|
612
|
+
const result = checker.checkFileAccess("/app/secrets/key.txt", policy);
|
|
613
|
+
expect(result.allowed).toBe(false);
|
|
614
|
+
if (!result.allowed) {
|
|
615
|
+
expect(result.reason).toContain("not in allowed paths");
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("should deny access for path matching denied patterns", () => {
|
|
620
|
+
const policy: CommandSecurityPolicy = {
|
|
621
|
+
deniedPaths: ["**/.env", "**/secrets/**"],
|
|
622
|
+
};
|
|
623
|
+
const result = checker.checkFileAccess("/app/.env", policy);
|
|
624
|
+
expect(result.allowed).toBe(false);
|
|
625
|
+
if (!result.allowed) {
|
|
626
|
+
expect(result.reason).toContain("blocked by security policy");
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("should deny access when path matches both allowed and denied (denied takes precedence)", () => {
|
|
631
|
+
const policy: CommandSecurityPolicy = {
|
|
632
|
+
allowedPaths: ["/app/**"],
|
|
633
|
+
deniedPaths: ["/app/secrets/**"],
|
|
634
|
+
};
|
|
635
|
+
const result = checker.checkFileAccess("/app/secrets/key.txt", policy);
|
|
636
|
+
expect(result.allowed).toBe(false);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("should handle relative paths", () => {
|
|
640
|
+
// Use process.cwd() to ensure cross-platform compatibility
|
|
641
|
+
const baseDir = process.cwd().replace(/\\/g, "/");
|
|
642
|
+
const checker = new PermissionChecker(baseDir);
|
|
643
|
+
const policy: CommandSecurityPolicy = {
|
|
644
|
+
allowedPaths: [`${baseDir}/src/**`],
|
|
645
|
+
};
|
|
646
|
+
const result = checker.checkFileAccess("./src/file.ts", policy);
|
|
647
|
+
expect(result.allowed).toBe(true);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ===========================================================================
|
|
652
|
+
// checkNetworkAccess() Tests
|
|
653
|
+
// ===========================================================================
|
|
654
|
+
|
|
655
|
+
describe("checkNetworkAccess", () => {
|
|
656
|
+
const checker = new PermissionChecker();
|
|
657
|
+
|
|
658
|
+
it("should deny access for empty host", () => {
|
|
659
|
+
const result = checker.checkNetworkAccess("", {});
|
|
660
|
+
expect(result.allowed).toBe(false);
|
|
661
|
+
if (!result.allowed) {
|
|
662
|
+
expect(result.reason).toBe("Host is required");
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("should allow access when no policy restrictions", () => {
|
|
667
|
+
const result = checker.checkNetworkAccess("api.example.com", {});
|
|
668
|
+
expect(result.allowed).toBe(true);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("should allow access for host matching allowed hosts", () => {
|
|
672
|
+
const policy: CommandSecurityPolicy = {
|
|
673
|
+
allowedHosts: ["api.example.com", "localhost"],
|
|
674
|
+
};
|
|
675
|
+
const result = checker.checkNetworkAccess("api.example.com", policy);
|
|
676
|
+
expect(result.allowed).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("should deny access for host not matching allowed hosts", () => {
|
|
680
|
+
const policy: CommandSecurityPolicy = {
|
|
681
|
+
allowedHosts: ["api.example.com"],
|
|
682
|
+
};
|
|
683
|
+
const result = checker.checkNetworkAccess("evil.com", policy);
|
|
684
|
+
expect(result.allowed).toBe(false);
|
|
685
|
+
if (!result.allowed) {
|
|
686
|
+
expect(result.reason).toContain("not in allowed hosts");
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("should deny access for host matching denied hosts", () => {
|
|
691
|
+
const policy: CommandSecurityPolicy = {
|
|
692
|
+
deniedHosts: ["*.evil.com", "blocked.example.com"],
|
|
693
|
+
};
|
|
694
|
+
const result = checker.checkNetworkAccess("blocked.example.com", policy);
|
|
695
|
+
expect(result.allowed).toBe(false);
|
|
696
|
+
if (!result.allowed) {
|
|
697
|
+
expect(result.reason).toContain("blocked by security policy");
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("should deny access when host matches both allowed and denied (denied takes precedence)", () => {
|
|
702
|
+
const policy: CommandSecurityPolicy = {
|
|
703
|
+
allowedHosts: ["*.example.com"],
|
|
704
|
+
deniedHosts: ["internal.example.com"],
|
|
705
|
+
};
|
|
706
|
+
const result = checker.checkNetworkAccess("internal.example.com", policy);
|
|
707
|
+
expect(result.allowed).toBe(false);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("should support wildcard host patterns", () => {
|
|
711
|
+
const policy: CommandSecurityPolicy = {
|
|
712
|
+
allowedHosts: ["*.example.com"],
|
|
713
|
+
};
|
|
714
|
+
expect(checker.checkNetworkAccess("api.example.com", policy).allowed).toBe(true);
|
|
715
|
+
expect(checker.checkNetworkAccess("sub.api.example.com", policy).allowed).toBe(true);
|
|
716
|
+
expect(checker.checkNetworkAccess("example.com", policy).allowed).toBe(true);
|
|
717
|
+
expect(checker.checkNetworkAccess("notexample.com", policy).allowed).toBe(false);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("should support full wildcard (*) for allowing all hosts", () => {
|
|
721
|
+
const policy: CommandSecurityPolicy = {
|
|
722
|
+
allowedHosts: ["*"],
|
|
723
|
+
};
|
|
724
|
+
expect(checker.checkNetworkAccess("any.host.com", policy).allowed).toBe(true);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("should be case-insensitive for host matching", () => {
|
|
728
|
+
const policy: CommandSecurityPolicy = {
|
|
729
|
+
allowedHosts: ["API.Example.COM"],
|
|
730
|
+
};
|
|
731
|
+
expect(checker.checkNetworkAccess("api.example.com", policy).allowed).toBe(true);
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// ===========================================================================
|
|
736
|
+
// checkPolicy() Tests
|
|
737
|
+
// ===========================================================================
|
|
738
|
+
|
|
739
|
+
describe("checkPolicy", () => {
|
|
740
|
+
const checker = new PermissionChecker("/app");
|
|
741
|
+
|
|
742
|
+
it("should deny for empty action", () => {
|
|
743
|
+
const result = checker.checkPolicy("", "resource", {});
|
|
744
|
+
expect(result.allowed).toBe(false);
|
|
745
|
+
if (!result.allowed) {
|
|
746
|
+
expect(result.reason).toBe("Action is required");
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("should deny for empty resource", () => {
|
|
751
|
+
const result = checker.checkPolicy("read", "", {});
|
|
752
|
+
expect(result.allowed).toBe(false);
|
|
753
|
+
if (!result.allowed) {
|
|
754
|
+
expect(result.reason).toBe("Resource is required");
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it("should delegate file: resources to checkFileAccess", () => {
|
|
759
|
+
const policy: CommandSecurityPolicy = {
|
|
760
|
+
allowedPaths: ["/app/data/**"],
|
|
761
|
+
};
|
|
762
|
+
const result = checker.checkPolicy("read", "file:/app/data/file.txt", policy);
|
|
763
|
+
expect(result.allowed).toBe(true);
|
|
764
|
+
|
|
765
|
+
const result2 = checker.checkPolicy("read", "file:/app/secrets/key.txt", policy);
|
|
766
|
+
expect(result2.allowed).toBe(false);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("should delegate http/https URLs to checkNetworkAccess", () => {
|
|
770
|
+
const policy: CommandSecurityPolicy = {
|
|
771
|
+
allowedHosts: ["api.example.com"],
|
|
772
|
+
};
|
|
773
|
+
const result = checker.checkPolicy("fetch", "https://api.example.com/data", policy);
|
|
774
|
+
expect(result.allowed).toBe(true);
|
|
775
|
+
|
|
776
|
+
const result2 = checker.checkPolicy("fetch", "http://evil.com/data", policy);
|
|
777
|
+
expect(result2.allowed).toBe(false);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("should delegate host: resources to checkNetworkAccess", () => {
|
|
781
|
+
const policy: CommandSecurityPolicy = {
|
|
782
|
+
allowedHosts: ["localhost"],
|
|
783
|
+
};
|
|
784
|
+
const result = checker.checkPolicy("connect", "host:localhost", policy);
|
|
785
|
+
expect(result.allowed).toBe(true);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("should allow unknown resource types by default", () => {
|
|
789
|
+
const result = checker.checkPolicy("execute", "shell:ls", {});
|
|
790
|
+
expect(result.allowed).toBe(true);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("should handle invalid URLs gracefully", () => {
|
|
794
|
+
const result = checker.checkPolicy("fetch", "https://invalid url with spaces", {});
|
|
795
|
+
expect(result.allowed).toBe(false);
|
|
796
|
+
if (!result.allowed) {
|
|
797
|
+
expect(result.reason).toContain("Invalid URL");
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ===========================================================================
|
|
803
|
+
// createPermissionChecker() Tests
|
|
804
|
+
// ===========================================================================
|
|
805
|
+
|
|
806
|
+
describe("createPermissionChecker", () => {
|
|
807
|
+
it("should create a PermissionChecker instance", () => {
|
|
808
|
+
const checker = createPermissionChecker();
|
|
809
|
+
expect(checker).toBeInstanceOf(PermissionChecker);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("should create checker with custom base directory", () => {
|
|
813
|
+
const checker = createPermissionChecker("/custom/path");
|
|
814
|
+
expect(checker).toBeInstanceOf(PermissionChecker);
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// ===========================================================================
|
|
819
|
+
// Integration with CommandSecurityPolicy
|
|
820
|
+
// ===========================================================================
|
|
821
|
+
|
|
822
|
+
describe("CommandSecurityPolicy integration", () => {
|
|
823
|
+
it("should validate complete policy with all fields", () => {
|
|
824
|
+
const checker = new PermissionChecker("/app");
|
|
825
|
+
const policy: CommandSecurityPolicy = {
|
|
826
|
+
allowedPaths: ["/app/src/**", "/app/config/**"],
|
|
827
|
+
deniedPaths: ["/app/config/secrets/**", "**/.env"],
|
|
828
|
+
allowedHosts: ["api.example.com", "localhost", "*.trusted.com"],
|
|
829
|
+
deniedHosts: ["*.evil.com"],
|
|
830
|
+
requiresAuth: true,
|
|
831
|
+
maxExecutionTime: 30000,
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// File access tests
|
|
835
|
+
expect(checker.checkFileAccess("/app/src/index.ts", policy).allowed).toBe(true);
|
|
836
|
+
expect(checker.checkFileAccess("/app/config/app.json", policy).allowed).toBe(true);
|
|
837
|
+
expect(checker.checkFileAccess("/app/config/secrets/key.txt", policy).allowed).toBe(false);
|
|
838
|
+
|
|
839
|
+
// Network access tests
|
|
840
|
+
expect(checker.checkNetworkAccess("api.example.com", policy).allowed).toBe(true);
|
|
841
|
+
expect(checker.checkNetworkAccess("sub.trusted.com", policy).allowed).toBe(true);
|
|
842
|
+
expect(checker.checkNetworkAccess("malware.evil.com", policy).allowed).toBe(false);
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
});
|