circle-ir 3.198.0 → 3.200.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4380,6 +4380,10 @@ function extractJavaScriptTypes(tree, cache) {
4380
4380
  for (const cls of classes) {
4381
4381
  types.push(extractJSClassInfo(cls));
4382
4382
  }
4383
+ const interfaces = getNodesFromCache(tree.rootNode, "interface_declaration", cache);
4384
+ for (const iface of interfaces) {
4385
+ types.push(extractTSInterfaceInfo(iface));
4386
+ }
4383
4387
  const functions = getNodesFromCache(tree.rootNode, "function_declaration", cache);
4384
4388
  if (functions.length > 0) {
4385
4389
  const moduleFunctions = [];
@@ -4715,6 +4719,73 @@ function extractSelfAssignments(body2, fields, fieldNames) {
4715
4719
  }
4716
4720
  }
4717
4721
  }
4722
+ function extractTSInterfaceInfo(node) {
4723
+ const name2 = getIdentifier(node, "name") ?? "Anonymous";
4724
+ const parents = [];
4725
+ for (let i2 = 0; i2 < node.childCount; i2++) {
4726
+ const child = node.child(i2);
4727
+ if (!child || child.type !== "extends_type_clause") continue;
4728
+ for (let j = 0; j < child.childCount; j++) {
4729
+ const gc = child.child(j);
4730
+ if (!gc) continue;
4731
+ if (gc.type === "type_identifier" || gc.type === "generic_type" || gc.type === "nested_type_identifier") {
4732
+ parents.push(getNodeText(gc));
4733
+ }
4734
+ }
4735
+ }
4736
+ const body2 = node.childForFieldName("body");
4737
+ const methods = [];
4738
+ const fields = [];
4739
+ if (body2) {
4740
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4741
+ const member = body2.child(i2);
4742
+ if (!member) continue;
4743
+ if (member.type === "method_signature") {
4744
+ const methodName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4745
+ const params = member.childForFieldName("parameters");
4746
+ const returnType = member.childForFieldName("return_type");
4747
+ methods.push({
4748
+ name: methodName,
4749
+ return_type: returnType ? getNodeText(returnType).replace(/^:\s*/, "") : null,
4750
+ parameters: params ? extractJSParameters(params) : [],
4751
+ annotations: [],
4752
+ // An interface member has no body and therefore no modifiers in the
4753
+ // class sense; `readonly` / `?` live on property signatures.
4754
+ modifiers: [],
4755
+ start_line: member.startPosition.row + 1,
4756
+ end_line: member.endPosition.row + 1
4757
+ });
4758
+ continue;
4759
+ }
4760
+ if (member.type === "property_signature") {
4761
+ const fieldName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4762
+ const typeNode = member.childForFieldName("type");
4763
+ const modifiers = [];
4764
+ const text = getNodeText(member);
4765
+ if (/^\s*readonly\b/.test(text)) modifiers.push("readonly");
4766
+ if (/\?\s*:/.test(text)) modifiers.push("optional");
4767
+ fields.push({
4768
+ name: fieldName,
4769
+ type: typeNode ? getNodeText(typeNode).replace(/^:\s*/, "") : null,
4770
+ modifiers,
4771
+ annotations: []
4772
+ });
4773
+ }
4774
+ }
4775
+ }
4776
+ return {
4777
+ name: name2,
4778
+ kind: "interface",
4779
+ package: null,
4780
+ extends: parents[0] ?? null,
4781
+ implements: parents.slice(1),
4782
+ annotations: [],
4783
+ methods,
4784
+ fields,
4785
+ start_line: node.startPosition.row + 1,
4786
+ end_line: node.endPosition.row + 1
4787
+ };
4788
+ }
4718
4789
  function extractJSClassInfo(node) {
4719
4790
  const name2 = getIdentifier(node, "name") ?? "Anonymous";
4720
4791
  let extendsType = null;
@@ -11011,6 +11082,26 @@ var DEFAULT_SOURCES = [
11011
11082
  { annotation: "FormParam", type: "http_param", severity: "high", param_tainted: true },
11012
11083
  { annotation: "PathParam", type: "http_path", severity: "medium", param_tainted: true },
11013
11084
  { annotation: "HeaderParam", type: "http_header", severity: "high", param_tainted: true },
11085
+ // JAX-RS params the block was missing (jakarta.ws.rs). Java-scoped — the
11086
+ // JS decorator ecosystem has its own distinct names and should not match.
11087
+ { annotation: "CookieParam", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
11088
+ { annotation: "MatrixParam", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11089
+ { annotation: "BeanParam", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
11090
+ // Micronaut (io.micronaut.http.annotation). @Body / @Header / @CookieValue /
11091
+ // @PathVariable reuse Spring/JAX-RS names already covered above; these three
11092
+ // are Micronaut-specific. Java-scoped.
11093
+ { annotation: "QueryValue", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11094
+ { annotation: "Part", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
11095
+ { annotation: "RequestBean", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
11096
+ // Quarkus / RESTEasy Reactive (org.jboss.resteasy.reactive) — the `@Rest*`
11097
+ // shortcut annotations that infer the binding name from the parameter name.
11098
+ // Java-scoped.
11099
+ { annotation: "RestQuery", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11100
+ { annotation: "RestPath", type: "http_path", severity: "medium", param_tainted: true, languages: ["java"] },
11101
+ { annotation: "RestHeader", type: "http_header", severity: "high", param_tainted: true, languages: ["java"] },
11102
+ { annotation: "RestForm", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11103
+ { annotation: "RestCookie", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
11104
+ { annotation: "RestMatrix", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11014
11105
  // Jenkins data-binding: every parameter of an annotated constructor is wired
11015
11106
  // from form/JSON user input at construction time. Method-level annotation —
11016
11107
  // all params tainted.
@@ -12689,6 +12780,25 @@ var DEFAULT_SINKS = [
12689
12780
  { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12690
12781
  { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12691
12782
  { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12783
+ // TypeORM QueryBuilder raw fragments (CWE-89). TypeORM's raw `.query(sql)`
12784
+ // escape hatch is already covered by the classless `query` sink in the JS
12785
+ // plugin (getBuiltinSinks), so it is NOT re-registered here — the dedup
12786
+ // would discard the duplicate anyway. What was uncovered is the
12787
+ // QueryBuilder fragment API: `andWhere`/`orWhere`/`andHaving`/`orHaving`
12788
+ // take raw SQL strings. These names are TypeORM-idiomatic, so classless +
12789
+ // language-scoped is safe and recovers the dominant chained-builder shape
12790
+ // (`repo.createQueryBuilder('u').andWhere(raw)`) that class matching cannot
12791
+ // reach — the receiver of `.andWhere(...)` is the factory call, not a
12792
+ // class-named variable. Generic `where`/`having` are deliberately NOT added
12793
+ // (too generic for classless; a `*QueryBuilder`-scoped entry would be
12794
+ // near-dead since receivers are rarely named that). Precision is inherent
12795
+ // to the flow: parameterised use (`:id` placeholders + a params object)
12796
+ // keeps input out of the string literal, so a finding fires only on
12797
+ // concatenation.
12798
+ { method: "andWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12799
+ { method: "orWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12800
+ { method: "andHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12801
+ { method: "orHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12692
12802
  // Browser DOM XSS sinks
12693
12803
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
12694
12804
  // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
@@ -12890,6 +13000,12 @@ var DEFAULT_SINKS = [
12890
13000
  { method: "executemany", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12891
13001
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12892
13002
  { method: "extra", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["python"] },
13003
+ // Django RawSQL expression (django.db.models.expressions.RawSQL) — the raw
13004
+ // SQL fragment in arg[0] is embedded verbatim into the query when the
13005
+ // annotate/filter/order_by containing it runs. Distinctive constructor name,
13006
+ // so classless + python-scoped is safe. Parameterised use passes user input
13007
+ // through arg[1] (`RawSQL("... %s", [uid])`), which keeps it out of arg[0].
13008
+ { method: "RawSQL", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12893
13009
  // Python Path Traversal
12894
13010
  // Language-scoped: classless `open` collides with Java I/O / JS DOM.
12895
13011
  { method: "open", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
@@ -4426,6 +4426,10 @@ function extractJavaScriptTypes(tree, cache) {
4426
4426
  for (const cls of classes) {
4427
4427
  types.push(extractJSClassInfo(cls));
4428
4428
  }
4429
+ const interfaces = getNodesFromCache(tree.rootNode, "interface_declaration", cache);
4430
+ for (const iface of interfaces) {
4431
+ types.push(extractTSInterfaceInfo(iface));
4432
+ }
4429
4433
  const functions = getNodesFromCache(tree.rootNode, "function_declaration", cache);
4430
4434
  if (functions.length > 0) {
4431
4435
  const moduleFunctions = [];
@@ -4761,6 +4765,73 @@ function extractSelfAssignments(body2, fields, fieldNames) {
4761
4765
  }
4762
4766
  }
4763
4767
  }
4768
+ function extractTSInterfaceInfo(node) {
4769
+ const name2 = getIdentifier(node, "name") ?? "Anonymous";
4770
+ const parents = [];
4771
+ for (let i2 = 0; i2 < node.childCount; i2++) {
4772
+ const child = node.child(i2);
4773
+ if (!child || child.type !== "extends_type_clause") continue;
4774
+ for (let j = 0; j < child.childCount; j++) {
4775
+ const gc = child.child(j);
4776
+ if (!gc) continue;
4777
+ if (gc.type === "type_identifier" || gc.type === "generic_type" || gc.type === "nested_type_identifier") {
4778
+ parents.push(getNodeText(gc));
4779
+ }
4780
+ }
4781
+ }
4782
+ const body2 = node.childForFieldName("body");
4783
+ const methods = [];
4784
+ const fields = [];
4785
+ if (body2) {
4786
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4787
+ const member = body2.child(i2);
4788
+ if (!member) continue;
4789
+ if (member.type === "method_signature") {
4790
+ const methodName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4791
+ const params = member.childForFieldName("parameters");
4792
+ const returnType = member.childForFieldName("return_type");
4793
+ methods.push({
4794
+ name: methodName,
4795
+ return_type: returnType ? getNodeText(returnType).replace(/^:\s*/, "") : null,
4796
+ parameters: params ? extractJSParameters(params) : [],
4797
+ annotations: [],
4798
+ // An interface member has no body and therefore no modifiers in the
4799
+ // class sense; `readonly` / `?` live on property signatures.
4800
+ modifiers: [],
4801
+ start_line: member.startPosition.row + 1,
4802
+ end_line: member.endPosition.row + 1
4803
+ });
4804
+ continue;
4805
+ }
4806
+ if (member.type === "property_signature") {
4807
+ const fieldName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4808
+ const typeNode = member.childForFieldName("type");
4809
+ const modifiers = [];
4810
+ const text = getNodeText(member);
4811
+ if (/^\s*readonly\b/.test(text)) modifiers.push("readonly");
4812
+ if (/\?\s*:/.test(text)) modifiers.push("optional");
4813
+ fields.push({
4814
+ name: fieldName,
4815
+ type: typeNode ? getNodeText(typeNode).replace(/^:\s*/, "") : null,
4816
+ modifiers,
4817
+ annotations: []
4818
+ });
4819
+ }
4820
+ }
4821
+ }
4822
+ return {
4823
+ name: name2,
4824
+ kind: "interface",
4825
+ package: null,
4826
+ extends: parents[0] ?? null,
4827
+ implements: parents.slice(1),
4828
+ annotations: [],
4829
+ methods,
4830
+ fields,
4831
+ start_line: node.startPosition.row + 1,
4832
+ end_line: node.endPosition.row + 1
4833
+ };
4834
+ }
4764
4835
  function extractJSClassInfo(node) {
4765
4836
  const name2 = getIdentifier(node, "name") ?? "Anonymous";
4766
4837
  let extendsType = null;
@@ -10406,6 +10477,26 @@ var DEFAULT_SOURCES = [
10406
10477
  { annotation: "FormParam", type: "http_param", severity: "high", param_tainted: true },
10407
10478
  { annotation: "PathParam", type: "http_path", severity: "medium", param_tainted: true },
10408
10479
  { annotation: "HeaderParam", type: "http_header", severity: "high", param_tainted: true },
10480
+ // JAX-RS params the block was missing (jakarta.ws.rs). Java-scoped — the
10481
+ // JS decorator ecosystem has its own distinct names and should not match.
10482
+ { annotation: "CookieParam", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
10483
+ { annotation: "MatrixParam", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10484
+ { annotation: "BeanParam", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10485
+ // Micronaut (io.micronaut.http.annotation). @Body / @Header / @CookieValue /
10486
+ // @PathVariable reuse Spring/JAX-RS names already covered above; these three
10487
+ // are Micronaut-specific. Java-scoped.
10488
+ { annotation: "QueryValue", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10489
+ { annotation: "Part", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10490
+ { annotation: "RequestBean", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10491
+ // Quarkus / RESTEasy Reactive (org.jboss.resteasy.reactive) — the `@Rest*`
10492
+ // shortcut annotations that infer the binding name from the parameter name.
10493
+ // Java-scoped.
10494
+ { annotation: "RestQuery", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10495
+ { annotation: "RestPath", type: "http_path", severity: "medium", param_tainted: true, languages: ["java"] },
10496
+ { annotation: "RestHeader", type: "http_header", severity: "high", param_tainted: true, languages: ["java"] },
10497
+ { annotation: "RestForm", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10498
+ { annotation: "RestCookie", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
10499
+ { annotation: "RestMatrix", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10409
10500
  // Jenkins data-binding: every parameter of an annotated constructor is wired
10410
10501
  // from form/JSON user input at construction time. Method-level annotation —
10411
10502
  // all params tainted.
@@ -12084,6 +12175,25 @@ var DEFAULT_SINKS = [
12084
12175
  { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12085
12176
  { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12086
12177
  { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12178
+ // TypeORM QueryBuilder raw fragments (CWE-89). TypeORM's raw `.query(sql)`
12179
+ // escape hatch is already covered by the classless `query` sink in the JS
12180
+ // plugin (getBuiltinSinks), so it is NOT re-registered here — the dedup
12181
+ // would discard the duplicate anyway. What was uncovered is the
12182
+ // QueryBuilder fragment API: `andWhere`/`orWhere`/`andHaving`/`orHaving`
12183
+ // take raw SQL strings. These names are TypeORM-idiomatic, so classless +
12184
+ // language-scoped is safe and recovers the dominant chained-builder shape
12185
+ // (`repo.createQueryBuilder('u').andWhere(raw)`) that class matching cannot
12186
+ // reach — the receiver of `.andWhere(...)` is the factory call, not a
12187
+ // class-named variable. Generic `where`/`having` are deliberately NOT added
12188
+ // (too generic for classless; a `*QueryBuilder`-scoped entry would be
12189
+ // near-dead since receivers are rarely named that). Precision is inherent
12190
+ // to the flow: parameterised use (`:id` placeholders + a params object)
12191
+ // keeps input out of the string literal, so a finding fires only on
12192
+ // concatenation.
12193
+ { method: "andWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12194
+ { method: "orWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12195
+ { method: "andHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12196
+ { method: "orHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12087
12197
  // Browser DOM XSS sinks
12088
12198
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
12089
12199
  // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
@@ -12285,6 +12395,12 @@ var DEFAULT_SINKS = [
12285
12395
  { method: "executemany", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12286
12396
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12287
12397
  { method: "extra", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["python"] },
12398
+ // Django RawSQL expression (django.db.models.expressions.RawSQL) — the raw
12399
+ // SQL fragment in arg[0] is embedded verbatim into the query when the
12400
+ // annotate/filter/order_by containing it runs. Distinctive constructor name,
12401
+ // so classless + python-scoped is safe. Parameterised use passes user input
12402
+ // through arg[1] (`RawSQL("... %s", [uid])`), which keeps it out of arg[0].
12403
+ { method: "RawSQL", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12288
12404
  // Python Path Traversal
12289
12405
  // Language-scoped: classless `open` collides with Java I/O / JS DOM.
12290
12406
  { method: "open", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
@@ -4360,6 +4360,10 @@ function extractJavaScriptTypes(tree, cache) {
4360
4360
  for (const cls of classes) {
4361
4361
  types.push(extractJSClassInfo(cls));
4362
4362
  }
4363
+ const interfaces = getNodesFromCache(tree.rootNode, "interface_declaration", cache);
4364
+ for (const iface of interfaces) {
4365
+ types.push(extractTSInterfaceInfo(iface));
4366
+ }
4363
4367
  const functions = getNodesFromCache(tree.rootNode, "function_declaration", cache);
4364
4368
  if (functions.length > 0) {
4365
4369
  const moduleFunctions = [];
@@ -4695,6 +4699,73 @@ function extractSelfAssignments(body2, fields, fieldNames) {
4695
4699
  }
4696
4700
  }
4697
4701
  }
4702
+ function extractTSInterfaceInfo(node) {
4703
+ const name2 = getIdentifier(node, "name") ?? "Anonymous";
4704
+ const parents = [];
4705
+ for (let i2 = 0; i2 < node.childCount; i2++) {
4706
+ const child = node.child(i2);
4707
+ if (!child || child.type !== "extends_type_clause") continue;
4708
+ for (let j = 0; j < child.childCount; j++) {
4709
+ const gc = child.child(j);
4710
+ if (!gc) continue;
4711
+ if (gc.type === "type_identifier" || gc.type === "generic_type" || gc.type === "nested_type_identifier") {
4712
+ parents.push(getNodeText(gc));
4713
+ }
4714
+ }
4715
+ }
4716
+ const body2 = node.childForFieldName("body");
4717
+ const methods = [];
4718
+ const fields = [];
4719
+ if (body2) {
4720
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4721
+ const member = body2.child(i2);
4722
+ if (!member) continue;
4723
+ if (member.type === "method_signature") {
4724
+ const methodName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4725
+ const params = member.childForFieldName("parameters");
4726
+ const returnType = member.childForFieldName("return_type");
4727
+ methods.push({
4728
+ name: methodName,
4729
+ return_type: returnType ? getNodeText(returnType).replace(/^:\s*/, "") : null,
4730
+ parameters: params ? extractJSParameters(params) : [],
4731
+ annotations: [],
4732
+ // An interface member has no body and therefore no modifiers in the
4733
+ // class sense; `readonly` / `?` live on property signatures.
4734
+ modifiers: [],
4735
+ start_line: member.startPosition.row + 1,
4736
+ end_line: member.endPosition.row + 1
4737
+ });
4738
+ continue;
4739
+ }
4740
+ if (member.type === "property_signature") {
4741
+ const fieldName = getIdentifier(member, "name") ?? getNodeText(member.child(0) ?? member);
4742
+ const typeNode = member.childForFieldName("type");
4743
+ const modifiers = [];
4744
+ const text = getNodeText(member);
4745
+ if (/^\s*readonly\b/.test(text)) modifiers.push("readonly");
4746
+ if (/\?\s*:/.test(text)) modifiers.push("optional");
4747
+ fields.push({
4748
+ name: fieldName,
4749
+ type: typeNode ? getNodeText(typeNode).replace(/^:\s*/, "") : null,
4750
+ modifiers,
4751
+ annotations: []
4752
+ });
4753
+ }
4754
+ }
4755
+ }
4756
+ return {
4757
+ name: name2,
4758
+ kind: "interface",
4759
+ package: null,
4760
+ extends: parents[0] ?? null,
4761
+ implements: parents.slice(1),
4762
+ annotations: [],
4763
+ methods,
4764
+ fields,
4765
+ start_line: node.startPosition.row + 1,
4766
+ end_line: node.endPosition.row + 1
4767
+ };
4768
+ }
4698
4769
  function extractJSClassInfo(node) {
4699
4770
  const name2 = getIdentifier(node, "name") ?? "Anonymous";
4700
4771
  let extendsType = null;
@@ -10340,6 +10411,26 @@ var DEFAULT_SOURCES = [
10340
10411
  { annotation: "FormParam", type: "http_param", severity: "high", param_tainted: true },
10341
10412
  { annotation: "PathParam", type: "http_path", severity: "medium", param_tainted: true },
10342
10413
  { annotation: "HeaderParam", type: "http_header", severity: "high", param_tainted: true },
10414
+ // JAX-RS params the block was missing (jakarta.ws.rs). Java-scoped — the
10415
+ // JS decorator ecosystem has its own distinct names and should not match.
10416
+ { annotation: "CookieParam", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
10417
+ { annotation: "MatrixParam", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10418
+ { annotation: "BeanParam", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10419
+ // Micronaut (io.micronaut.http.annotation). @Body / @Header / @CookieValue /
10420
+ // @PathVariable reuse Spring/JAX-RS names already covered above; these three
10421
+ // are Micronaut-specific. Java-scoped.
10422
+ { annotation: "QueryValue", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10423
+ { annotation: "Part", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10424
+ { annotation: "RequestBean", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
10425
+ // Quarkus / RESTEasy Reactive (org.jboss.resteasy.reactive) — the `@Rest*`
10426
+ // shortcut annotations that infer the binding name from the parameter name.
10427
+ // Java-scoped.
10428
+ { annotation: "RestQuery", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10429
+ { annotation: "RestPath", type: "http_path", severity: "medium", param_tainted: true, languages: ["java"] },
10430
+ { annotation: "RestHeader", type: "http_header", severity: "high", param_tainted: true, languages: ["java"] },
10431
+ { annotation: "RestForm", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10432
+ { annotation: "RestCookie", type: "http_cookie", severity: "high", param_tainted: true, languages: ["java"] },
10433
+ { annotation: "RestMatrix", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
10343
10434
  // Jenkins data-binding: every parameter of an annotated constructor is wired
10344
10435
  // from form/JSON user input at construction time. Method-level annotation —
10345
10436
  // all params tainted.
@@ -12018,6 +12109,25 @@ var DEFAULT_SINKS = [
12018
12109
  { method: "each", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12019
12110
  { method: "get", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12020
12111
  { method: "exec", class: "Connection", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"], allow_unresolved_receiver: true },
12112
+ // TypeORM QueryBuilder raw fragments (CWE-89). TypeORM's raw `.query(sql)`
12113
+ // escape hatch is already covered by the classless `query` sink in the JS
12114
+ // plugin (getBuiltinSinks), so it is NOT re-registered here — the dedup
12115
+ // would discard the duplicate anyway. What was uncovered is the
12116
+ // QueryBuilder fragment API: `andWhere`/`orWhere`/`andHaving`/`orHaving`
12117
+ // take raw SQL strings. These names are TypeORM-idiomatic, so classless +
12118
+ // language-scoped is safe and recovers the dominant chained-builder shape
12119
+ // (`repo.createQueryBuilder('u').andWhere(raw)`) that class matching cannot
12120
+ // reach — the receiver of `.andWhere(...)` is the factory call, not a
12121
+ // class-named variable. Generic `where`/`having` are deliberately NOT added
12122
+ // (too generic for classless; a `*QueryBuilder`-scoped entry would be
12123
+ // near-dead since receivers are rarely named that). Precision is inherent
12124
+ // to the flow: parameterised use (`:id` placeholders + a params object)
12125
+ // keeps input out of the string literal, so a finding fires only on
12126
+ // concatenation.
12127
+ { method: "andWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12128
+ { method: "orWhere", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12129
+ { method: "andHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12130
+ { method: "orHaving", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12021
12131
  // Browser DOM XSS sinks
12022
12132
  { method: "setAttribute", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
12023
12133
  // Angular DomSanitizer.bypassSecurityTrust* — CWE-79 (#184 Sprint 55).
@@ -12219,6 +12329,12 @@ var DEFAULT_SINKS = [
12219
12329
  { method: "executemany", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12220
12330
  { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12221
12331
  { method: "extra", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["python"] },
12332
+ // Django RawSQL expression (django.db.models.expressions.RawSQL) — the raw
12333
+ // SQL fragment in arg[0] is embedded verbatim into the query when the
12334
+ // annotate/filter/order_by containing it runs. Distinctive constructor name,
12335
+ // so classless + python-scoped is safe. Parameterised use passes user input
12336
+ // through arg[1] (`RawSQL("... %s", [uid])`), which keeps it out of arg[0].
12337
+ { method: "RawSQL", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["python"] },
12222
12338
  // Python Path Traversal
12223
12339
  // Language-scoped: classless `open` collides with Java I/O / JS DOM.
12224
12340
  { method: "open", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
@@ -100,6 +100,12 @@ function extractJavaScriptTypes(tree, cache) {
100
100
  for (const cls of classes) {
101
101
  types.push(extractJSClassInfo(cls));
102
102
  }
103
+ // Extract TypeScript interfaces. Only the TS/TSX grammars produce
104
+ // `interface_declaration`, so this is a no-op for plain JavaScript.
105
+ const interfaces = getNodesFromCache(tree.rootNode, 'interface_declaration', cache);
106
+ for (const iface of interfaces) {
107
+ types.push(extractTSInterfaceInfo(iface));
108
+ }
103
109
  // Extract standalone functions as a module-like type
104
110
  const functions = getNodesFromCache(tree.rootNode, 'function_declaration', cache);
105
111
  if (functions.length > 0) {
@@ -516,6 +522,97 @@ function extractSelfAssignments(body, fields, fieldNames) {
516
522
  /**
517
523
  * Extract JavaScript class information.
518
524
  */
525
+ /**
526
+ * Extract a TypeScript `interface_declaration` as a `TypeInfo`.
527
+ *
528
+ * The TS grammar produces `interface_declaration` → `interface_body`, whose
529
+ * members are `property_signature` (fields, including function-typed ones
530
+ * such as `onEvent: (e: string) => void`) and `method_signature` (methods).
531
+ * Java interfaces are handled separately by `extractInterfaceInfo`, whose body
532
+ * shape is different — the two cannot share an implementation.
533
+ *
534
+ * Needed because a type contract declared as an interface was previously
535
+ * dropped entirely: `interface UserRepo` alongside
536
+ * `class SqlUserRepo implements UserRepo` yielded one `TypeInfo`, the class.
537
+ * Cross-instance taint analysis (Issue #1) needs the declared contract to
538
+ * resolve a field's type when the declaration site is an interface.
539
+ */
540
+ function extractTSInterfaceInfo(node) {
541
+ const name = getIdentifier(node, 'name') ?? 'Anonymous';
542
+ // `interface A extends B, C` — tree-sitter-typescript wraps these in an
543
+ // `extends_type_clause`. `TypeInfo.extends` is a single string, so the first
544
+ // parent goes there and the rest land in `implements`, which mirrors how a
545
+ // consumer reads "the contracts this type carries".
546
+ const parents = [];
547
+ for (let i = 0; i < node.childCount; i++) {
548
+ const child = node.child(i);
549
+ if (!child || child.type !== 'extends_type_clause')
550
+ continue;
551
+ for (let j = 0; j < child.childCount; j++) {
552
+ const gc = child.child(j);
553
+ if (!gc)
554
+ continue;
555
+ if (gc.type === 'type_identifier' || gc.type === 'generic_type' || gc.type === 'nested_type_identifier') {
556
+ parents.push(getNodeText(gc));
557
+ }
558
+ }
559
+ }
560
+ const body = node.childForFieldName('body');
561
+ const methods = [];
562
+ const fields = [];
563
+ if (body) {
564
+ for (let i = 0; i < body.childCount; i++) {
565
+ const member = body.child(i);
566
+ if (!member)
567
+ continue;
568
+ if (member.type === 'method_signature') {
569
+ const methodName = getIdentifier(member, 'name') ?? getNodeText(member.child(0) ?? member);
570
+ const params = member.childForFieldName('parameters');
571
+ const returnType = member.childForFieldName('return_type');
572
+ methods.push({
573
+ name: methodName,
574
+ return_type: returnType ? getNodeText(returnType).replace(/^:\s*/, '') : null,
575
+ parameters: params ? extractJSParameters(params) : [],
576
+ annotations: [],
577
+ // An interface member has no body and therefore no modifiers in the
578
+ // class sense; `readonly` / `?` live on property signatures.
579
+ modifiers: [],
580
+ start_line: member.startPosition.row + 1,
581
+ end_line: member.endPosition.row + 1,
582
+ });
583
+ continue;
584
+ }
585
+ if (member.type === 'property_signature') {
586
+ const fieldName = getIdentifier(member, 'name') ?? getNodeText(member.child(0) ?? member);
587
+ const typeNode = member.childForFieldName('type');
588
+ const modifiers = [];
589
+ const text = getNodeText(member);
590
+ if (/^\s*readonly\b/.test(text))
591
+ modifiers.push('readonly');
592
+ if (/\?\s*:/.test(text))
593
+ modifiers.push('optional');
594
+ fields.push({
595
+ name: fieldName,
596
+ type: typeNode ? getNodeText(typeNode).replace(/^:\s*/, '') : null,
597
+ modifiers,
598
+ annotations: [],
599
+ });
600
+ }
601
+ }
602
+ }
603
+ return {
604
+ name,
605
+ kind: 'interface',
606
+ package: null,
607
+ extends: parents[0] ?? null,
608
+ implements: parents.slice(1),
609
+ annotations: [],
610
+ methods,
611
+ fields,
612
+ start_line: node.startPosition.row + 1,
613
+ end_line: node.endPosition.row + 1,
614
+ };
615
+ }
519
616
  function extractJSClassInfo(node) {
520
617
  const name = getIdentifier(node, 'name') ?? 'Anonymous';
521
618
  // Extract superclass (extends)