circle-ir 3.148.0 → 3.150.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.
@@ -30732,7 +30732,7 @@ var SinkSemanticsPass = class {
30732
30732
  return { droppedCount: 0, registrySize: 0 };
30733
30733
  }
30734
30734
  const registry = buildRegistry(entries);
30735
- const sinks = graph.ir.taint.sinks;
30735
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
30736
30736
  let droppedCount = 0;
30737
30737
  const kept = sinks.filter((sink) => {
30738
30738
  if (!sink.class || !sink.method) return true;
@@ -30753,6 +30753,191 @@ var SinkSemanticsPass = class {
30753
30753
  }
30754
30754
  };
30755
30755
 
30756
+ // src/analysis/passes/cli-main-reflection-suppress-pass.ts
30757
+ var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
30758
+ "forName",
30759
+ "newInstance",
30760
+ "invoke",
30761
+ "getMethod",
30762
+ "getDeclaredMethod",
30763
+ "getConstructor",
30764
+ "getDeclaredConstructor",
30765
+ "loadClass",
30766
+ "defineClass"
30767
+ ]);
30768
+ var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
30769
+ // Spring MVC
30770
+ "RestController",
30771
+ "Controller",
30772
+ // Spring stereotype beans
30773
+ "Service",
30774
+ "Repository",
30775
+ "Component",
30776
+ // JAX-RS resource class
30777
+ "Path",
30778
+ // Servlet 3.0 annotation-based servlet
30779
+ "WebServlet",
30780
+ // JSR-356 WebSocket endpoint
30781
+ "ServerEndpoint",
30782
+ // Declarative HTTP client
30783
+ "FeignClient"
30784
+ ]);
30785
+ var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
30786
+ // Spring MVC
30787
+ "RequestMapping",
30788
+ "GetMapping",
30789
+ "PostMapping",
30790
+ "PutMapping",
30791
+ "DeleteMapping",
30792
+ "PatchMapping",
30793
+ // Spring messaging / WebSocket
30794
+ "MessageMapping",
30795
+ "SubscribeMapping",
30796
+ // Spring messaging listeners
30797
+ "KafkaListener",
30798
+ "KafkaHandler",
30799
+ "RabbitListener",
30800
+ "RabbitHandler",
30801
+ "JmsListener",
30802
+ "StreamListener",
30803
+ // Spring Cloud AWS
30804
+ "SqsListener",
30805
+ "SqsHandler",
30806
+ // Spring application events
30807
+ "EventListener",
30808
+ // CRON / scheduled
30809
+ "Scheduled",
30810
+ // JAX-RS
30811
+ "Path",
30812
+ "GET",
30813
+ "POST",
30814
+ "PUT",
30815
+ "DELETE",
30816
+ "PATCH",
30817
+ "HEAD",
30818
+ "OPTIONS",
30819
+ // Jenkins Stapler form-binding
30820
+ "DataBoundConstructor",
30821
+ "DataBoundSetter"
30822
+ ]);
30823
+ var TIER_1_SUPERTYPES = /* @__PURE__ */ new Set([
30824
+ "HttpServlet",
30825
+ "GenericServlet",
30826
+ "Filter",
30827
+ "HandlerInterceptor",
30828
+ "AsyncHandlerInterceptor",
30829
+ "CommandLineRunner",
30830
+ "ApplicationRunner",
30831
+ "SimpleChannelInboundHandler",
30832
+ "ChannelInboundHandler",
30833
+ "ChannelInboundHandlerAdapter",
30834
+ "ChannelDuplexHandler",
30835
+ "NettyRequestProcessor",
30836
+ "Converter",
30837
+ "SingleValueConverter",
30838
+ "ConverterMatcher",
30839
+ "AbstractReflectionConverter",
30840
+ "AbstractSingleValueConverter",
30841
+ "AbstractCollectionConverter"
30842
+ ]);
30843
+ function normalizeAnnotation(raw) {
30844
+ let s = raw.trim();
30845
+ if (s.startsWith("@")) s = s.slice(1);
30846
+ const parenIdx = s.indexOf("(");
30847
+ if (parenIdx >= 0) s = s.slice(0, parenIdx);
30848
+ const genericIdx = s.indexOf("<");
30849
+ if (genericIdx >= 0) s = s.slice(0, genericIdx);
30850
+ const dotIdx = s.lastIndexOf(".");
30851
+ if (dotIdx >= 0) s = s.slice(dotIdx + 1);
30852
+ return s.trim();
30853
+ }
30854
+ function normalizeSupertype(raw) {
30855
+ let s = raw.trim();
30856
+ const genericIdx = s.indexOf("<");
30857
+ if (genericIdx >= 0) s = s.slice(0, genericIdx);
30858
+ const dotIdx = s.lastIndexOf(".");
30859
+ if (dotIdx >= 0) s = s.slice(dotIdx + 1);
30860
+ return s.trim();
30861
+ }
30862
+ function isMainMethod(name2, paramTypes) {
30863
+ if (name2 !== "main") return false;
30864
+ if (paramTypes.length !== 1) return false;
30865
+ const t = paramTypes[0];
30866
+ if (!t) return false;
30867
+ const bare = t.replace(/\s+/g, "");
30868
+ return bare === "String[]" || bare === "java.lang.String[]";
30869
+ }
30870
+ var CliMainReflectionSuppressPass = class {
30871
+ name = "cli-main-reflection-suppress";
30872
+ category = "security";
30873
+ run(ctx) {
30874
+ const { graph, language } = ctx;
30875
+ if (language !== "java") {
30876
+ return { cliMainSignal: false, droppedCount: 0 };
30877
+ }
30878
+ const types = graph.ir.types;
30879
+ if (!types || types.length === 0) {
30880
+ return { cliMainSignal: false, droppedCount: 0 };
30881
+ }
30882
+ let hasMain = false;
30883
+ let hasFrameworkSignal = false;
30884
+ for (const type of types) {
30885
+ for (const ann of type.annotations) {
30886
+ if (TIER_1_CLASS_ANNOTATIONS.has(normalizeAnnotation(ann))) {
30887
+ hasFrameworkSignal = true;
30888
+ break;
30889
+ }
30890
+ }
30891
+ if (hasFrameworkSignal) break;
30892
+ if (type.extends && TIER_1_SUPERTYPES.has(normalizeSupertype(type.extends))) {
30893
+ hasFrameworkSignal = true;
30894
+ break;
30895
+ }
30896
+ for (const impl of type.implements) {
30897
+ if (TIER_1_SUPERTYPES.has(normalizeSupertype(impl))) {
30898
+ hasFrameworkSignal = true;
30899
+ break;
30900
+ }
30901
+ }
30902
+ if (hasFrameworkSignal) break;
30903
+ for (const method of type.methods) {
30904
+ for (const ann of method.annotations) {
30905
+ if (TIER_1_METHOD_ANNOTATIONS.has(normalizeAnnotation(ann))) {
30906
+ hasFrameworkSignal = true;
30907
+ break;
30908
+ }
30909
+ }
30910
+ if (hasFrameworkSignal) break;
30911
+ if (!hasMain) {
30912
+ const paramTypes = method.parameters.map((p) => p.type);
30913
+ if (isMainMethod(method.name, paramTypes)) {
30914
+ hasMain = true;
30915
+ }
30916
+ }
30917
+ }
30918
+ if (hasFrameworkSignal) break;
30919
+ }
30920
+ const cliMainSignal = hasMain && !hasFrameworkSignal;
30921
+ if (!cliMainSignal) {
30922
+ return { cliMainSignal: false, droppedCount: 0 };
30923
+ }
30924
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
30925
+ let droppedCount = 0;
30926
+ const kept = sinks.filter((sink) => {
30927
+ if (sink.type !== "code_injection") return true;
30928
+ if (!sink.method) return true;
30929
+ if (!REFLECTION_SINK_METHODS.has(sink.method)) return true;
30930
+ droppedCount++;
30931
+ return false;
30932
+ });
30933
+ if (droppedCount > 0) {
30934
+ sinks.length = 0;
30935
+ sinks.push(...kept);
30936
+ }
30937
+ return { cliMainSignal: true, droppedCount };
30938
+ }
30939
+ };
30940
+
30756
30941
  // src/analysis/passes/taint-propagation-pass.ts
