cognium-dev 3.147.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/cli.js +304 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -26043,6 +26043,18 @@ function findJavaResponseWriterXssFindings(code, file) {
|
|
|
26043
26043
|
if (respNames.size === 0)
|
|
26044
26044
|
return out2;
|
|
26045
26045
|
const safeWrapRe = /\b(?:Encode\.(?:forHtml|forHtmlAttribute|forHtmlContent|forJavaScript)|StringEscapeUtils\.escape(?:Html3|Html4|EcmaScript|Xml)|HtmlUtils\.htmlEscape(?:Decimal|Hex)?|Escaper\.escapeHtml|HtmlEscapers\.(?:escapeHtml|htmlEscaper)|Encoder\.encodeForHTML(?:Attribute)?|Jsoup\.clean)\s*\(/;
|
|
26046
|
+
const sanitizedVars = new Set;
|
|
26047
|
+
const sanAssignRe = /(?:^|[\s;{},(])(?:final\s+)?(?:[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*[^=]/;
|
|
26048
|
+
for (const line of lines) {
|
|
26049
|
+
const t = line.trim();
|
|
26050
|
+
if (!t || t.startsWith("//") || t.startsWith("*"))
|
|
26051
|
+
continue;
|
|
26052
|
+
if (!safeWrapRe.test(t))
|
|
26053
|
+
continue;
|
|
26054
|
+
const am = t.match(sanAssignRe);
|
|
26055
|
+
if (am)
|
|
26056
|
+
sanitizedVars.add(am[1]);
|
|
26057
|
+
}
|
|
26046
26058
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26047
26059
|
const raw = lines[i2];
|
|
26048
26060
|
const trimmed = raw.trim();
|
|
@@ -26063,6 +26075,14 @@ function findJavaResponseWriterXssFindings(code, file) {
|
|
|
26063
26075
|
continue;
|
|
26064
26076
|
if (!/[A-Za-z_]\w*/.test(argsTrim))
|
|
26065
26077
|
continue;
|
|
26078
|
+
if (sanitizedVars.size > 0) {
|
|
26079
|
+
const stripped = argsTrim.replace(/"(?:\\.|[^"\\])*"/g, "");
|
|
26080
|
+
const idents = stripped.match(/\b[A-Za-z_]\w*\b/g) ?? [];
|
|
26081
|
+
const nonKeyword = idents.filter((x) => x !== "null" && x !== "true" && x !== "false");
|
|
26082
|
+
if (nonKeyword.length > 0 && nonKeyword.every((x) => sanitizedVars.has(x))) {
|
|
26083
|
+
continue;
|
|
26084
|
+
}
|
|
26085
|
+
}
|
|
26066
26086
|
out2.push({
|
|
26067
26087
|
id: `xss-${file}-${i2 + 1}`,
|
|
26068
26088
|
pass: "language-sources",
|
|
@@ -30323,7 +30343,7 @@ class SinkSemanticsPass {
|
|
|
30323
30343
|
return { droppedCount: 0, registrySize: 0 };
|
|
30324
30344
|
}
|
|
30325
30345
|
const registry = buildRegistry(entries);
|
|
30326
|
-
const sinks = graph.ir.taint.sinks;
|
|
30346
|
+
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
30327
30347
|
let droppedCount = 0;
|
|
30328
30348
|
const kept = sinks.filter((sink) => {
|
|
30329
30349
|
if (!sink.class || !sink.method)
|
|
@@ -30346,6 +30366,194 @@ class SinkSemanticsPass {
|
|
|
30346
30366
|
}
|
|
30347
30367
|
}
|
|
30348
30368
|
|
|
30369
|
+
// ../circle-ir/dist/analysis/passes/cli-main-reflection-suppress-pass.js
|
|
30370
|
+
var REFLECTION_SINK_METHODS = new Set([
|
|
30371
|
+
"forName",
|
|
30372
|
+
"newInstance",
|
|
30373
|
+
"invoke",
|
|
30374
|
+
"getMethod",
|
|
30375
|
+
"getDeclaredMethod",
|
|
30376
|
+
"getConstructor",
|
|
30377
|
+
"getDeclaredConstructor",
|
|
30378
|
+
"loadClass",
|
|
30379
|
+
"defineClass"
|
|
30380
|
+
]);
|
|
30381
|
+
var TIER_1_CLASS_ANNOTATIONS = new Set([
|
|
30382
|
+
"RestController",
|
|
30383
|
+
"Controller",
|
|
30384
|
+
"Service",
|
|
30385
|
+
"Repository",
|
|
30386
|
+
"Component",
|
|
30387
|
+
"Path",
|
|
30388
|
+
"WebServlet",
|
|
30389
|
+
"ServerEndpoint",
|
|
30390
|
+
"FeignClient"
|
|
30391
|
+
]);
|
|
30392
|
+
var TIER_1_METHOD_ANNOTATIONS = new Set([
|
|
30393
|
+
"RequestMapping",
|
|
30394
|
+
"GetMapping",
|
|
30395
|
+
"PostMapping",
|
|
30396
|
+
"PutMapping",
|
|
30397
|
+
"DeleteMapping",
|
|
30398
|
+
"PatchMapping",
|
|
30399
|
+
"MessageMapping",
|
|
30400
|
+
"SubscribeMapping",
|
|
30401
|
+
"KafkaListener",
|
|
30402
|
+
"KafkaHandler",
|
|
30403
|
+
"RabbitListener",
|
|
30404
|
+
"RabbitHandler",
|
|
30405
|
+
"JmsListener",
|
|
30406
|
+
"StreamListener",
|
|
30407
|
+
"SqsListener",
|
|
30408
|
+
"SqsHandler",
|
|
30409
|
+
"EventListener",
|
|
30410
|
+
"Scheduled",
|
|
30411
|
+
"Path",
|
|
30412
|
+
"GET",
|
|
30413
|
+
"POST",
|
|
30414
|
+
"PUT",
|
|
30415
|
+
"DELETE",
|
|
30416
|
+
"PATCH",
|
|
30417
|
+
"HEAD",
|
|
30418
|
+
"OPTIONS",
|
|
30419
|
+
"DataBoundConstructor",
|
|
30420
|
+
"DataBoundSetter"
|
|
30421
|
+
]);
|
|
30422
|
+
var TIER_1_SUPERTYPES = new Set([
|
|
30423
|
+
"HttpServlet",
|
|
30424
|
+
"GenericServlet",
|
|
30425
|
+
"Filter",
|
|
30426
|
+
"HandlerInterceptor",
|
|
30427
|
+
"AsyncHandlerInterceptor",
|
|
30428
|
+
"CommandLineRunner",
|
|
30429
|
+
"ApplicationRunner",
|
|
30430
|
+
"SimpleChannelInboundHandler",
|
|
30431
|
+
"ChannelInboundHandler",
|
|
30432
|
+
"ChannelInboundHandlerAdapter",
|
|
30433
|
+
"ChannelDuplexHandler",
|
|
30434
|
+
"NettyRequestProcessor",
|
|
30435
|
+
"Converter",
|
|
30436
|
+
"SingleValueConverter",
|
|
30437
|
+
"ConverterMatcher",
|
|
30438
|
+
"AbstractReflectionConverter",
|
|
30439
|
+
"AbstractSingleValueConverter",
|
|
30440
|
+
"AbstractCollectionConverter"
|
|
30441
|
+
]);
|
|
30442
|
+
function normalizeAnnotation(raw) {
|
|
30443
|
+
let s = raw.trim();
|
|
30444
|
+
if (s.startsWith("@"))
|
|
30445
|
+
s = s.slice(1);
|
|
30446
|
+
const parenIdx = s.indexOf("(");
|
|
30447
|
+
if (parenIdx >= 0)
|
|
30448
|
+
s = s.slice(0, parenIdx);
|
|
30449
|
+
const genericIdx = s.indexOf("<");
|
|
30450
|
+
if (genericIdx >= 0)
|
|
30451
|
+
s = s.slice(0, genericIdx);
|
|
30452
|
+
const dotIdx = s.lastIndexOf(".");
|
|
30453
|
+
if (dotIdx >= 0)
|
|
30454
|
+
s = s.slice(dotIdx + 1);
|
|
30455
|
+
return s.trim();
|
|
30456
|
+
}
|
|
30457
|
+
function normalizeSupertype(raw) {
|
|
30458
|
+
let s = raw.trim();
|
|
30459
|
+
const genericIdx = s.indexOf("<");
|
|
30460
|
+
if (genericIdx >= 0)
|
|
30461
|
+
s = s.slice(0, genericIdx);
|
|
30462
|
+
const dotIdx = s.lastIndexOf(".");
|
|
30463
|
+
if (dotIdx >= 0)
|
|
30464
|
+
s = s.slice(dotIdx + 1);
|
|
30465
|
+
return s.trim();
|
|
30466
|
+
}
|
|
30467
|
+
function isMainMethod(name2, paramTypes) {
|
|
30468
|
+
if (name2 !== "main")
|
|
30469
|
+
return false;
|
|
30470
|
+
if (paramTypes.length !== 1)
|
|
30471
|
+
return false;
|
|
30472
|
+
const t = paramTypes[0];
|
|
30473
|
+
if (!t)
|
|
30474
|
+
return false;
|
|
30475
|
+
const bare = t.replace(/\s+/g, "");
|
|
30476
|
+
return bare === "String[]" || bare === "java.lang.String[]";
|
|
30477
|
+
}
|
|
30478
|
+
|
|
30479
|
+
class CliMainReflectionSuppressPass {
|
|
30480
|
+
name = "cli-main-reflection-suppress";
|
|
30481
|
+
category = "security";
|
|
30482
|
+
run(ctx) {
|
|
30483
|
+
const { graph, language } = ctx;
|
|
30484
|
+
if (language !== "java") {
|
|
30485
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
30486
|
+
}
|
|
30487
|
+
const types = graph.ir.types;
|
|
30488
|
+
if (!types || types.length === 0) {
|
|
30489
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
30490
|
+
}
|
|
30491
|
+
let hasMain = false;
|
|
30492
|
+
let hasFrameworkSignal = false;
|
|
30493
|
+
for (const type of types) {
|
|
30494
|
+
for (const ann of type.annotations) {
|
|
30495
|
+
if (TIER_1_CLASS_ANNOTATIONS.has(normalizeAnnotation(ann))) {
|
|
30496
|
+
hasFrameworkSignal = true;
|
|
30497
|
+
break;
|
|
30498
|
+
}
|
|
30499
|
+
}
|
|
30500
|
+
if (hasFrameworkSignal)
|
|
30501
|
+
break;
|
|
30502
|
+
if (type.extends && TIER_1_SUPERTYPES.has(normalizeSupertype(type.extends))) {
|
|
30503
|
+
hasFrameworkSignal = true;
|
|
30504
|
+
break;
|
|
30505
|
+
}
|
|
30506
|
+
for (const impl of type.implements) {
|
|
30507
|
+
if (TIER_1_SUPERTYPES.has(normalizeSupertype(impl))) {
|
|
30508
|
+
hasFrameworkSignal = true;
|
|
30509
|
+
break;
|
|
30510
|
+
}
|
|
30511
|
+
}
|
|
30512
|
+
if (hasFrameworkSignal)
|
|
30513
|
+
break;
|
|
30514
|
+
for (const method of type.methods) {
|
|
30515
|
+
for (const ann of method.annotations) {
|
|
30516
|
+
if (TIER_1_METHOD_ANNOTATIONS.has(normalizeAnnotation(ann))) {
|
|
30517
|
+
hasFrameworkSignal = true;
|
|
30518
|
+
break;
|
|
30519
|
+
}
|
|
30520
|
+
}
|
|
30521
|
+
if (hasFrameworkSignal)
|
|
30522
|
+
break;
|
|
30523
|
+
if (!hasMain) {
|
|
30524
|
+
const paramTypes = method.parameters.map((p) => p.type);
|
|
30525
|
+
if (isMainMethod(method.name, paramTypes)) {
|
|
30526
|
+
hasMain = true;
|
|
30527
|
+
}
|
|
30528
|
+
}
|
|
30529
|
+
}
|
|
30530
|
+
if (hasFrameworkSignal)
|
|
30531
|
+
break;
|
|
30532
|
+
}
|
|
30533
|
+
const cliMainSignal = hasMain && !hasFrameworkSignal;
|
|
30534
|
+
if (!cliMainSignal) {
|
|
30535
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
30536
|
+
}
|
|
30537
|
+
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
30538
|
+
let droppedCount = 0;
|
|
30539
|
+
const kept = sinks.filter((sink) => {
|
|
30540
|
+
if (sink.type !== "code_injection")
|
|
30541
|
+
return true;
|
|
30542
|
+
if (!sink.method)
|
|
30543
|
+
return true;
|
|
30544
|
+
if (!REFLECTION_SINK_METHODS.has(sink.method))
|
|
30545
|
+
return true;
|
|
30546
|
+
droppedCount++;
|
|
30547
|
+
return false;
|
|
30548
|
+
});
|
|
30549
|
+
if (droppedCount > 0) {
|
|
30550
|
+
sinks.length = 0;
|
|
30551
|
+
sinks.push(...kept);
|
|
30552
|
+
}
|
|
30553
|
+
return { cliMainSignal: true, droppedCount };
|
|
30554
|
+
}
|
|
30555
|
+
}
|
|
30556
|
+
|
|
30349
30557
|
// ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
|
|
30350
30558
|
class TaintPropagationPass {
|
|
30351
30559
|
name = "taint-propagation";
|
|
@@ -31713,7 +31921,7 @@ function findTaintBridges2(result) {
|
|
|
31713
31921
|
}
|
|
31714
31922
|
|
|
31715
31923
|
// ../circle-ir/dist/analysis/entry-point-detection.js
|
|
31716
|
-
var
|
|
31924
|
+
var TIER_1_METHOD_ANNOTATIONS2 = new Set([
|
|
31717
31925
|
"RequestMapping",
|
|
31718
31926
|
"GetMapping",
|
|
31719
31927
|
"PostMapping",
|
|
@@ -31743,7 +31951,7 @@ var TIER_1_METHOD_ANNOTATIONS = new Set([
|
|
|
31743
31951
|
"DataBoundConstructor",
|
|
31744
31952
|
"DataBoundSetter"
|
|
31745
31953
|
]);
|
|
31746
|
-
var
|
|
31954
|
+
var TIER_1_CLASS_ANNOTATIONS2 = new Set([
|
|
31747
31955
|
"RestController",
|
|
31748
31956
|
"Controller",
|
|
31749
31957
|
"Service",
|
|
@@ -31893,10 +32101,10 @@ function classifyEntryPointTier(method, enclosingType, ctx) {
|
|
|
31893
32101
|
if (classShapeIsLibraryFacade(enclosingType)) {
|
|
31894
32102
|
return "TIER_3_LIBRARY_API";
|
|
31895
32103
|
}
|
|
31896
|
-
if (annotationsInclude(method.annotations,
|
|
32104
|
+
if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS2)) {
|
|
31897
32105
|
return "TIER_1_ENTRY_POINT";
|
|
31898
32106
|
}
|
|
31899
|
-
if (enclosingType && annotationsInclude(enclosingType.annotations,
|
|
32107
|
+
if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS2)) {
|
|
31900
32108
|
return "TIER_1_ENTRY_POINT";
|
|
31901
32109
|
}
|
|
31902
32110
|
if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
|
|
@@ -32879,6 +33087,37 @@ var CLOSE_METHODS = new Set([
|
|
|
32879
33087
|
"shutdownNow",
|
|
32880
33088
|
"terminate"
|
|
32881
33089
|
]);
|
|
33090
|
+
var WRAPPER_CTORS = new Set([
|
|
33091
|
+
"BufferedInputStream",
|
|
33092
|
+
"BufferedOutputStream",
|
|
33093
|
+
"BufferedReader",
|
|
33094
|
+
"BufferedWriter",
|
|
33095
|
+
"InputStreamReader",
|
|
33096
|
+
"OutputStreamWriter",
|
|
33097
|
+
"DataInputStream",
|
|
33098
|
+
"DataOutputStream",
|
|
33099
|
+
"PrintStream",
|
|
33100
|
+
"PrintWriter",
|
|
33101
|
+
"LineNumberReader",
|
|
33102
|
+
"PushbackInputStream",
|
|
33103
|
+
"PushbackReader",
|
|
33104
|
+
"SequenceInputStream",
|
|
33105
|
+
"GZIPInputStream",
|
|
33106
|
+
"GZIPOutputStream",
|
|
33107
|
+
"ZipInputStream",
|
|
33108
|
+
"ZipOutputStream",
|
|
33109
|
+
"InflaterInputStream",
|
|
33110
|
+
"DeflaterOutputStream",
|
|
33111
|
+
"CheckedInputStream",
|
|
33112
|
+
"CheckedOutputStream"
|
|
33113
|
+
]);
|
|
33114
|
+
var WORKER_METHODS = new Set([
|
|
33115
|
+
"run",
|
|
33116
|
+
"call",
|
|
33117
|
+
"accept",
|
|
33118
|
+
"get",
|
|
33119
|
+
"apply"
|
|
33120
|
+
]);
|
|
32882
33121
|
|
|
32883
33122
|
class ResourceLeakPass {
|
|
32884
33123
|
name = "resource-leak";
|
|
@@ -32915,6 +33154,12 @@ class ResourceLeakPass {
|
|
|
32915
33154
|
if (this.isFactoryMethod(methodInfo.method)) {
|
|
32916
33155
|
continue;
|
|
32917
33156
|
}
|
|
33157
|
+
if (this.isWrappedByCloseableCtor(graph.ir.calls, resourceVar, openLine, methodEnd)) {
|
|
33158
|
+
continue;
|
|
33159
|
+
}
|
|
33160
|
+
if (this.isClosedInNestedWorker(graph, codeLines, resourceVar, methodInfo, openLine, methodEnd)) {
|
|
33161
|
+
continue;
|
|
33162
|
+
}
|
|
32918
33163
|
const closeCall = graph.ir.calls.find((c) => CLOSE_METHODS.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd);
|
|
32919
33164
|
const snippet = (codeLines[openLine - 1] ?? "").trim();
|
|
32920
33165
|
if (!closeCall) {
|
|
@@ -33011,6 +33256,57 @@ class ResourceLeakPass {
|
|
|
33011
33256
|
return false;
|
|
33012
33257
|
return FACTORY_METHOD_NAME_RE.test(method.name);
|
|
33013
33258
|
}
|
|
33259
|
+
isWrappedByCloseableCtor(calls, variable, fromLine, toLine) {
|
|
33260
|
+
for (const call of calls) {
|
|
33261
|
+
if (!WRAPPER_CTORS.has(call.method_name))
|
|
33262
|
+
continue;
|
|
33263
|
+
if (call.location.line < fromLine || call.location.line > toLine)
|
|
33264
|
+
continue;
|
|
33265
|
+
for (const arg of call.arguments) {
|
|
33266
|
+
if (arg.variable === variable)
|
|
33267
|
+
return true;
|
|
33268
|
+
}
|
|
33269
|
+
}
|
|
33270
|
+
return false;
|
|
33271
|
+
}
|
|
33272
|
+
isClosedInNestedWorker(graph, lines, variable, methodInfo, fromLine, toLine) {
|
|
33273
|
+
const fieldNames = new Set(methodInfo.type.fields.map((f) => f.name));
|
|
33274
|
+
let candidateField = null;
|
|
33275
|
+
if (fieldNames.has(variable)) {
|
|
33276
|
+
candidateField = variable;
|
|
33277
|
+
} else {
|
|
33278
|
+
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`);
|
|
33279
|
+
for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
|
|
33280
|
+
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
33281
|
+
if (m && fieldNames.has(m[1])) {
|
|
33282
|
+
candidateField = m[1];
|
|
33283
|
+
break;
|
|
33284
|
+
}
|
|
33285
|
+
}
|
|
33286
|
+
}
|
|
33287
|
+
if (!candidateField)
|
|
33288
|
+
return false;
|
|
33289
|
+
for (const call of graph.ir.calls) {
|
|
33290
|
+
if (call.receiver !== candidateField)
|
|
33291
|
+
continue;
|
|
33292
|
+
if (!CLOSE_METHODS.has(call.method_name))
|
|
33293
|
+
continue;
|
|
33294
|
+
const closeLine = call.location.line;
|
|
33295
|
+
if (closeLine < fromLine || closeLine > toLine)
|
|
33296
|
+
continue;
|
|
33297
|
+
const enclosing = graph.methodAtLine(closeLine);
|
|
33298
|
+
if (!enclosing)
|
|
33299
|
+
continue;
|
|
33300
|
+
if (enclosing.method === methodInfo.method)
|
|
33301
|
+
continue;
|
|
33302
|
+
if (!WORKER_METHODS.has(enclosing.method.name))
|
|
33303
|
+
continue;
|
|
33304
|
+
if (enclosing.method.start_line > fromLine && enclosing.method.start_line <= toLine) {
|
|
33305
|
+
return true;
|
|
33306
|
+
}
|
|
33307
|
+
}
|
|
33308
|
+
return false;
|
|
33309
|
+
}
|
|
33014
33310
|
}
|
|
33015
33311
|
|
|
33016
33312
|
// ../circle-ir/dist/graph/scope-graph.js
|
|
@@ -41335,6 +41631,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
41335
41631
|
pipeline.add(new SinkFilterPass);
|
|
41336
41632
|
if (!disabledPasses.has("sink-semantics"))
|
|
41337
41633
|
pipeline.add(new SinkSemanticsPass);
|
|
41634
|
+
if (!disabledPasses.has("cli-main-reflection-suppress"))
|
|
41635
|
+
pipeline.add(new CliMainReflectionSuppressPass);
|
|
41338
41636
|
pipeline.add(new TaintPropagationPass);
|
|
41339
41637
|
pipeline.add(new InterproceduralPass({
|
|
41340
41638
|
enableEntryPointGate: options.enableEntryPointGate ?? true
|
|
@@ -42262,7 +42560,7 @@ var colors = {
|
|
|
42262
42560
|
};
|
|
42263
42561
|
|
|
42264
42562
|
// src/version.ts
|
|
42265
|
-
var version = "3.
|
|
42563
|
+
var version = "3.150.0";
|
|
42266
42564
|
|
|
42267
42565
|
// src/formatters.ts
|
|
42268
42566
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.150.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@cognium/project-profile-detect": "^1.1.0",
|
|
69
|
-
"circle-ir": "^3.
|
|
69
|
+
"circle-ir": "^3.150.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|