@wooojin/forgen 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/.claude-plugin/plugin.json +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PreToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* 도구 실행 전 위험 명령어 차단 및 컨텍스트 리마인더 주입.
|
|
6
|
+
* - rm -rf, git push --force 등 위험 패턴 감지
|
|
7
|
+
* - 활성 모드 상태 리마인더 주입
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { createLogger } from '../core/logger.js';
|
|
13
|
+
const log = createLogger('pre-tool-use');
|
|
14
|
+
import { HookError } from '../core/errors.js';
|
|
15
|
+
import { readStdinJSON } from './shared/read-stdin.js';
|
|
16
|
+
import { atomicWriteJSON } from './shared/atomic-write.js';
|
|
17
|
+
import { withFileLockSync, FileLockError } from './shared/file-lock.js';
|
|
18
|
+
import { sanitizeId } from './shared/sanitize-id.js';
|
|
19
|
+
import { incrementEvidence } from '../engine/solution-writer.js';
|
|
20
|
+
import { isReflectionCandidate } from './compound-reflection.js';
|
|
21
|
+
import { isHookEnabled } from './hook-config.js';
|
|
22
|
+
import { approve, approveWithWarning, deny, failOpen } from './shared/hook-response.js';
|
|
23
|
+
import { FORGEN_HOME, STATE_DIR } from '../core/paths.js';
|
|
24
|
+
const FAIL_COUNTER_PATH = path.join(STATE_DIR, 'pre-tool-fail-counter.json');
|
|
25
|
+
const FAIL_CLOSE_THRESHOLD = 3; // 연속 3회 파싱 실패 시에만 reject
|
|
26
|
+
/** RegExp 안전성 검증 (ReDoS 방지) — 매칭/비매칭 양쪽 모두 테스트 */
|
|
27
|
+
function isSafeRegex(pattern, flags) {
|
|
28
|
+
try {
|
|
29
|
+
const re = new RegExp(pattern, flags);
|
|
30
|
+
const testStr = 'a'.repeat(25);
|
|
31
|
+
// 매칭 성공 케이스
|
|
32
|
+
let start = Date.now();
|
|
33
|
+
re.test(testStr);
|
|
34
|
+
if (Date.now() - start >= 100)
|
|
35
|
+
return false;
|
|
36
|
+
// 매칭 실패 케이스 (ReDoS는 주로 여기서 발생)
|
|
37
|
+
start = Date.now();
|
|
38
|
+
re.test(`${testStr}!`);
|
|
39
|
+
return Date.now() - start < 100;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** JSON에서 패턴 로드 (패키지 내장 + 사용자 커스텀 병합) */
|
|
46
|
+
function loadDangerousPatterns() {
|
|
47
|
+
const results = [];
|
|
48
|
+
// 1. 패키지 내장 패턴 (dangerous-patterns.json)
|
|
49
|
+
try {
|
|
50
|
+
const builtinPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dangerous-patterns.json');
|
|
51
|
+
const raw = JSON.parse(fs.readFileSync(builtinPath, 'utf-8'));
|
|
52
|
+
for (const entry of raw) {
|
|
53
|
+
if (!isSafeRegex(entry.pattern, entry.flags ?? '')) {
|
|
54
|
+
log.debug(`내장 패턴 건너뜀 (ReDoS 위험): ${entry.description}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
results.push({
|
|
58
|
+
pattern: new RegExp(entry.pattern, entry.flags ?? ''),
|
|
59
|
+
description: entry.description,
|
|
60
|
+
severity: entry.severity,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// JSON 로드 실패 시 하드코딩 폴백 (최소 안전장치)
|
|
66
|
+
results.push({ pattern: /rm\s+(-rf|-fr)\s+[/~]/, description: 'rm -rf on root/home path', severity: 'block' }, { pattern: /curl\s+.*\|\s*(ba)?sh/, description: 'curl pipe to shell', severity: 'block' }, { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, description: 'fork bomb', severity: 'block' });
|
|
67
|
+
}
|
|
68
|
+
// 2. 사용자 커스텀 패턴 (~/.compound/dangerous-patterns.json)
|
|
69
|
+
try {
|
|
70
|
+
const customPath = path.join(FORGEN_HOME, 'dangerous-patterns.json');
|
|
71
|
+
if (fs.existsSync(customPath)) {
|
|
72
|
+
const custom = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
|
|
73
|
+
for (const entry of custom) {
|
|
74
|
+
if (!isSafeRegex(entry.pattern, entry.flags ?? '')) {
|
|
75
|
+
log.debug(`사용자 커스텀 패턴 건너뜀 (ReDoS 위험): ${entry.description}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
results.push({
|
|
79
|
+
pattern: new RegExp(entry.pattern, entry.flags ?? ''),
|
|
80
|
+
description: entry.description,
|
|
81
|
+
severity: entry.severity,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
log.debug('사용자 커스텀 위험 패턴 로드 실패');
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
/** 위험 Bash 명령어 패턴 (패키지 내장 + 사용자 커스텀 병합) */
|
|
92
|
+
export const DANGEROUS_PATTERNS = loadDangerousPatterns();
|
|
93
|
+
const REMINDER_INTERVAL = 10; // 10회 호출당 1회 리마인더
|
|
94
|
+
const REMINDER_COUNTER_PATH = path.join(STATE_DIR, 'reminder-counter.json');
|
|
95
|
+
/** 위험 명령어 검사 (순수 함수) */
|
|
96
|
+
export function checkDangerousCommand(toolName, toolInput) {
|
|
97
|
+
if (toolName !== 'Bash')
|
|
98
|
+
return { action: 'pass' };
|
|
99
|
+
const command = typeof toolInput === 'string'
|
|
100
|
+
? toolInput
|
|
101
|
+
: (toolInput.command ?? '');
|
|
102
|
+
for (const { pattern, description, severity } of DANGEROUS_PATTERNS) {
|
|
103
|
+
if (pattern.test(command)) {
|
|
104
|
+
return { action: severity, description, command: command.slice(0, 100) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { action: 'pass' };
|
|
108
|
+
}
|
|
109
|
+
/** 카운터 기반 리마인더 표시 여부 판정 (순수 함수 — I/O 없음) */
|
|
110
|
+
export function shouldShowReminder(count, interval = REMINDER_INTERVAL) {
|
|
111
|
+
return count > 0 && count % interval === 0;
|
|
112
|
+
}
|
|
113
|
+
/** 카운터 기반 리마인더 표시 여부 (I/O 포함 — main에서 사용) */
|
|
114
|
+
function shouldShowReminderIO() {
|
|
115
|
+
try {
|
|
116
|
+
let count;
|
|
117
|
+
if (fs.existsSync(REMINDER_COUNTER_PATH)) {
|
|
118
|
+
const data = JSON.parse(fs.readFileSync(REMINDER_COUNTER_PATH, 'utf-8'));
|
|
119
|
+
count = (data.count ?? 0) + 1;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// 파일 없음 = 최초 호출: 1부터 시작하여 10번째 호출에 첫 리마인더 표시
|
|
123
|
+
count = 1;
|
|
124
|
+
}
|
|
125
|
+
atomicWriteJSON(REMINDER_COUNTER_PATH, { count });
|
|
126
|
+
return shouldShowReminder(count);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 활성 모드 상태를 리마인더로 수집 */
|
|
133
|
+
function getActiveReminders() {
|
|
134
|
+
const reminders = [];
|
|
135
|
+
if (!fs.existsSync(STATE_DIR))
|
|
136
|
+
return reminders;
|
|
137
|
+
try {
|
|
138
|
+
for (const f of fs.readdirSync(STATE_DIR)) {
|
|
139
|
+
if (!f.endsWith('-state.json') || f.startsWith('context-guard') || f.startsWith('skill-cache'))
|
|
140
|
+
continue;
|
|
141
|
+
try {
|
|
142
|
+
const data = JSON.parse(fs.readFileSync(path.join(STATE_DIR, f), 'utf-8'));
|
|
143
|
+
if (data.active) {
|
|
144
|
+
const mode = f.replace('-state.json', '');
|
|
145
|
+
reminders.push(`[${mode}] mode active`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { /* skip corrupt files */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
log.debug('상태 디렉토리 읽기 실패', e);
|
|
153
|
+
}
|
|
154
|
+
return reminders;
|
|
155
|
+
}
|
|
156
|
+
/** 연속 파싱 실패 카운터 관리 */
|
|
157
|
+
function getAndIncrementFailCount() {
|
|
158
|
+
try {
|
|
159
|
+
let count = 0;
|
|
160
|
+
if (fs.existsSync(FAIL_COUNTER_PATH)) {
|
|
161
|
+
const data = JSON.parse(fs.readFileSync(FAIL_COUNTER_PATH, 'utf-8'));
|
|
162
|
+
count = (data.count ?? 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
count = 1;
|
|
166
|
+
}
|
|
167
|
+
atomicWriteJSON(FAIL_COUNTER_PATH, { count, updatedAt: new Date().toISOString() });
|
|
168
|
+
return count;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function resetFailCount() {
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(FAIL_COUNTER_PATH))
|
|
177
|
+
fs.unlinkSync(FAIL_COUNTER_PATH);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
log.debug('fail counter reset failed — counter stays elevated', e);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Compound v3: detect if Edit/Write code reflects injected solution identifiers.
|
|
185
|
+
*
|
|
186
|
+
* false positive 방지를 위한 3중 필터 적용 (compound-reflection.ts):
|
|
187
|
+
* 1. 시간 윈도우: 주입 후 15분 이내만 반영 인정
|
|
188
|
+
* 2. 매칭 비율: 유효 식별자의 50% 이상 매칭 필요
|
|
189
|
+
* 3. 공통 식별자 차단: 프레임워크 기본 용어 제외
|
|
190
|
+
*/
|
|
191
|
+
function checkCompoundReflection(toolName, toolInput, sessionId) {
|
|
192
|
+
if (toolName !== 'Edit' && toolName !== 'Write')
|
|
193
|
+
return;
|
|
194
|
+
const code = String(toolInput.new_string ?? toolInput.content ?? '');
|
|
195
|
+
if (!code || code.length < 10)
|
|
196
|
+
return;
|
|
197
|
+
const cachePath = path.join(STATE_DIR, `injection-cache-${sanitizeId(sessionId)}.json`);
|
|
198
|
+
if (!fs.existsSync(cachePath))
|
|
199
|
+
return;
|
|
200
|
+
// PR2c-1 + M-2 fix: lock-narrowing.
|
|
201
|
+
// cache lock 안에서는 cache 갱신(_sessionCounted 비트)만 수행하고,
|
|
202
|
+
// evidence 갱신은 lock 밖에서 수행한다. 이전 구조는 cache lock을 잡은 채로
|
|
203
|
+
// 매 솔루션마다 .md 파일 lock을 잡아서 cache lock holding time이 N×해졌고
|
|
204
|
+
// 다른 hook이 cache lock을 잡지 못해 FileLockError가 빈발할 수 있었다.
|
|
205
|
+
const reflectedNames = [];
|
|
206
|
+
const newlySessionCounted = [];
|
|
207
|
+
try {
|
|
208
|
+
withFileLockSync(cachePath, () => {
|
|
209
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
210
|
+
if (!Array.isArray(cache.solutions))
|
|
211
|
+
return;
|
|
212
|
+
const now = new Date();
|
|
213
|
+
let mutated = false;
|
|
214
|
+
for (const sol of cache.solutions) {
|
|
215
|
+
if (!Array.isArray(sol.identifiers) || sol.identifiers.length === 0)
|
|
216
|
+
continue;
|
|
217
|
+
const result = isReflectionCandidate({
|
|
218
|
+
identifiers: sol.identifiers,
|
|
219
|
+
code,
|
|
220
|
+
injectedAt: sol.injectedAt ?? '',
|
|
221
|
+
now,
|
|
222
|
+
});
|
|
223
|
+
if (result.reflected) {
|
|
224
|
+
reflectedNames.push(sol.name);
|
|
225
|
+
if (!sol._sessionCounted) {
|
|
226
|
+
sol._sessionCounted = true;
|
|
227
|
+
mutated = true;
|
|
228
|
+
newlySessionCounted.push(sol.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (mutated) {
|
|
233
|
+
// mode 0o600 — solution-injector와 일관성
|
|
234
|
+
atomicWriteJSON(cachePath, cache, { mode: 0o600, dirMode: 0o700 });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
if (e instanceof FileLockError) {
|
|
240
|
+
log.warn('compound reflection lock 실패 — write skipped', e);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
log.debug('compound reflection 체크 실패', e);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Evidence 갱신은 lock 밖에서 (M-2 fix). solution-writer가 자체 lock을 가지므로 안전.
|
|
248
|
+
for (const name of reflectedNames) {
|
|
249
|
+
updateSolutionEvidence(name, 'reflected');
|
|
250
|
+
}
|
|
251
|
+
for (const name of newlySessionCounted) {
|
|
252
|
+
updateSolutionEvidence(name, 'sessions');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Update evidence counter in a solution file.
|
|
257
|
+
* PR2b: solution-writer.incrementEvidence로 위임. lock + fresh re-read + atomic write.
|
|
258
|
+
*
|
|
259
|
+
* Exported for use by solution-injector.
|
|
260
|
+
*/
|
|
261
|
+
export function updateSolutionEvidence(solutionName, field) {
|
|
262
|
+
incrementEvidence(solutionName, field);
|
|
263
|
+
}
|
|
264
|
+
async function main() {
|
|
265
|
+
const data = await readStdinJSON();
|
|
266
|
+
if (!data) {
|
|
267
|
+
// graceful fail-close: consecutive failure counter.
|
|
268
|
+
// At threshold, block with a user-visible deny message (the block itself
|
|
269
|
+
// is actionable — the user needs to know why their tool call was
|
|
270
|
+
// rejected). Below threshold, pass SILENTLY via plain approve() so a
|
|
271
|
+
// transient parse glitch doesn't leak `systemMessage` noise to the
|
|
272
|
+
// user's terminal on every tool call. stderr still gets the counter
|
|
273
|
+
// for `forgen doctor` / log inspection. Mirrors `db-guard.ts:85-96`.
|
|
274
|
+
const failCount = getAndIncrementFailCount();
|
|
275
|
+
if (failCount >= FAIL_CLOSE_THRESHOLD) {
|
|
276
|
+
console.log(deny(`[Forgen] PreToolUse: stdin parse failed ${failCount} consecutive times — blocking for safety.`));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
process.stderr.write(`[ch-hook] pre-tool-use stdin parse failed (${failCount}/${FAIL_CLOSE_THRESHOLD})\n`);
|
|
280
|
+
console.log(approve());
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// 정상 파싱 성공 시 연속 실패 카운터 리셋
|
|
285
|
+
resetFailCount();
|
|
286
|
+
if (!isHookEnabled('pre-tool-use')) {
|
|
287
|
+
console.log(approve());
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const toolName = data.tool_name ?? data.toolName ?? '';
|
|
291
|
+
const toolInput = data.tool_input ?? data.toolInput ?? {};
|
|
292
|
+
const sessionId = data.session_id ?? 'default';
|
|
293
|
+
// Bash 도구: 위험 명령어 감지
|
|
294
|
+
const check = checkDangerousCommand(toolName, toolInput);
|
|
295
|
+
if (check.action === 'block') {
|
|
296
|
+
console.log(deny(`[Forgen] Dangerous command blocked: ${check.description}\nCommand: ${check.command}`));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (check.action === 'warn') {
|
|
300
|
+
console.log(approveWithWarning(`<compound-tool-warning>\n[Forgen] ⚠ Dangerous command detected: ${check.description}\nProceed with caution.\n</compound-tool-warning>`));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Compound v3: Code Reflection check (non-blocking)
|
|
304
|
+
try {
|
|
305
|
+
checkCompoundReflection(toolName, toolInput, sessionId);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
log.debug('compound reflection check 실패', e);
|
|
309
|
+
}
|
|
310
|
+
// 활성 모드 리마인더 (10회 호출당 1회 — 결정적 카운터 기반)
|
|
311
|
+
const reminders = getActiveReminders();
|
|
312
|
+
if (reminders.length > 0 && shouldShowReminderIO()) {
|
|
313
|
+
console.log(approveWithWarning(`<compound-reminder>\n${reminders.join('\n')}\n</compound-reminder>`));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
console.log(approve());
|
|
317
|
+
}
|
|
318
|
+
main().catch((e) => {
|
|
319
|
+
const hookErr = new HookError(e instanceof Error ? e.message : String(e), {
|
|
320
|
+
hookName: 'pre-tool-use', eventType: 'PreToolUse', cause: e,
|
|
321
|
+
});
|
|
322
|
+
process.stderr.write(`[ch-hook] ${hookErr.name}: ${hookErr.message}\n`);
|
|
323
|
+
// fail-open: approve on internal error to avoid blocking all tool calls
|
|
324
|
+
console.log(failOpen());
|
|
325
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security module for filtering prompt injection attacks from solution content
|
|
3
|
+
* before it gets injected into Claude's context.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This is a shared utility, NOT a standalone hook.
|
|
6
|
+
* Used by: solution-injector.ts (via import)
|
|
7
|
+
* Exported via: lib.ts (public API for programmatic use)
|
|
8
|
+
* Not registered in hooks.json — intentional.
|
|
9
|
+
*/
|
|
10
|
+
type Severity = 'block' | 'warn';
|
|
11
|
+
type Category = 'injection' | 'exfiltration' | 'obfuscation';
|
|
12
|
+
interface SecurityPattern {
|
|
13
|
+
id: string;
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
severity: Severity;
|
|
16
|
+
category: Category;
|
|
17
|
+
}
|
|
18
|
+
export interface ScanFinding {
|
|
19
|
+
patternId: string;
|
|
20
|
+
severity: Severity;
|
|
21
|
+
category: Category;
|
|
22
|
+
matchedText: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ScanResult {
|
|
25
|
+
verdict: 'safe' | 'warn' | 'block';
|
|
26
|
+
findings: ScanFinding[];
|
|
27
|
+
sanitized: string;
|
|
28
|
+
}
|
|
29
|
+
/** Structured security patterns with severity and category metadata */
|
|
30
|
+
export declare const SECURITY_PATTERNS: SecurityPattern[];
|
|
31
|
+
/**
|
|
32
|
+
* Legacy flat array for backwards-compatible exports.
|
|
33
|
+
* Contains only the RegExp patterns from SECURITY_PATTERNS.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PROMPT_INJECTION_PATTERNS: RegExp[];
|
|
36
|
+
/** Normalize text for injection detection: strip zero-width chars, NFKC normalize */
|
|
37
|
+
export declare function normalizeForInjectionCheck(text: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Escape ALL XML-like tags in text to prevent tag injection.
|
|
40
|
+
*/
|
|
41
|
+
export declare function escapeAllXmlTags(text: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Combined filter: checks for prompt injection and escapes XML tags.
|
|
44
|
+
* - block 패턴 매칭 → verdict: 'block', sanitized: ''
|
|
45
|
+
* - warn만 매칭 → verdict: 'warn', sanitized: XML 이스케이프된 텍스트
|
|
46
|
+
* - 매칭 없음 → verdict: 'safe', sanitized: XML 이스케이프된 텍스트
|
|
47
|
+
*/
|
|
48
|
+
export declare function filterSolutionContent(text: string): ScanResult;
|
|
49
|
+
/**
|
|
50
|
+
* Check if text contains prompt injection patterns.
|
|
51
|
+
* Normalizes Unicode before matching to prevent bypass via homoglyphs/zero-width chars.
|
|
52
|
+
*
|
|
53
|
+
* @returns true only when a 'block'-severity pattern matches (하위 호환 유지).
|
|
54
|
+
*/
|
|
55
|
+
export declare function containsPromptInjection(text: string): boolean;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security module for filtering prompt injection attacks from solution content
|
|
3
|
+
* before it gets injected into Claude's context.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This is a shared utility, NOT a standalone hook.
|
|
6
|
+
* Used by: solution-injector.ts (via import)
|
|
7
|
+
* Exported via: lib.ts (public API for programmatic use)
|
|
8
|
+
* Not registered in hooks.json — intentional.
|
|
9
|
+
*/
|
|
10
|
+
// ── Pattern registry ───────────────────────────────────────────────────────
|
|
11
|
+
/** Structured security patterns with severity and category metadata */
|
|
12
|
+
export const SECURITY_PATTERNS = [
|
|
13
|
+
// --- injection / block: 명시적 지시 무효화 ---
|
|
14
|
+
{
|
|
15
|
+
id: 'ignore-previous-instructions',
|
|
16
|
+
pattern: /ignore\s+(all\s+)?previous\s+instructions/i,
|
|
17
|
+
severity: 'block',
|
|
18
|
+
category: 'injection',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'ignore-above-prior',
|
|
22
|
+
pattern: /ignore\s+(all\s+)?(above|prior)/i,
|
|
23
|
+
severity: 'block',
|
|
24
|
+
category: 'injection',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'disregard-previous',
|
|
28
|
+
pattern: /disregard\s+(all\s+)?(previous|above|prior)/i,
|
|
29
|
+
severity: 'block',
|
|
30
|
+
category: 'injection',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'new-instructions',
|
|
34
|
+
pattern: /new\s+instructions?\s*:/i,
|
|
35
|
+
severity: 'block',
|
|
36
|
+
category: 'injection',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'forget-context',
|
|
40
|
+
pattern: /forget\s+(everything|all|previous|your)/i,
|
|
41
|
+
severity: 'block',
|
|
42
|
+
category: 'injection',
|
|
43
|
+
},
|
|
44
|
+
// --- injection / block: 특수 태그 ---
|
|
45
|
+
{
|
|
46
|
+
id: 'system-tag',
|
|
47
|
+
pattern: /<\s*\/?system\s*>/i,
|
|
48
|
+
severity: 'block',
|
|
49
|
+
category: 'injection',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'assistant-tag',
|
|
53
|
+
pattern: /<\s*\/?assistant\s*>/i,
|
|
54
|
+
severity: 'block',
|
|
55
|
+
category: 'injection',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'user-tag',
|
|
59
|
+
pattern: /<\s*\/?user\s*>/i,
|
|
60
|
+
severity: 'block',
|
|
61
|
+
category: 'injection',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'human-tag',
|
|
65
|
+
pattern: /<\s*\/?human\s*>/i,
|
|
66
|
+
severity: 'block',
|
|
67
|
+
category: 'injection',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'inst-tag',
|
|
71
|
+
pattern: /\[INST\]/i,
|
|
72
|
+
severity: 'block',
|
|
73
|
+
category: 'injection',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'sys-tag',
|
|
77
|
+
pattern: /<<SYS>>/i,
|
|
78
|
+
severity: 'block',
|
|
79
|
+
category: 'injection',
|
|
80
|
+
},
|
|
81
|
+
// --- injection / block: 한국어 명시적 인젝션 ---
|
|
82
|
+
{
|
|
83
|
+
id: 'ko-ignore-previous',
|
|
84
|
+
pattern: /이전\s*(지시|명령|설정|규칙).*무시/,
|
|
85
|
+
severity: 'block',
|
|
86
|
+
category: 'injection',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'ko-ignore-all',
|
|
90
|
+
pattern: /(모든|전부|앞의|위의)\s*(지시|명령|설정).*무시/,
|
|
91
|
+
severity: 'block',
|
|
92
|
+
category: 'injection',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'ko-forget',
|
|
96
|
+
pattern: /(잊어|잊으|잊어버려|잊어라)/,
|
|
97
|
+
severity: 'block',
|
|
98
|
+
category: 'injection',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'ko-you-are-now',
|
|
102
|
+
pattern: /넌\s+이제부터/,
|
|
103
|
+
severity: 'block',
|
|
104
|
+
category: 'injection',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'ko-new-role',
|
|
108
|
+
pattern: /새로운\s*(역할|지시|명령|규칙)/,
|
|
109
|
+
severity: 'block',
|
|
110
|
+
category: 'injection',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'ko-change-role',
|
|
114
|
+
pattern: /너(는|의)\s*(역할|정체).*바꿔/,
|
|
115
|
+
severity: 'block',
|
|
116
|
+
category: 'injection',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'ko-system-prompt',
|
|
120
|
+
pattern: /(시스템|어시스턴트)\s*(프롬프트|메시지)/,
|
|
121
|
+
severity: 'block',
|
|
122
|
+
category: 'injection',
|
|
123
|
+
},
|
|
124
|
+
// --- injection / warn: 맥락에 따라 합법적일 수 있는 패턴 ---
|
|
125
|
+
{
|
|
126
|
+
id: 'you-are-now',
|
|
127
|
+
pattern: /you\s+are\s+now/i,
|
|
128
|
+
severity: 'warn',
|
|
129
|
+
category: 'injection',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'act-as',
|
|
133
|
+
pattern: /act\s+as\s+(a|an|if)\b/i,
|
|
134
|
+
severity: 'warn',
|
|
135
|
+
category: 'injection',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'pretend-to',
|
|
139
|
+
pattern: /pretend\s+(you|to\s+be)/i,
|
|
140
|
+
severity: 'warn',
|
|
141
|
+
category: 'injection',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'ko-pretend',
|
|
145
|
+
pattern: /인척\s*(해|하세요|해봐)/,
|
|
146
|
+
severity: 'warn',
|
|
147
|
+
category: 'injection',
|
|
148
|
+
},
|
|
149
|
+
// --- exfiltration / block: 비밀키·파일 유출 ---
|
|
150
|
+
{
|
|
151
|
+
id: 'exfil-secret-curl',
|
|
152
|
+
pattern: /curl.*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)/i,
|
|
153
|
+
severity: 'block',
|
|
154
|
+
category: 'exfiltration',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'exfil-secret-file',
|
|
158
|
+
pattern: /cat\s+[^\n]*(\.env|credentials|\.netrc)/i,
|
|
159
|
+
severity: 'block',
|
|
160
|
+
category: 'exfiltration',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'exfil-wget-post',
|
|
164
|
+
pattern: /wget\s+--post-data[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD)/i,
|
|
165
|
+
severity: 'block',
|
|
166
|
+
category: 'exfiltration',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'exfil-nc-pipe',
|
|
170
|
+
pattern: /\|\s*nc\s+\S+\s+\d+/,
|
|
171
|
+
severity: 'block',
|
|
172
|
+
category: 'exfiltration',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'exfil-ssh-key-read',
|
|
176
|
+
pattern: /cat\s+[^\n]*\.ssh\/(id_rsa|id_ed25519|authorized_keys)/i,
|
|
177
|
+
severity: 'block',
|
|
178
|
+
category: 'exfiltration',
|
|
179
|
+
},
|
|
180
|
+
// --- destructive / block: 파괴적 명령 패턴 ---
|
|
181
|
+
{
|
|
182
|
+
id: 'destruct-rm-rf-root',
|
|
183
|
+
pattern: /rm\s+-[^\n]*rf\s+\//,
|
|
184
|
+
severity: 'block',
|
|
185
|
+
category: 'exfiltration',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'destruct-chmod-777',
|
|
189
|
+
pattern: /chmod\s+(-R\s+)?777\s+\//,
|
|
190
|
+
severity: 'block',
|
|
191
|
+
category: 'exfiltration',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'destruct-drop-database',
|
|
195
|
+
pattern: /DROP\s+(DATABASE|TABLE)\s+/i,
|
|
196
|
+
severity: 'block',
|
|
197
|
+
category: 'exfiltration',
|
|
198
|
+
},
|
|
199
|
+
// --- obfuscation / warn: base64 디코드 파이프 ---
|
|
200
|
+
{
|
|
201
|
+
id: 'obfusc-base64-decode',
|
|
202
|
+
pattern: /base64\s+(-d|--decode)\s*\|/,
|
|
203
|
+
severity: 'warn',
|
|
204
|
+
category: 'obfuscation',
|
|
205
|
+
},
|
|
206
|
+
// --- obfuscation / block: 난독화 실행 ---
|
|
207
|
+
{
|
|
208
|
+
id: 'obfusc-echo-exec',
|
|
209
|
+
pattern: /echo\s+[^\n]*\|\s*(bash|sh|python|node)/i,
|
|
210
|
+
severity: 'block',
|
|
211
|
+
category: 'obfuscation',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'obfusc-eval-dynamic',
|
|
215
|
+
pattern: /eval\s*\(\s*(atob|Buffer\.from|decodeURI)/i,
|
|
216
|
+
severity: 'block',
|
|
217
|
+
category: 'obfuscation',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
/**
|
|
221
|
+
* Legacy flat array for backwards-compatible exports.
|
|
222
|
+
* Contains only the RegExp patterns from SECURITY_PATTERNS.
|
|
223
|
+
*/
|
|
224
|
+
export const PROMPT_INJECTION_PATTERNS = SECURITY_PATTERNS.map((sp) => sp.pattern);
|
|
225
|
+
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
226
|
+
/** Normalize text for injection detection: strip zero-width chars, NFKC normalize */
|
|
227
|
+
export function normalizeForInjectionCheck(text) {
|
|
228
|
+
return text
|
|
229
|
+
.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD]/g, '')
|
|
230
|
+
.normalize('NFKC');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Escape ALL XML-like tags in text to prevent tag injection.
|
|
234
|
+
*/
|
|
235
|
+
export function escapeAllXmlTags(text) {
|
|
236
|
+
return text.replace(/<\/?[a-zA-Z][\w-]*(?:\s[^>]*)?\/?>/g, (match) => match.replace(/</g, '<').replace(/>/g, '>'));
|
|
237
|
+
}
|
|
238
|
+
// ── Core scan ─────────────────────────────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* Scan text against all SECURITY_PATTERNS and return structured findings.
|
|
241
|
+
*/
|
|
242
|
+
function scanText(text) {
|
|
243
|
+
const normalized = normalizeForInjectionCheck(text);
|
|
244
|
+
const findings = [];
|
|
245
|
+
for (const sp of SECURITY_PATTERNS) {
|
|
246
|
+
const match = sp.pattern.exec(normalized);
|
|
247
|
+
if (match) {
|
|
248
|
+
findings.push({
|
|
249
|
+
patternId: sp.id,
|
|
250
|
+
severity: sp.severity,
|
|
251
|
+
category: sp.category,
|
|
252
|
+
matchedText: match[0],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
}
|
|
258
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
259
|
+
/**
|
|
260
|
+
* Combined filter: checks for prompt injection and escapes XML tags.
|
|
261
|
+
* - block 패턴 매칭 → verdict: 'block', sanitized: ''
|
|
262
|
+
* - warn만 매칭 → verdict: 'warn', sanitized: XML 이스케이프된 텍스트
|
|
263
|
+
* - 매칭 없음 → verdict: 'safe', sanitized: XML 이스케이프된 텍스트
|
|
264
|
+
*/
|
|
265
|
+
export function filterSolutionContent(text) {
|
|
266
|
+
const findings = scanText(text);
|
|
267
|
+
const hasBlock = findings.some((f) => f.severity === 'block');
|
|
268
|
+
if (hasBlock) {
|
|
269
|
+
return { verdict: 'block', findings, sanitized: '' };
|
|
270
|
+
}
|
|
271
|
+
const hasWarn = findings.some((f) => f.severity === 'warn');
|
|
272
|
+
const sanitized = escapeAllXmlTags(text);
|
|
273
|
+
if (hasWarn) {
|
|
274
|
+
return { verdict: 'warn', findings, sanitized };
|
|
275
|
+
}
|
|
276
|
+
return { verdict: 'safe', findings: [], sanitized };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if text contains prompt injection patterns.
|
|
280
|
+
* Normalizes Unicode before matching to prevent bypass via homoglyphs/zero-width chars.
|
|
281
|
+
*
|
|
282
|
+
* @returns true only when a 'block'-severity pattern matches (하위 호환 유지).
|
|
283
|
+
*/
|
|
284
|
+
export function containsPromptInjection(text) {
|
|
285
|
+
const findings = scanText(text);
|
|
286
|
+
return findings.some((f) => f.severity === 'block');
|
|
287
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forgen — PreToolUse: Rate Limiter Hook
|
|
4
|
+
*
|
|
5
|
+
* MCP 도구 호출 빈도를 제한하여 남용을 방지합니다.
|
|
6
|
+
* 기본 제한: 30회/분
|
|
7
|
+
*/
|
|
8
|
+
interface RateLimitState {
|
|
9
|
+
calls: number[];
|
|
10
|
+
}
|
|
11
|
+
/** 상태 파일 로드 (스키마 검증 포함) */
|
|
12
|
+
export declare function loadRateLimitState(): RateLimitState;
|
|
13
|
+
/** 상태 파일 저장 (atomic write로 동시 세션 안전) */
|
|
14
|
+
export declare function saveRateLimitState(state: RateLimitState): void;
|
|
15
|
+
/** 오래된 호출 기록 정리 + 제한 초과 여부 판정 (순수 함수) */
|
|
16
|
+
export declare function checkRateLimit(state: RateLimitState, now?: number, limit?: number): {
|
|
17
|
+
exceeded: boolean;
|
|
18
|
+
count: number;
|
|
19
|
+
updatedState: RateLimitState;
|
|
20
|
+
};
|
|
21
|
+
export {};
|