30757
30942
  var TaintPropagationPass = class {
30758
30943
  name = "taint-propagation";
@@ -31544,7 +31729,7 @@ function pickScopedSource(sources, sinkLine, methodName, types, taintedVar) {
31544
31729
  }
31545
31730
 
31546
31731
  // src/analysis/entry-point-detection.ts
31547
- var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
31732
+ var TIER_1_METHOD_ANNOTATIONS2 = /* @__PURE__ */ new Set([
31548
31733
  // Spring MVC
31549
31734
  "RequestMapping",
31550
31735
  "GetMapping",
@@ -31587,7 +31772,7 @@ var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
31587
31772
  "DataBoundConstructor",
31588
31773
  "DataBoundSetter"
31589
31774
  ]);
31590
- var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
31775
+ var TIER_1_CLASS_ANNOTATIONS2 = /* @__PURE__ */ new Set([
31591
31776
  // Spring MVC
31592
31777
  "RestController",
31593
31778
  "Controller",
@@ -31754,10 +31939,10 @@ function classifyEntryPointTier(method, enclosingType, ctx) {
31754
31939
  if (classShapeIsLibraryFacade(enclosingType)) {
31755
31940
  return "TIER_3_LIBRARY_API";
31756
31941
  }
31757
- if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS)) {
31942
+ if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS2)) {
31758
31943
  return "TIER_1_ENTRY_POINT";
31759
31944
  }
31760
- if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS)) {
31945
+ if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS2)) {
31761
31946
  return "TIER_1_ENTRY_POINT";
31762
31947
  }
