@getmikk/core 2.0.12 → 2.0.14
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 +12 -3
- package/package.json +1 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +272 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/adr-manager.ts +5 -4
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-writer.ts +3 -2
- package/src/contract/lock-compiler.ts +34 -0
- package/src/contract/lock-reader.ts +62 -5
- package/src/contract/schema.ts +10 -0
- package/src/index.ts +14 -1
- package/src/parser/error-recovery.ts +646 -0
- package/src/parser/index.ts +330 -74
- package/src/parser/oxc-parser.ts +3 -2
- package/src/parser/tree-sitter/parser.ts +59 -9
- package/src/parser/tree-sitter/queries.ts +27 -0
- package/src/parser/types.ts +1 -1
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +176 -0
- package/src/utils/atomic-write.ts +131 -0
- package/src/utils/fs.ts +76 -25
- package/src/utils/language-registry.ts +95 -0
- package/src/utils/minimatch.ts +49 -6
- package/tests/adr-manager.test.ts +6 -0
- package/tests/artifact-transaction.test.ts +73 -0
- package/tests/contract.test.ts +12 -0
- package/tests/dead-code.test.ts +12 -0
- package/tests/esm-resolver.test.ts +6 -0
- package/tests/fs.test.ts +22 -1
- package/tests/fuzzy-match.test.ts +6 -0
- package/tests/go-parser.test.ts +7 -0
- package/tests/graph.test.ts +10 -0
- package/tests/hash.test.ts +6 -0
- package/tests/impact-classified.test.ts +13 -0
- package/tests/js-parser.test.ts +10 -0
- package/tests/language-registry.test.ts +64 -0
- package/tests/parse-diagnostics.test.ts +115 -0
- package/tests/parser.test.ts +36 -0
- package/tests/tree-sitter-parser.test.ts +201 -0
- package/tests/ts-parser.test.ts +6 -0
package/README.md
CHANGED
|
@@ -15,13 +15,16 @@ Foundation package for the Mikk ecosystem. All other packages depend on core —
|
|
|
15
15
|
|
|
16
16
|
### Parsers
|
|
17
17
|
|
|
18
|
-
Three
|
|
18
|
+
Three parser families follow the same interface: `parse(filePath, content)` → `ParsedFile`.
|
|
19
19
|
|
|
20
20
|
**TypeScript / TSX**
|
|
21
|
-
Uses
|
|
21
|
+
Uses OXC (Rust parser). Extracts: functions (name, params with types, return type, start/end line, async flag, decorators, generics), classes (methods, properties, inheritance), imports (named, default, namespace, type-only) with full resolution (tsconfig `paths` alias resolution, recursive `extends` chain, index file inference, extension inference). Every extracted function has its exact byte-accurate body location.
|
|
22
22
|
|
|
23
23
|
**JavaScript / JSX**
|
|
24
|
-
Uses
|
|
24
|
+
Uses OXC with `ScriptKind` inference (detects JS/JSX/CJS/MJS). Handles: JSX expression containers, default exports, CommonJS `module.exports`, re-exports via barrel files.
|
|
25
|
+
|
|
26
|
+
**Polyglot (Tree-sitter)**
|
|
27
|
+
Python, Java, Kotlin (`.kt`, `.kts`), Swift, C/C++ (`.cpp`, `.cc`, `.cxx`, `.hpp`, `.hxx`, `.hh`), C#, Rust, PHP, and Ruby via tree-sitter grammars.
|
|
25
28
|
|
|
26
29
|
**Go**
|
|
27
30
|
Regex + stateful scanning. No Go toolchain dependency. Extracts: functions, methods (with receiver types), structs, interfaces, package imports. `go.mod` used for project boundary detection.
|
|
@@ -81,6 +84,12 @@ Lock format v1.7.0:
|
|
|
81
84
|
|
|
82
85
|
Read and write `mikk.json` and `mikk.lock.json`. `LockReader.write()` uses atomic temp-file + rename to prevent corruption.
|
|
83
86
|
|
|
87
|
+
`AdrManager` writes `mikk.json` atomically as well (temp file + rename + file lock), reducing corruption risk in concurrent agent workflows.
|
|
88
|
+
|
|
89
|
+
### Parse Diagnostics
|
|
90
|
+
|
|
91
|
+
`parseFilesWithDiagnostics` returns both parsed files and parser/read/import-resolution diagnostics. This enables strict parse enforcement in CLI commands (`mikk init --strict-parsing`, `mikk analyze --strict-parsing`) for high-assurance pipelines.
|
|
92
|
+
|
|
84
93
|
### AdrManager
|
|
85
94
|
|
|
86
95
|
CRUD for Architectural Decision Records in `mikk.json`. Add, update, remove, list, and get individual decisions. ADRs surface in all AI context queries via the MCP server.
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis modules - Type Flow and Taint Analysis for semantic code understanding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { TypeFlowAnalyzer } from './type-flow.js'
|
|
6
|
+
export type { TypeFlowInfo, TypeParam, TypeEdge, TypeFlowResult } from './type-flow.js'
|
|
7
|
+
|
|
8
|
+
export { TaintAnalyzer } from './taint-analysis.js'
|
|
9
|
+
export type { TaintSource, TaintSink, TaintFlow, DataFlowResult } from './taint-analysis.js'
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Flow & Taint Analysis — tracks data propagation through code
|
|
3
|
+
* for security vulnerability detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface TaintSource {
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
16
|
+
patterns: RegExp[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TaintSink {
|
|
20
|
+
name: string
|
|
21
|
+
description: string
|
|
22
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
23
|
+
patterns: RegExp[]
|
|
24
|
+
sanitizers: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TaintFlow {
|
|
28
|
+
source: string
|
|
29
|
+
sink: string
|
|
30
|
+
path: string[]
|
|
31
|
+
vulnerability: string
|
|
32
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
33
|
+
confidence: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DataFlowResult {
|
|
37
|
+
flows: TaintFlow[]
|
|
38
|
+
summary: {
|
|
39
|
+
totalFlows: number
|
|
40
|
+
critical: number
|
|
41
|
+
high: number
|
|
42
|
+
medium: number
|
|
43
|
+
low: number
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Default Taint Sources and Sinks
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const DEFAULT_TAINT_SOURCES: TaintSource[] = [
|
|
52
|
+
{
|
|
53
|
+
name: 'user-input',
|
|
54
|
+
description: 'User-controlled input',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
patterns: [
|
|
57
|
+
/req\.(body|query|params|headers)/i,
|
|
58
|
+
/request\.(body|query|params|headers)/i,
|
|
59
|
+
/input\(/i,
|
|
60
|
+
/process\.env/i,
|
|
61
|
+
/process\.argv/i,
|
|
62
|
+
/\bstdin\b/i,
|
|
63
|
+
/readline\(/i,
|
|
64
|
+
/readFile\(/i,
|
|
65
|
+
/fetch\(/i,
|
|
66
|
+
/axios\(/i,
|
|
67
|
+
/http\.request\(/i,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'filesystem',
|
|
72
|
+
description: 'File system input',
|
|
73
|
+
severity: 'medium',
|
|
74
|
+
patterns: [
|
|
75
|
+
/readFile\(/i,
|
|
76
|
+
/readFileSync\(/i,
|
|
77
|
+
/readdir\(/i,
|
|
78
|
+
/createReadStream\(/i,
|
|
79
|
+
/fs\.readFile\(/i,
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'database',
|
|
84
|
+
description: 'Database query results',
|
|
85
|
+
severity: 'medium',
|
|
86
|
+
patterns: [
|
|
87
|
+
/query\(/i,
|
|
88
|
+
/\.find\(/i,
|
|
89
|
+
/\.select\(/i,
|
|
90
|
+
/execute\(/i,
|
|
91
|
+
/\.fetch\(/i,
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'network',
|
|
96
|
+
description: 'Network/API responses',
|
|
97
|
+
severity: 'medium',
|
|
98
|
+
patterns: [
|
|
99
|
+
/fetch\(/i,
|
|
100
|
+
/axios\(/i,
|
|
101
|
+
/http\.get\(/i,
|
|
102
|
+
/https\.get\(/i,
|
|
103
|
+
/request\(/i,
|
|
104
|
+
/\.json\(\)/i,
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
const DEFAULT_TAINT_SINKS: TaintSink[] = [
|
|
110
|
+
{
|
|
111
|
+
name: 'sql-query',
|
|
112
|
+
description: 'SQL query execution',
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
patterns: [
|
|
115
|
+
/execute\s*\(/i,
|
|
116
|
+
/query\s*\(/i,
|
|
117
|
+
/\.exec\(/i,
|
|
118
|
+
/cursor\.execute\(/i,
|
|
119
|
+
/db\.query\(/i,
|
|
120
|
+
],
|
|
121
|
+
sanitizers: ['escape', 'sanitize', 'param', 'bind', 'prepare'],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'command-injection',
|
|
125
|
+
description: 'OS command execution',
|
|
126
|
+
severity: 'critical',
|
|
127
|
+
patterns: [
|
|
128
|
+
/exec\s*\(/i,
|
|
129
|
+
/spawn\s*\(/i,
|
|
130
|
+
/execSync\s*\(/i,
|
|
131
|
+
/system\s*\(/i,
|
|
132
|
+
/popen\s*\(/i,
|
|
133
|
+
/child_process\./i,
|
|
134
|
+
],
|
|
135
|
+
sanitizers: ['execFile', 'spawnSync', 'execFileSync'],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'code-execution',
|
|
139
|
+
description: 'Dynamic code execution',
|
|
140
|
+
severity: 'critical',
|
|
141
|
+
patterns: [
|
|
142
|
+
/\beval\s*\(/i,
|
|
143
|
+
/\bFunction\s*\(/i,
|
|
144
|
+
/setTimeout\s*\(\s*\w+\s*,/i,
|
|
145
|
+
/setInterval\s*\(\s*\w+\s*,/i,
|
|
146
|
+
],
|
|
147
|
+
sanitizers: [],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'path-traversal',
|
|
151
|
+
description: 'File system operations',
|
|
152
|
+
severity: 'high',
|
|
153
|
+
patterns: [
|
|
154
|
+
/readFile\(/i,
|
|
155
|
+
/writeFile\(/i,
|
|
156
|
+
/open\(/i,
|
|
157
|
+
/createReadStream\(/i,
|
|
158
|
+
/stat\(/i,
|
|
159
|
+
/lstat\(/i,
|
|
160
|
+
/access\(/i,
|
|
161
|
+
/exists\(/i,
|
|
162
|
+
],
|
|
163
|
+
sanitizers: ['normalize', 'resolve', 'basename', 'dirname', 'join'],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'xss',
|
|
167
|
+
description: 'HTML/JS injection',
|
|
168
|
+
severity: 'high',
|
|
169
|
+
patterns: [
|
|
170
|
+
/\.innerHTML\s*=/i,
|
|
171
|
+
/\.outerHTML\s*=/i,
|
|
172
|
+
/dangerouslySetInnerHTML/i,
|
|
173
|
+
/document\.write\(/i,
|
|
174
|
+
/\.html\s*\(/i,
|
|
175
|
+
],
|
|
176
|
+
sanitizers: ['escape', 'sanitize', 'text', 'encode', 'DOMPurify'],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'prototype-pollution',
|
|
180
|
+
description: 'Object prototype manipulation',
|
|
181
|
+
severity: 'high',
|
|
182
|
+
patterns: [
|
|
183
|
+
/\[\s*['"]__proto__['"]\s*\]/i,
|
|
184
|
+
/\[\s*['"]constructor['"]\s*\]/i,
|
|
185
|
+
/Object\.assign\s*\(\s*\w+\s*,\s*\w+\s*\)/i,
|
|
186
|
+
],
|
|
187
|
+
sanitizers: [],
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Taint Analyzer
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export class TaintAnalyzer {
|
|
196
|
+
private lock: MikkLock
|
|
197
|
+
private sources: TaintSource[]
|
|
198
|
+
private sinks: TaintSink[]
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
lock: MikkLock,
|
|
202
|
+
sources?: TaintSource[],
|
|
203
|
+
sinks?: TaintSink[]
|
|
204
|
+
) {
|
|
205
|
+
this.lock = lock
|
|
206
|
+
this.sources = sources || DEFAULT_TAINT_SOURCES
|
|
207
|
+
this.sinks = sinks || DEFAULT_TAINT_SINKS
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Analyze the codebase for taint flows.
|
|
212
|
+
*/
|
|
213
|
+
analyze(): DataFlowResult {
|
|
214
|
+
const flows: TaintFlow[] = []
|
|
215
|
+
const allFunctions = Object.values(this.lock.functions)
|
|
216
|
+
|
|
217
|
+
// Find functions that contain taint sources
|
|
218
|
+
const sourceFunctions = this.findTaintSources(allFunctions)
|
|
219
|
+
|
|
220
|
+
// Find functions that contain taint sinks
|
|
221
|
+
const sinkFunctions = this.findTaintSinks(allFunctions)
|
|
222
|
+
|
|
223
|
+
// Trace taint flows through call graph
|
|
224
|
+
for (const sourceFn of sourceFunctions) {
|
|
225
|
+
for (const sinkFn of sinkFunctions) {
|
|
226
|
+
if (sourceFn.id === sinkFn.fn.id) continue
|
|
227
|
+
|
|
228
|
+
const flow = this.traceTaintFlow(sourceFn, sinkFn.fn, sinkFn.sink, allFunctions)
|
|
229
|
+
if (flow) {
|
|
230
|
+
flows.push(flow)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
flows,
|
|
237
|
+
summary: {
|
|
238
|
+
totalFlows: flows.length,
|
|
239
|
+
critical: flows.filter(f => f.severity === 'critical').length,
|
|
240
|
+
high: flows.filter(f => f.severity === 'high').length,
|
|
241
|
+
medium: flows.filter(f => f.severity === 'medium').length,
|
|
242
|
+
low: flows.filter(f => f.severity === 'low').length,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find functions that contain taint sources.
|
|
249
|
+
*/
|
|
250
|
+
private findTaintSources(functions: MikkLockFunction[]): MikkLockFunction[] {
|
|
251
|
+
const sources: MikkLockFunction[] = []
|
|
252
|
+
|
|
253
|
+
for (const fn of functions) {
|
|
254
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
255
|
+
|
|
256
|
+
for (const source of this.sources) {
|
|
257
|
+
for (const pattern of source.patterns) {
|
|
258
|
+
if (pattern.test(fnText)) {
|
|
259
|
+
sources.push(fn)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return sources
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Find functions that contain taint sinks.
|
|
271
|
+
*/
|
|
272
|
+
private findTaintSinks(functions: MikkLockFunction[]): Array<{ fn: MikkLockFunction; sink: TaintSink }> {
|
|
273
|
+
const sinks: Array<{ fn: MikkLockFunction; sink: TaintSink }> = []
|
|
274
|
+
|
|
275
|
+
for (const fn of functions) {
|
|
276
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
277
|
+
|
|
278
|
+
for (const sink of this.sinks) {
|
|
279
|
+
for (const pattern of sink.patterns) {
|
|
280
|
+
if (pattern.test(fnText)) {
|
|
281
|
+
sinks.push({ fn, sink })
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return sinks
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Trace taint flow from source to sink through call graph.
|
|
293
|
+
*/
|
|
294
|
+
private traceTaintFlow(
|
|
295
|
+
source: MikkLockFunction,
|
|
296
|
+
sinkFn: MikkLockFunction,
|
|
297
|
+
sink: TaintSink,
|
|
298
|
+
allFunctions: MikkLockFunction[]
|
|
299
|
+
): TaintFlow | null {
|
|
300
|
+
// Direct call: source calls sink directly
|
|
301
|
+
if (source.calls?.includes(sinkFn.id)) {
|
|
302
|
+
return {
|
|
303
|
+
source: source.name,
|
|
304
|
+
sink: sinkFn.name,
|
|
305
|
+
path: [source.name, sinkFn.name],
|
|
306
|
+
vulnerability: `${source.name} -> ${sinkFn.name}`,
|
|
307
|
+
severity: sink.severity,
|
|
308
|
+
confidence: 0.9,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if there's a path through the call graph
|
|
313
|
+
const path = this.findPath(source.id, sinkFn.id, allFunctions)
|
|
314
|
+
if (path) {
|
|
315
|
+
return {
|
|
316
|
+
source: source.name,
|
|
317
|
+
sink: sinkFn.name,
|
|
318
|
+
path: path.map(id => this.lock.functions[id]?.name || id),
|
|
319
|
+
vulnerability: `${source.name} -> ${path.length - 1} intermediate(s) -> ${sinkFn.name}`,
|
|
320
|
+
severity: sink.severity,
|
|
321
|
+
confidence: 0.7,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Find path from source to sink through call graph.
|
|
330
|
+
*/
|
|
331
|
+
private findPath(
|
|
332
|
+
sourceId: string,
|
|
333
|
+
sinkId: string,
|
|
334
|
+
allFunctions: MikkLockFunction[]
|
|
335
|
+
): string[] | null {
|
|
336
|
+
const visited = new Set<string>()
|
|
337
|
+
const path: string[] = []
|
|
338
|
+
|
|
339
|
+
function dfs(currentId: string): boolean {
|
|
340
|
+
if (currentId === sinkId) {
|
|
341
|
+
path.push(currentId)
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (visited.has(currentId)) return false
|
|
346
|
+
visited.add(currentId)
|
|
347
|
+
path.push(currentId)
|
|
348
|
+
|
|
349
|
+
const fn = allFunctions.find(f => f.id === currentId)
|
|
350
|
+
if (fn?.calls) {
|
|
351
|
+
for (const calleeId of fn.calls) {
|
|
352
|
+
if (dfs(calleeId)) return true
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
path.pop()
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (dfs(sourceId)) {
|
|
361
|
+
return path
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if a function has sanitizers that mitigate taint.
|
|
369
|
+
*/
|
|
370
|
+
private hasSanitizer(fn: MikkLockFunction, sink: TaintSink): boolean {
|
|
371
|
+
const fnText = `${fn.name} ${fn.purpose || ''}`.toLowerCase()
|
|
372
|
+
|
|
373
|
+
for (const sanitizer of sink.sanitizers) {
|
|
374
|
+
if (fnText.includes(sanitizer.toLowerCase())) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get security findings from taint analysis.
|
|
384
|
+
*/
|
|
385
|
+
getFindings(): Array<{
|
|
386
|
+
severity: string
|
|
387
|
+
title: string
|
|
388
|
+
file: string
|
|
389
|
+
line: number
|
|
390
|
+
description: string
|
|
391
|
+
}> {
|
|
392
|
+
const result = this.analyze()
|
|
393
|
+
const findings: Array<{
|
|
394
|
+
severity: string
|
|
395
|
+
title: string
|
|
396
|
+
file: string
|
|
397
|
+
line: number
|
|
398
|
+
description: string
|
|
399
|
+
}> = []
|
|
400
|
+
|
|
401
|
+
for (const flow of result.flows) {
|
|
402
|
+
const sourceFn = Object.values(this.lock.functions).find(
|
|
403
|
+
f => f.name === flow.source
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if (sourceFn) {
|
|
407
|
+
findings.push({
|
|
408
|
+
severity: flow.severity,
|
|
409
|
+
title: `Potential ${flow.sink} vulnerability`,
|
|
410
|
+
file: sourceFn.file,
|
|
411
|
+
line: sourceFn.startLine,
|
|
412
|
+
description: `Tainted data from ${flow.source} flows to ${flow.sink} via: ${flow.path.join(' -> ')}`,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return findings
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Flow Analysis — tracks type propagation through function calls
|
|
3
|
+
* and provides type-aware code understanding beyond syntactic parsing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
7
|
+
import type { DependencyGraph, GraphEdge } from '../graph/types.js'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface TypeFlowInfo {
|
|
14
|
+
/** Function ID */
|
|
15
|
+
functionId: string
|
|
16
|
+
/** Inferred parameter types */
|
|
17
|
+
paramTypes: TypeParam[]
|
|
18
|
+
/** Inferred return type */
|
|
19
|
+
returnType: string
|
|
20
|
+
/** Types that flow into this function from callers */
|
|
21
|
+
incomingTypes: TypeEdge[]
|
|
22
|
+
/** Types that flow out to callees */
|
|
23
|
+
outgoingTypes: TypeEdge[]
|
|
24
|
+
/** Type confidence score (0-1) */
|
|
25
|
+
confidence: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TypeParam {
|
|
29
|
+
name: string
|
|
30
|
+
type: string
|
|
31
|
+
source: 'annotation' | 'inference' | 'usage'
|
|
32
|
+
confidence: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TypeEdge {
|
|
36
|
+
from: string
|
|
37
|
+
to: string
|
|
38
|
+
type: string
|
|
39
|
+
confidence: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TypeFlowResult {
|
|
43
|
+
flows: Map<string, TypeFlowInfo>
|
|
44
|
+
summary: {
|
|
45
|
+
totalFunctions: number
|
|
46
|
+
typedFunctions: number
|
|
47
|
+
inferredFunctions: number
|
|
48
|
+
averageConfidence: number
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Type Flow Analyzer
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export class TypeFlowAnalyzer {
|
|
57
|
+
private lock: MikkLock
|
|
58
|
+
private graph: DependencyGraph
|
|
59
|
+
|
|
60
|
+
constructor(lock: MikkLock, graph: DependencyGraph) {
|
|
61
|
+
this.lock = lock
|
|
62
|
+
this.graph = graph
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Analyze type flow across the entire codebase.
|
|
67
|
+
*/
|
|
68
|
+
analyze(): TypeFlowResult {
|
|
69
|
+
const flows = new Map<string, TypeFlowInfo>()
|
|
70
|
+
const allFunctions = Object.values(this.lock.functions)
|
|
71
|
+
|
|
72
|
+
// Phase 1: Extract explicit type annotations
|
|
73
|
+
for (const fn of allFunctions) {
|
|
74
|
+
flows.set(fn.id, this.extractExplicitTypes(fn))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Phase 2: Propagate types through call graph
|
|
78
|
+
this.propagateTypes(flows)
|
|
79
|
+
|
|
80
|
+
// Phase 3: Compute summary statistics
|
|
81
|
+
const summary = this.computeSummary(flows)
|
|
82
|
+
|
|
83
|
+
return { flows, summary }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get type flow for a specific function.
|
|
88
|
+
*/
|
|
89
|
+
getFunctionFlow(functionId: string): TypeFlowInfo | null {
|
|
90
|
+
const result = this.analyze()
|
|
91
|
+
return result.flows.get(functionId) ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find all functions that return a specific type.
|
|
96
|
+
*/
|
|
97
|
+
findFunctionsByReturnType(typeName: string): MikkLockFunction[] {
|
|
98
|
+
const result = this.analyze()
|
|
99
|
+
const matches: MikkLockFunction[] = []
|
|
100
|
+
|
|
101
|
+
for (const [fnId, flow] of result.flows) {
|
|
102
|
+
if (flow.returnType.toLowerCase().includes(typeName.toLowerCase())) {
|
|
103
|
+
const fn = this.lock.functions[fnId]
|
|
104
|
+
if (fn) matches.push(fn)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return matches
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find all functions that accept a specific parameter type.
|
|
113
|
+
*/
|
|
114
|
+
findFunctionsByParamType(typeName: string): MikkLockFunction[] {
|
|
115
|
+
const result = this.analyze()
|
|
116
|
+
const matches: MikkLockFunction[] = []
|
|
117
|
+
|
|
118
|
+
for (const [fnId, flow] of result.flows) {
|
|
119
|
+
for (const param of flow.paramTypes) {
|
|
120
|
+
if (param.type.toLowerCase().includes(typeName.toLowerCase())) {
|
|
121
|
+
const fn = this.lock.functions[fnId]
|
|
122
|
+
if (fn) matches.push(fn)
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return matches
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract explicit type annotations from function metadata.
|
|
133
|
+
*/
|
|
134
|
+
private extractExplicitTypes(fn: MikkLockFunction): TypeFlowInfo {
|
|
135
|
+
const paramTypes: TypeParam[] = []
|
|
136
|
+
|
|
137
|
+
if (fn.params) {
|
|
138
|
+
for (const param of fn.params) {
|
|
139
|
+
paramTypes.push({
|
|
140
|
+
name: param.name,
|
|
141
|
+
type: param.type || 'unknown',
|
|
142
|
+
source: param.type ? 'annotation' : 'inference',
|
|
143
|
+
confidence: param.type ? 1.0 : 0.3,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
functionId: fn.id,
|
|
150
|
+
paramTypes,
|
|
151
|
+
returnType: fn.returnType || 'unknown',
|
|
152
|
+
incomingTypes: [],
|
|
153
|
+
outgoingTypes: [],
|
|
154
|
+
confidence: this.computeConfidence(paramTypes, fn.returnType),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Propagate types through the call graph.
|
|
160
|
+
*/
|
|
161
|
+
private propagateTypes(flows: Map<string, TypeFlowInfo>): void {
|
|
162
|
+
// Build type propagation edges
|
|
163
|
+
for (const [fnId, flow] of flows) {
|
|
164
|
+
const fn = this.lock.functions[fnId]
|
|
165
|
+
if (!fn) continue
|
|
166
|
+
|
|
167
|
+
// Find outgoing calls
|
|
168
|
+
for (const calleeId of fn.calls || []) {
|
|
169
|
+
const calleeFlow = flows.get(calleeId)
|
|
170
|
+
if (!calleeFlow) continue
|
|
171
|
+
|
|
172
|
+
// Create type edges for parameters
|
|
173
|
+
for (let i = 0; i < calleeFlow.paramTypes.length; i++) {
|
|
174
|
+
const param = calleeFlow.paramTypes[i]
|
|
175
|
+
if (param.type !== 'unknown') {
|
|
176
|
+
flow.outgoingTypes.push({
|
|
177
|
+
from: fnId,
|
|
178
|
+
to: calleeId,
|
|
179
|
+
type: param.type,
|
|
180
|
+
confidence: param.confidence * 0.8,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create type edges for return type
|
|
186
|
+
if (calleeFlow.returnType !== 'unknown') {
|
|
187
|
+
flow.incomingTypes.push({
|
|
188
|
+
from: calleeId,
|
|
189
|
+
to: fnId,
|
|
190
|
+
type: calleeFlow.returnType,
|
|
191
|
+
confidence: calleeFlow.confidence * 0.8,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Compute confidence score for type information.
|
|
200
|
+
*/
|
|
201
|
+
private computeConfidence(paramTypes: TypeParam[], returnType?: string): number {
|
|
202
|
+
let totalConfidence = 0
|
|
203
|
+
let count = 0
|
|
204
|
+
|
|
205
|
+
for (const param of paramTypes) {
|
|
206
|
+
totalConfidence += param.confidence
|
|
207
|
+
count++
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (returnType && returnType !== 'unknown') {
|
|
211
|
+
totalConfidence += 0.9
|
|
212
|
+
count++
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return count > 0 ? totalConfidence / count : 0.1
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Compute summary statistics.
|
|
220
|
+
*/
|
|
221
|
+
private computeSummary(flows: Map<string, TypeFlowInfo>): TypeFlowResult['summary'] {
|
|
222
|
+
const totalFunctions = flows.size
|
|
223
|
+
let typedFunctions = 0
|
|
224
|
+
let inferredFunctions = 0
|
|
225
|
+
let totalConfidence = 0
|
|
226
|
+
|
|
227
|
+
for (const flow of flows.values()) {
|
|
228
|
+
if (flow.returnType !== 'unknown') {
|
|
229
|
+
typedFunctions++
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const hasInferred = flow.paramTypes.some(p => p.source === 'inference' || p.source === 'usage')
|
|
233
|
+
if (hasInferred) {
|
|
234
|
+
inferredFunctions++
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
totalConfidence += flow.confidence
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
totalFunctions,
|
|
242
|
+
typedFunctions,
|
|
243
|
+
inferredFunctions,
|
|
244
|
+
averageConfidence: totalFunctions > 0 ? totalConfidence / totalFunctions : 0,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|