From d06601d5e02fd6d21156e0ace2d349ad5a5e9e21 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 15:54:33 +0100 Subject: [PATCH 01/18] JS-824 Add rule S7639 --- packages/jsts/src/rules/S7639/index.ts | 17 +++++ packages/jsts/src/rules/S7639/rule.ts | 87 ++++++++++++++++++++++ packages/jsts/src/rules/S7639/unit.test.ts | 38 ++++++++++ 3 files changed, 142 insertions(+) create mode 100644 packages/jsts/src/rules/S7639/index.ts create mode 100644 packages/jsts/src/rules/S7639/rule.ts create mode 100644 packages/jsts/src/rules/S7639/unit.test.ts diff --git a/packages/jsts/src/rules/S7639/index.ts b/packages/jsts/src/rules/S7639/index.ts new file mode 100644 index 00000000000..508776d5366 --- /dev/null +++ b/packages/jsts/src/rules/S7639/index.ts @@ -0,0 +1,17 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +export { rule } from './rule.js'; diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts new file mode 100644 index 00000000000..0283c432f6e --- /dev/null +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -0,0 +1,87 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript + +import type { Rule } from 'eslint'; +import type estree from 'estree'; +import { generateMeta, isMemberWithProperty, isRequireModule } from '../helpers/index.js'; + +const blockChainModules = [ "ethers", "viem/accounts", "tronweb" ]; +const mnemonicTakingFunctions = [ "fromPhrase", "mnemonicToAccount", "fromMnemonic" ]; + +export const rule: Rule.RuleModule = { + meta: generateMeta(meta, { + messages: { + reviewBlockchainSeedPhrase: `Revoke and change this seed phrase, as it is compromised.`, + }, + }), + create(context: Rule.RuleContext) { + let isBcModuleImported = false; + + return { + Program() { + isBcModuleImported = false; + }, + + ImportDeclaration(node: estree.Node) { + const { source } = node as estree.ImportDeclaration; + if (blockChainModules.includes(String(source.value))) { + isBcModuleImported = true; + } + }, + + CallExpression(node: estree.Node) { + const call = node as estree.CallExpression; + const { callee, arguments: args } = call; + + if (isRequireModule(call, ...dbModules)) { + isBcModuleImported = true; + return; + } + + const isUnsafeFunction = mnemonicTakingFunctions.some(func => + isMemberWithProperty(callee, func) + ); + + const isHardcodedString = (arg: estree.Expression) => { + if (arg.type === 'Literal' && typeof arg.value === 'string') { + return true; + } + if (arg.type === 'TemplateLiteral') { + return arg.expressions.length === 0; + } + if (arg.type === 'BinaryExpression' && arg.operator === '+') { + return isHardcodedString(arg.left) && isHardcodedString(arg.right); + } + return false; + } + + if ( + isBcModuleImported && + isUnsafeFunction && + args.length > 0 && + isHardcodedString(args[0]) + ) { + context.report({ + messageId: 'reviewBlockchainSeedPhrase', + node: callee, + }); + } + }, + }; + }, +}; diff --git a/packages/jsts/src/rules/S7639/unit.test.ts b/packages/jsts/src/rules/S7639/unit.test.ts new file mode 100644 index 00000000000..cc531d2632b --- /dev/null +++ b/packages/jsts/src/rules/S7639/unit.test.ts @@ -0,0 +1,38 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +import { rule } from './index.js'; +import { NoTypeCheckingRuleTester } from '../../../tests/tools/testers/rule-tester.js'; +import { describe, it } from 'node:test'; + +describe('S7639', () => { + it('S7639', () => { + const ruleTester = new NoTypeCheckingRuleTester(); + + ruleTester.run('Revoke and change this seed phrase, as it is compromised.', rule, { + valid: [ + `const a = 42;`, + `function foo() { return 'bar'; }`, + ], + invalid: [ + { + code: `const seedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";`, + errors: [{ message: 'Revoke and change this seed phrase, as it is compromised.' }], + }, + ], + }); + } +}); From 6a6bcc99e625f42cdcf6f99faa24cbe3252758b3 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 16:09:40 +0100 Subject: [PATCH 02/18] =?UTF-8?q?rule=20logic=20first=20pass=20done=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/jsts/src/rules/S7639/rule.ts | 76 +++++++------ packages/jsts/src/rules/S7639/unit.test.ts | 126 ++++++++++++++++++++- 2 files changed, 159 insertions(+), 43 deletions(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 0283c432f6e..61ff7448865 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -20,65 +20,67 @@ import type { Rule } from 'eslint'; import type estree from 'estree'; import { generateMeta, isMemberWithProperty, isRequireModule } from '../helpers/index.js'; -const blockChainModules = [ "ethers", "viem/accounts", "tronweb" ]; -const mnemonicTakingFunctions = [ "fromPhrase", "mnemonicToAccount", "fromMnemonic" ]; +const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb']; + +const MNEMONIC_FUNCTIONS = ['fromPhrase', 'mnemonicToAccount', 'fromMnemonic']; export const rule: Rule.RuleModule = { - meta: generateMeta(meta, { + meta: generateMeta(meta,, { messages: { reviewBlockchainSeedPhrase: `Revoke and change this seed phrase, as it is compromised.`, }, }), create(context: Rule.RuleContext) { - let isBcModuleImported = false; + let isBlockchainModuleImported = false; + + function isHardcodedString(expr: estree.Expression): boolean { + switch (expr.type) { + case 'Literal': + return typeof expr.value === 'string'; + case 'TemplateLiteral': + return expr.expressions.length === 0; + case 'BinaryExpression': + return expr.operator === '+' && + isHardcodedString(expr.left) && + isHardcodedString(expr.right); + default: + return false; + } + } + + function isMnemonicFunction(callee: estree.Expression | estree.Super): boolean { + return MNEMONIC_FUNCTIONS.some(func => isMemberWithProperty(callee, func)); + } return { Program() { - isBcModuleImported = false; + isBlockchainModuleImported = false; }, - ImportDeclaration(node: estree.Node) { - const { source } = node as estree.ImportDeclaration; - if (blockChainModules.includes(String(source.value))) { - isBcModuleImported = true; + ImportDeclaration(node: estree.ImportDeclaration) { + if (BLOCKCHAIN_MODULES.includes(node.source.value as any)) { + isBlockchainModuleImported = true; } }, - CallExpression(node: estree.Node) { - const call = node as estree.CallExpression; - const { callee, arguments: args } = call; - - if (isRequireModule(call, ...dbModules)) { - isBcModuleImported = true; + CallExpression(node: estree.CallExpression) { + // Check for require() calls + if (isRequireModule(node, ...BLOCKCHAIN_MODULES)) { + isBlockchainModuleImported = true; return; } - const isUnsafeFunction = mnemonicTakingFunctions.some(func => - isMemberWithProperty(callee, func) - ); - - const isHardcodedString = (arg: estree.Expression) => { - if (arg.type === 'Literal' && typeof arg.value === 'string') { - return true; - } - if (arg.type === 'TemplateLiteral') { - return arg.expressions.length === 0; - } - if (arg.type === 'BinaryExpression' && arg.operator === '+') { - return isHardcodedString(arg.left) && isHardcodedString(arg.right); - } - return false; - } - + // Report hardcoded seed phrases in mnemonic functions if ( - isBcModuleImported && - isUnsafeFunction && - args.length > 0 && - isHardcodedString(args[0]) + isBlockchainModuleImported && + isMnemonicFunction(node.callee) && + node.arguments.length > 0 && + node.arguments[0].type !== 'SpreadElement' && + isHardcodedString(node.arguments[0]) ) { context.report({ messageId: 'reviewBlockchainSeedPhrase', - node: callee, + node: node.arguments[0], }); } }, diff --git a/packages/jsts/src/rules/S7639/unit.test.ts b/packages/jsts/src/rules/S7639/unit.test.ts index cc531d2632b..32bee79d9b7 100644 --- a/packages/jsts/src/rules/S7639/unit.test.ts +++ b/packages/jsts/src/rules/S7639/unit.test.ts @@ -22,17 +22,131 @@ describe('S7639', () => { it('S7639', () => { const ruleTester = new NoTypeCheckingRuleTester(); - ruleTester.run('Revoke and change this seed phrase, as it is compromised.', rule, { + ruleTester.run('Hardcoded seed phrases should not be used', rule, { valid: [ - `const a = 42;`, - `function foo() { return 'bar'; }`, + // No blockchain module imported + { + code: ` + const wallet = Wallet.fromPhrase("test phrase"); + `, + }, + // Variable used instead of hardcoded string + { + code: ` + import { Wallet } from 'ethers'; + const seedPhrase = process.env.SEED_PHRASE; + const wallet = Wallet.fromPhrase(seedPhrase); + `, + }, + // Function call result used + { + code: ` + import { Wallet } from 'ethers'; + const wallet = Wallet.fromPhrase(getSeedPhrase()); + `, + }, + // Different method name + { + code: ` + import { Wallet } from 'ethers'; + const wallet = Wallet.createRandom(); + `, + }, + // No arguments passed + { + code: ` + import { Wallet } from 'ethers'; + const wallet = Wallet.fromPhrase(); + `, + }, + // Template literal with expression + { + code: ` + import { mnemonicToAccount } from 'viem/accounts'; + const account = mnemonicToAccount(\`\${getPhrase()}\`); + `, + }, + // CommonJS require with variable + { + code: ` + const ethers = require('ethers'); + const phrase = readFromFile(); + const wallet = ethers.Wallet.fromPhrase(phrase); + `, + }, ], invalid: [ + // Ethers with hardcoded string literal + { + code: ` + import { Wallet } from 'ethers'; + const wallet = Wallet.fromPhrase("Round Outgoing Yanni Gripped Buoyant Iodine Victoriously"); + `, + errors: 1, + }, + // Viem with hardcoded string + { + code: ` + import { mnemonicToAccount } from 'viem/accounts'; + const account = mnemonicToAccount('test test test test test test test test test test test junk'); + `, + errors: 1, + }, + // TronWeb with hardcoded string + { + code: ` + import TronWeb from 'tronweb'; + const wallet = TronWeb.fromMnemonic("candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"); + `, + errors: 1, + }, + // Template literal without expressions + { + code: ` + import { Wallet } from 'ethers'; + const wallet = Wallet.fromPhrase(\`abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\`); + `, + errors: 1, + }, + // String concatenation + { + code: ` + import { HDNodeWallet } from 'ethers'; + const wallet = HDNodeWallet.fromPhrase("abandon abandon " + "abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon about"); + `, + errors: 1, + }, + // CommonJS require + { + code: ` + const ethers = require('ethers'); + const wallet = ethers.HDNodeWallet.fromPhrase("test test test test test test test test test test test junk"); + `, + errors: 1, + }, + // Multiple violations in same file + { + code: ` + import { HDNodeWallet } from 'ethers'; + import { mnemonicToAccount } from 'viem/accounts'; + + const wallet1 = HDNodeWallet.fromPhrase("seed phrase one"); + const account = mnemonicToAccount("seed phrase two"); + `, + errors: 2, + ], + }, + // Nested function calls { - code: `const seedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";`, - errors: [{ message: 'Revoke and change this seed phrase, as it is compromised.' }], + code: ` + const TronWeb = require('tronweb'); + function createWallet() { + return TronWeb.fromMnemonic("test mnemonic phrase here"); + } + `, + errors: 1, }, ], }); - } + }); }); From 6cb3f93cafad03446b33a28caa4d98abdbed426b Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 16:12:44 +0100 Subject: [PATCH 03/18] add meta --- packages/jsts/src/rules/S7639/meta.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/jsts/src/rules/S7639/meta.ts diff --git a/packages/jsts/src/rules/S7639/meta.ts b/packages/jsts/src/rules/S7639/meta.ts new file mode 100644 index 00000000000..395f4f31395 --- /dev/null +++ b/packages/jsts/src/rules/S7639/meta.ts @@ -0,0 +1,18 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +export const implementation = 'original'; +export const eslintId = 'reviewBlockchainSeedPhrase'; From 3cd18d9517693fc183b8ce2c60da9883a4aad2d2 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 16:17:45 +0100 Subject: [PATCH 04/18] typo --- packages/jsts/src/rules/S7639/rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 61ff7448865..5aada43a0ba 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -25,7 +25,7 @@ const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb']; const MNEMONIC_FUNCTIONS = ['fromPhrase', 'mnemonicToAccount', 'fromMnemonic']; export const rule: Rule.RuleModule = { - meta: generateMeta(meta,, { + meta: generateMeta(meta, { messages: { reviewBlockchainSeedPhrase: `Revoke and change this seed phrase, as it is compromised.`, }, From 8f51b7a6b8c50b357d7f8a19f726dff2a8f23c4b Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 16:20:49 +0100 Subject: [PATCH 05/18] typo --- packages/jsts/src/rules/S7639/rule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 5aada43a0ba..04254996513 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -19,6 +19,7 @@ import type { Rule } from 'eslint'; import type estree from 'estree'; import { generateMeta, isMemberWithProperty, isRequireModule } from '../helpers/index.js'; +import * as meta from './generated-meta.js'; const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb']; From b0264deb383726df7b6f7d4f17fb44b30159532c Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Wed, 12 Nov 2025 16:31:39 +0100 Subject: [PATCH 06/18] Add base use case --- packages/jsts/src/rules/S7639/rule.ts | 31 +++++++++++++++++----- packages/jsts/src/rules/S7639/unit.test.ts | 10 ++++++- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 04254996513..b7f63f77a37 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -33,6 +33,7 @@ export const rule: Rule.RuleModule = { }), create(context: Rule.RuleContext) { let isBlockchainModuleImported = false; + const hardcodedVariables = new Map(); function isHardcodedString(expr: estree.Expression): boolean { switch (expr.type) { @@ -40,15 +41,21 @@ export const rule: Rule.RuleModule = { return typeof expr.value === 'string'; case 'TemplateLiteral': return expr.expressions.length === 0; - case 'BinaryExpression': - return expr.operator === '+' && - isHardcodedString(expr.left) && - isHardcodedString(expr.right); + case 'Identifier': + return hardcodedVariables.has(expr.name); default: return false; } } + function getReportNode(expr: estree.Expression): estree.Node { + // If it's an identifier that references a hardcoded string, report the original declaration + if (expr.type === 'Identifier' && hardcodedVariables.has(expr.name)) { + return hardcodedVariables.get(expr.name)!; + } + return expr; + } + function isMnemonicFunction(callee: estree.Expression | estree.Super): boolean { return MNEMONIC_FUNCTIONS.some(func => isMemberWithProperty(callee, func)); } @@ -56,6 +63,7 @@ export const rule: Rule.RuleModule = { return { Program() { isBlockchainModuleImported = false; + hardcodedVariables.clear(); }, ImportDeclaration(node: estree.ImportDeclaration) { @@ -64,14 +72,23 @@ export const rule: Rule.RuleModule = { } }, + VariableDeclarator(node: estree.VariableDeclarator) { + if ( + node.id.type === 'Identifier' && + node.init && + (node.init.type === 'Literal' && typeof node.init.value === 'string' || + node.init.type === 'TemplateLiteral' && node.init.expressions.length === 0) + ) { + hardcodedVariables.set(node.id.name, node.init); + } + }, + CallExpression(node: estree.CallExpression) { - // Check for require() calls if (isRequireModule(node, ...BLOCKCHAIN_MODULES)) { isBlockchainModuleImported = true; return; } - // Report hardcoded seed phrases in mnemonic functions if ( isBlockchainModuleImported && isMnemonicFunction(node.callee) && @@ -81,7 +98,7 @@ export const rule: Rule.RuleModule = { ) { context.report({ messageId: 'reviewBlockchainSeedPhrase', - node: node.arguments[0], + node: getReportNode(node.arguments[0]), }); } }, diff --git a/packages/jsts/src/rules/S7639/unit.test.ts b/packages/jsts/src/rules/S7639/unit.test.ts index 32bee79d9b7..27c6522e277 100644 --- a/packages/jsts/src/rules/S7639/unit.test.ts +++ b/packages/jsts/src/rules/S7639/unit.test.ts @@ -134,7 +134,6 @@ describe('S7639', () => { const account = mnemonicToAccount("seed phrase two"); `, errors: 2, - ], }, // Nested function calls { @@ -146,6 +145,15 @@ describe('S7639', () => { `, errors: 1, }, + // HDNodeWallet with named import and variable assignment + { + code: ` + import { HDNodeWallet } from 'ethers' + const mnemonic = 'Pigeons Petted Bushes Effectively Once Krusty Defeated Grapes' + const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic) + `, + errors: 1, + }, ], }); }); From 139ed189534371cfe8dbefc91cb7639956f138c4 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Thu, 13 Nov 2025 15:56:37 +0100 Subject: [PATCH 07/18] actual JS files --- packages/jsts/src/rules/S7639/cb.fixture.ts | 36 +++++ packages/jsts/src/rules/S7639/cb.test.ts | 25 +++ packages/jsts/src/rules/S7639/meta.ts | 3 +- packages/jsts/src/rules/S7639/unit.test.ts | 160 -------------------- 4 files changed, 63 insertions(+), 161 deletions(-) create mode 100644 packages/jsts/src/rules/S7639/cb.fixture.ts create mode 100644 packages/jsts/src/rules/S7639/cb.test.ts delete mode 100644 packages/jsts/src/rules/S7639/unit.test.ts diff --git a/packages/jsts/src/rules/S7639/cb.fixture.ts b/packages/jsts/src/rules/S7639/cb.fixture.ts new file mode 100644 index 00000000000..53203c6c312 --- /dev/null +++ b/packages/jsts/src/rules/S7639/cb.fixture.ts @@ -0,0 +1,36 @@ +import { Wallet } from 'ethers'; +import { mnemonicToAccount } from 'viem/accounts'; +import TronWeb from 'tronweb'; + +// Variable used instead of hardcoded string +const seedPhrase = process.env.SEED_PHRASE; +const wallet = Wallet.fromPhrase(seedPhrase); + +// Function call result used +const wallet = Wallet.fromPhrase(getSeedPhrase()); + +// No arguments passed +const wallet = Wallet.fromPhrase(); + +// Template literal with expression +const account = mnemonicToAccount(`${getPhrase()}`); + +// Ethers with hardcoded string literal +const wallet = Wallet.fromPhrase("Round Outgoing Yanni Gripped Buoyant Iodine Victoriously"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} + +// Viem with hardcoded string +const account = mnemonicToAccount('test test test test test test test test test test test junk'); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} + +// TronWeb with hardcoded string +const wallet = TronWeb.fromMnemonic("candy sugar pudding cream honey rich smooth crumble sweet treat"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} + +// Template literal without expressions +const wallet = Wallet.fromPhrase(`abandon abandon abandon abandon abandon abandon abandon abandon about`); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} + +// CommonJS require +const ethers = require('ethers'); +const wallet = ethers.HDNodeWallet.fromPhrase("test test test test test test test test test test test junk"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} + +// HDNodeWallet with named import and variable assignment +const mnemonic = 'Pigeons Petted Bushes Effectively Once Krusty Defeated Grapes'; // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} +const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic); diff --git a/packages/jsts/src/rules/S7639/cb.test.ts b/packages/jsts/src/rules/S7639/cb.test.ts new file mode 100644 index 00000000000..ecf70af965c --- /dev/null +++ b/packages/jsts/src/rules/S7639/cb.test.ts @@ -0,0 +1,25 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript +import { check } from '../../../tests/tools/testers/comment-based/index.js'; +import { rule } from './index.js'; +import { describe } from 'node:test'; +import * as meta from './generated-meta.js'; + +describe(`Rule S7639`, () => { + check(meta, rule, import.meta.dirname); +}); diff --git a/packages/jsts/src/rules/S7639/meta.ts b/packages/jsts/src/rules/S7639/meta.ts index 395f4f31395..4e11860a3f6 100644 --- a/packages/jsts/src/rules/S7639/meta.ts +++ b/packages/jsts/src/rules/S7639/meta.ts @@ -14,5 +14,6 @@ * You should have received a copy of the Sonar Source-Available License * along with this program; if not, see https://sonarsource.com/license/ssal/ */ +// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript export const implementation = 'original'; -export const eslintId = 'reviewBlockchainSeedPhrase'; +export const eslintId = 'review-blockchain-mnemonic'; diff --git a/packages/jsts/src/rules/S7639/unit.test.ts b/packages/jsts/src/rules/S7639/unit.test.ts deleted file mode 100644 index 27c6522e277..00000000000 --- a/packages/jsts/src/rules/S7639/unit.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * SonarQube JavaScript Plugin - * Copyright (C) 2011-2025 SonarSource Sàrl - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the Sonar Source-Available License for more details. - * - * You should have received a copy of the Sonar Source-Available License - * along with this program; if not, see https://sonarsource.com/license/ssal/ - */ -import { rule } from './index.js'; -import { NoTypeCheckingRuleTester } from '../../../tests/tools/testers/rule-tester.js'; -import { describe, it } from 'node:test'; - -describe('S7639', () => { - it('S7639', () => { - const ruleTester = new NoTypeCheckingRuleTester(); - - ruleTester.run('Hardcoded seed phrases should not be used', rule, { - valid: [ - // No blockchain module imported - { - code: ` - const wallet = Wallet.fromPhrase("test phrase"); - `, - }, - // Variable used instead of hardcoded string - { - code: ` - import { Wallet } from 'ethers'; - const seedPhrase = process.env.SEED_PHRASE; - const wallet = Wallet.fromPhrase(seedPhrase); - `, - }, - // Function call result used - { - code: ` - import { Wallet } from 'ethers'; - const wallet = Wallet.fromPhrase(getSeedPhrase()); - `, - }, - // Different method name - { - code: ` - import { Wallet } from 'ethers'; - const wallet = Wallet.createRandom(); - `, - }, - // No arguments passed - { - code: ` - import { Wallet } from 'ethers'; - const wallet = Wallet.fromPhrase(); - `, - }, - // Template literal with expression - { - code: ` - import { mnemonicToAccount } from 'viem/accounts'; - const account = mnemonicToAccount(\`\${getPhrase()}\`); - `, - }, - // CommonJS require with variable - { - code: ` - const ethers = require('ethers'); - const phrase = readFromFile(); - const wallet = ethers.Wallet.fromPhrase(phrase); - `, - }, - ], - invalid: [ - // Ethers with hardcoded string literal - { - code: ` - import { Wallet } from 'ethers'; - const wallet = Wallet.fromPhrase("Round Outgoing Yanni Gripped Buoyant Iodine Victoriously"); - `, - errors: 1, - }, - // Viem with hardcoded string - { - code: ` - import { mnemonicToAccount } from 'viem/accounts'; - const account = mnemonicToAccount('test test test test test test test test test test test junk'); - `, - errors: 1, - }, - // TronWeb with hardcoded string - { - code: ` - import TronWeb from 'tronweb'; - const wallet = TronWeb.fromMnemonic("candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"); - `, - errors: 1, - }, - // Template literal without expressions - { - code: ` - import { Wallet } from 'ethers'; - const wallet = Wallet.fromPhrase(\`abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\`); - `, - errors: 1, - }, - // String concatenation - { - code: ` - import { HDNodeWallet } from 'ethers'; - const wallet = HDNodeWallet.fromPhrase("abandon abandon " + "abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon about"); - `, - errors: 1, - }, - // CommonJS require - { - code: ` - const ethers = require('ethers'); - const wallet = ethers.HDNodeWallet.fromPhrase("test test test test test test test test test test test junk"); - `, - errors: 1, - }, - // Multiple violations in same file - { - code: ` - import { HDNodeWallet } from 'ethers'; - import { mnemonicToAccount } from 'viem/accounts'; - - const wallet1 = HDNodeWallet.fromPhrase("seed phrase one"); - const account = mnemonicToAccount("seed phrase two"); - `, - errors: 2, - }, - // Nested function calls - { - code: ` - const TronWeb = require('tronweb'); - function createWallet() { - return TronWeb.fromMnemonic("test mnemonic phrase here"); - } - `, - errors: 1, - }, - // HDNodeWallet with named import and variable assignment - { - code: ` - import { HDNodeWallet } from 'ethers' - const mnemonic = 'Pigeons Petted Bushes Effectively Once Krusty Defeated Grapes' - const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic) - `, - errors: 1, - }, - ], - }); - }); -}); From ca8aef98addf283eabf7af53be24b19d193149dd Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Thu, 13 Nov 2025 16:41:44 +0100 Subject: [PATCH 08/18] fix incorrect template --- packages/jsts/src/rules/S7639/cb.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jsts/src/rules/S7639/cb.test.ts b/packages/jsts/src/rules/S7639/cb.test.ts index ecf70af965c..ad69620e0ed 100644 --- a/packages/jsts/src/rules/S7639/cb.test.ts +++ b/packages/jsts/src/rules/S7639/cb.test.ts @@ -15,11 +15,11 @@ * along with this program; if not, see https://sonarsource.com/license/ssal/ */ // https://sonarsource.github.io/rspec/#/rspec/S7639/javascript -import { check } from '../../../tests/tools/testers/comment-based/index.js'; +import { test } from '../../../tests/tools/testers/comment-based/checker.js'; import { rule } from './index.js'; import { describe } from 'node:test'; import * as meta from './generated-meta.js'; describe(`Rule S7639`, () => { - check(meta, rule, import.meta.dirname); + test(meta, rule, import.meta.dirname); }); From a3cd2218d6e871d71b623b8ac6f2a8ac700ad425 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Fri, 14 Nov 2025 11:06:06 +0100 Subject: [PATCH 09/18] fake commit --- packages/jsts/src/rules/S7639/rule.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index b7f63f77a37..f24183d5cad 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -22,7 +22,6 @@ import { generateMeta, isMemberWithProperty, isRequireModule } from '../helpers/ import * as meta from './generated-meta.js'; const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb']; - const MNEMONIC_FUNCTIONS = ['fromPhrase', 'mnemonicToAccount', 'fromMnemonic']; export const rule: Rule.RuleModule = { @@ -76,8 +75,8 @@ export const rule: Rule.RuleModule = { if ( node.id.type === 'Identifier' && node.init && - (node.init.type === 'Literal' && typeof node.init.value === 'string' || - node.init.type === 'TemplateLiteral' && node.init.expressions.length === 0) + ((node.init.type === 'Literal' && typeof node.init.value === 'string') || + (node.init.type === 'TemplateLiteral' && node.init.expressions.length === 0)) ) { hardcodedVariables.set(node.id.name, node.init); } From 4e012dc1ebb12d6d65355cd738038730f85b9d41 Mon Sep 17 00:00:00 2001 From: "Loris S." <91723853+loris-s-sonarsource@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:25:16 +0100 Subject: [PATCH 10/18] fake commit --- packages/jsts/src/rules/S7639/cb.fixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jsts/src/rules/S7639/cb.fixture.ts b/packages/jsts/src/rules/S7639/cb.fixture.ts index 53203c6c312..d180c96fc06 100644 --- a/packages/jsts/src/rules/S7639/cb.fixture.ts +++ b/packages/jsts/src/rules/S7639/cb.fixture.ts @@ -29,7 +29,7 @@ const wallet = Wallet.fromPhrase(`abandon abandon abandon abandon abandon abando // CommonJS require const ethers = require('ethers'); -const wallet = ethers.HDNodeWallet.fromPhrase("test test test test test test test test test test test junk"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} +const wallet = ethers.HDNodeWallet.fromPhrase("fake test test test test test test test test test test junk"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} // HDNodeWallet with named import and variable assignment const mnemonic = 'Pigeons Petted Bushes Effectively Once Krusty Defeated Grapes'; // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}} From d98fcc7a86cac4dd389f72337ae541631100af2a Mon Sep 17 00:00:00 2001 From: "Loris S." <91723853+loris-s-sonarsource@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:41:13 +0100 Subject: [PATCH 11/18] Apply SQ recos --- packages/jsts/src/rules/S7639/rule.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index f24183d5cad..b6948b44a45 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -50,7 +50,9 @@ export const rule: Rule.RuleModule = { function getReportNode(expr: estree.Expression): estree.Node { // If it's an identifier that references a hardcoded string, report the original declaration if (expr.type === 'Identifier' && hardcodedVariables.has(expr.name)) { - return hardcodedVariables.get(expr.name)!; + const nodeName = hardcodedVariables.get(expr.name); + if (nodeName) { + return nodeName; } return expr; } @@ -66,7 +68,7 @@ export const rule: Rule.RuleModule = { }, ImportDeclaration(node: estree.ImportDeclaration) { - if (BLOCKCHAIN_MODULES.includes(node.source.value as any)) { + if (BLOCKCHAIN_MODULES.includes(node.source.value as estree.Literal)) { isBlockchainModuleImported = true; } }, From 697f8a7b0947dff07aa3ee3a570c51979d438cd5 Mon Sep 17 00:00:00 2001 From: "Loris S." <91723853+loris-s-sonarsource@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:46:10 +0100 Subject: [PATCH 12/18] oversight (sorry for the noise guys) --- packages/jsts/src/rules/S7639/rule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index b6948b44a45..8b670255bed 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -53,6 +53,7 @@ export const rule: Rule.RuleModule = { const nodeName = hardcodedVariables.get(expr.name); if (nodeName) { return nodeName; + } } return expr; } From 6d0e196763b2677ee4370c1c213161fc3538eb2a Mon Sep 17 00:00:00 2001 From: "Loris S." <91723853+loris-s-sonarsource@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:50:27 +0100 Subject: [PATCH 13/18] change typecast --- packages/jsts/src/rules/S7639/rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 8b670255bed..40d0da0622e 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -69,7 +69,7 @@ export const rule: Rule.RuleModule = { }, ImportDeclaration(node: estree.ImportDeclaration) { - if (BLOCKCHAIN_MODULES.includes(node.source.value as estree.Literal)) { + if (BLOCKCHAIN_MODULES.includes(node.source.value as string)) { isBlockchainModuleImported = true; } }, From 3dcced9b42c6520d0465eea3809020603467c686 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Mon, 17 Nov 2025 10:41:00 +0100 Subject: [PATCH 14/18] Add its --- .../test/expected/jsts/file-for-rules/javascript-S7639.json | 6 ++++++ its/sources/jsts/custom/S7639.js | 4 ++++ packages/jsts/src/rules/S7639/cb.fixture.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json create mode 100644 its/sources/jsts/custom/S7639.js diff --git a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json new file mode 100644 index 00000000000..d0321955635 --- /dev/null +++ b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json @@ -0,0 +1,6 @@ +{ +"file-for-rules:S6739.js": [ +3 +] +} + diff --git a/its/sources/jsts/custom/S7639.js b/its/sources/jsts/custom/S7639.js new file mode 100644 index 00000000000..12385fe56ea --- /dev/null +++ b/its/sources/jsts/custom/S7639.js @@ -0,0 +1,4 @@ +import { HDNodeWallet } from 'ethers' + +const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously' +const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic) diff --git a/packages/jsts/src/rules/S7639/cb.fixture.ts b/packages/jsts/src/rules/S7639/cb.fixture.ts index d180c96fc06..fc323b6ba4e 100644 --- a/packages/jsts/src/rules/S7639/cb.fixture.ts +++ b/packages/jsts/src/rules/S7639/cb.fixture.ts @@ -1,4 +1,4 @@ -import { Wallet } from 'ethers'; +import { Wallet, HDNodeWallet } from 'ethers'; import { mnemonicToAccount } from 'viem/accounts'; import TronWeb from 'tronweb'; From 3742378f6137de77b42247a4119538646cd947b1 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Mon, 17 Nov 2025 10:46:36 +0100 Subject: [PATCH 15/18] added guardrail to avoid collecting all strings --- packages/jsts/src/rules/S7639/rule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index 40d0da0622e..d1d1fd47435 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -76,6 +76,7 @@ export const rule: Rule.RuleModule = { VariableDeclarator(node: estree.VariableDeclarator) { if ( + isBlockchainModuleImported && node.id.type === 'Identifier' && node.init && ((node.init.type === 'Literal' && typeof node.init.value === 'string') || From e3b4b0ce76a99ac0cbc73fabb29a7e3edbdf7cff Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Mon, 17 Nov 2025 10:48:26 +0100 Subject: [PATCH 16/18] tweak oversight --- its/sources/jsts/custom/S7639.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/sources/jsts/custom/S7639.js b/its/sources/jsts/custom/S7639.js index 12385fe56ea..b67d9c56e5d 100644 --- a/its/sources/jsts/custom/S7639.js +++ b/its/sources/jsts/custom/S7639.js @@ -1,4 +1,4 @@ import { HDNodeWallet } from 'ethers' -const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously' +const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously' // Noncompliant const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic) From af5a8099e9f130d512c1f77453e693aa0583e293 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Mon, 17 Nov 2025 17:27:27 +0100 Subject: [PATCH 17/18] remove newline --- .../src/test/expected/jsts/file-for-rules/javascript-S7639.json | 1 - 1 file changed, 1 deletion(-) diff --git a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json index d0321955635..27bfee5e1e4 100644 --- a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json +++ b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json @@ -3,4 +3,3 @@ 3 ] } - From 7c0a354dbe8c13459a418d1d648d0bf6b1e8a7b2 Mon Sep 17 00:00:00 2001 From: Loris Sierra Date: Mon, 17 Nov 2025 17:54:12 +0100 Subject: [PATCH 18/18] finally getting out of that rabbit hole --- .../jsts/file-for-rules/javascript-S1451.json | 3 +++ .../jsts/file-for-rules/javascript-S7639.json | 2 +- its/sources/jsts/custom/S7639.js | 6 +++--- packages/jsts/src/rules/S7639/rule.ts | 11 +++++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S1451.json b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S1451.json index 8430e04eae1..3cdf632e966 100644 --- a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S1451.json +++ b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S1451.json @@ -209,6 +209,9 @@ "file-for-rules:S6859.js": [ 0 ], +"file-for-rules:S7639.js": [ +0 +], "file-for-rules:boundOrAssignedEvalOrArguments.js": [ 0 ] diff --git a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json index 27bfee5e1e4..a98662484d3 100644 --- a/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json +++ b/its/ruling/src/test/expected/jsts/file-for-rules/javascript-S7639.json @@ -1,5 +1,5 @@ { -"file-for-rules:S6739.js": [ +"file-for-rules:S7639.js": [ 3 ] } diff --git a/its/sources/jsts/custom/S7639.js b/its/sources/jsts/custom/S7639.js index b67d9c56e5d..6a8afe08e09 100644 --- a/its/sources/jsts/custom/S7639.js +++ b/its/sources/jsts/custom/S7639.js @@ -1,4 +1,4 @@ -import { HDNodeWallet } from 'ethers' +import { HDNodeWallet } from 'ethers'; -const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously' // Noncompliant -const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic) +const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously'; // Noncompliant +const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic); diff --git a/packages/jsts/src/rules/S7639/rule.ts b/packages/jsts/src/rules/S7639/rule.ts index d1d1fd47435..870c57e1713 100644 --- a/packages/jsts/src/rules/S7639/rule.ts +++ b/packages/jsts/src/rules/S7639/rule.ts @@ -18,7 +18,12 @@ import type { Rule } from 'eslint'; import type estree from 'estree'; -import { generateMeta, isMemberWithProperty, isRequireModule } from '../helpers/index.js'; +import { + generateMeta, + isIdentifier, + isMemberWithProperty, + isRequireModule, +} from '../helpers/index.js'; import * as meta from './generated-meta.js'; const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb']; @@ -59,7 +64,9 @@ export const rule: Rule.RuleModule = { } function isMnemonicFunction(callee: estree.Expression | estree.Super): boolean { - return MNEMONIC_FUNCTIONS.some(func => isMemberWithProperty(callee, func)); + return MNEMONIC_FUNCTIONS.some( + func => isMemberWithProperty(callee, func) || isIdentifier(callee, func), + ); } return {