31763
31948
  if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
@@ -32710,6 +32895,44 @@ var CLOSE_METHODS = /* @__PURE__ */ new Set([
32710
32895
  "shutdownNow",
32711
32896
  "terminate"
32712
32897
  ]);
32898
+ var WRAPPER_CTORS = /* @__PURE__ */ new Set([
32899
+ // java.io wrappers
32900
+ "BufferedInputStream",
32901
+ "BufferedOutputStream",
32902
+ "BufferedReader",
32903
+ "BufferedWriter",
32904
+ "InputStreamReader",
32905
+ "OutputStreamWriter",
32906
+ "DataInputStream",
32907
+ "DataOutputStream",
32908
+ "PrintStream",
32909
+ "PrintWriter",
32910
+ "LineNumberReader",
32911
+ "PushbackInputStream",
32912
+ "PushbackReader",
32913
+ "SequenceInputStream",
32914
+ // java.util.zip wrappers
32915
+ "GZIPInputStream",
32916
+ "GZIPOutputStream",
32917
+ "ZipInputStream",
32918
+ "ZipOutputStream",
32919
+ "InflaterInputStream",
32920
+ "DeflaterOutputStream",
32921
+ "CheckedInputStream",
32922
+ "CheckedOutputStream"
32923
+ ]);
32924
+ var WORKER_METHODS = /* @__PURE__ */ new Set([
32925
+ "run",
32926
+ // Runnable, Thread
32927
+ "call",
32928
+ // Callable
32929
+ "accept",
32930
+ // Consumer
32931
+ "get",
32932
+ // Supplier
32933
+ "apply"
32934
+ // Function
32935
+ ]);
32713
32936
  var ResourceLeakPass = class {
32714
32937
  name = "resource-leak";
32715
32938
  category = "reliability";
@@ -32741,6 +32964,24 @@ var ResourceLeakPass = class {
32741
32964
  if (this.isFactoryMethod(methodInfo.method)) {
32742
32965
  continue;
32743
32966
  }
32967
+ if (this.isWrappedByCloseableCtor(
32968
+ graph.ir.calls,
32969
+ resourceVar,
32970
+ openLine,
32971
+ methodEnd
32972
+ )) {
32973
+ continue;
32974
+ }
32975
+ if (this.isClosedInNestedWorker(
32976
+ graph,
32977
+ codeLines,
32978
+ resourceVar,
32979
+ methodInfo,
32980
+ openLine,
32981
+ methodEnd
32982
+ )) {
32983
+ continue;
32984
+ }
32744
32985
  const closeCall = graph.ir.calls.find(
32745
32986
  (c) => CLOSE_METHODS.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
32746
32987
  );
@@ -32860,6 +33101,73 @@ var ResourceLeakPass = class {
32860
33101
  if (!method.return_type || method.return_type === "void") return false;
32861
33102
  return FACTORY_METHOD_NAME_RE.test(method.name);
32862
33103
  }
33104
+ /**
33105
+ * #226 — true if `variable` is passed as a constructor argument to a
33106
+ * known Closeable wrapper call within the enclosing method's line
33107
+ * range. Ownership of the inner stream transfers to the wrapper.
33108
+ *
33109
+ * The check is deliberately per-method: cross-method wrapping does
33110
+ * not apply because the inner reference has already escaped the
33111
+ * scope by then.
33112
+ */
33113
+ isWrappedByCloseableCtor(calls, variable, fromLine, toLine) {
33114
+ for (const call of calls) {
33115
+ if (!WRAPPER_CTORS.has(call.method_name)) continue;
33116
+ if (call.location.line < fromLine || call.location.line > toLine) continue;
33117
+ for (const arg of call.arguments) {
33118
+ if (arg.variable === variable) return true;
33119
+ }
33120
+ }
33121
+ return false;
33122
+ }
33123
+ /**
33124
+ * #227 — true if `variable` refers to a field of the enclosing class
33125
+ * that is closed inside a worker-literal method (Runnable#run,
33126
+ * Callable#call, Consumer#accept, ...) declared in the same enclosing
33127
+ * method. Ownership transfers to the executor thread.
33128
+ *
33129
+ * The heuristic is intentionally conservative: it requires both
33130
+ * (a) the resource variable to match a declared field name on the
33131
+ * enclosing class (so a stray local named `selector` cannot
33132
+ * accidentally opt in to the suppression), AND
33133
+ * (b) a `<field>.close()` (or CLOSE_METHODS) call to appear on a
33134
+ * line inside a method whose name is one of WORKER_METHODS and
33135
+ * whose start_line is strictly within the enclosing method's
33136
+ * body (nested literal indicator).
33137
+ */
33138
+ isClosedInNestedWorker(graph, lines, variable, methodInfo, fromLine, toLine) {
33139
+ const fieldNames = new Set(methodInfo.type.fields.map((f) => f.name));
33140
+ let candidateField = null;
33141
+ if (fieldNames.has(variable)) {
33142
+ candidateField = variable;
33143
+ } else {
33144
+ const thisAssignRe = new RegExp(
33145
+ `(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`
33146
+ );
33147
+ for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
33148
+ const m = thisAssignRe.exec(lines[l - 1] ?? "");
33149
+ if (m && fieldNames.has(m[1])) {
33150
+ candidateField = m[1];
33151
+ break;
33152
+ }
33153
+ }
33154
+ }
33155
+ if (!candidateField) return false;
33156
+ for (const call of graph.ir.calls) {
33157
+ if (call.receiver !== candidateField) continue;
33158
+ if (!CLOSE_METHODS.has(call.method_name)) continue;
33159
+ const closeLine = call.location.line;
33160
+ if (closeLine < fromLine || closeLine > toLine) continue;
33161
+ const enclosing = graph.methodAtLine(closeLine);
33162
+ if (!enclosing) continue;
33163
+ if (enclosing.method === methodInfo.method) continue;
33164
+ if (!WORKER_METHODS.has(enclosing.method.name)) continue;
33165
+ if (enclosing.method.start_line > fromLine && enclosing.method.start_line <= toLine) {
33166
+ return true;
33167
+ }
33168
+ }
33169
+ return false;
33170
+ }
32863
33171
  };
32864
33172
 
32865
33173
  // src/graph/scope-graph.ts
@@ -40319,6 +40627,8 @@ async function analyze(code, filePath, language, options = {}) {
40319
40627
  if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
40320
40628
  pipeline.add(new SinkFilterPass());
40321
40629
  if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
40630
+ if (!disabledPasses.has("cli-main-reflection-suppress"))
40631
+ pipeline.add(new CliMainReflectionSuppressPass());
40322
40632
  pipeline.add(new TaintPropagationPass());
40323
40633
  pipeline.add(new InterproceduralPass({
40324
40634
  enableEntryPointGate: options.enableEntryPointGate ?? true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.148.0",
3
+ "version": "3.150.0",
4
4
  "description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",