memento-mcp-server 1.16.2-a → 1.16.3-a
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/dist/domains/anchor/services/anchor/n-hop-search-service.d.ts.map +1 -1
- package/dist/domains/anchor/services/anchor/n-hop-search-service.js +4 -5
- package/dist/domains/anchor/services/anchor/n-hop-search-service.js.map +1 -1
- package/dist/domains/embedding/services/embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/embedding-service.js +5 -2
- package/dist/domains/embedding/services/embedding-service.js.map +1 -1
- package/dist/domains/embedding/services/gemini-embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/gemini-embedding-service.js +6 -3
- package/dist/domains/embedding/services/gemini-embedding-service.js.map +1 -1
- package/dist/domains/embedding/services/lightweight-embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/lightweight-embedding-service.js +4 -2
- package/dist/domains/embedding/services/lightweight-embedding-service.js.map +1 -1
- package/dist/domains/embedding/services/minilm-embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/minilm-embedding-service.js +6 -3
- package/dist/domains/embedding/services/minilm-embedding-service.js.map +1 -1
- package/dist/domains/embedding/services/openai-embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/openai-embedding-service.js +5 -2
- package/dist/domains/embedding/services/openai-embedding-service.js.map +1 -1
- package/dist/domains/embedding/services/unified-embedding-service.d.ts.map +1 -1
- package/dist/domains/embedding/services/unified-embedding-service.js +6 -3
- package/dist/domains/embedding/services/unified-embedding-service.js.map +1 -1
- package/dist/domains/forgetting/services/forgetting-policy-service.d.ts.map +1 -1
- package/dist/domains/forgetting/services/forgetting-policy-service.js +3 -1
- package/dist/domains/forgetting/services/forgetting-policy-service.js.map +1 -1
- package/dist/domains/memory/services/memory-embedding-service.d.ts +1 -0
- package/dist/domains/memory/services/memory-embedding-service.d.ts.map +1 -1
- package/dist/domains/memory/services/memory-embedding-service.js +43 -39
- package/dist/domains/memory/services/memory-embedding-service.js.map +1 -1
- package/dist/domains/memory/services/memory-neighbor-service.d.ts.map +1 -1
- package/dist/domains/memory/services/memory-neighbor-service.js +13 -4
- package/dist/domains/memory/services/memory-neighbor-service.js.map +1 -1
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js +5 -6
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js.map +1 -1
- package/dist/domains/memory/tools/forget-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/forget-tool.js +5 -2
- package/dist/domains/memory/tools/forget-tool.js.map +1 -1
- package/dist/domains/memory/tools/memory-injection-prompt.d.ts.map +1 -1
- package/dist/domains/memory/tools/memory-injection-prompt.js +3 -1
- package/dist/domains/memory/tools/memory-injection-prompt.js.map +1 -1
- package/dist/domains/memory/tools/pin-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/pin-tool.js +5 -2
- package/dist/domains/memory/tools/pin-tool.js.map +1 -1
- package/dist/domains/memory/tools/unpin-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/unpin-tool.js +5 -2
- package/dist/domains/memory/tools/unpin-tool.js.map +1 -1
- package/dist/domains/monitoring/services/error-logging-service.d.ts +5 -0
- package/dist/domains/monitoring/services/error-logging-service.d.ts.map +1 -1
- package/dist/domains/monitoring/services/error-logging-service.js +28 -10
- package/dist/domains/monitoring/services/error-logging-service.js.map +1 -1
- package/dist/domains/monitoring/services/performance-alert-service.d.ts.map +1 -1
- package/dist/domains/monitoring/services/performance-alert-service.js +5 -2
- package/dist/domains/monitoring/services/performance-alert-service.js.map +1 -1
- package/dist/domains/relation/services/relation-graph.d.ts.map +1 -1
- package/dist/domains/relation/services/relation-graph.js +3 -4
- package/dist/domains/relation/services/relation-graph.js.map +1 -1
- package/dist/domains/search/algorithms/hybrid-search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/hybrid-search-engine.js +15 -10
- package/dist/domains/search/algorithms/hybrid-search-engine.js.map +1 -1
- package/dist/domains/search/algorithms/search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/search-engine.js +8 -4
- package/dist/domains/search/algorithms/search-engine.js.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine-migration.d.ts.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine-migration.js +9 -4
- package/dist/domains/search/algorithms/vector-search-engine-migration.js.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine.d.ts +1 -0
- package/dist/domains/search/algorithms/vector-search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine.js +25 -22
- package/dist/domains/search/algorithms/vector-search-engine.js.map +1 -1
- package/dist/domains/search/repositories/vector-performance.repository.d.ts.map +1 -1
- package/dist/domains/search/repositories/vector-performance.repository.js +3 -1
- package/dist/domains/search/repositories/vector-performance.repository.js.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.d.ts +1 -0
- package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.js +154 -149
- package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
- package/dist/domains/search/services/vector-search/vector-index-manager.d.ts.map +1 -1
- package/dist/domains/search/services/vector-search/vector-index-manager.js +7 -3
- package/dist/domains/search/services/vector-search/vector-index-manager.js.map +1 -1
- package/dist/domains/search/services/vector-search/vector-performance-tester.d.ts.map +1 -1
- package/dist/domains/search/services/vector-search/vector-performance-tester.js +3 -1
- package/dist/domains/search/services/vector-search/vector-performance-tester.js.map +1 -1
- package/dist/infrastructure/database/database/init.d.ts.map +1 -1
- package/dist/infrastructure/database/database/init.js +29 -7
- package/dist/infrastructure/database/database/init.js.map +1 -1
- package/dist/infrastructure/database/database/migrate.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migrate.js +5 -2
- package/dist/infrastructure/database/database/migrate.js.map +1 -1
- package/dist/infrastructure/database/database/migration/backup-manager.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migration/backup-manager.js +11 -5
- package/dist/infrastructure/database/database/migration/backup-manager.js.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-detector.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-detector.js +3 -1
- package/dist/infrastructure/database/database/migration/migration-detector.js.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-logger.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-logger.js +5 -2
- package/dist/infrastructure/database/database/migration/migration-logger.js.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-runner.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migration/migration-runner.js +24 -13
- package/dist/infrastructure/database/database/migration/migration-runner.js.map +1 -1
- package/dist/infrastructure/database/database/migration/schema-version-manager.d.ts.map +1 -1
- package/dist/infrastructure/database/database/migration/schema-version-manager.js +7 -3
- package/dist/infrastructure/database/database/migration/schema-version-manager.js.map +1 -1
- package/dist/infrastructure/database/database-optimizer.d.ts.map +1 -1
- package/dist/infrastructure/database/database-optimizer.js +7 -6
- package/dist/infrastructure/database/database-optimizer.js.map +1 -1
- package/dist/infrastructure/database/migration-history-service.d.ts.map +1 -1
- package/dist/infrastructure/database/migration-history-service.js +3 -1
- package/dist/infrastructure/database/migration-history-service.js.map +1 -1
- package/dist/infrastructure/logging/triple-extraction-logger.d.ts +6 -0
- package/dist/infrastructure/logging/triple-extraction-logger.d.ts.map +1 -1
- package/dist/infrastructure/logging/triple-extraction-logger.js +50 -12
- package/dist/infrastructure/logging/triple-extraction-logger.js.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.js +5 -2
- package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
- package/dist/infrastructure/scheduler/file-logger.d.ts +4 -1
- package/dist/infrastructure/scheduler/file-logger.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/file-logger.js +46 -16
- package/dist/infrastructure/scheduler/file-logger.js.map +1 -1
- package/dist/scripts/check-migration-status.d.ts.map +1 -1
- package/dist/scripts/check-migration-status.js +19 -6
- package/dist/scripts/check-migration-status.js.map +1 -1
- package/dist/services/quality-assurance/quality-recorder.js +2 -2
- package/dist/services/quality-assurance/quality-recorder.js.map +1 -1
- package/dist/services/quality-assurance/quality-threshold-manager.js +1 -1
- package/dist/services/quality-assurance/quality-threshold-manager.js.map +1 -1
- package/dist/shared/config/environment.d.ts.map +1 -1
- package/dist/shared/config/environment.js +3 -1
- package/dist/shared/config/environment.js.map +1 -1
- package/dist/shared/utils/fts5-migration-status.d.ts.map +1 -1
- package/dist/shared/utils/fts5-migration-status.js +6 -2
- package/dist/shared/utils/fts5-migration-status.js.map +1 -1
- package/dist/shared/utils/logger.d.ts.map +1 -1
- package/dist/shared/utils/logger.js +15 -2
- package/dist/shared/utils/logger.js.map +1 -1
- package/dist/shared/utils/path-validator.d.ts +27 -0
- package/dist/shared/utils/path-validator.d.ts.map +1 -0
- package/dist/shared/utils/path-validator.js +166 -0
- package/dist/shared/utils/path-validator.js.map +1 -0
- package/dist/shared/utils/pii-masker.d.ts +31 -0
- package/dist/shared/utils/pii-masker.d.ts.map +1 -1
- package/dist/shared/utils/pii-masker.js +99 -0
- package/dist/shared/utils/pii-masker.js.map +1 -1
- package/dist/shared/utils/procedural-memory-extractor.d.ts.map +1 -1
- package/dist/shared/utils/procedural-memory-extractor.js +39 -33
- package/dist/shared/utils/procedural-memory-extractor.js.map +1 -1
- package/dist/shared/utils/prompt-template-loader.d.ts +6 -0
- package/dist/shared/utils/prompt-template-loader.d.ts.map +1 -1
- package/dist/shared/utils/prompt-template-loader.js +20 -8
- package/dist/shared/utils/prompt-template-loader.js.map +1 -1
- package/dist/shared/utils/reflection-notes-merge.d.ts.map +1 -1
- package/dist/shared/utils/reflection-notes-merge.js +5 -4
- package/dist/shared/utils/reflection-notes-merge.js.map +1 -1
- package/dist/shared/utils/sql-security-validator.d.ts +25 -0
- package/dist/shared/utils/sql-security-validator.d.ts.map +1 -0
- package/dist/shared/utils/sql-security-validator.js +67 -0
- package/dist/shared/utils/sql-security-validator.js.map +1 -0
- package/dist/shared/utils/write-coalescing.d.ts.map +1 -1
- package/dist/shared/utils/write-coalescing.js +7 -3
- package/dist/shared/utils/write-coalescing.js.map +1 -1
- package/package.json +2 -1
- package/scripts/backup-daily.bat +10 -2
- package/scripts/backup-embeddings.js +22 -2
- package/scripts/check-path-traversal.ts +370 -0
- package/scripts/check-pii-masking.ts +360 -0
- package/scripts/check-sql-injection.ts +610 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SQL Injection 취약점 검사 스크립트
|
|
4
|
+
*
|
|
5
|
+
* PRD 0019: 보안 강화 (Phase 1) - SQL Injection 방지
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* tsx scripts/check-sql-injection.ts
|
|
9
|
+
* tsx scripts/check-sql-injection.ts --ci
|
|
10
|
+
* tsx scripts/check-sql-injection.ts --directory src/
|
|
11
|
+
*
|
|
12
|
+
* 목표:
|
|
13
|
+
* - 모든 동적 쿼리가 파라미터 바인딩으로 전환됨
|
|
14
|
+
* - SQL Injection 취약점 0개
|
|
15
|
+
* - CI/CD 통합 가능
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
19
|
+
import { readdir } from 'fs/promises';
|
|
20
|
+
import { join, relative } from 'path';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CLI 옵션
|
|
24
|
+
*/
|
|
25
|
+
interface CliOptions {
|
|
26
|
+
ci?: boolean;
|
|
27
|
+
directory?: string;
|
|
28
|
+
exclude?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* SQL Injection 취약점 발견 위치
|
|
33
|
+
*/
|
|
34
|
+
interface SqlInjectionLocation {
|
|
35
|
+
file: string;
|
|
36
|
+
line: number;
|
|
37
|
+
column: number;
|
|
38
|
+
pattern: string; // 발견된 패턴 종류
|
|
39
|
+
context: string; // 해당 라인 내용
|
|
40
|
+
severity: 'high' | 'medium' | 'low'; // 심각도
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 검사 결과
|
|
45
|
+
*/
|
|
46
|
+
interface CheckResult {
|
|
47
|
+
total: number;
|
|
48
|
+
locations: SqlInjectionLocation[];
|
|
49
|
+
byFile: Map<string, SqlInjectionLocation[]>;
|
|
50
|
+
byPattern: Map<string, number>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 명령줄 인자 파싱
|
|
55
|
+
*/
|
|
56
|
+
function parseArgs(): CliOptions {
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
const options: CliOptions = {
|
|
59
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/*.d.ts', '**/*.spec.ts']
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
const arg = args[i];
|
|
64
|
+
if (arg === '--ci') {
|
|
65
|
+
options.ci = true;
|
|
66
|
+
} else if (arg === '--directory' && args[i + 1]) {
|
|
67
|
+
options.directory = args[i + 1];
|
|
68
|
+
i++;
|
|
69
|
+
} else if (arg === '--exclude' && args[i + 1]) {
|
|
70
|
+
if (!options.exclude) {
|
|
71
|
+
options.exclude = [];
|
|
72
|
+
}
|
|
73
|
+
options.exclude.push(args[i + 1]);
|
|
74
|
+
i++;
|
|
75
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
76
|
+
printHelp();
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return options;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 도움말 출력
|
|
86
|
+
*/
|
|
87
|
+
function printHelp(): void {
|
|
88
|
+
console.log(`
|
|
89
|
+
SQL Injection 취약점 검사 스크립트
|
|
90
|
+
|
|
91
|
+
사용법:
|
|
92
|
+
tsx scripts/check-sql-injection.ts [options]
|
|
93
|
+
|
|
94
|
+
옵션:
|
|
95
|
+
--ci CI 모드 (취약점 발견 시 exit code 1 반환)
|
|
96
|
+
--directory <path> 검사할 디렉토리 (기본값: src/)
|
|
97
|
+
--exclude <pattern> 제외할 파일 패턴 (여러 번 사용 가능)
|
|
98
|
+
--help, -h 도움말 출력
|
|
99
|
+
|
|
100
|
+
예제:
|
|
101
|
+
tsx scripts/check-sql-injection.ts
|
|
102
|
+
tsx scripts/check-sql-injection.ts --ci
|
|
103
|
+
tsx scripts/check-sql-injection.ts --directory src/
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 패턴 매칭
|
|
109
|
+
*/
|
|
110
|
+
function matchesPattern(path: string, pattern: string): boolean {
|
|
111
|
+
if (pattern.includes('**/node_modules/**')) {
|
|
112
|
+
return path.includes('node_modules');
|
|
113
|
+
}
|
|
114
|
+
if (pattern.includes('**/dist/**')) {
|
|
115
|
+
return path.includes('dist');
|
|
116
|
+
}
|
|
117
|
+
if (pattern.includes('**/*.spec.ts')) {
|
|
118
|
+
return path.endsWith('.spec.ts');
|
|
119
|
+
}
|
|
120
|
+
if (pattern.includes('**/*.d.ts')) {
|
|
121
|
+
return path.endsWith('.d.ts');
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 파일이 제외 패턴에 해당하는지 확인
|
|
128
|
+
*/
|
|
129
|
+
function shouldExclude(filePath: string, exclude: string[]): boolean {
|
|
130
|
+
for (const pattern of exclude) {
|
|
131
|
+
if (matchesPattern(filePath, pattern)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 파일 검색 (재귀적)
|
|
140
|
+
*/
|
|
141
|
+
async function findFiles(directory: string, exclude: string[]): Promise<string[]> {
|
|
142
|
+
const files: string[] = [];
|
|
143
|
+
const absoluteDir = join(process.cwd(), directory);
|
|
144
|
+
|
|
145
|
+
async function walkDir(dir: string): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
148
|
+
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const fullPath = join(dir, entry.name);
|
|
151
|
+
|
|
152
|
+
if (shouldExclude(fullPath, exclude)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
await walkDir(fullPath);
|
|
158
|
+
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
|
|
159
|
+
files.push(fullPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// 디렉토리 읽기 실패 시 무시
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await walkDir(absoluteDir);
|
|
168
|
+
return files;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* SQL Injection 취약점 패턴 검색
|
|
173
|
+
*
|
|
174
|
+
* 검색하는 패턴:
|
|
175
|
+
* 1. 문자열 연결을 통한 SQL 쿼리 생성: sql +=, query +=, sql = sql +
|
|
176
|
+
* 2. 템플릿 리터럴로 동적 테이블명/컬럼명: FROM ${, JOIN ${, WHERE ${ (일부는 허용 가능)
|
|
177
|
+
* 3. 파라미터 바인딩 미사용: '...' + variable, "..." + variable
|
|
178
|
+
*/
|
|
179
|
+
function findSqlInjectionPatterns(filePath: string): SqlInjectionLocation[] {
|
|
180
|
+
const locations: SqlInjectionLocation[] = [];
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
184
|
+
const lines = content.split('\n');
|
|
185
|
+
|
|
186
|
+
// 패턴 1: 문자열 연결을 통한 SQL 쿼리 생성
|
|
187
|
+
// sql +=, query +=, sql = sql + 등
|
|
188
|
+
const stringConcatenationPattern = /\b(sql|query|stmt|statement)\s*([+]=|=.*\+)/gi;
|
|
189
|
+
|
|
190
|
+
// 패턴 2: 템플릿 리터럴로 동적 테이블명/컬럼명 사용
|
|
191
|
+
// FROM ${, JOIN ${, WHERE ${ 등 (일부는 허용 가능하지만 검사 대상)
|
|
192
|
+
const templateLiteralPattern = /\b(FROM|JOIN|WHERE|SELECT|INSERT|UPDATE|DELETE|INTO|SET)\s+\$\{/gi;
|
|
193
|
+
|
|
194
|
+
// 패턴 3: 파라미터 바인딩 미사용 (문자열 연결)
|
|
195
|
+
// '...' + variable, "..." + variable (SQL 쿼리 컨텍스트에서)
|
|
196
|
+
const parameterBindingPattern = /(['"]).*?\1\s*\+\s*\w+/g;
|
|
197
|
+
|
|
198
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
199
|
+
const line = lines[lineIndex];
|
|
200
|
+
const trimmedLine = line.trim();
|
|
201
|
+
|
|
202
|
+
// 주석 제외
|
|
203
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*')) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// SQL 관련 키워드가 있는 라인만 검사 (성능 최적화)
|
|
208
|
+
const hasSqlKeyword = /sql|query|SELECT|INSERT|UPDATE|DELETE|FROM|JOIN|WHERE/i.test(line);
|
|
209
|
+
if (!hasSqlKeyword) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 패턴 1: 문자열 연결 검사
|
|
214
|
+
let match;
|
|
215
|
+
const concatPattern = /\b(sql|query|stmt|statement)\s*([+]=|=.*\+)/gi;
|
|
216
|
+
while ((match = concatPattern.exec(line)) !== null) {
|
|
217
|
+
// false positive 제거: 주석 처리된 코드나 문자열 내부는 제외
|
|
218
|
+
const beforeMatch = line.substring(0, match.index);
|
|
219
|
+
const stringCount = (beforeMatch.match(/['"]/g) || []).length;
|
|
220
|
+
if (stringCount % 2 === 1) {
|
|
221
|
+
// 문자열 내부에 있음
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 정적 문자열 연결은 허용 (예: query += ' ORDER BY ...')
|
|
226
|
+
// 변수나 템플릿 리터럴이 포함된 경우만 감지
|
|
227
|
+
const afterMatch = line.substring(match.index + match[0].length);
|
|
228
|
+
const hasVariable = /\$\{|\+\s*\w+/.test(afterMatch);
|
|
229
|
+
if (!hasVariable) {
|
|
230
|
+
// 정적 문자열만 연결하는 경우는 안전함
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// conditions.join(' AND ') 패턴은 이미 파라미터 바인딩을 포함하고 있어 안전함
|
|
235
|
+
if (line.includes('conditions.join') && line.includes('AND')) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// placeholders는 '?'로만 구성되어 있어 안전함
|
|
240
|
+
if (line.includes('placeholders') && line.includes('IN (')) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// reflectionNotesLike는 buildReflectionNotesSearchCondition()에서 생성되며 이미 '?' 플레이스홀더를 포함하고 있어 안전함
|
|
245
|
+
if (line.includes('reflectionNotesLike')) {
|
|
246
|
+
// 이전 라인들에서 buildReflectionNotesSearchCondition() 호출 확인
|
|
247
|
+
let isFromSafeBuilder = false;
|
|
248
|
+
for (let i = lineIndex - 1; i >= Math.max(0, lineIndex - 5); i--) {
|
|
249
|
+
const prevLine = lines[i];
|
|
250
|
+
if (prevLine.includes('buildReflectionNotesSearchCondition') ||
|
|
251
|
+
(prevLine.includes('reflectionNotesLike') && prevLine.includes('?'))) {
|
|
252
|
+
isFromSafeBuilder = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (isFromSafeBuilder || line.includes('?')) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
locations.push({
|
|
262
|
+
file: filePath,
|
|
263
|
+
line: lineIndex + 1,
|
|
264
|
+
column: match.index + 1,
|
|
265
|
+
pattern: 'string-concatenation',
|
|
266
|
+
context: trimmedLine,
|
|
267
|
+
severity: 'high'
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 패턴 2: 템플릿 리터럴로 동적 테이블명/컬럼명 사용
|
|
272
|
+
// FROM ${, JOIN ${ 등은 동적 테이블명일 가능성이 높음
|
|
273
|
+
const templatePattern = /\b(FROM|JOIN|DELETE FROM)\s+\$\{/gi;
|
|
274
|
+
while ((match = templatePattern.exec(line)) !== null) {
|
|
275
|
+
// false positive 제거: 주석 처리된 코드나 문자열 내부는 제외
|
|
276
|
+
const beforeMatch = line.substring(0, match.index);
|
|
277
|
+
const stringCount = (beforeMatch.match(/['"]/g) || []).length;
|
|
278
|
+
if (stringCount % 2 === 1) {
|
|
279
|
+
// 문자열 내부에 있음
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// validateTableName() 또는 getTableName() 호출이 있는지 확인
|
|
284
|
+
const tableVarPattern = /\$\{(\w+)\}/;
|
|
285
|
+
const tableMatch = line.match(tableVarPattern);
|
|
286
|
+
if (tableMatch) {
|
|
287
|
+
const varName = tableMatch[1];
|
|
288
|
+
// 이전 라인들에서 validateTableName() 또는 getTableName() 호출 찾기
|
|
289
|
+
let isValidated = false;
|
|
290
|
+
// 더 넓은 범위로 검색 (함수 내에서 변수가 재사용될 수 있음)
|
|
291
|
+
// 같은 파일 내에서 검색 (최대 300라인)
|
|
292
|
+
for (let i = lineIndex - 1; i >= Math.max(0, lineIndex - 300); i--) {
|
|
293
|
+
const prevLine = lines[i];
|
|
294
|
+
// 직접 호출 패턴
|
|
295
|
+
if (new RegExp(`validateTableName\\(${varName}\\)|validateTableName\\(.*${varName}`).test(prevLine)) {
|
|
296
|
+
isValidated = true;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
// 변수 할당 패턴: tableName = getTableName() 또는 this.getTableName() 또는 getVectorTableName()
|
|
300
|
+
// 더 유연한 패턴 매칭: 변수명과 getTableName이 같은 라인에 있으면 안전함
|
|
301
|
+
if (prevLine.includes(varName) && (
|
|
302
|
+
prevLine.includes('getTableName') ||
|
|
303
|
+
prevLine.includes('getVectorTableName') ||
|
|
304
|
+
prevLine.includes('getValidatedVectorTableName')
|
|
305
|
+
)) {
|
|
306
|
+
// const tableName = this.getTableName(...) 패턴 확인
|
|
307
|
+
if (new RegExp(`${varName}\\s*=|const\\s+${varName}\\s*=|let\\s+${varName}\\s*=`).test(prevLine)) {
|
|
308
|
+
isValidated = true;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 함수 경계 검사 제거 - 같은 파일 내에서만 검색하도록 함
|
|
313
|
+
// 메서드 호출 패턴: this.getTableName(provider) 또는 this.getVectorTableName(provider)
|
|
314
|
+
if (new RegExp(`this\\.getTableName|this\\.getVectorTableName`).test(prevLine)) {
|
|
315
|
+
// 다음 라인에 해당 변수가 있으면 안전함
|
|
316
|
+
if (i === lineIndex - 1 || (lines[i + 1] && lines[i + 1].includes(varName))) {
|
|
317
|
+
isValidated = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// getVectorTableName 직접 호출 (private 메서드)
|
|
322
|
+
if (new RegExp(`getVectorTableName\\(|getValidatedVectorTableName\\(`).test(prevLine) &&
|
|
323
|
+
(i === lineIndex - 1 || (lines[i + 1] && lines[i + 1].includes(varName)))) {
|
|
324
|
+
isValidated = true;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
// testTable 같은 경우: sqlite_master에서 가져온 값이므로 안전
|
|
328
|
+
if (varName.includes('Table') && prevLine.includes('sqlite_master')) {
|
|
329
|
+
isValidated = true;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
// 하드코딩된 기본값 패턴: ?? 'memory_item_vec_tfidf'
|
|
333
|
+
if (prevLine.includes(`??`) && prevLine.includes('memory_item_vec')) {
|
|
334
|
+
isValidated = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
// VECTOR_SEARCH_CONFIG.tableNames에서 직접 가져온 값은 안전함
|
|
338
|
+
if (prevLine.includes('VECTOR_SEARCH_CONFIG.tableNames')) {
|
|
339
|
+
isValidated = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
// sqlite_master에서 가져온 table.name은 안전함
|
|
343
|
+
if (prevLine.includes('table.name') || prevLine.includes('tableName = table.name')) {
|
|
344
|
+
isValidated = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
// this.getTableName() 호출이 바로 이전 라인에 있는 경우
|
|
348
|
+
if (i === lineIndex - 1 && new RegExp(`this\\.getTableName`).test(prevLine)) {
|
|
349
|
+
isValidated = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (isValidated) {
|
|
354
|
+
// 화이트리스트 검증을 거친 경우 허용
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
locations.push({
|
|
360
|
+
file: filePath,
|
|
361
|
+
line: lineIndex + 1,
|
|
362
|
+
column: match.index + 1,
|
|
363
|
+
pattern: 'dynamic-table-name',
|
|
364
|
+
context: trimmedLine,
|
|
365
|
+
severity: 'medium' // 화이트리스트 검증이 있으면 허용 가능
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 패턴 3: WHERE 절에서 템플릿 리터럴 사용 (조건부)
|
|
370
|
+
const whereTemplatePattern = /WHERE.*\$\{/gi;
|
|
371
|
+
while ((match = whereTemplatePattern.exec(line)) !== null) {
|
|
372
|
+
// false positive 제거
|
|
373
|
+
const beforeMatch = line.substring(0, match.index);
|
|
374
|
+
const stringCount = (beforeMatch.match(/['"]/g) || []).length;
|
|
375
|
+
if (stringCount % 2 === 1) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 파라미터 바인딩(? 플레이스홀더)이 있는지 확인
|
|
380
|
+
const hasPlaceholder = line.includes('?');
|
|
381
|
+
|
|
382
|
+
// conditions.join(' AND ') 패턴은 이미 파라미터 바인딩을 포함하고 있어 안전함
|
|
383
|
+
if (line.includes('conditions.join') && line.includes('AND')) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// config.filter는 하드코딩된 값이므로 안전함
|
|
388
|
+
if (line.includes('config.filter')) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// reflectionNotesLike는 이미 '?' 플레이스홀더를 포함하고 있어 안전함
|
|
393
|
+
if (line.includes('reflectionNotesLike') && line.includes('?')) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// placeholders 변수가 '?'로만 구성되어 있는 경우 허용
|
|
398
|
+
// 예: ${placeholders} where placeholders = '?,?,?'
|
|
399
|
+
const placeholderVarPattern = /\$\{(\w+)\}/;
|
|
400
|
+
const placeholderMatch = line.match(placeholderVarPattern);
|
|
401
|
+
if (placeholderMatch) {
|
|
402
|
+
const varName = placeholderMatch[1];
|
|
403
|
+
// 이전 라인들에서 변수 정의 찾기
|
|
404
|
+
for (let i = lineIndex - 1; i >= Math.max(0, lineIndex - 10); i--) {
|
|
405
|
+
const prevLine = lines[i];
|
|
406
|
+
// placeholders 변수가 '?'로만 구성되어 있는지 확인
|
|
407
|
+
if (new RegExp(`\\b${varName}\\s*=\\s*.*map.*\\?.*join`).test(prevLine)) {
|
|
408
|
+
// placeholders는 안전함
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// placeholders 변수명 자체가 placeholders인 경우도 허용 (일반적인 패턴)
|
|
412
|
+
if (varName === 'placeholders' && prevLine.includes('map') && prevLine.includes('?')) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// placeholders 변수명 자체가 placeholders인 경우도 허용
|
|
417
|
+
if (varName === 'placeholders' && line.includes('IN (')) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!hasPlaceholder) {
|
|
423
|
+
locations.push({
|
|
424
|
+
file: filePath,
|
|
425
|
+
line: lineIndex + 1,
|
|
426
|
+
column: match.index + 1,
|
|
427
|
+
pattern: 'where-template-literal',
|
|
428
|
+
context: trimmedLine,
|
|
429
|
+
severity: 'high'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 패턴 4: 문자열 연결로 SQL 쿼리 구성 (파라미터 바인딩 미사용)
|
|
435
|
+
// 'SELECT * FROM table WHERE id = ' + variable 같은 패턴
|
|
436
|
+
const sqlStringConcatPattern = /(['"])\s*(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*?\1\s*\+\s*\w+/gi;
|
|
437
|
+
while ((match = sqlStringConcatPattern.exec(line)) !== null) {
|
|
438
|
+
// false positive 제거
|
|
439
|
+
const beforeMatch = line.substring(0, match.index);
|
|
440
|
+
const stringCount = (beforeMatch.match(/['"]/g) || []).length;
|
|
441
|
+
if (stringCount % 2 === 1) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
locations.push({
|
|
446
|
+
file: filePath,
|
|
447
|
+
line: lineIndex + 1,
|
|
448
|
+
column: match.index + 1,
|
|
449
|
+
pattern: 'sql-string-concatenation',
|
|
450
|
+
context: trimmedLine,
|
|
451
|
+
severity: 'high'
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// 파일 읽기 실패 시 무시
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return locations;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* SQL Injection 취약점 검사
|
|
464
|
+
*/
|
|
465
|
+
function checkSqlInjection(files: string[]): CheckResult {
|
|
466
|
+
const locations: SqlInjectionLocation[] = [];
|
|
467
|
+
const byFile = new Map<string, SqlInjectionLocation[]>();
|
|
468
|
+
const byPattern = new Map<string, number>();
|
|
469
|
+
|
|
470
|
+
for (const file of files) {
|
|
471
|
+
const fileLocations = findSqlInjectionPatterns(file);
|
|
472
|
+
locations.push(...fileLocations);
|
|
473
|
+
|
|
474
|
+
if (fileLocations.length > 0) {
|
|
475
|
+
byFile.set(file, fileLocations);
|
|
476
|
+
|
|
477
|
+
for (const loc of fileLocations) {
|
|
478
|
+
const count = byPattern.get(loc.pattern) || 0;
|
|
479
|
+
byPattern.set(loc.pattern, count + 1);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
total: locations.length,
|
|
486
|
+
locations,
|
|
487
|
+
byFile,
|
|
488
|
+
byPattern
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 결과 출력
|
|
494
|
+
*/
|
|
495
|
+
function printResults(
|
|
496
|
+
result: CheckResult,
|
|
497
|
+
projectRoot: string
|
|
498
|
+
): void {
|
|
499
|
+
console.log('\n🔍 SQL Injection 취약점 검사 결과\n');
|
|
500
|
+
|
|
501
|
+
if (result.total === 0) {
|
|
502
|
+
console.log('✅ SQL Injection 취약점이 발견되지 않았습니다.\n');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
console.log(`⚠️ 발견된 취약점: ${result.total}개\n`);
|
|
507
|
+
|
|
508
|
+
// 패턴별 통계
|
|
509
|
+
if (result.byPattern.size > 0) {
|
|
510
|
+
console.log('📈 패턴별 통계:');
|
|
511
|
+
const sortedPatterns = Array.from(result.byPattern.entries())
|
|
512
|
+
.sort((a, b) => b[1] - a[1]);
|
|
513
|
+
|
|
514
|
+
const patternNames: Record<string, string> = {
|
|
515
|
+
'string-concatenation': '문자열 연결을 통한 쿼리 생성',
|
|
516
|
+
'dynamic-table-name': '동적 테이블명 사용 (템플릿 리터럴)',
|
|
517
|
+
'where-template-literal': 'WHERE 절 템플릿 리터럴 (파라미터 바인딩 없음)',
|
|
518
|
+
'sql-string-concatenation': 'SQL 문자열 연결 (파라미터 바인딩 미사용)'
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
for (const [pattern, count] of sortedPatterns) {
|
|
522
|
+
const name = patternNames[pattern] || pattern;
|
|
523
|
+
console.log(` ${name}: ${count}개`);
|
|
524
|
+
}
|
|
525
|
+
console.log('');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 파일별 상세 정보
|
|
529
|
+
console.log('📁 파일별 취약점 목록:\n');
|
|
530
|
+
const sortedFiles = Array.from(result.byFile.entries())
|
|
531
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
532
|
+
|
|
533
|
+
for (const [file, locations] of sortedFiles) {
|
|
534
|
+
const relativePath = relative(projectRoot, file);
|
|
535
|
+
console.log(` ${relativePath} (${locations.length}개):`);
|
|
536
|
+
|
|
537
|
+
for (const loc of locations) {
|
|
538
|
+
const severityIcon = loc.severity === 'high' ? '🔴' : loc.severity === 'medium' ? '🟡' : '🟢';
|
|
539
|
+
console.log(` ${severityIcon} 라인 ${loc.line}:${loc.column} - ${loc.pattern}`);
|
|
540
|
+
console.log(` ${loc.context.substring(0, 80)}${loc.context.length > 80 ? '...' : ''}`);
|
|
541
|
+
}
|
|
542
|
+
console.log('');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
console.log('💡 권장 사항:');
|
|
546
|
+
console.log(' - 모든 사용자 입력값은 파라미터 바인딩(?) 사용');
|
|
547
|
+
console.log(' - 동적 테이블명은 화이트리스트 검증 후 사용');
|
|
548
|
+
console.log(' - 문자열 연결 대신 템플릿 리터럴 + 파라미터 바인딩 사용\n');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 메인 함수
|
|
553
|
+
*/
|
|
554
|
+
async function main(): Promise<void> {
|
|
555
|
+
const options = parseArgs();
|
|
556
|
+
const projectRoot = process.cwd();
|
|
557
|
+
const directory = options.directory || 'src/';
|
|
558
|
+
const exclude = options.exclude || ['**/node_modules/**', '**/dist/**', '**/*.d.ts', '**/*.spec.ts'];
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
// 파일 검색
|
|
562
|
+
console.log(`🔍 파일 검색 중... (디렉토리: ${directory})`);
|
|
563
|
+
const files = await findFiles(directory, exclude);
|
|
564
|
+
|
|
565
|
+
if (files.length === 0) {
|
|
566
|
+
console.log('⚠️ 검사할 파일이 없습니다.');
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
console.log(` 발견된 파일: ${files.length}개\n`);
|
|
571
|
+
|
|
572
|
+
// SQL Injection 취약점 검사
|
|
573
|
+
console.log('🔎 SQL Injection 취약점 검사 중...');
|
|
574
|
+
const result = checkSqlInjection(files);
|
|
575
|
+
|
|
576
|
+
// 결과 출력
|
|
577
|
+
printResults(result, projectRoot);
|
|
578
|
+
|
|
579
|
+
// CI 모드: exit code 처리
|
|
580
|
+
if (options.ci) {
|
|
581
|
+
if (result.total > 0) {
|
|
582
|
+
console.log(`❌ CI 실패: SQL Injection 취약점 ${result.total}개가 발견되었습니다.`);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
} else {
|
|
585
|
+
console.log(`✅ CI 통과: SQL Injection 취약점이 없습니다.`);
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
// 일반 모드: 정보만 출력
|
|
590
|
+
if (result.total > 0) {
|
|
591
|
+
console.log('💡 팁: --ci 옵션을 사용하면 CI/CD 파이프라인에 통합할 수 있습니다.');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error('❌ 오류 발생:', error instanceof Error ? error.message : String(error));
|
|
596
|
+
if (error instanceof Error && error.stack) {
|
|
597
|
+
console.error(error.stack);
|
|
598
|
+
}
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 스크립트 직접 실행 시
|
|
604
|
+
if (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1])) {
|
|
605
|
+
main().catch(error => {
|
|
606
|
+
console.error('❌ 치명적 오류:', error);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|