principles-disciple 1.62.0 → 1.63.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/hooks/gate.ts +27 -205
- package/tests/hooks/gate-rule-host-pipeline.test.ts +159 -334
- package/tests/service/evolution-worker.compilation-backfill.test.ts +5 -1
- package/src/hooks/bash-risk.ts +0 -175
- package/src/hooks/edit-verification.ts +0 -302
- package/src/hooks/gfi-gate.ts +0 -186
- package/src/hooks/progressive-trust-gate.ts +0 -183
- package/src/hooks/thinking-checkpoint.ts +0 -76
- package/tests/hooks/bash-risk-integration.test.ts +0 -137
- package/tests/hooks/bash-risk.test.ts +0 -81
- package/tests/hooks/edit-verification.test.ts +0 -678
- package/tests/hooks/gate-edit-verification-p1.test.ts +0 -632
- package/tests/hooks/gate-pipeline-integration.test.ts +0 -404
- package/tests/hooks/gate.test.ts +0 -271
- package/tests/hooks/gfi-gate-unit.test.ts +0 -422
- package/tests/hooks/gfi-gate.test.ts +0 -669
- package/tests/hooks/thinking-gate.test.ts +0 -313
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progressive Gate Module (EP-Only Version)
|
|
3
|
-
*
|
|
4
|
-
* EP (Evolution Points) 是唯一的门控机制。
|
|
5
|
-
*
|
|
6
|
-
* **EP 门控逻辑:**
|
|
7
|
-
* - Seed (0分): 只读 + 基础文档
|
|
8
|
-
* - Sprout (50分): 单文件编辑
|
|
9
|
-
* - Sapling (200分): 多文件 + 测试 + 子智能体
|
|
10
|
-
* - Tree (500分): 重构 + 风险路径
|
|
11
|
-
* - Forest (1000分): 完全自主
|
|
12
|
-
*
|
|
13
|
-
* **风险路径控制:**
|
|
14
|
-
* - 低等级不能修改风险路径
|
|
15
|
-
* - 高等级解锁风险路径权限
|
|
16
|
-
*
|
|
17
|
-
* **不再有:**
|
|
18
|
-
* - Trust Score (30-100) 系统
|
|
19
|
-
* - Stage 1-4 分级
|
|
20
|
-
* - Plan Approval 白名单机制
|
|
21
|
-
* - 基于行数的限制
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
|
|
25
|
-
import type { WorkspaceContext } from '../core/workspace-context.js';
|
|
26
|
-
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
27
|
-
import { recordGateBlockAndReturn } from './gate-block-helper.js';
|
|
28
|
-
|
|
29
|
-
// ═══ P-16: Core Governance Files — Exempt from all Blocking ═══
|
|
30
|
-
// 这些文件是团队协作的基础,必须始终放行,不受 GFI 和 Risk Path 限制
|
|
31
|
-
// 可通过 PROFILE.core_governance_files 扩展(merge 而非覆盖)
|
|
32
|
-
const DEFAULT_CORE_GOVERNANCE_PATTERNS = [
|
|
33
|
-
'PLAN.md',
|
|
34
|
-
'AGENTS.md',
|
|
35
|
-
'VERSION.md',
|
|
36
|
-
'.team/',
|
|
37
|
-
'MEMORY.md',
|
|
38
|
-
'SOUL.md',
|
|
39
|
-
'IDENTITY.md',
|
|
40
|
-
'USER.md',
|
|
41
|
-
'HEARTBEAT.md',
|
|
42
|
-
'BOOTSTRAP.md',
|
|
43
|
-
'PRINCIPLES.md',
|
|
44
|
-
'TEAM_ROLE.md',
|
|
45
|
-
'REPAIR_OPERATING_PROMPT.md',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get effective core governance patterns from PROFILE config, merged with defaults.
|
|
50
|
-
* PROFILE.core_governance_files extends (not replaces) the default list.
|
|
51
|
-
*/
|
|
52
|
-
function getCoreGovernancePatterns(profile?: { core_governance_files?: string[] }): string[] {
|
|
53
|
-
const base = DEFAULT_CORE_GOVERNANCE_PATTERNS;
|
|
54
|
-
const extra = profile?.core_governance_files ?? [];
|
|
55
|
-
return Array.from(new Set([...base, ...extra]));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Check if a file path matches a core governance pattern.
|
|
60
|
-
* Core governance files are exempt from all gate blocking (P-16).
|
|
61
|
-
*/
|
|
62
|
-
function isCoreGovernanceFile(filePath?: string, corePatterns?: string[]): boolean {
|
|
63
|
-
if (!filePath) return false;
|
|
64
|
-
const patterns = corePatterns ?? DEFAULT_CORE_GOVERNANCE_PATTERNS;
|
|
65
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
66
|
-
return patterns.some(pattern =>
|
|
67
|
-
pattern.endsWith('/')
|
|
68
|
-
? normalized.includes(pattern)
|
|
69
|
-
: normalized.endsWith(pattern) || normalized.includes(`/${pattern}`)
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Build EP gate rejection reason
|
|
77
|
-
*/
|
|
78
|
-
export function buildEvolutionGateReason(
|
|
79
|
-
tier: number,
|
|
80
|
-
tierName: string,
|
|
81
|
-
reason: string
|
|
82
|
-
): string {
|
|
83
|
-
return `[EP Gate] Tier ${tier} (${tierName}): ${reason}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Internal helper to call the shared block helper with progressive-trust-gate source tag.
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
function block(
|
|
92
|
-
filePath: string,
|
|
93
|
-
reason: string,
|
|
94
|
-
wctx: WorkspaceContext,
|
|
95
|
-
toolName: string,
|
|
96
|
-
|
|
97
|
-
logger: { warn?: (message: string) => void; error?: (message: string) => void },
|
|
98
|
-
|
|
99
|
-
sessionId?: string
|
|
100
|
-
): PluginHookBeforeToolCallResult {
|
|
101
|
-
return recordGateBlockAndReturn(wctx, {
|
|
102
|
-
filePath,
|
|
103
|
-
reason,
|
|
104
|
-
toolName,
|
|
105
|
-
sessionId,
|
|
106
|
-
blockSource: 'progressive-trust-gate',
|
|
107
|
-
}, logger);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Check EP-based gate
|
|
112
|
-
*
|
|
113
|
-
* @param event - The tool call event
|
|
114
|
-
* @param wctx - Workspace context
|
|
115
|
-
* @param relPath - Relative path to target file
|
|
116
|
-
* @param risky - Whether the path is a risk path
|
|
117
|
-
* @param lineChanges - Estimated line changes (kept for interface compatibility, not used for gating)
|
|
118
|
-
* @param logger - Logger instance
|
|
119
|
-
* @param ctx - Hook context
|
|
120
|
-
* @param profile - Gate profile containing risk_paths config
|
|
121
|
-
* @returns PluginHookBeforeToolCallResult to block, or undefined to allow
|
|
122
|
-
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
export function checkProgressiveTrustGate(
|
|
126
|
-
event: PluginHookBeforeToolCallEvent,
|
|
127
|
-
wctx: WorkspaceContext,
|
|
128
|
-
relPath: string,
|
|
129
|
-
risky: boolean,
|
|
130
|
-
lineChanges: number,
|
|
131
|
-
|
|
132
|
-
logger: { warn?: (message: string) => void; error?: (message: string) => void; info?: (message: string) => void },
|
|
133
|
-
|
|
134
|
-
ctx: { workspaceDir?: string; sessionId?: string },
|
|
135
|
-
profile?: { risk_paths: string[]; core_governance_files?: string[] }
|
|
136
|
-
): PluginHookBeforeToolCallResult | void {
|
|
137
|
-
// P-16: Core governance files are exempt from all gate blocking
|
|
138
|
-
if (isCoreGovernanceFile(relPath, getCoreGovernancePatterns(profile))) {
|
|
139
|
-
logger.info?.(`[PD_GATE:P-16] Core governance file exempt — bypass all gates: ${relPath}`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// EP is the only gate now - use actual gate decision
|
|
144
|
-
if (!ctx.workspaceDir) {
|
|
145
|
-
logger.warn?.('[PD_GATE] No workspaceDir, skipping EP gate check');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Call EP gate - this is the actual gate, not simulation
|
|
150
|
-
const epDecision = checkEvolutionGate(ctx.workspaceDir, {
|
|
151
|
-
toolName: event.toolName,
|
|
152
|
-
isRiskPath: risky,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const currentTier = epDecision.currentTier ?? 1;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const tierName = getTierName(currentTier);
|
|
159
|
-
|
|
160
|
-
logger.info?.(`[PD_GATE] EP Gate: Tier ${currentTier} (${tierName}), Tool: ${event.toolName}, Risk: ${risky}, Allowed: ${epDecision.allowed}`);
|
|
161
|
-
|
|
162
|
-
if (!epDecision.allowed) {
|
|
163
|
-
const reason = buildEvolutionGateReason(currentTier, tierName, epDecision.reason ?? 'Unknown restriction');
|
|
164
|
-
return block(relPath, reason, wctx, event.toolName, logger, ctx.sessionId);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Gate passed - allow
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Get tier name from tier number
|
|
173
|
-
*/
|
|
174
|
-
function getTierName(tier: number): string {
|
|
175
|
-
const names: Record<number, string> = {
|
|
176
|
-
1: 'Seed',
|
|
177
|
-
2: 'Sprout',
|
|
178
|
-
3: 'Sapling',
|
|
179
|
-
4: 'Tree',
|
|
180
|
-
5: 'Forest',
|
|
181
|
-
};
|
|
182
|
-
return names[tier] ?? 'Unknown';
|
|
183
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thinking Checkpoint Module
|
|
3
|
-
*
|
|
4
|
-
* Enforces P-10 deep reflection requirement for high-risk tool operations.
|
|
5
|
-
*
|
|
6
|
-
* **Responsibilities:**
|
|
7
|
-
* - Check if high-risk tools have recent deep thinking (T-01 through T-10)
|
|
8
|
-
* - Block high-risk operations without preceding deep reflection
|
|
9
|
-
* - Configurable time window for thinking validity (default 5 minutes)
|
|
10
|
-
* - Provide clear guidance on required action (deep_reflect tool usage)
|
|
11
|
-
*
|
|
12
|
-
* **Configuration:**
|
|
13
|
-
* - Thinking checkpoint settings from profile.thinking_checkpoint
|
|
14
|
-
* - Window duration for thinking validity
|
|
15
|
-
* - High-risk tool list
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { hasRecentThinking } from '../core/session-tracker.js';
|
|
19
|
-
import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
|
|
20
|
-
import {
|
|
21
|
-
THINKING_CHECKPOINT_WINDOW_MS,
|
|
22
|
-
THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS
|
|
23
|
-
} from '../config/index.js';
|
|
24
|
-
|
|
25
|
-
export interface ThinkingCheckpointConfig {
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
window_ms?: number;
|
|
28
|
-
high_risk_tools?: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Checks if a tool call requires a recent deep thinking checkpoint.
|
|
33
|
-
*
|
|
34
|
-
* This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
|
|
35
|
-
* be preceded by deep reflection within the configured time window.
|
|
36
|
-
*
|
|
37
|
-
* @param event - The before_tool_call event
|
|
38
|
-
* @param config - Thinking checkpoint configuration from profile
|
|
39
|
-
* @param sessionId - Current session ID
|
|
40
|
-
* @param logger - Optional logger for info messages
|
|
41
|
-
* @returns Block result if thinking required, undefined otherwise
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export function checkThinkingCheckpoint(
|
|
46
|
-
event: PluginHookBeforeToolCallEvent,
|
|
47
|
-
config: ThinkingCheckpointConfig,
|
|
48
|
-
sessionId: string | undefined,
|
|
49
|
-
|
|
50
|
-
logger?: { info?: (message: string) => void }
|
|
51
|
-
): PluginHookBeforeToolCallResult | undefined {
|
|
52
|
-
const enabled = config.enabled ?? false;
|
|
53
|
-
const windowMs = config.window_ms ?? THINKING_CHECKPOINT_WINDOW_MS;
|
|
54
|
-
const highRiskTools = config.high_risk_tools ?? [...THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS];
|
|
55
|
-
|
|
56
|
-
if (!enabled || !sessionId) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const isHighRisk = highRiskTools.includes(event.toolName);
|
|
61
|
-
if (!isHighRisk) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const hasThinking = hasRecentThinking(sessionId, windowMs);
|
|
66
|
-
if (!hasThinking) {
|
|
67
|
-
logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
block: true,
|
|
71
|
-
blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs/60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { analyzeBashCommand } from '../../src/hooks/bash-risk.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Integration tests for bash-risk module
|
|
6
|
-
* Tests zero-width character detection and command chain analysis
|
|
7
|
-
*/
|
|
8
|
-
describe('Bash Risk Analysis - Integration', () => {
|
|
9
|
-
describe('Zero-width character detection', () => {
|
|
10
|
-
it('should block commands with zero-width space (U+200B)', () => {
|
|
11
|
-
// Zero-width space injected between characters
|
|
12
|
-
const maliciousCmd = 'rm\u200B -rf /';
|
|
13
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
14
|
-
expect(result).toBe('dangerous');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should block commands with zero-width non-joiner (U+200C)', () => {
|
|
18
|
-
const maliciousCmd = 'rm\u200C -rf /';
|
|
19
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
20
|
-
expect(result).toBe('dangerous');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should block commands with zero-width joiner (U+200D)', () => {
|
|
24
|
-
const maliciousCmd = 'rm\u200D -rf /';
|
|
25
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
26
|
-
expect(result).toBe('dangerous');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should block commands with word joiner (U+2060)', () => {
|
|
30
|
-
const maliciousCmd = 'rm\u2060 -rf /';
|
|
31
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
32
|
-
expect(result).toBe('dangerous');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should block commands with zero-width invisible separator (U+FEFF)', () => {
|
|
36
|
-
const maliciousCmd = 'rm\uFEFF -rf /';
|
|
37
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
38
|
-
expect(result).toBe('dangerous');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should block commands with multiple zero-width characters', () => {
|
|
42
|
-
const maliciousCmd = 'rm\u200B\u200C\u200D -rf /';
|
|
43
|
-
const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
|
|
44
|
-
expect(result).toBe('dangerous');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should allow normal commands without zero-width characters', () => {
|
|
48
|
-
const normalCmd = 'rm -rf /tmp/test';
|
|
49
|
-
const result = analyzeBashCommand(normalCmd, ['^rm\\s'], [], { warn: vi.fn() });
|
|
50
|
-
expect(result).toBe('safe');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should detect zero-width characters hidden in normal-looking commands', () => {
|
|
54
|
-
// Command looks like 'git status' but contains hidden chars
|
|
55
|
-
const hiddenCmd = 'git\u200B status';
|
|
56
|
-
const result = analyzeBashCommand(hiddenCmd, [], [], { warn: vi.fn() });
|
|
57
|
-
expect(result).toBe('dangerous');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('Cyrillic homograph attack detection', () => {
|
|
62
|
-
it('should de-obfuscate Cyrillic characters and match dangerous patterns', () => {
|
|
63
|
-
// Cyrillic 'р' (U+0440) looks like Latin 'p' - 'g\u0440ush' → 'gpush' then 'push'
|
|
64
|
-
// After toLowerCase + deobfuscation: 'gpush' won't match dangerous, but 'push' alone might
|
|
65
|
-
// Actually: '\u0440' maps to 'p' so 'g\u0440ush' → 'gp' + 'ush' = 'gpush'
|
|
66
|
-
// Let's use Cyrillic 'е' which maps to 'e': '\u0435' → 'e'
|
|
67
|
-
const cyrillicCmd = 'r\u0435m -rf /tmp';
|
|
68
|
-
const result = analyzeBashCommand(cyrillicCmd, [], ['^rem'], { warn: vi.fn() });
|
|
69
|
-
expect(result).toBe('dangerous');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should handle Cyrillic а being converted to Latin a', () => {
|
|
73
|
-
// Cyrillic 'а' (U+0430) maps to Latin 'a' - 's\u0430do' → 'sado'
|
|
74
|
-
// After de-obfuscation: 'sado' - doesn't match dangerous patterns
|
|
75
|
-
const mixedCmd = 's\u0430do apt update';
|
|
76
|
-
const result = analyzeBashCommand(mixedCmd, [], ['^sudo'], { warn: vi.fn() });
|
|
77
|
-
expect(result).toBe('normal');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('Command chain tokenization', () => {
|
|
82
|
-
it('should detect dangerous commands in chains using &&', () => {
|
|
83
|
-
const chainCmd = 'echo "hello" && rm -rf /tmp/test';
|
|
84
|
-
const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
|
|
85
|
-
expect(result).toBe('dangerous');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should detect dangerous commands in chains using ||', () => {
|
|
89
|
-
const chainCmd = 'ls || rm -rf /tmp/test';
|
|
90
|
-
const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
|
|
91
|
-
expect(result).toBe('dangerous');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should detect dangerous commands in chains using ;', () => {
|
|
95
|
-
const chainCmd = 'ls ; rm -rf /tmp/test';
|
|
96
|
-
const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
|
|
97
|
-
expect(result).toBe('dangerous');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should return safe if no segment matches dangerous patterns', () => {
|
|
101
|
-
const safeChain = 'echo "hello" && echo "world"';
|
|
102
|
-
const result = analyzeBashCommand(safeChain, [], ['rm\\s+-rf'], { warn: vi.fn() });
|
|
103
|
-
expect(result).toBe('normal');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('Fail-closed behavior', () => {
|
|
108
|
-
it('should return dangerous for invalid regex in dangerousPatterns', () => {
|
|
109
|
-
const cmd = 'ls';
|
|
110
|
-
const result = analyzeBashCommand(cmd, [], ['[invalid'], { warn: vi.fn() });
|
|
111
|
-
expect(result).toBe('dangerous'); // Fail-closed
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should ignore invalid regex in safePatterns', () => {
|
|
115
|
-
const cmd = 'echo hello';
|
|
116
|
-
const warnFn = vi.fn();
|
|
117
|
-
const result = analyzeBashCommand(cmd, ['[invalid'], [], { warn: warnFn });
|
|
118
|
-
// Should not crash, should return normal (not all segments safe)
|
|
119
|
-
expect(result).toBe('normal');
|
|
120
|
-
expect(warnFn).toHaveBeenCalled();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('Safe pattern override', () => {
|
|
125
|
-
it('should return safe when all segments match safe patterns', () => {
|
|
126
|
-
const safeCmd = 'git status';
|
|
127
|
-
const result = analyzeBashCommand(safeCmd, ['^git\\s+status', '^ls'], []);
|
|
128
|
-
expect(result).toBe('safe');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should return normal when not all segments are safe', () => {
|
|
132
|
-
const mixedCmd = 'git status && rm -rf /tmp';
|
|
133
|
-
const result = analyzeBashCommand(mixedCmd, ['^git\\s+status'], ['rm\\s+-rf']);
|
|
134
|
-
expect(result).toBe('dangerous');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { analyzeBashCommand } from '../../src/hooks/bash-risk.js';
|
|
3
|
-
|
|
4
|
-
describe('analyzeBashCommand', () => {
|
|
5
|
-
it('should return safe for commands matching safe patterns', () => {
|
|
6
|
-
const result = analyzeBashCommand('npm install lodash', ['^npm\\s+install'], []);
|
|
7
|
-
expect(result).toBe('safe');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('should return dangerous for commands matching dangerous patterns', () => {
|
|
11
|
-
const result = analyzeBashCommand('rm -rf /', [], ['rm\\s+.*-rf']);
|
|
12
|
-
expect(result).toBe('dangerous');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should return normal for commands not in safe/dangerous lists', () => {
|
|
16
|
-
const result = analyzeBashCommand('npm install lodash', [], []);
|
|
17
|
-
expect(result).toBe('normal');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should de-obfuscate Cyrillic lookalikes', () => {
|
|
21
|
-
// Using Cyrillic 'rеset' (with Cyrillic 'е' U+0435) instead of 'reset'
|
|
22
|
-
// This should de-obfuscate to 'git reset --hard' and match the dangerous pattern
|
|
23
|
-
const result = analyzeBashCommand('git rеset --hard', [], ['git\\s+(push\\s+.*--force|reset\\s+--hard|clean\\s+-fd)']);
|
|
24
|
-
expect(result).toBe('dangerous');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should tokenize command chains', () => {
|
|
28
|
-
const result = analyzeBashCommand('npm install && npm test', ['^npm\\s+install'], ['npm\\s+publish']);
|
|
29
|
-
expect(result).toBe('normal');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should fail-closed on invalid dangerous regex', () => {
|
|
33
|
-
const mockLogger = { warn: vi.fn() };
|
|
34
|
-
const result = analyzeBashCommand('echo test', ['^echo'], ['invalid('], mockLogger);
|
|
35
|
-
expect(result).toBe('dangerous'); // Fail-closed behavior
|
|
36
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
37
|
-
expect.stringContaining('Invalid dangerous bash regex')
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should ignore safe pattern on invalid safe regex', () => {
|
|
42
|
-
const mockLogger = { warn: vi.fn() };
|
|
43
|
-
const result = analyzeBashCommand('echo test', ['invalid('], [], mockLogger);
|
|
44
|
-
expect(result).toBe('normal'); // Not safe because safe pattern is invalid and ignored
|
|
45
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
46
|
-
expect.stringContaining('Invalid safe bash regex')
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should strip $() from commands before pattern matching', () => {
|
|
51
|
-
const result = analyzeBashCommand('$(npm install)', ['^npm\\s+install'], []);
|
|
52
|
-
expect(result).toBe('safe');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should strip backticks from commands before pattern matching', () => {
|
|
56
|
-
const result = analyzeBashCommand('`npm install`', ['^npm\\s+install'], []);
|
|
57
|
-
expect(result).toBe('safe');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should handle command chains with semicolon separator', () => {
|
|
61
|
-
const result = analyzeBashCommand('npm install ; npm test', ['^npm\\s+install'], ['rm\\s+']);
|
|
62
|
-
expect(result).toBe('normal'); // Second command not safe
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should handle command chains with OR separator', () => {
|
|
66
|
-
const result = analyzeBashCommand('npm install || npm test', ['^npm\\s+install'], ['rm\\s+']);
|
|
67
|
-
expect(result).toBe('normal');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should convert uppercase Cyrillic to lowercase Latin', () => {
|
|
71
|
-
// Using uppercase Cyrillic 'Е' (U+0415) which should convert to 'e'
|
|
72
|
-
// 'git REsET --hard' should become 'git reset --hard' and match the dangerous pattern
|
|
73
|
-
const result = analyzeBashCommand('git rEsET --hard', [], ['git\\s+(push\\s+.*--force|reset\\s+--hard|clean\\s+-fd)']);
|
|
74
|
-
expect(result).toBe('dangerous');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should handle additional confusable Unicode characters', () => {
|
|
78
|
-
const result = analyzeBashCommand('del node_modules', [], ['rm\\s+']);
|
|
79
|
-
expect(result).toBe('normal'); // 'del' is not in dangerous patterns
|
|
80
|
-
});
|
|
81
|
-
});
|