@nahisaho/musubix-security 3.0.11 → 3.0.15

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.
@@ -1,159 +1,263 @@
1
1
  /**
2
2
  * @fileoverview Go Language Extractor
3
3
  * @module @nahisaho/musubix-security/extractors/go-extractor
4
- * @trace TSK-005, TSK-006, TSK-007, TSK-008
5
- * @trace REQ-SEC-LANG-002, REQ-SEC-CFG-001, REQ-SEC-DFG-001
4
+ * @trace TSK-GO-001, TSK-GO-002, TSK-GO-003, TSK-GO-004, TSK-GO-005
5
+ * @trace REQ-SEC-GO-001, REQ-SEC-GO-002, REQ-SEC-GO-003, REQ-SEC-GO-004
6
+ * @trace REQ-SEC-GO-005, REQ-SEC-GO-006, REQ-SEC-GO-007, REQ-SEC-GO-008
6
7
  */
7
8
  import { BaseExtractor, } from './base-extractor.js';
8
9
  /**
9
- * Go framework patterns
10
+ * Go framework models with proper FrameworkModel interface
11
+ * @trace REQ-SEC-GO-004
10
12
  */
11
- const GO_FRAMEWORK_PATTERNS = [
12
- // net/http
13
+ const GO_FRAMEWORK_MODELS = [
14
+ // net/http (Standard Library HTTP Server)
13
15
  {
14
- id: 'go-net-http',
15
16
  name: 'net/http',
16
- language: 'go',
17
- sourcePatterns: [
18
- { pattern: 'r.URL.Query', type: 'user_input' },
19
- { pattern: 'r.FormValue', type: 'user_input' },
20
- { pattern: 'r.PostFormValue', type: 'user_input' },
21
- { pattern: 'r.Header.Get', type: 'user_input' },
22
- { pattern: 'r.Body', type: 'user_input' },
23
- { pattern: 'r.Cookie', type: 'user_input' },
17
+ languages: ['go'],
18
+ sources: [
19
+ { pattern: /r\.URL\.Query\(\)/, type: 'user_input', description: 'URL query parameters', taintLabel: 'user_input' },
20
+ { pattern: /r\.FormValue\(/, type: 'user_input', description: 'Form value access', taintLabel: 'user_input' },
21
+ { pattern: /r\.PostFormValue\(/, type: 'user_input', description: 'POST form value', taintLabel: 'user_input' },
22
+ { pattern: /r\.Header\.Get\(/, type: 'user_input', description: 'HTTP header access', taintLabel: 'user_input' },
23
+ { pattern: /r\.Body/, type: 'user_input', description: 'Request body', taintLabel: 'user_input' },
24
+ { pattern: /r\.Cookies\(\)/, type: 'user_input', description: 'Cookie access', taintLabel: 'user_input' },
24
25
  ],
25
- sinkPatterns: [
26
- { pattern: 'w.Write', type: 'xss' },
27
- { pattern: 'fmt.Fprintf(w,', type: 'xss' },
28
- { pattern: 'template.Execute', type: 'xss' },
29
- { pattern: 'http.Redirect', type: 'open_redirect' },
26
+ sinks: [
27
+ { pattern: /fmt\.Fprintf\(w,/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
28
+ { pattern: /w\.Write\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
29
+ { pattern: /http\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
30
30
  ],
31
- sanitizerPatterns: [
32
- { pattern: 'html.EscapeString', type: 'xss' },
33
- { pattern: 'template.HTMLEscapeString', type: 'xss' },
34
- { pattern: 'url.PathEscape', type: 'path_traversal' },
31
+ sanitizers: [
32
+ { pattern: /html\.EscapeString\(/, sanitizes: ['xss'] },
33
+ { pattern: /template\.HTMLEscapeString\(/, sanitizes: ['xss'] },
35
34
  ],
36
35
  },
37
- // Gin framework
36
+ // database/sql (Standard Library Database)
37
+ {
38
+ name: 'database/sql',
39
+ languages: ['go'],
40
+ sources: [],
41
+ sinks: [
42
+ { pattern: /db\.Query\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
43
+ { pattern: /db\.QueryRow\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
44
+ { pattern: /db\.Exec\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
45
+ { pattern: /db\.Prepare\([^)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
46
+ { pattern: /fmt\.Sprintf\([^)]*SELECT/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
47
+ ],
48
+ sanitizers: [
49
+ { pattern: /db\.Query\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
50
+ { pattern: /db\.QueryRow\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
51
+ ],
52
+ },
53
+ // os/exec (Command Execution)
54
+ {
55
+ name: 'os/exec',
56
+ languages: ['go'],
57
+ sources: [
58
+ { pattern: /os\.Args/, type: 'user_input', description: 'Command line arguments', taintLabel: 'user_input' },
59
+ { pattern: /os\.Getenv\(/, type: 'user_input', description: 'Environment variable', taintLabel: 'env_input' },
60
+ ],
61
+ sinks: [
62
+ { pattern: /exec\.Command\(/, type: 'command', vulnerabilityType: 'command_injection', severity: 'critical' },
63
+ { pattern: /exec\.CommandContext\(/, type: 'command', vulnerabilityType: 'command_injection', severity: 'critical' },
64
+ ],
65
+ sanitizers: [],
66
+ },
67
+ // os (File Operations)
68
+ {
69
+ name: 'os',
70
+ languages: ['go'],
71
+ sources: [],
72
+ sinks: [
73
+ { pattern: /os\.Open\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
74
+ { pattern: /os\.OpenFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
75
+ { pattern: /os\.Create\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
76
+ { pattern: /os\.ReadFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
77
+ { pattern: /os\.WriteFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
78
+ { pattern: /ioutil\.ReadFile\(/, type: 'file', vulnerabilityType: 'path_traversal', severity: 'high' },
79
+ ],
80
+ sanitizers: [
81
+ { pattern: /filepath\.Clean\(/, sanitizes: ['path_traversal'] },
82
+ { pattern: /filepath\.Base\(/, sanitizes: ['path_traversal'] },
83
+ ],
84
+ },
85
+ // encoding/xml (XML Processing)
86
+ {
87
+ name: 'encoding/xml',
88
+ languages: ['go'],
89
+ sources: [],
90
+ sinks: [
91
+ { pattern: /xml\.Unmarshal\(/, type: 'xml', vulnerabilityType: 'xxe', severity: 'high' },
92
+ { pattern: /xml\.NewDecoder\(/, type: 'xml', vulnerabilityType: 'xxe', severity: 'high' },
93
+ ],
94
+ sanitizers: [],
95
+ },
96
+ // Gin Framework
38
97
  {
39
- id: 'go-gin',
40
98
  name: 'Gin',
41
- language: 'go',
42
- sourcePatterns: [
43
- { pattern: 'c.Query', type: 'user_input' },
44
- { pattern: 'c.Param', type: 'user_input' },
45
- { pattern: 'c.PostForm', type: 'user_input' },
46
- { pattern: 'c.GetHeader', type: 'user_input' },
47
- { pattern: 'c.ShouldBind', type: 'user_input' },
48
- { pattern: 'c.BindJSON', type: 'user_input' },
99
+ languages: ['go'],
100
+ sources: [
101
+ { pattern: /c\.Query\(/, type: 'user_input', description: 'Gin query parameter', taintLabel: 'user_input' },
102
+ { pattern: /c\.Param\(/, type: 'user_input', description: 'Gin path parameter', taintLabel: 'user_input' },
103
+ { pattern: /c\.PostForm\(/, type: 'user_input', description: 'Gin POST form', taintLabel: 'user_input' },
104
+ { pattern: /c\.ShouldBindJSON\(/, type: 'user_input', description: 'Gin JSON binding', taintLabel: 'user_input' },
105
+ { pattern: /c\.GetHeader\(/, type: 'user_input', description: 'Gin header access', taintLabel: 'user_input' },
106
+ { pattern: /c\.Cookie\(/, type: 'user_input', description: 'Gin cookie access', taintLabel: 'user_input' },
49
107
  ],
50
- sinkPatterns: [
51
- { pattern: 'c.String', type: 'xss' },
52
- { pattern: 'c.HTML', type: 'xss' },
53
- { pattern: 'c.Redirect', type: 'open_redirect' },
108
+ sinks: [
109
+ { pattern: /c\.HTML\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
110
+ { pattern: /c\.String\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
111
+ { pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
54
112
  ],
55
- sanitizerPatterns: [],
113
+ sanitizers: [],
56
114
  },
57
- // database/sql
115
+ // Echo Framework
58
116
  {
59
- id: 'go-database-sql',
60
- name: 'database/sql',
61
- language: 'go',
62
- sourcePatterns: [],
63
- sinkPatterns: [
64
- { pattern: 'db.Query', type: 'sql_injection' },
65
- { pattern: 'db.Exec', type: 'sql_injection' },
66
- { pattern: 'db.QueryRow', type: 'sql_injection' },
67
- { pattern: 'tx.Query', type: 'sql_injection' },
68
- { pattern: 'tx.Exec', type: 'sql_injection' },
117
+ name: 'Echo',
118
+ languages: ['go'],
119
+ sources: [
120
+ { pattern: /c\.QueryParam\(/, type: 'user_input', description: 'Echo query parameter', taintLabel: 'user_input' },
121
+ { pattern: /c\.Param\(/, type: 'user_input', description: 'Echo path parameter', taintLabel: 'user_input' },
122
+ { pattern: /c\.FormValue\(/, type: 'user_input', description: 'Echo form value', taintLabel: 'user_input' },
123
+ { pattern: /c\.Bind\(/, type: 'user_input', description: 'Echo request binding', taintLabel: 'user_input' },
124
+ { pattern: /c\.Request\(\)\.Header\.Get\(/, type: 'user_input', description: 'Echo header access', taintLabel: 'user_input' },
69
125
  ],
70
- sanitizerPatterns: [
71
- { pattern: 'db.Prepare', type: 'sql_injection' },
126
+ sinks: [
127
+ { pattern: /c\.HTML\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'high' },
128
+ { pattern: /c\.String\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
129
+ { pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
72
130
  ],
131
+ sanitizers: [],
73
132
  },
74
- // os/exec
133
+ // Fiber Framework
75
134
  {
76
- id: 'go-os-exec',
77
- name: 'os/exec',
78
- language: 'go',
79
- sourcePatterns: [],
80
- sinkPatterns: [
81
- { pattern: 'exec.Command', type: 'command_injection' },
82
- { pattern: 'exec.CommandContext', type: 'command_injection' },
135
+ name: 'Fiber',
136
+ languages: ['go'],
137
+ sources: [
138
+ { pattern: /c\.Query\(/, type: 'user_input', description: 'Fiber query parameter', taintLabel: 'user_input' },
139
+ { pattern: /c\.Params\(/, type: 'user_input', description: 'Fiber path parameter', taintLabel: 'user_input' },
140
+ { pattern: /c\.FormValue\(/, type: 'user_input', description: 'Fiber form value', taintLabel: 'user_input' },
141
+ { pattern: /c\.BodyParser\(/, type: 'user_input', description: 'Fiber body parser', taintLabel: 'user_input' },
142
+ { pattern: /c\.Get\(/, type: 'user_input', description: 'Fiber header access', taintLabel: 'user_input' },
143
+ ],
144
+ sinks: [
145
+ { pattern: /c\.SendString\(/, type: 'xss', vulnerabilityType: 'xss', severity: 'medium' },
146
+ { pattern: /c\.Redirect\(/, type: 'redirect', vulnerabilityType: 'open_redirect', severity: 'medium' },
83
147
  ],
84
- sanitizerPatterns: [],
148
+ sanitizers: [],
85
149
  },
86
- // io/ioutil & os
150
+ // GORM ORM
87
151
  {
88
- id: 'go-file-io',
89
- name: 'file-io',
90
- language: 'go',
91
- sourcePatterns: [],
92
- sinkPatterns: [
93
- { pattern: 'os.Open', type: 'path_traversal' },
94
- { pattern: 'os.Create', type: 'path_traversal' },
95
- { pattern: 'os.ReadFile', type: 'path_traversal' },
96
- { pattern: 'os.WriteFile', type: 'path_traversal' },
97
- { pattern: 'ioutil.ReadFile', type: 'path_traversal' },
98
- { pattern: 'ioutil.WriteFile', type: 'path_traversal' },
152
+ name: 'GORM',
153
+ languages: ['go'],
154
+ sources: [],
155
+ sinks: [
156
+ { pattern: /db\.Raw\(/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
157
+ { pattern: /db\.Exec\(/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
158
+ { pattern: /db\.Where\([^,)]*\+/, type: 'sql', vulnerabilityType: 'sql_injection', severity: 'critical' },
99
159
  ],
100
- sanitizerPatterns: [
101
- { pattern: 'filepath.Clean', type: 'path_traversal' },
102
- { pattern: 'filepath.Abs', type: 'path_traversal' },
160
+ sanitizers: [
161
+ { pattern: /db\.Where\([^,]+,\s*\w+\)/, sanitizes: ['sql_injection'] },
162
+ ],
163
+ },
164
+ // Go SSRF vulnerabilities
165
+ {
166
+ name: 'Go SSRF',
167
+ languages: ['go'],
168
+ sources: [],
169
+ sinks: [
170
+ { pattern: /http\.Get\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
171
+ { pattern: /http\.Post\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
172
+ { pattern: /http\.NewRequest\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
173
+ { pattern: /client\.Do\(/, type: 'ssrf', vulnerabilityType: 'ssrf', severity: 'high' },
174
+ ],
175
+ sanitizers: [
176
+ { pattern: /url\.Parse\(/, sanitizes: ['ssrf'] },
103
177
  ],
104
178
  },
105
179
  ];
106
180
  /**
107
- * Go language extractor implementation
181
+ * Check if a Go identifier is exported (starts with uppercase)
182
+ * @param name The identifier name to check
183
+ * @returns true if exported, false otherwise
184
+ */
185
+ function isExported(name) {
186
+ if (!name || name.length === 0)
187
+ return false;
188
+ const firstChar = name.charAt(0);
189
+ return firstChar >= 'A' && firstChar <= 'Z';
190
+ }
191
+ /**
192
+ * Go Language Extractor
193
+ * @trace TSK-GO-001, REQ-SEC-GO-001
108
194
  */
109
195
  export class GoExtractor extends BaseExtractor {
196
+ language = 'go';
197
+ extensions = ['.go'];
110
198
  parser = null;
111
199
  tree = null;
112
200
  nodeIdCounter = 0;
113
201
  blockIdCounter = 0;
114
202
  /**
115
- * Create a new Go extractor
203
+ * Get framework models for Go
204
+ * @trace REQ-SEC-GO-004
116
205
  */
117
- constructor(options) {
118
- super('go', options);
119
- this.frameworkModels = GO_FRAMEWORK_PATTERNS;
206
+ getFrameworkModels() {
207
+ return GO_FRAMEWORK_MODELS;
120
208
  }
121
209
  /**
122
210
  * Initialize tree-sitter parser
211
+ * @trace REQ-SEC-GO-002
123
212
  */
124
213
  async initParser() {
125
214
  if (this.parser)
126
215
  return;
127
216
  try {
128
- // Dynamic import for tree-sitter
129
217
  const Parser = (await import('tree-sitter')).default;
130
218
  const Go = (await import('tree-sitter-go')).default;
131
219
  this.parser = new Parser();
132
220
  this.parser.setLanguage(Go);
133
221
  }
134
222
  catch {
135
- // Tree-sitter not available, will use fallback parsing
223
+ // tree-sitter-go not available, use fallback
136
224
  this.parser = null;
137
225
  }
138
226
  }
139
227
  /**
140
- * Parse Go source code into AST
228
+ * Build AST from source code
229
+ * @trace TSK-GO-002, REQ-SEC-GO-002
141
230
  */
142
- async parseAST(code, filePath) {
231
+ async buildAST(source, filePath) {
143
232
  await this.initParser();
233
+ this.nodeIdCounter = 0;
234
+ const astNodes = new Map();
235
+ const astEdges = [];
236
+ let ast;
144
237
  if (this.parser) {
145
- this.tree = this.parser.parse(code);
146
- return this.convertTreeSitterNode(this.tree.rootNode, filePath);
238
+ this.tree = this.parser.parse(source);
239
+ ast = this.convertTreeSitterNode(this.tree.rootNode, filePath, astNodes, astEdges);
240
+ }
241
+ else {
242
+ ast = this.createFallbackAST(source, filePath, astNodes);
147
243
  }
148
- // Fallback: minimal AST structure
149
- return this.createFallbackAST(code, filePath);
244
+ return { ast, astNodes, astEdges };
150
245
  }
151
246
  /**
152
- * Convert tree-sitter node to our AST format
247
+ * Convert tree-sitter node to AST format
153
248
  */
154
- convertTreeSitterNode(node, filePath) {
249
+ convertTreeSitterNode(node, filePath, astNodes, astEdges) {
155
250
  const id = `${filePath}#${this.nodeIdCounter++}`;
156
- const children = node.namedChildren.map((child) => this.convertTreeSitterNode(child, filePath));
251
+ const childIds = [];
252
+ for (const child of node.namedChildren) {
253
+ const childNode = this.convertTreeSitterNode(child, filePath, astNodes, astEdges);
254
+ childIds.push(childNode.id);
255
+ astEdges.push({
256
+ from: id,
257
+ to: childNode.id,
258
+ label: 'child',
259
+ });
260
+ }
157
261
  const astNode = {
158
262
  id,
159
263
  type: node.type,
@@ -165,641 +269,485 @@ export class GoExtractor extends BaseExtractor {
165
269
  startColumn: node.startPosition.column,
166
270
  endColumn: node.endPosition.column,
167
271
  },
168
- children,
169
- metadata: this.extractNodeMetadata(node),
272
+ properties: this.extractNodeProperties(node),
273
+ children: childIds,
274
+ metadata: {},
170
275
  };
171
- // Set parent references
172
- for (const child of children) {
173
- child.parent = astNode;
174
- }
276
+ astNodes.set(id, astNode);
175
277
  return astNode;
176
278
  }
177
279
  /**
178
- * Extract metadata from tree-sitter node
280
+ * Extract properties from node
281
+ * @trace REQ-SEC-GO-003
179
282
  */
180
- extractNodeMetadata(node) {
181
- const metadata = {};
283
+ extractNodeProperties(node) {
284
+ const props = {};
182
285
  switch (node.type) {
183
- case 'function_declaration':
286
+ case 'function_declaration': {
287
+ const nameNode = node.childForFieldName('name');
288
+ const paramsNode = node.childForFieldName('parameters');
289
+ const resultNode = node.childForFieldName('result');
290
+ const name = nameNode?.text;
291
+ props.name = name;
292
+ props.parameters = paramsNode?.namedChildren.map((p) => p.text) ?? [];
293
+ props.returnType = resultNode?.text;
294
+ props.isExported = name ? isExported(name) : false;
295
+ break;
296
+ }
184
297
  case 'method_declaration': {
185
298
  const nameNode = node.childForFieldName('name');
299
+ const receiverNode = node.childForFieldName('receiver');
186
300
  const paramsNode = node.childForFieldName('parameters');
187
301
  const resultNode = node.childForFieldName('result');
188
- metadata.name = nameNode?.text;
189
- metadata.parameters = this.extractParameters(paramsNode);
190
- metadata.returnType = resultNode?.text;
191
- if (node.type === 'method_declaration') {
192
- const receiverNode = node.childForFieldName('receiver');
193
- metadata.receiver = receiverNode?.text;
194
- }
302
+ const name = nameNode?.text;
303
+ props.name = name;
304
+ props.receiver = receiverNode?.text;
305
+ props.parameters = paramsNode?.namedChildren.map((p) => p.text) ?? [];
306
+ props.returnType = resultNode?.text;
307
+ props.isExported = name ? isExported(name) : false;
195
308
  break;
196
309
  }
197
310
  case 'type_declaration': {
198
- const specs = node.namedChildren.filter((c) => c.type === 'type_spec');
199
- metadata.types = specs.map((spec) => ({
200
- name: spec.childForFieldName('name')?.text,
201
- kind: spec.childForFieldName('type')?.type,
202
- }));
311
+ const specNode = node.namedChildren[0];
312
+ if (specNode?.type === 'type_spec') {
313
+ const nameNode = specNode.childForFieldName('name');
314
+ const typeNode = specNode.childForFieldName('type');
315
+ const name = nameNode?.text;
316
+ props.name = name;
317
+ props.underlyingType = typeNode?.type;
318
+ props.isExported = name ? isExported(name) : false;
319
+ }
203
320
  break;
204
321
  }
205
- case 'import_declaration': {
206
- const specs = node.namedChildren.filter((c) => c.type === 'import_spec' || c.type === 'import_spec_list');
207
- metadata.imports = this.extractImports(specs);
322
+ case 'struct_type': {
323
+ const fields = [];
324
+ for (const fieldDecl of node.namedChildren) {
325
+ if (fieldDecl.type === 'field_declaration') {
326
+ const fieldNameNode = fieldDecl.childForFieldName('name');
327
+ const fieldTypeNode = fieldDecl.childForFieldName('type');
328
+ const tagNode = fieldDecl.namedChildren.find((n) => n.type === 'raw_string_literal');
329
+ fields.push({
330
+ name: fieldNameNode?.text ?? '',
331
+ type: fieldTypeNode?.text ?? '',
332
+ tag: tagNode?.text,
333
+ });
334
+ }
335
+ }
336
+ props.fields = fields;
337
+ break;
338
+ }
339
+ case 'interface_type': {
340
+ const methods = [];
341
+ for (const methodSpec of node.namedChildren) {
342
+ if (methodSpec.type === 'method_spec') {
343
+ const methodNameNode = methodSpec.childForFieldName('name');
344
+ if (methodNameNode) {
345
+ methods.push(methodNameNode.text);
346
+ }
347
+ }
348
+ }
349
+ props.methods = methods;
208
350
  break;
209
351
  }
210
352
  case 'call_expression': {
211
- const funcNode = node.childForFieldName('function');
353
+ const functionNode = node.childForFieldName('function');
212
354
  const argsNode = node.childForFieldName('arguments');
213
- metadata.callee = funcNode?.text;
214
- metadata.argumentCount = argsNode?.namedChildCount ?? 0;
355
+ props.functionName = functionNode?.text;
356
+ props.argumentCount = argsNode?.namedChildCount ?? 0;
357
+ break;
358
+ }
359
+ case 'selector_expression': {
360
+ const operandNode = node.childForFieldName('operand');
361
+ const fieldNode = node.childForFieldName('field');
362
+ props.operand = operandNode?.text;
363
+ props.field = fieldNode?.text;
364
+ break;
365
+ }
366
+ case 'short_var_declaration':
367
+ case 'var_declaration':
368
+ case 'assignment_statement': {
369
+ const leftNode = node.childForFieldName('left');
370
+ const rightNode = node.childForFieldName('right');
371
+ props.left = leftNode?.text;
372
+ props.right = rightNode?.text;
373
+ break;
374
+ }
375
+ case 'package_clause': {
376
+ const pkgNameNode = node.childForFieldName('name');
377
+ props.packageName = pkgNameNode?.text;
378
+ break;
379
+ }
380
+ case 'import_declaration': {
381
+ const specs = [];
382
+ for (const child of node.namedChildren) {
383
+ if (child.type === 'import_spec' || child.type === 'import_spec_list') {
384
+ const pathNode = child.childForFieldName('path') ?? child;
385
+ specs.push(pathNode.text.replace(/"/g, ''));
386
+ }
387
+ }
388
+ props.imports = specs;
215
389
  break;
216
390
  }
217
391
  case 'if_statement':
392
+ props.hasElse = node.namedChildren.some((n) => n.type === 'block' && n !== node.namedChildren[0]);
393
+ break;
218
394
  case 'for_statement':
395
+ props.hasRange = node.text.includes('range');
396
+ break;
219
397
  case 'switch_statement':
220
- metadata.isControlFlow = true;
398
+ case 'type_switch_statement':
399
+ props.caseCount = node.namedChildren.filter((n) => n.type === 'expression_case' || n.type === 'type_case').length;
221
400
  break;
222
401
  case 'go_statement':
223
- metadata.isConcurrent = true;
402
+ props.isGoroutine = true;
224
403
  break;
225
404
  case 'defer_statement':
226
- metadata.isDeferred = true;
405
+ props.isDeferred = true;
406
+ break;
407
+ case 'select_statement':
408
+ props.caseCount = node.namedChildren.filter((n) => n.type === 'communication_case').length;
227
409
  break;
228
410
  }
229
- return metadata;
230
- }
231
- /**
232
- * Extract function parameters
233
- */
234
- extractParameters(paramsNode) {
235
- if (!paramsNode)
236
- return [];
237
- const params = [];
238
- const paramDecls = paramsNode.namedChildren.filter((c) => c.type === 'parameter_declaration');
239
- for (const decl of paramDecls) {
240
- const names = decl.childrenForFieldName('name');
241
- const typeNode = decl.childForFieldName('type');
242
- const typeName = typeNode?.text ?? 'unknown';
243
- for (const nameNode of names) {
244
- params.push({ name: nameNode.text, type: typeName });
245
- }
246
- }
247
- return params;
248
- }
249
- /**
250
- * Extract import specifications
251
- */
252
- extractImports(specs) {
253
- const imports = [];
254
- for (const spec of specs) {
255
- if (spec.type === 'import_spec') {
256
- const pathNode = spec.childForFieldName('path');
257
- const aliasNode = spec.childForFieldName('name');
258
- imports.push({
259
- path: pathNode?.text?.replace(/"/g, '') ?? '',
260
- alias: aliasNode?.text,
261
- });
262
- }
263
- else if (spec.type === 'import_spec_list') {
264
- imports.push(...this.extractImports(spec.namedChildren));
265
- }
266
- }
267
- return imports;
411
+ return props;
268
412
  }
269
413
  /**
270
- * Build control flow graph
414
+ * Create fallback AST when tree-sitter is unavailable
271
415
  */
272
- async buildCFG(ast) {
273
- const blocks = [];
274
- const edges = [];
275
- // Find all functions
276
- const functions = this.findNodesByType(ast, [
277
- 'function_declaration',
278
- 'method_declaration',
279
- 'func_literal',
280
- ]);
281
- for (const func of functions) {
282
- const funcCFG = this.buildFunctionCFG(func);
283
- blocks.push(...funcCFG.blocks);
284
- edges.push(...funcCFG.edges);
285
- }
286
- // Create entry and exit blocks
287
- const entry = {
288
- id: `${ast.location.file}#entry`,
289
- statements: [],
290
- predecessors: [],
291
- successors: blocks.filter((b) => b.predecessors.length === 0).map((b) => b.id),
292
- };
293
- const exit = {
294
- id: `${ast.location.file}#exit`,
295
- statements: [],
296
- predecessors: blocks.filter((b) => b.successors.length === 0).map((b) => b.id),
297
- successors: [],
298
- };
299
- return {
300
- entry: entry.id,
301
- exit: exit.id,
302
- blocks: [entry, ...blocks, exit],
303
- edges,
416
+ createFallbackAST(source, filePath, astNodes) {
417
+ const lines = source.split('\n');
418
+ const id = `${filePath}#root`;
419
+ const ast = {
420
+ id,
421
+ type: 'source_file',
422
+ text: source,
423
+ location: {
424
+ file: filePath,
425
+ startLine: 1,
426
+ endLine: lines.length,
427
+ startColumn: 0,
428
+ endColumn: lines[lines.length - 1]?.length ?? 0,
429
+ },
430
+ properties: { fallback: true, lineCount: lines.length },
431
+ children: [],
432
+ metadata: {},
304
433
  };
434
+ astNodes.set(id, ast);
435
+ return ast;
305
436
  }
306
437
  /**
307
- * Build CFG for a single function
308
- */
309
- buildFunctionCFG(func) {
310
- const blocks = [];
311
- const edges = [];
312
- const bodyNode = func.children.find((c) => c.type === 'block');
313
- if (!bodyNode) {
314
- return { blocks, edges };
315
- }
316
- // Process function body
317
- const { blocks: bodyBlocks, edges: bodyEdges } = this.processBlock(bodyNode);
318
- blocks.push(...bodyBlocks);
319
- edges.push(...bodyEdges);
320
- return { blocks, edges };
321
- }
322
- /**
323
- * Process a block of statements
438
+ * Build Data Flow Graph
439
+ * @trace TSK-GO-003, REQ-SEC-GO-005
324
440
  */
325
- processBlock(block) {
326
- const blocks = [];
441
+ async buildDFG(astNodes, _astEdges, frameworkModels) {
442
+ const nodes = new Map();
327
443
  const edges = [];
328
- let currentBlock = null;
329
- for (const stmt of block.children) {
330
- if (this.isControlFlowStatement(stmt)) {
331
- // End current block
332
- if (currentBlock && currentBlock.statements.length > 0) {
333
- blocks.push(currentBlock);
334
- }
335
- // Process control flow statement
336
- const cfResult = this.processControlFlow(stmt);
337
- blocks.push(...cfResult.blocks);
338
- edges.push(...cfResult.edges);
339
- // Start new block
340
- currentBlock = null;
341
- }
342
- else {
343
- // Add to current block
344
- if (!currentBlock) {
345
- currentBlock = {
346
- id: `${block.location.file}#block_${this.blockIdCounter++}`,
347
- statements: [],
348
- predecessors: [],
349
- successors: [],
350
- };
351
- }
352
- currentBlock.statements.push(stmt.id);
353
- }
354
- }
355
- // Add final block
356
- if (currentBlock && currentBlock.statements.length > 0) {
357
- blocks.push(currentBlock);
358
- }
359
- // Connect sequential blocks
360
- for (let i = 0; i < blocks.length - 1; i++) {
361
- if (blocks[i].successors.length === 0) {
362
- blocks[i].successors.push(blocks[i + 1].id);
363
- blocks[i + 1].predecessors.push(blocks[i].id);
364
- edges.push({
365
- from: blocks[i].id,
366
- to: blocks[i + 1].id,
367
- type: 'sequential',
368
- });
369
- }
370
- }
371
- return { blocks, edges };
372
- }
373
- /**
374
- * Check if statement is control flow
375
- */
376
- isControlFlowStatement(stmt) {
377
- return [
378
- 'if_statement',
379
- 'for_statement',
380
- 'switch_statement',
381
- 'select_statement',
382
- 'return_statement',
383
- 'go_statement',
384
- 'defer_statement',
385
- ].includes(stmt.type);
386
- }
387
- /**
388
- * Process control flow statement
389
- */
390
- processControlFlow(stmt) {
391
- const blocks = [];
392
- const edges = [];
393
- switch (stmt.type) {
394
- case 'if_statement': {
395
- // Create condition block
396
- const condBlock = {
397
- id: `${stmt.location.file}#if_cond_${this.blockIdCounter++}`,
398
- statements: [stmt.id],
399
- predecessors: [],
400
- successors: [],
401
- };
402
- blocks.push(condBlock);
403
- // Process then branch
404
- const thenBlock = stmt.children.find((c) => c.type === 'block');
405
- if (thenBlock) {
406
- const { blocks: thenBlocks, edges: thenEdges } = this.processBlock(thenBlock);
407
- blocks.push(...thenBlocks);
408
- edges.push(...thenEdges);
409
- if (thenBlocks.length > 0) {
410
- condBlock.successors.push(thenBlocks[0].id);
411
- thenBlocks[0].predecessors.push(condBlock.id);
412
- edges.push({
413
- from: condBlock.id,
414
- to: thenBlocks[0].id,
415
- type: 'conditional',
416
- condition: 'true',
444
+ const sources = [];
445
+ const sinks = [];
446
+ const models = frameworkModels.length > 0 ? frameworkModels : GO_FRAMEWORK_MODELS;
447
+ for (const [_nodeId, astNode] of astNodes) {
448
+ if (!astNode.text)
449
+ continue;
450
+ // Check for sources
451
+ for (const model of models) {
452
+ for (const source of model.sources) {
453
+ if (source.pattern.test(astNode.text)) {
454
+ const dfgId = `dfg_source_${astNode.id}`;
455
+ nodes.set(dfgId, {
456
+ id: dfgId,
457
+ astNodeId: astNode.id,
458
+ nodeType: 'source',
459
+ taintLabel: source.taintLabel,
460
+ expression: astNode.text,
461
+ location: astNode.location,
462
+ properties: { sourceType: source.type, framework: model.name },
417
463
  });
464
+ sources.push(dfgId);
418
465
  }
419
466
  }
420
- // Process else branch
421
- const elseClause = stmt.children.find((c) => c.type === 'block' && c !== thenBlock);
422
- if (elseClause) {
423
- const { blocks: elseBlocks, edges: elseEdges } = this.processBlock(elseClause);
424
- blocks.push(...elseBlocks);
425
- edges.push(...elseEdges);
426
- if (elseBlocks.length > 0) {
427
- condBlock.successors.push(elseBlocks[0].id);
428
- elseBlocks[0].predecessors.push(condBlock.id);
429
- edges.push({
430
- from: condBlock.id,
431
- to: elseBlocks[0].id,
432
- type: 'conditional',
433
- condition: 'false',
467
+ // Check for sinks
468
+ for (const sink of model.sinks) {
469
+ if (sink.pattern.test(astNode.text)) {
470
+ const dfgId = `dfg_sink_${astNode.id}`;
471
+ nodes.set(dfgId, {
472
+ id: dfgId,
473
+ astNodeId: astNode.id,
474
+ nodeType: 'sink',
475
+ expression: astNode.text,
476
+ location: astNode.location,
477
+ properties: {
478
+ sinkType: sink.type,
479
+ vulnerabilityType: sink.vulnerabilityType,
480
+ severity: sink.severity,
481
+ framework: model.name,
482
+ },
434
483
  });
484
+ sinks.push(dfgId);
435
485
  }
436
486
  }
437
- break;
438
- }
439
- case 'for_statement': {
440
- // Create loop header block
441
- const headerBlock = {
442
- id: `${stmt.location.file}#for_header_${this.blockIdCounter++}`,
443
- statements: [stmt.id],
444
- predecessors: [],
445
- successors: [],
446
- };
447
- blocks.push(headerBlock);
448
- // Process loop body
449
- const bodyBlock = stmt.children.find((c) => c.type === 'block');
450
- if (bodyBlock) {
451
- const { blocks: bodyBlocks, edges: bodyEdges } = this.processBlock(bodyBlock);
452
- blocks.push(...bodyBlocks);
453
- edges.push(...bodyEdges);
454
- if (bodyBlocks.length > 0) {
455
- // Entry to body
456
- headerBlock.successors.push(bodyBlocks[0].id);
457
- bodyBlocks[0].predecessors.push(headerBlock.id);
458
- edges.push({
459
- from: headerBlock.id,
460
- to: bodyBlocks[0].id,
461
- type: 'conditional',
462
- condition: 'true',
463
- });
464
- // Back edge
465
- const lastBody = bodyBlocks[bodyBlocks.length - 1];
466
- lastBody.successors.push(headerBlock.id);
467
- headerBlock.predecessors.push(lastBody.id);
468
- edges.push({
469
- from: lastBody.id,
470
- to: headerBlock.id,
471
- type: 'back',
487
+ // Check for sanitizers
488
+ for (const sanitizer of model.sanitizers) {
489
+ if (sanitizer.pattern.test(astNode.text)) {
490
+ const dfgId = `dfg_sanitizer_${astNode.id}`;
491
+ nodes.set(dfgId, {
492
+ id: dfgId,
493
+ astNodeId: astNode.id,
494
+ nodeType: 'sanitizer',
495
+ expression: astNode.text,
496
+ location: astNode.location,
497
+ properties: { sanitizes: sanitizer.sanitizes, framework: model.name },
472
498
  });
473
499
  }
474
500
  }
475
- break;
476
501
  }
477
- case 'return_statement': {
478
- const returnBlock = {
479
- id: `${stmt.location.file}#return_${this.blockIdCounter++}`,
480
- statements: [stmt.id],
481
- predecessors: [],
482
- successors: [], // Will connect to exit
483
- };
484
- blocks.push(returnBlock);
485
- break;
486
- }
487
- default: {
488
- const block = {
489
- id: `${stmt.location.file}#stmt_${this.blockIdCounter++}`,
490
- statements: [stmt.id],
491
- predecessors: [],
492
- successors: [],
493
- };
494
- blocks.push(block);
495
- }
496
- }
497
- return { blocks, edges };
498
- }
499
- /**
500
- * Build data flow graph
501
- */
502
- async buildDFG(ast, cfg) {
503
- const nodes = [];
504
- const edges = [];
505
- const nodeMap = new Map();
506
- // Process all assignments and declarations
507
- const assignments = this.findNodesByType(ast, [
508
- 'short_var_declaration',
509
- 'var_declaration',
510
- 'const_declaration',
511
- 'assignment_statement',
512
- ]);
513
- for (const assign of assignments) {
514
- const dfgNodes = this.createDFGNodesForAssignment(assign, nodeMap);
515
- nodes.push(...dfgNodes);
516
- }
517
- // Process function parameters as sources
518
- const functions = this.findNodesByType(ast, [
519
- 'function_declaration',
520
- 'method_declaration',
521
- ]);
522
- for (const func of functions) {
523
- const paramNodes = this.createDFGNodesForParameters(func, nodeMap);
524
- nodes.push(...paramNodes);
525
- }
526
- // Build edges based on data dependencies
527
- edges.push(...this.buildDFGEdges(nodes, nodeMap));
528
- return {
529
- nodes,
530
- edges,
531
- };
532
- }
533
- /**
534
- * Create DFG nodes for assignment
535
- */
536
- createDFGNodesForAssignment(assign, nodeMap) {
537
- const nodes = [];
538
- // Get left-hand side (defined variables)
539
- const lhs = assign.children.filter((c) => ['identifier', 'expression_list'].includes(c.type));
540
- for (const target of lhs) {
541
- if (target.type === 'identifier') {
542
- const node = {
543
- id: `dfg_${target.id}`,
544
- astNodeId: target.id,
545
- variable: target.text,
502
+ // Handle variable declarations and assignments
503
+ if (astNode.type === 'short_var_declaration' ||
504
+ astNode.type === 'var_declaration' ||
505
+ astNode.type === 'assignment_statement') {
506
+ const dfgId = `dfg_assign_${astNode.id}`;
507
+ nodes.set(dfgId, {
508
+ id: dfgId,
509
+ astNodeId: astNode.id,
510
+ nodeType: 'propagator',
511
+ variable: astNode.properties.left,
546
512
  operation: 'write',
547
- location: target.location,
548
- predecessors: [],
549
- successors: [],
550
- };
551
- nodes.push(node);
552
- nodeMap.set(target.text, node);
513
+ expression: astNode.text,
514
+ location: astNode.location,
515
+ properties: {},
516
+ });
553
517
  }
554
518
  }
555
- return nodes;
519
+ return { nodes, edges, sources, sinks };
556
520
  }
557
521
  /**
558
- * Create DFG nodes for function parameters
522
+ * Build Control Flow Graph
523
+ * @trace TSK-GO-004, REQ-SEC-GO-006
559
524
  */
560
- createDFGNodesForParameters(func, nodeMap) {
561
- const nodes = [];
562
- const params = func.metadata?.parameters;
563
- if (params) {
564
- for (const param of params) {
565
- const node = {
566
- id: `dfg_param_${func.id}_${param.name}`,
567
- astNodeId: func.id,
568
- variable: param.name,
569
- operation: 'param',
570
- location: func.location,
525
+ async buildCFG(astNodes, _astEdges) {
526
+ const blocks = new Map();
527
+ const edges = [];
528
+ const entryBlocks = [];
529
+ const exitBlocks = [];
530
+ // Find functions and build CFG for each
531
+ for (const [_nodeId, astNode] of astNodes) {
532
+ if (astNode.type === 'function_declaration' || astNode.type === 'method_declaration') {
533
+ const blockId = `block_${this.blockIdCounter++}`;
534
+ blocks.set(blockId, {
535
+ id: blockId,
536
+ statements: [astNode.id],
571
537
  predecessors: [],
572
538
  successors: [],
573
- taintLabel: this.inferTaintLabel(param.name, param.type),
574
- };
575
- nodes.push(node);
576
- nodeMap.set(param.name, node);
577
- }
578
- }
579
- return nodes;
580
- }
581
- /**
582
- * Infer taint label from parameter
583
- */
584
- inferTaintLabel(name, type) {
585
- // HTTP request parameters
586
- if (name === 'r' && type.includes('Request'))
587
- return 'user_input';
588
- if (name === 'req' && type.includes('Request'))
589
- return 'user_input';
590
- if (name === 'c' && type.includes('Context'))
591
- return 'user_input'; // Gin context
592
- return undefined;
593
- }
594
- /**
595
- * Build DFG edges
596
- */
597
- buildDFGEdges(nodes, nodeMap) {
598
- const edges = [];
599
- // Connect reads to their definitions
600
- for (const node of nodes) {
601
- if (node.operation === 'read') {
602
- const def = nodeMap.get(node.variable);
603
- if (def && def !== node) {
604
- edges.push({
605
- from: def.id,
606
- to: node.id,
607
- type: 'data',
608
- variable: node.variable,
609
- });
610
- def.successors.push(node.id);
611
- node.predecessors.push(def.id);
612
- }
613
- }
614
- }
615
- return edges;
616
- }
617
- /**
618
- * Build symbol table
619
- */
620
- async buildSymbolTable(ast) {
621
- const global = new Map();
622
- const scopes = [];
623
- // Extract package name
624
- const packageNode = this.findNodesByType(ast, ['package_clause'])[0];
625
- const packageName = packageNode?.children[0]?.text ?? 'main';
626
- // Process imports
627
- const importNodes = this.findNodesByType(ast, ['import_declaration']);
628
- for (const importDecl of importNodes) {
629
- const imports = importDecl.metadata?.imports;
630
- if (imports) {
631
- for (const imp of imports) {
632
- const alias = imp.alias ?? imp.path.split('/').pop() ?? imp.path;
633
- global.set(alias, {
634
- name: alias,
635
- kind: 'import',
636
- type: imp.path,
637
- location: importDecl.location,
638
- scope: 'global',
639
- });
640
- }
641
- }
642
- }
643
- // Process type declarations
644
- const typeDecls = this.findNodesByType(ast, ['type_declaration']);
645
- for (const typeDecl of typeDecls) {
646
- const types = typeDecl.metadata?.types;
647
- if (types) {
648
- for (const t of types) {
649
- global.set(t.name, {
650
- name: t.name,
651
- kind: 'type',
652
- type: t.kind,
653
- location: typeDecl.location,
654
- scope: 'global',
655
- exported: this.isExported(t.name),
656
- });
657
- }
658
- }
659
- }
660
- // Process function declarations
661
- const funcDecls = this.findNodesByType(ast, [
662
- 'function_declaration',
663
- 'method_declaration',
664
- ]);
665
- for (const funcDecl of funcDecls) {
666
- const name = funcDecl.metadata?.name;
667
- const params = funcDecl.metadata?.parameters;
668
- const returnType = funcDecl.metadata?.returnType;
669
- const receiver = funcDecl.metadata?.receiver;
670
- if (name) {
671
- const fullName = receiver ? `${receiver}.${name}` : name;
672
- global.set(fullName, {
673
- name: fullName,
674
- kind: 'function',
675
- type: `func(${params?.map((p) => p.type).join(', ') ?? ''}) ${returnType ?? ''}`,
676
- location: funcDecl.location,
677
- scope: 'global',
678
- exported: this.isExported(name),
679
539
  });
680
- // Create function scope
681
- const funcScope = new Map();
682
- if (params) {
683
- for (const param of params) {
684
- funcScope.set(param.name, {
685
- name: param.name,
686
- kind: 'parameter',
687
- type: param.type,
688
- location: funcDecl.location,
689
- scope: fullName,
690
- });
691
- }
692
- }
693
- scopes.push({ scopeId: fullName, symbols: funcScope });
540
+ entryBlocks.push(blockId);
541
+ exitBlocks.push(blockId);
694
542
  }
695
543
  }
696
- // Process var/const declarations
697
- const varDecls = this.findNodesByType(ast, ['var_declaration', 'const_declaration']);
698
- for (const varDecl of varDecls) {
699
- const specs = varDecl.children.filter((c) => c.type === 'var_spec' || c.type === 'const_spec');
700
- for (const spec of specs) {
701
- const nameNode = spec.children.find((c) => c.type === 'identifier');
702
- const typeNode = spec.children.find((c) => c.type.includes('type'));
703
- if (nameNode) {
704
- global.set(nameNode.text, {
705
- name: nameNode.text,
706
- kind: varDecl.type === 'var_declaration' ? 'variable' : 'constant',
707
- type: typeNode?.text ?? 'unknown',
708
- location: varDecl.location,
709
- scope: 'global',
710
- exported: this.isExported(nameNode.text),
711
- });
712
- }
713
- }
544
+ // Add entry and exit blocks if none found
545
+ if (entryBlocks.length === 0) {
546
+ const entryId = 'entry_block';
547
+ const exitId = 'exit_block';
548
+ blocks.set(entryId, {
549
+ id: entryId,
550
+ statements: [],
551
+ predecessors: [],
552
+ successors: [exitId],
553
+ isEntry: true,
554
+ });
555
+ blocks.set(exitId, {
556
+ id: exitId,
557
+ statements: [],
558
+ predecessors: [entryId],
559
+ successors: [],
560
+ isExit: true,
561
+ });
562
+ entryBlocks.push(entryId);
563
+ exitBlocks.push(exitId);
564
+ edges.push({
565
+ from: entryId,
566
+ to: exitId,
567
+ edgeType: 'normal',
568
+ });
714
569
  }
715
570
  return {
716
- global,
717
- scopes,
718
- packageName,
571
+ blocks,
572
+ edges,
573
+ entryBlocks,
574
+ exitBlocks,
575
+ entry: entryBlocks[0],
576
+ exit: exitBlocks[exitBlocks.length - 1],
719
577
  };
720
578
  }
721
579
  /**
722
- * Check if Go identifier is exported (starts with uppercase)
723
- */
724
- isExported(name) {
725
- return /^[A-Z]/.test(name);
726
- }
727
- /**
728
- * Detect framework usage
580
+ * Extract symbols from AST
581
+ * @trace TSK-GO-005, REQ-SEC-GO-007
729
582
  */
730
- async detectFrameworks(ast, symbols) {
731
- const detected = [];
732
- const imports = new Set();
733
- // Collect all imports
734
- for (const [, entry] of symbols.global) {
735
- if (entry.kind === 'import' && entry.type) {
736
- imports.add(entry.type);
583
+ async extractSymbols(astNodes) {
584
+ const symbols = new Map();
585
+ const functions = new Map();
586
+ const classes = new Map();
587
+ const scopes = new Map();
588
+ // Package scope
589
+ let packageName = 'main';
590
+ for (const [, astNode] of astNodes) {
591
+ if (astNode.type === 'package_clause' && astNode.properties.packageName) {
592
+ packageName = astNode.properties.packageName;
593
+ break;
737
594
  }
738
595
  }
739
- // Check against framework patterns
740
- for (const framework of this.frameworkModels) {
741
- let matched = false;
742
- switch (framework.id) {
743
- case 'go-net-http':
744
- matched = imports.has('net/http');
745
- break;
746
- case 'go-gin':
747
- matched = imports.has('github.com/gin-gonic/gin');
596
+ const packageScope = {
597
+ id: packageName,
598
+ symbols: [],
599
+ kind: 'global',
600
+ };
601
+ scopes.set(packageName, packageScope);
602
+ for (const [_nodeId, astNode] of astNodes) {
603
+ switch (astNode.type) {
604
+ case 'function_declaration': {
605
+ const funcId = `func_${astNode.id}`;
606
+ const name = astNode.properties.name ?? 'anonymous';
607
+ const params = astNode.properties.parameters ?? [];
608
+ const isExp = astNode.properties.isExported ?? false;
609
+ const funcSymbol = {
610
+ name,
611
+ kind: 'function',
612
+ location: astNode.location,
613
+ scopeId: packageName,
614
+ properties: {
615
+ isExported: isExp,
616
+ },
617
+ parameters: params.map((p, i) => ({
618
+ name: p,
619
+ index: i,
620
+ })),
621
+ returnType: astNode.properties.returnType,
622
+ };
623
+ functions.set(funcId, funcSymbol);
624
+ symbols.set(funcId, funcSymbol);
625
+ packageScope.symbols.push(funcId);
748
626
  break;
749
- case 'go-database-sql':
750
- matched = imports.has('database/sql');
627
+ }
628
+ case 'method_declaration': {
629
+ const methodId = `method_${astNode.id}`;
630
+ const name = astNode.properties.name ?? 'anonymous';
631
+ const params = astNode.properties.parameters ?? [];
632
+ const receiver = astNode.properties.receiver;
633
+ const isExp = astNode.properties.isExported ?? false;
634
+ const methodSymbol = {
635
+ name,
636
+ kind: 'method',
637
+ location: astNode.location,
638
+ scopeId: packageName,
639
+ properties: {
640
+ receiver,
641
+ isExported: isExp,
642
+ },
643
+ parameters: params.map((p, i) => ({
644
+ name: p,
645
+ index: i,
646
+ })),
647
+ returnType: astNode.properties.returnType,
648
+ };
649
+ functions.set(methodId, methodSymbol);
650
+ symbols.set(methodId, methodSymbol);
651
+ packageScope.symbols.push(methodId);
751
652
  break;
752
- case 'go-os-exec':
753
- matched = imports.has('os/exec');
653
+ }
654
+ case 'type_declaration': {
655
+ const typeId = `type_${astNode.id}`;
656
+ const name = astNode.properties.name ?? 'Anonymous';
657
+ const underlyingType = astNode.properties.underlyingType;
658
+ const isExp = astNode.properties.isExported ?? false;
659
+ if (underlyingType === 'struct_type') {
660
+ const structSymbol = {
661
+ name,
662
+ kind: 'class',
663
+ location: astNode.location,
664
+ scopeId: packageName,
665
+ methods: [],
666
+ properties: [],
667
+ };
668
+ classes.set(typeId, structSymbol);
669
+ const structAsSymbol = {
670
+ name,
671
+ kind: 'class',
672
+ location: astNode.location,
673
+ scopeId: packageName,
674
+ properties: {
675
+ structType: true,
676
+ isExported: isExp,
677
+ },
678
+ };
679
+ symbols.set(typeId, structAsSymbol);
680
+ }
681
+ else if (underlyingType === 'interface_type') {
682
+ const interfaceSymbol = {
683
+ name,
684
+ kind: 'interface',
685
+ location: astNode.location,
686
+ scopeId: packageName,
687
+ properties: {
688
+ isExported: isExp,
689
+ },
690
+ };
691
+ symbols.set(typeId, interfaceSymbol);
692
+ }
693
+ else {
694
+ // Type alias
695
+ const typeSymbol = {
696
+ name,
697
+ kind: 'type',
698
+ location: astNode.location,
699
+ scopeId: packageName,
700
+ properties: {
701
+ underlyingType,
702
+ isExported: isExp,
703
+ },
704
+ };
705
+ symbols.set(typeId, typeSymbol);
706
+ }
707
+ packageScope.symbols.push(typeId);
754
708
  break;
755
- case 'go-file-io':
756
- matched = imports.has('os') || imports.has('io/ioutil');
709
+ }
710
+ case 'const_declaration':
711
+ case 'var_declaration': {
712
+ const varId = `var_${astNode.id}`;
713
+ const name = astNode.properties.left ?? 'anonymous';
714
+ const isConst = astNode.type === 'const_declaration';
715
+ const varSymbol = {
716
+ name,
717
+ kind: isConst ? 'constant' : 'variable',
718
+ location: astNode.location,
719
+ scopeId: packageName,
720
+ properties: {
721
+ isExported: isExported(name),
722
+ },
723
+ };
724
+ symbols.set(varId, varSymbol);
725
+ packageScope.symbols.push(varId);
757
726
  break;
758
- }
759
- if (matched) {
760
- detected.push(framework);
727
+ }
761
728
  }
762
729
  }
763
- return detected;
764
- }
765
- /**
766
- * Create fallback AST for when tree-sitter is unavailable
767
- */
768
- createFallbackAST(code, filePath) {
769
- const lines = code.split('\n');
770
730
  return {
771
- id: `${filePath}#root`,
772
- type: 'source_file',
773
- text: code,
774
- location: {
775
- file: filePath,
776
- startLine: 1,
777
- endLine: lines.length,
778
- startColumn: 0,
779
- endColumn: lines[lines.length - 1]?.length ?? 0,
780
- },
781
- children: [],
782
- metadata: {
783
- fallback: true,
784
- lineCount: lines.length,
785
- },
731
+ symbols,
732
+ functions,
733
+ classes,
734
+ scopes,
735
+ global: symbols,
786
736
  };
787
737
  }
788
738
  /**
789
- * Find nodes by type(s) in AST
739
+ * Check if identifier is exported (public)
740
+ * Go exports identifiers starting with uppercase
790
741
  */
791
- findNodesByType(ast, types) {
792
- const results = [];
793
- const visit = (node) => {
794
- if (types.includes(node.type)) {
795
- results.push(node);
796
- }
797
- for (const child of node.children) {
798
- visit(child);
799
- }
800
- };
801
- visit(ast);
802
- return results;
742
+ isExported(name) {
743
+ return isExported(name);
803
744
  }
804
745
  }
746
+ /**
747
+ * Create Go extractor instance
748
+ * @trace REQ-SEC-GO-001
749
+ */
750
+ export function createGoExtractor() {
751
+ return new GoExtractor();
752
+ }
805
753
  //# sourceMappingURL=go-extractor.js.map