activo 0.2.2 → 0.3.1
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/README.md +87 -3
- package/dist/core/llm/ollama.d.ts +2 -0
- package/dist/core/llm/ollama.d.ts.map +1 -1
- package/dist/core/llm/ollama.js +26 -0
- package/dist/core/llm/ollama.js.map +1 -1
- package/dist/core/tools/ast.d.ts +81 -0
- package/dist/core/tools/ast.d.ts.map +1 -0
- package/dist/core/tools/ast.js +700 -0
- package/dist/core/tools/ast.js.map +1 -0
- package/dist/core/tools/cache.d.ts +19 -0
- package/dist/core/tools/cache.d.ts.map +1 -0
- package/dist/core/tools/cache.js +497 -0
- package/dist/core/tools/cache.js.map +1 -0
- package/dist/core/tools/cssAnalysis.d.ts +3 -0
- package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
- package/dist/core/tools/cssAnalysis.js +270 -0
- package/dist/core/tools/cssAnalysis.js.map +1 -0
- package/dist/core/tools/dependencyAnalysis.d.ts +3 -0
- package/dist/core/tools/dependencyAnalysis.d.ts.map +1 -0
- package/dist/core/tools/dependencyAnalysis.js +295 -0
- package/dist/core/tools/dependencyAnalysis.js.map +1 -0
- package/dist/core/tools/embeddings.d.ts +8 -0
- package/dist/core/tools/embeddings.d.ts.map +1 -0
- package/dist/core/tools/embeddings.js +631 -0
- package/dist/core/tools/embeddings.js.map +1 -0
- package/dist/core/tools/frontendAst.d.ts +6 -0
- package/dist/core/tools/frontendAst.d.ts.map +1 -0
- package/dist/core/tools/frontendAst.js +680 -0
- package/dist/core/tools/frontendAst.js.map +1 -0
- package/dist/core/tools/htmlAnalysis.d.ts +3 -0
- package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/htmlAnalysis.js +398 -0
- package/dist/core/tools/htmlAnalysis.js.map +1 -0
- package/dist/core/tools/index.d.ts +13 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +27 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/javaAst.d.ts +6 -0
- package/dist/core/tools/javaAst.d.ts.map +1 -0
- package/dist/core/tools/javaAst.js +678 -0
- package/dist/core/tools/javaAst.js.map +1 -0
- package/dist/core/tools/memory.d.ts +11 -0
- package/dist/core/tools/memory.d.ts.map +1 -0
- package/dist/core/tools/memory.js +551 -0
- package/dist/core/tools/memory.js.map +1 -0
- package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
- package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
- package/dist/core/tools/mybatisAnalysis.js +251 -0
- package/dist/core/tools/mybatisAnalysis.js.map +1 -0
- package/dist/core/tools/openapiAnalysis.d.ts +3 -0
- package/dist/core/tools/openapiAnalysis.d.ts.map +1 -0
- package/dist/core/tools/openapiAnalysis.js +356 -0
- package/dist/core/tools/openapiAnalysis.js.map +1 -0
- package/dist/core/tools/pythonAnalysis.d.ts +3 -0
- package/dist/core/tools/pythonAnalysis.d.ts.map +1 -0
- package/dist/core/tools/pythonAnalysis.js +387 -0
- package/dist/core/tools/pythonAnalysis.js.map +1 -0
- package/dist/core/tools/sqlAnalysis.d.ts +3 -0
- package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/sqlAnalysis.js +250 -0
- package/dist/core/tools/sqlAnalysis.js.map +1 -0
- package/package.json +2 -1
- package/src/core/llm/ollama.ts +30 -0
- package/src/core/tools/ast.ts +826 -0
- package/src/core/tools/cache.ts +570 -0
- package/src/core/tools/cssAnalysis.ts +324 -0
- package/src/core/tools/dependencyAnalysis.ts +363 -0
- package/src/core/tools/embeddings.ts +746 -0
- package/src/core/tools/frontendAst.ts +802 -0
- package/src/core/tools/htmlAnalysis.ts +466 -0
- package/src/core/tools/index.ts +27 -1
- package/src/core/tools/javaAst.ts +812 -0
- package/src/core/tools/memory.ts +655 -0
- package/src/core/tools/mybatisAnalysis.ts +322 -0
- package/src/core/tools/openapiAnalysis.ts +431 -0
- package/src/core/tools/pythonAnalysis.ts +477 -0
- package/src/core/tools/sqlAnalysis.ts +298 -0
- package/FINAL_SIMPLIFIED_SPEC.md +0 -456
- package/TODO.md +0 -193
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Tool, ToolResult } from "./types.js";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
interface MybatisStatement {
|
|
6
|
+
id: string;
|
|
7
|
+
type: "select" | "insert" | "update" | "delete" | "sql";
|
|
8
|
+
parameterType?: string;
|
|
9
|
+
resultType?: string;
|
|
10
|
+
resultMap?: string;
|
|
11
|
+
sql: string;
|
|
12
|
+
line: number;
|
|
13
|
+
issues: string[];
|
|
14
|
+
dynamicElements: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MybatisMapper {
|
|
18
|
+
file: string;
|
|
19
|
+
namespace: string;
|
|
20
|
+
statements: MybatisStatement[];
|
|
21
|
+
resultMaps: string[];
|
|
22
|
+
sqlFragments: string[];
|
|
23
|
+
issues: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MybatisAnalysisResult {
|
|
27
|
+
file: string;
|
|
28
|
+
isMyBatis: boolean;
|
|
29
|
+
mapper?: MybatisMapper;
|
|
30
|
+
summary: {
|
|
31
|
+
statements: number;
|
|
32
|
+
withIssues: number;
|
|
33
|
+
injectionRisks: number;
|
|
34
|
+
dynamicSqlCount: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// MyBatis XML 파싱 및 분석
|
|
39
|
+
function analyzeMyBatisXml(filePath: string): MybatisAnalysisResult {
|
|
40
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
41
|
+
const lines = content.split("\n");
|
|
42
|
+
|
|
43
|
+
// MyBatis XML 여부 확인
|
|
44
|
+
const isMyBatis =
|
|
45
|
+
content.includes("mybatis") ||
|
|
46
|
+
content.includes("ibatis") ||
|
|
47
|
+
content.includes('PUBLIC "-//mybatis.org') ||
|
|
48
|
+
content.includes('PUBLIC "-//ibatis.org') ||
|
|
49
|
+
/<mapper\s+namespace=/i.test(content);
|
|
50
|
+
|
|
51
|
+
if (!isMyBatis) {
|
|
52
|
+
return {
|
|
53
|
+
file: filePath,
|
|
54
|
+
isMyBatis: false,
|
|
55
|
+
summary: { statements: 0, withIssues: 0, injectionRisks: 0, dynamicSqlCount: 0 },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const statements: MybatisStatement[] = [];
|
|
60
|
+
const resultMaps: string[] = [];
|
|
61
|
+
const sqlFragments: string[] = [];
|
|
62
|
+
const mapperIssues: string[] = [];
|
|
63
|
+
|
|
64
|
+
// namespace 추출
|
|
65
|
+
const namespaceMatch = content.match(/<mapper\s+namespace=["']([^"']+)["']/);
|
|
66
|
+
const namespace = namespaceMatch ? namespaceMatch[1] : "unknown";
|
|
67
|
+
|
|
68
|
+
// resultMap 추출
|
|
69
|
+
const resultMapRegex = /<resultMap\s+[^>]*id=["']([^"']+)["'][^>]*>/g;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = resultMapRegex.exec(content)) !== null) {
|
|
72
|
+
resultMaps.push(match[1]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// sql fragment 추출
|
|
76
|
+
const sqlFragmentRegex = /<sql\s+[^>]*id=["']([^"']+)["'][^>]*>/g;
|
|
77
|
+
while ((match = sqlFragmentRegex.exec(content)) !== null) {
|
|
78
|
+
sqlFragments.push(match[1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// statement 추출 함수
|
|
82
|
+
const extractStatements = (type: "select" | "insert" | "update" | "delete") => {
|
|
83
|
+
const regex = new RegExp(
|
|
84
|
+
`<${type}\\s+([^>]*)>([\\s\\S]*?)</${type}>`,
|
|
85
|
+
"gi"
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
while ((match = regex.exec(content)) !== null) {
|
|
89
|
+
const attrs = match[1];
|
|
90
|
+
const sqlContent = match[2];
|
|
91
|
+
|
|
92
|
+
// 속성 파싱
|
|
93
|
+
const idMatch = attrs.match(/id=["']([^"']+)["']/);
|
|
94
|
+
const paramMatch = attrs.match(/parameterType=["']([^"']+)["']/);
|
|
95
|
+
const resultTypeMatch = attrs.match(/resultType=["']([^"']+)["']/);
|
|
96
|
+
const resultMapMatch = attrs.match(/resultMap=["']([^"']+)["']/);
|
|
97
|
+
|
|
98
|
+
const id = idMatch ? idMatch[1] : "unknown";
|
|
99
|
+
const issues: string[] = [];
|
|
100
|
+
const dynamicElements: string[] = [];
|
|
101
|
+
|
|
102
|
+
// ${} 사용 검사 (SQL Injection 위험)
|
|
103
|
+
const dollarBraceMatches = sqlContent.match(/\$\{[^}]+\}/g) || [];
|
|
104
|
+
if (dollarBraceMatches.length > 0) {
|
|
105
|
+
issues.push(`SQL Injection 위험: ${dollarBraceMatches.join(", ")} 사용`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 동적 SQL 요소 추출
|
|
109
|
+
const dynamicTags = ["if", "choose", "when", "otherwise", "where", "set", "foreach", "trim", "bind"];
|
|
110
|
+
dynamicTags.forEach((tag) => {
|
|
111
|
+
const tagRegex = new RegExp(`<${tag}[\\s>]`, "gi");
|
|
112
|
+
const count = (sqlContent.match(tagRegex) || []).length;
|
|
113
|
+
if (count > 0) {
|
|
114
|
+
dynamicElements.push(`${tag}(${count})`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// SELECT * 검사
|
|
119
|
+
if (/SELECT\s+\*\s+FROM/i.test(sqlContent)) {
|
|
120
|
+
issues.push("SELECT * 사용");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// WHERE 없는 UPDATE/DELETE
|
|
124
|
+
if ((type === "update" || type === "delete") && !/<where>|WHERE/i.test(sqlContent)) {
|
|
125
|
+
issues.push("WHERE 절 없음 - 전체 테이블 영향 위험");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 복잡한 동적 SQL
|
|
129
|
+
if (dynamicElements.length >= 5) {
|
|
130
|
+
issues.push(`복잡한 동적 SQL (${dynamicElements.length}개 요소)`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 중첩 foreach
|
|
134
|
+
const foreachCount = (sqlContent.match(/<foreach/gi) || []).length;
|
|
135
|
+
if (foreachCount >= 2) {
|
|
136
|
+
issues.push(`중첩 foreach ${foreachCount}개 - 성능 검토 필요`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// LIKE '%${...}%' 패턴
|
|
140
|
+
if (/LIKE\s+['"]?%?\$\{/i.test(sqlContent)) {
|
|
141
|
+
issues.push("LIKE + ${} 패턴 - SQL Injection 및 인덱스 문제");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ORDER BY ${} 패턴
|
|
145
|
+
if (/ORDER\s+BY\s+\$\{/i.test(sqlContent)) {
|
|
146
|
+
issues.push("ORDER BY ${} - SQL Injection 위험");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 라인 번호 찾기
|
|
150
|
+
const statementIndex = content.indexOf(match[0]);
|
|
151
|
+
let lineNum = 1;
|
|
152
|
+
for (let i = 0; i < statementIndex; i++) {
|
|
153
|
+
if (content[i] === "\n") lineNum++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// SQL 정리
|
|
157
|
+
const cleanSql = sqlContent
|
|
158
|
+
.replace(/<[^>]+>/g, " ") // XML 태그 제거
|
|
159
|
+
.replace(/\s+/g, " ")
|
|
160
|
+
.trim();
|
|
161
|
+
|
|
162
|
+
statements.push({
|
|
163
|
+
id,
|
|
164
|
+
type,
|
|
165
|
+
parameterType: paramMatch ? paramMatch[1] : undefined,
|
|
166
|
+
resultType: resultTypeMatch ? resultTypeMatch[1] : undefined,
|
|
167
|
+
resultMap: resultMapMatch ? resultMapMatch[1] : undefined,
|
|
168
|
+
sql: cleanSql,
|
|
169
|
+
line: lineNum,
|
|
170
|
+
issues,
|
|
171
|
+
dynamicElements,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// 각 statement 타입 처리
|
|
177
|
+
extractStatements("select");
|
|
178
|
+
extractStatements("insert");
|
|
179
|
+
extractStatements("update");
|
|
180
|
+
extractStatements("delete");
|
|
181
|
+
|
|
182
|
+
// mapper 레벨 이슈 검사
|
|
183
|
+
if (!namespaceMatch) {
|
|
184
|
+
mapperIssues.push("namespace 미정의");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 미사용 resultMap 검사 (간단한 체크)
|
|
188
|
+
resultMaps.forEach((rm) => {
|
|
189
|
+
const usageCount = (content.match(new RegExp(`resultMap=["']${rm}["']`, "g")) || []).length;
|
|
190
|
+
if (usageCount <= 1) {
|
|
191
|
+
// 정의만 있고 사용 없음
|
|
192
|
+
mapperIssues.push(`미사용 가능성: resultMap '${rm}'`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 통계
|
|
197
|
+
const injectionRisks = statements.filter((s) =>
|
|
198
|
+
s.issues.some((i) => i.includes("Injection"))
|
|
199
|
+
).length;
|
|
200
|
+
const dynamicSqlCount = statements.filter((s) => s.dynamicElements.length > 0).length;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
file: filePath,
|
|
204
|
+
isMyBatis: true,
|
|
205
|
+
mapper: {
|
|
206
|
+
file: filePath,
|
|
207
|
+
namespace,
|
|
208
|
+
statements,
|
|
209
|
+
resultMaps,
|
|
210
|
+
sqlFragments,
|
|
211
|
+
issues: mapperIssues,
|
|
212
|
+
},
|
|
213
|
+
summary: {
|
|
214
|
+
statements: statements.length,
|
|
215
|
+
withIssues: statements.filter((s) => s.issues.length > 0).length,
|
|
216
|
+
injectionRisks,
|
|
217
|
+
dynamicSqlCount,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 도구 정의
|
|
223
|
+
export const mybatisTools: Tool[] = [
|
|
224
|
+
{
|
|
225
|
+
name: "mybatis_check",
|
|
226
|
+
description:
|
|
227
|
+
"MyBatis XML 매퍼 파일을 분석합니다. ${} SQL Injection 위험, 동적 SQL 복잡도, SELECT *, resultMap 사용 등을 검사합니다. DTD/namespace로 MyBatis 파일을 자동 감지합니다.",
|
|
228
|
+
parameters: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
path: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "분석할 XML 파일 또는 디렉토리 경로",
|
|
234
|
+
},
|
|
235
|
+
recursive: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "디렉토리인 경우 하위 폴더 포함 여부 (기본: true)",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
required: ["path"],
|
|
241
|
+
},
|
|
242
|
+
handler: async (args: Record<string, unknown>): Promise<ToolResult> => {
|
|
243
|
+
const targetPath = args.path as string;
|
|
244
|
+
const recursive = args.recursive !== false;
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(targetPath)) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
content: "",
|
|
250
|
+
error: `경로를 찾을 수 없습니다: ${targetPath}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const results: MybatisAnalysisResult[] = [];
|
|
255
|
+
const stats = fs.statSync(targetPath);
|
|
256
|
+
|
|
257
|
+
if (stats.isFile()) {
|
|
258
|
+
if (targetPath.endsWith(".xml")) {
|
|
259
|
+
const result = analyzeMyBatisXml(targetPath);
|
|
260
|
+
if (result.isMyBatis) {
|
|
261
|
+
results.push(result);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else if (stats.isDirectory()) {
|
|
265
|
+
const walkDir = (dir: string) => {
|
|
266
|
+
const files = fs.readdirSync(dir);
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
const filePath = path.join(dir, file);
|
|
269
|
+
const fileStat = fs.statSync(filePath);
|
|
270
|
+
if (fileStat.isDirectory() && recursive) {
|
|
271
|
+
if (!file.startsWith(".") && file !== "node_modules" && file !== "target" && file !== "build") {
|
|
272
|
+
walkDir(filePath);
|
|
273
|
+
}
|
|
274
|
+
} else if (file.endsWith(".xml")) {
|
|
275
|
+
const result = analyzeMyBatisXml(filePath);
|
|
276
|
+
if (result.isMyBatis) {
|
|
277
|
+
results.push(result);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
walkDir(targetPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 전체 통계
|
|
286
|
+
const totalStatements = results.reduce((sum, r) => sum + r.summary.statements, 0);
|
|
287
|
+
const totalWithIssues = results.reduce((sum, r) => sum + r.summary.withIssues, 0);
|
|
288
|
+
const totalInjectionRisks = results.reduce((sum, r) => sum + r.summary.injectionRisks, 0);
|
|
289
|
+
const totalDynamicSql = results.reduce((sum, r) => sum + r.summary.dynamicSqlCount, 0);
|
|
290
|
+
|
|
291
|
+
const output = {
|
|
292
|
+
analyzedMappers: results.length,
|
|
293
|
+
totalStatements,
|
|
294
|
+
statementsWithIssues: totalWithIssues,
|
|
295
|
+
injectionRisks: totalInjectionRisks,
|
|
296
|
+
dynamicSqlStatements: totalDynamicSql,
|
|
297
|
+
mappers: results.map((r) => ({
|
|
298
|
+
file: r.file,
|
|
299
|
+
namespace: r.mapper?.namespace,
|
|
300
|
+
resultMaps: r.mapper?.resultMaps,
|
|
301
|
+
sqlFragments: r.mapper?.sqlFragments,
|
|
302
|
+
mapperIssues: r.mapper?.issues,
|
|
303
|
+
statements: r.mapper?.statements.map((s) => ({
|
|
304
|
+
id: s.id,
|
|
305
|
+
type: s.type,
|
|
306
|
+
line: s.line,
|
|
307
|
+
parameterType: s.parameterType,
|
|
308
|
+
resultType: s.resultType || s.resultMap,
|
|
309
|
+
dynamicElements: s.dynamicElements,
|
|
310
|
+
issues: s.issues,
|
|
311
|
+
sql: s.sql.length > 150 ? s.sql.substring(0, 150) + "..." : s.sql,
|
|
312
|
+
})),
|
|
313
|
+
})),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
content: JSON.stringify(output, null, 2),
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
];
|