cognium-dev 3.178.0 → 3.180.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 +206 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -11683,12 +11683,20 @@ var DEFAULT_SINKS = [
|
|
|
11683
11683
|
{ method: "format", class: "String", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11684
11684
|
{ method: "format", class: "Formatter", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11685
11685
|
{ method: "printf", class: "System.out", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11686
|
+
{ method: "format", class: "MessageFormat", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11687
|
+
{ method: "printf", class: "PrintStream", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11688
|
+
{ method: "format", class: "PrintStream", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11689
|
+
{ method: "printf", class: "PrintWriter", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11690
|
+
{ method: "format", class: "PrintWriter", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11686
11691
|
{ method: "printf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11687
11692
|
{ method: "fprintf", type: "format_string", cwe: "CWE-134", severity: "high", arg_positions: [1], languages: ["python"] },
|
|
11688
11693
|
{ method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11689
11694
|
{ method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11690
11695
|
{ method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11691
11696
|
{ method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
11697
|
+
{ method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11698
|
+
{ method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11699
|
+
{ method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
11692
11700
|
{ method: "setHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
11693
11701
|
{ method: "writeHead", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [2], languages: ["javascript", "typescript"] },
|
|
11694
11702
|
{ method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
@@ -13680,6 +13688,11 @@ function calculateSinkConfidence(call, pattern) {
|
|
|
13680
13688
|
if (pattern.class && call.receiver) {
|
|
13681
13689
|
if (receiverMightBeClass(call.receiver, pattern.class)) {
|
|
13682
13690
|
confidence += 0.1;
|
|
13691
|
+
const receiverLc = call.receiver.toLowerCase();
|
|
13692
|
+
const classLc = pattern.class.toLowerCase();
|
|
13693
|
+
if (call.receiver === pattern.class || receiverLc === classLc) {
|
|
13694
|
+
confidence += 0.05;
|
|
13695
|
+
}
|
|
13683
13696
|
}
|
|
13684
13697
|
}
|
|
13685
13698
|
if (pattern.severity === "critical") {
|
|
@@ -32655,6 +32668,130 @@ function resolveFastjsonFromPom(pomXml) {
|
|
|
32655
32668
|
}
|
|
32656
32669
|
return null;
|
|
32657
32670
|
}
|
|
32671
|
+
function resolveFastjsonFromGradle(buildGradle) {
|
|
32672
|
+
if (!buildGradle)
|
|
32673
|
+
return null;
|
|
32674
|
+
const directRe = /['"(]\s*com\.alibaba:fastjson:([^'"$\s)]+)\s*['")]/;
|
|
32675
|
+
const direct = buildGradle.match(directRe);
|
|
32676
|
+
if (direct) {
|
|
32677
|
+
const version2 = direct[1];
|
|
32678
|
+
return { version: version2, noneAutotype: /_noneautotype/i.test(version2) };
|
|
32679
|
+
}
|
|
32680
|
+
const constraintsBlockRe = /['"(]\s*com\.alibaba:fastjson\s*['")]\s*\)?\s*\{[\s\S]*?\bversion\s*\{[\s\S]*?\b(?:strictly|require|prefer)\s+['"]([^'"\s]+)['"]/;
|
|
32681
|
+
const constraintsBlock = buildGradle.match(constraintsBlockRe);
|
|
32682
|
+
if (constraintsBlock) {
|
|
32683
|
+
const version2 = constraintsBlock[1];
|
|
32684
|
+
return { version: version2, noneAutotype: /_noneautotype/i.test(version2) };
|
|
32685
|
+
}
|
|
32686
|
+
const propRefRe = /['"(]\s*com\.alibaba:fastjson:\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?\s*['")]/;
|
|
32687
|
+
const propRef = buildGradle.match(propRefRe);
|
|
32688
|
+
if (!propRef)
|
|
32689
|
+
return null;
|
|
32690
|
+
const propName = propRef[1];
|
|
32691
|
+
const defRe = new RegExp(`\\b${propName}\\s*[=:]\\s*['"]([^'"\\s]+)['"]`);
|
|
32692
|
+
const def = buildGradle.match(defRe);
|
|
32693
|
+
if (!def)
|
|
32694
|
+
return null;
|
|
32695
|
+
const version = def[1];
|
|
32696
|
+
return { version, noneAutotype: /_noneautotype/i.test(version) };
|
|
32697
|
+
}
|
|
32698
|
+
function resolveFastjsonFromGradleCatalog(buildGradle, libsVersionsToml) {
|
|
32699
|
+
if (!buildGradle || !libsVersionsToml)
|
|
32700
|
+
return null;
|
|
32701
|
+
const librariesMatch = libsVersionsToml.match(/\[libraries\]([\s\S]*?)(?=\n\[|$)/);
|
|
32702
|
+
if (!librariesMatch)
|
|
32703
|
+
return null;
|
|
32704
|
+
const librariesBlock = librariesMatch[1];
|
|
32705
|
+
const moduleForm = librariesBlock.match(/^\s*([\w.-]+)\s*=\s*\{[^}]*\bmodule\s*=\s*"com\.alibaba:fastjson"[^}]*\}/m);
|
|
32706
|
+
const groupNameForm = librariesBlock.match(/^\s*([\w.-]+)\s*=\s*\{[^}]*\bgroup\s*=\s*"com\.alibaba"[^}]*\bname\s*=\s*"fastjson"[^}]*\}/m);
|
|
32707
|
+
const entry = moduleForm ?? groupNameForm;
|
|
32708
|
+
if (!entry)
|
|
32709
|
+
return null;
|
|
32710
|
+
const alias = entry[1];
|
|
32711
|
+
const entryBody = entry[0];
|
|
32712
|
+
const aliasEsc = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32713
|
+
const aliasRefRe = new RegExp(`\\blibs\\s*\\.\\s*${aliasEsc}\\b`);
|
|
32714
|
+
if (!aliasRefRe.test(buildGradle))
|
|
32715
|
+
return null;
|
|
32716
|
+
const inlineVersionM = entryBody.match(/\bversion\s*=\s*"([^"$][^"]*)"/);
|
|
32717
|
+
if (inlineVersionM) {
|
|
32718
|
+
const version2 = inlineVersionM[1];
|
|
32719
|
+
return { version: version2, noneAutotype: /_noneautotype/i.test(version2) };
|
|
32720
|
+
}
|
|
32721
|
+
const versionRefM = entryBody.match(/\bversion\.ref\s*=\s*"([^"]+)"/);
|
|
32722
|
+
if (!versionRefM)
|
|
32723
|
+
return null;
|
|
32724
|
+
const versionAlias = versionRefM[1];
|
|
32725
|
+
const versionsMatch = libsVersionsToml.match(/\[versions\]([\s\S]*?)(?=\n\[|$)/);
|
|
32726
|
+
if (!versionsMatch)
|
|
32727
|
+
return null;
|
|
32728
|
+
const versionsBlock = versionsMatch[1];
|
|
32729
|
+
const versionAliasEsc = versionAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32730
|
+
const versionLineRe = new RegExp(`^\\s*${versionAliasEsc}\\s*=\\s*"([^"]+)"`, "m");
|
|
32731
|
+
const versionLine = versionsBlock.match(versionLineRe);
|
|
32732
|
+
if (!versionLine)
|
|
32733
|
+
return null;
|
|
32734
|
+
const version = versionLine[1];
|
|
32735
|
+
return { version, noneAutotype: /_noneautotype/i.test(version) };
|
|
32736
|
+
}
|
|
32737
|
+
function resolvePyYamlFromRequirements(requirementsTxt) {
|
|
32738
|
+
if (!requirementsTxt)
|
|
32739
|
+
return null;
|
|
32740
|
+
for (const rawLine of requirementsTxt.split(/\r?\n/)) {
|
|
32741
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
32742
|
+
if (!line)
|
|
32743
|
+
continue;
|
|
32744
|
+
if (line.startsWith("-"))
|
|
32745
|
+
continue;
|
|
32746
|
+
const m = line.match(/^(pyyaml)\s*(==|>=|~=|>|===)\s*(\d+(?:\.\d+)*)/i);
|
|
32747
|
+
if (!m)
|
|
32748
|
+
continue;
|
|
32749
|
+
const version = m[3];
|
|
32750
|
+
return { version, safeByDefault: isPyYamlVersionSafeByDefault(version) };
|
|
32751
|
+
}
|
|
32752
|
+
return null;
|
|
32753
|
+
}
|
|
32754
|
+
function resolvePyYamlFromPyproject(pyprojectToml) {
|
|
32755
|
+
if (!pyprojectToml)
|
|
32756
|
+
return null;
|
|
32757
|
+
const poetryLine = pyprojectToml.match(/^\s*(pyyaml)\s*=\s*"([\^~=<>]*)(\d+(?:\.\d+)*)"/im);
|
|
32758
|
+
if (poetryLine) {
|
|
32759
|
+
const version = poetryLine[3];
|
|
32760
|
+
return { version, safeByDefault: isPyYamlVersionSafeByDefault(version) };
|
|
32761
|
+
}
|
|
32762
|
+
const poetryTable = pyprojectToml.match(/^\s*(pyyaml)\s*=\s*\{[^}]*\bversion\s*=\s*"([\^~=<>]*)(\d+(?:\.\d+)*)"/im);
|
|
32763
|
+
if (poetryTable) {
|
|
32764
|
+
const version = poetryTable[3];
|
|
32765
|
+
return { version, safeByDefault: isPyYamlVersionSafeByDefault(version) };
|
|
32766
|
+
}
|
|
32767
|
+
const pep621 = pyprojectToml.match(/"(pyyaml)\s*(==|>=|~=|>|===)\s*(\d+(?:\.\d+)*)/i);
|
|
32768
|
+
if (pep621) {
|
|
32769
|
+
const version = pep621[3];
|
|
32770
|
+
return { version, safeByDefault: isPyYamlVersionSafeByDefault(version) };
|
|
32771
|
+
}
|
|
32772
|
+
return null;
|
|
32773
|
+
}
|
|
32774
|
+
function isPyYamlVersionSafeByDefault(version) {
|
|
32775
|
+
const parts2 = version.split(".").map((s) => Number.parseInt(s, 10));
|
|
32776
|
+
if (parts2.length === 0 || !Number.isFinite(parts2[0]))
|
|
32777
|
+
return false;
|
|
32778
|
+
return parts2[0] >= 6;
|
|
32779
|
+
}
|
|
32780
|
+
function fileHasUnsafePyYamlLoader(sourceLines, sinkLine) {
|
|
32781
|
+
const start2 = Math.max(0, sinkLine - 1);
|
|
32782
|
+
const end = Math.min(sourceLines.length, start2 + 10);
|
|
32783
|
+
const unsafeRe = /\bLoader\s*=\s*(?:yaml\s*\.\s*)?(?:Loader|UnsafeLoader|FullLoader)\b/;
|
|
32784
|
+
for (let i2 = start2;i2 < end; i2++) {
|
|
32785
|
+
const ln = sourceLines[i2] ?? "";
|
|
32786
|
+
if (unsafeRe.test(ln))
|
|
32787
|
+
return true;
|
|
32788
|
+
const stripped = ln.trim();
|
|
32789
|
+
if (i2 > start2 && (stripped === ")" || stripped.endsWith(")"))) {
|
|
32790
|
+
return false;
|
|
32791
|
+
}
|
|
32792
|
+
}
|
|
32793
|
+
return false;
|
|
32794
|
+
}
|
|
32658
32795
|
function fileReenablesFastjsonAutotype(source) {
|
|
32659
32796
|
if (!source)
|
|
32660
32797
|
return false;
|
|
@@ -32688,6 +32825,8 @@ var JACKSON_METHODS = new Set(["readValue", "convertValue", "treeToValue"]);
|
|
|
32688
32825
|
var JACKSON_CLASSES = new Set(["ObjectMapper", "ObjectReader"]);
|
|
32689
32826
|
var SNAKEYAML_METHODS = new Set(["load", "loadAs", "loadAll"]);
|
|
32690
32827
|
var SNAKEYAML_CLASSES = new Set(["Yaml"]);
|
|
32828
|
+
var PYYAML_METHODS = new Set(["load"]);
|
|
32829
|
+
var PYYAML_CLASSES = new Set(["yaml"]);
|
|
32691
32830
|
|
|
32692
32831
|
class DeserializationSafetyGatePass {
|
|
32693
32832
|
dependencyContext;
|
|
@@ -32698,18 +32837,32 @@ class DeserializationSafetyGatePass {
|
|
|
32698
32837
|
}
|
|
32699
32838
|
run(ctx) {
|
|
32700
32839
|
const { graph, language, code } = ctx;
|
|
32701
|
-
if (language !== "java") {
|
|
32702
|
-
return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0 };
|
|
32840
|
+
if (language !== "java" && language !== "python") {
|
|
32841
|
+
return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0, droppedPyYaml: 0 };
|
|
32703
32842
|
}
|
|
32704
32843
|
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
32705
32844
|
const pomXml = this.dependencyContext?.java?.pomXml;
|
|
32706
|
-
const
|
|
32845
|
+
const buildGradle = this.dependencyContext?.java?.buildGradle;
|
|
32846
|
+
const libsVersionsToml = this.dependencyContext?.java?.libsVersionsToml;
|
|
32847
|
+
const fastjson = (pomXml ? resolveFastjsonFromPom(pomXml) : null) ?? (buildGradle ? resolveFastjsonFromGradle(buildGradle) : null) ?? (buildGradle && libsVersionsToml ? resolveFastjsonFromGradleCatalog(buildGradle, libsVersionsToml) : null);
|
|
32707
32848
|
const fastjsonHardened = fastjson?.noneAutotype === true && !fileReenablesFastjsonAutotype(code);
|
|
32708
32849
|
const jacksonSafe = !fileEnablesJacksonPolymorphism(code);
|
|
32709
32850
|
const snakeYamlSafe = fileConfiguresSnakeYamlSafely(code);
|
|
32851
|
+
const requirementsTxt = this.dependencyContext?.python?.requirementsTxt;
|
|
32852
|
+
const pyprojectToml = this.dependencyContext?.python?.pyprojectToml;
|
|
32853
|
+
const pyYaml = (requirementsTxt ? resolvePyYamlFromRequirements(requirementsTxt) : null) ?? (pyprojectToml ? resolvePyYamlFromPyproject(pyprojectToml) : null);
|
|
32854
|
+
const pyYamlSafeByDefault = pyYaml?.safeByDefault === true;
|
|
32855
|
+
let sourceLines = null;
|
|
32856
|
+
const getSourceLines = () => {
|
|
32857
|
+
if (sourceLines === null)
|
|
32858
|
+
sourceLines = code.split(`
|
|
32859
|
+
`);
|
|
32860
|
+
return sourceLines;
|
|
32861
|
+
};
|
|
32710
32862
|
let droppedFastjson = 0;
|
|
32711
32863
|
let droppedJackson = 0;
|
|
32712
32864
|
let droppedSnakeYaml = 0;
|
|
32865
|
+
let droppedPyYaml = 0;
|
|
32713
32866
|
const kept = sinks.filter((sink) => {
|
|
32714
32867
|
if (sink.type !== "deserialization")
|
|
32715
32868
|
return true;
|
|
@@ -32727,14 +32880,18 @@ class DeserializationSafetyGatePass {
|
|
|
32727
32880
|
droppedSnakeYaml++;
|
|
32728
32881
|
return false;
|
|
32729
32882
|
}
|
|
32883
|
+
if (language === "python" && pyYamlSafeByDefault && PYYAML_METHODS.has(sink.method) && (sink.class === undefined || PYYAML_CLASSES.has(sink.class)) && !fileHasUnsafePyYamlLoader(getSourceLines(), sink.line)) {
|
|
32884
|
+
droppedPyYaml++;
|
|
32885
|
+
return false;
|
|
32886
|
+
}
|
|
32730
32887
|
return true;
|
|
32731
32888
|
});
|
|
32732
|
-
const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml;
|
|
32889
|
+
const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml + droppedPyYaml;
|
|
32733
32890
|
if (totalDropped > 0) {
|
|
32734
32891
|
sinks.length = 0;
|
|
32735
32892
|
sinks.push(...kept);
|
|
32736
32893
|
}
|
|
32737
|
-
return { droppedFastjson, droppedJackson, droppedSnakeYaml };
|
|
32894
|
+
return { droppedFastjson, droppedJackson, droppedSnakeYaml, droppedPyYaml };
|
|
32738
32895
|
}
|
|
32739
32896
|
}
|
|
32740
32897
|
|
|
@@ -39624,6 +39781,47 @@ class ScanSecretsPass {
|
|
|
39624
39781
|
}
|
|
39625
39782
|
}
|
|
39626
39783
|
|
|
39784
|
+
// ../circle-ir/dist/analysis/passes/python-receiver-taint-format-pass.js
|
|
39785
|
+
class PythonReceiverTaintFormatPass {
|
|
39786
|
+
name = "python-receiver-taint-format";
|
|
39787
|
+
category = "security";
|
|
39788
|
+
run(ctx) {
|
|
39789
|
+
if (ctx.language !== "python") {
|
|
39790
|
+
return { findingsEmitted: 0 };
|
|
39791
|
+
}
|
|
39792
|
+
const { calls, meta } = ctx.graph.ir;
|
|
39793
|
+
const constProp = ctx.getResult("constant-propagation");
|
|
39794
|
+
const tainted = constProp.tainted;
|
|
39795
|
+
let count = 0;
|
|
39796
|
+
for (const call of calls) {
|
|
39797
|
+
if (call.method_name !== "format")
|
|
39798
|
+
continue;
|
|
39799
|
+
const receiver = call.receiver;
|
|
39800
|
+
if (!receiver)
|
|
39801
|
+
continue;
|
|
39802
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(receiver))
|
|
39803
|
+
continue;
|
|
39804
|
+
const scopedName = call.in_method ? `${call.in_method}:${receiver}` : receiver;
|
|
39805
|
+
if (!tainted.has(receiver) && !tainted.has(scopedName))
|
|
39806
|
+
continue;
|
|
39807
|
+
ctx.addFinding({
|
|
39808
|
+
id: `format_string-${meta.file}-${call.location.line}-${receiver}`,
|
|
39809
|
+
pass: this.name,
|
|
39810
|
+
category: "security",
|
|
39811
|
+
rule_id: "format_string",
|
|
39812
|
+
cwe: "CWE-134",
|
|
39813
|
+
severity: "medium",
|
|
39814
|
+
level: "warning",
|
|
39815
|
+
message: `Format-string vulnerability: the format template \`${receiver}\` ` + `is tainted (traces back to a taint source). Attacker can leak ` + `object attributes via \`{obj.__class__.__mro__[…]}\` chains or ` + `crash the process via unbounded specifiers. Use a literal ` + `format string and pass user input as an argument instead.`,
|
|
39816
|
+
file: meta.file,
|
|
39817
|
+
line: call.location.line
|
|
39818
|
+
});
|
|
39819
|
+
count++;
|
|
39820
|
+
}
|
|
39821
|
+
return { findingsEmitted: count };
|
|
39822
|
+
}
|
|
39823
|
+
}
|
|
39824
|
+
|
|
39627
39825
|
// ../circle-ir/dist/analysis/passes/spring4shell-pass.js
|
|
39628
39826
|
var CONTROLLER_ANNOTATIONS = new Set([
|
|
39629
39827
|
"Controller",
|
|
@@ -44295,6 +44493,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
44295
44493
|
pipeline.add(new LibraryProfileCwe22PathGatePass);
|
|
44296
44494
|
if (!disabledPasses.has("scan-secrets"))
|
|
44297
44495
|
pipeline.add(new ScanSecretsPass);
|
|
44496
|
+
if (!disabledPasses.has("python-receiver-taint-format"))
|
|
44497
|
+
pipeline.add(new PythonReceiverTaintFormatPass);
|
|
44298
44498
|
if (!disabledPasses.has("dead-code"))
|
|
44299
44499
|
pipeline.add(new DeadCodePass);
|
|
44300
44500
|
if (!disabledPasses.has("missing-await"))
|
|
@@ -45224,7 +45424,7 @@ var colors = {
|
|
|
45224
45424
|
};
|
|
45225
45425
|
|
|
45226
45426
|
// src/version.ts
|
|
45227
|
-
var version = "3.
|
|
45427
|
+
var version = "3.180.0";
|
|
45228
45428
|
|
|
45229
45429
|
// src/formatters.ts
|
|
45230
45430
|
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.180.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.180.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|