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.
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.d.ts +94 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.d.ts.map +1 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.js +337 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.js.map +1 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts +26 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts.map +1 -1
- package/dist/analysis/passes/resource-leak-pass.js +138 -0
- package/dist/analysis/passes/resource-leak-pass.js.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.js +10 -1
- package/dist/analysis/passes/sink-semantics-pass.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +10 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +315 -5
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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,
|
|
31942
|
+
if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS2)) {
|
|
31758
31943
|
return "TIER_1_ENTRY_POINT";
|
|
31759
31944
|
}
|
|
31760
|
-
if (enclosingType && annotationsInclude(enclosingType.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.
|
|
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",
|