Skip to content

Commit 5665691

Browse files
JS-824 Add rule S7639 (#5919)
1 parent 54b1c09 commit 5665691

File tree

8 files changed

+226
-0
lines changed

8 files changed

+226
-0
lines changed

its/ruling/src/test/expected/jsts/file-for-rules/javascript-S1451.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@
209209
"file-for-rules:S6859.js": [
210210
0
211211
],
212+
"file-for-rules:S7639.js": [
213+
0
214+
],
212215
"file-for-rules:boundOrAssignedEvalOrArguments.js": [
213216
0
214217
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"file-for-rules:S7639.js": [
3+
3
4+
]
5+
}

its/sources/jsts/custom/S7639.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { HDNodeWallet } from 'ethers';
2+
3+
const mnemonic = 'Powerful Burning Muppets Betrayed Clerks Meanwhile Superb Spies Denounced Silly Leeks Cautiously'; // Noncompliant
4+
const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Wallet, HDNodeWallet } from 'ethers';
2+
import { mnemonicToAccount } from 'viem/accounts';
3+
import TronWeb from 'tronweb';
4+
5+
// Variable used instead of hardcoded string
6+
const seedPhrase = process.env.SEED_PHRASE;
7+
const wallet = Wallet.fromPhrase(seedPhrase);
8+
9+
// Function call result used
10+
const wallet = Wallet.fromPhrase(getSeedPhrase());
11+
12+
// No arguments passed
13+
const wallet = Wallet.fromPhrase();
14+
15+
// Template literal with expression
16+
const account = mnemonicToAccount(`${getPhrase()}`);
17+
18+
// Ethers with hardcoded string literal
19+
const wallet = Wallet.fromPhrase("Round Outgoing Yanni Gripped Buoyant Iodine Victoriously"); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}}
20+
21+
// Viem with hardcoded string
22+
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.}}
23+
24+
// TronWeb with hardcoded string
25+
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.}}
26+
27+
// Template literal without expressions
28+
const wallet = Wallet.fromPhrase(`abandon abandon abandon abandon abandon abandon abandon abandon about`); // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}}
29+
30+
// CommonJS require
31+
const ethers = require('ethers');
32+
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.}}
33+
34+
// HDNodeWallet with named import and variable assignment
35+
const mnemonic = 'Pigeons Petted Bushes Effectively Once Krusty Defeated Grapes'; // Noncompliant {{Revoke and change this seed phrase, as it is compromised.}}
36+
const mnemonicWallet = HDNodeWallet.fromPhrase(mnemonic);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript
18+
import { test } from '../../../tests/tools/testers/comment-based/checker.js';
19+
import { rule } from './index.js';
20+
import { describe } from 'node:test';
21+
import * as meta from './generated-meta.js';
22+
23+
describe(`Rule S7639`, () => {
24+
test(meta, rule, import.meta.dirname);
25+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
export { rule } from './rule.js';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript
18+
export const implementation = 'original';
19+
export const eslintId = 'review-blockchain-mnemonic';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
// https://sonarsource.github.io/rspec/#/rspec/S7639/javascript
18+
19+
import type { Rule } from 'eslint';
20+
import type estree from 'estree';
21+
import {
22+
generateMeta,
23+
isIdentifier,
24+
isMemberWithProperty,
25+
isRequireModule,
26+
} from '../helpers/index.js';
27+
import * as meta from './generated-meta.js';
28+
29+
const BLOCKCHAIN_MODULES = ['ethers', 'viem/accounts', 'tronweb'];
30+
const MNEMONIC_FUNCTIONS = ['fromPhrase', 'mnemonicToAccount', 'fromMnemonic'];
31+
32+
export const rule: Rule.RuleModule = {
33+
meta: generateMeta(meta, {
34+
messages: {
35+
reviewBlockchainSeedPhrase: `Revoke and change this seed phrase, as it is compromised.`,
36+
},
37+
}),
38+
create(context: Rule.RuleContext) {
39+
let isBlockchainModuleImported = false;
40+
const hardcodedVariables = new Map<string, estree.Node>();
41+
42+
function isHardcodedString(expr: estree.Expression): boolean {
43+
switch (expr.type) {
44+
case 'Literal':
45+
return typeof expr.value === 'string';
46+
case 'TemplateLiteral':
47+
return expr.expressions.length === 0;
48+
case 'Identifier':
49+
return hardcodedVariables.has(expr.name);
50+
default:
51+
return false;
52+
}
53+
}
54+
55+
function getReportNode(expr: estree.Expression): estree.Node {
56+
// If it's an identifier that references a hardcoded string, report the original declaration
57+
if (expr.type === 'Identifier' && hardcodedVariables.has(expr.name)) {
58+
const nodeName = hardcodedVariables.get(expr.name);
59+
if (nodeName) {
60+
return nodeName;
61+
}
62+
}
63+
return expr;
64+
}
65+
66+
function isMnemonicFunction(callee: estree.Expression | estree.Super): boolean {
67+
return MNEMONIC_FUNCTIONS.some(
68+
func => isMemberWithProperty(callee, func) || isIdentifier(callee, func),
69+
);
70+
}
71+
72+
return {
73+
Program() {
74+
isBlockchainModuleImported = false;
75+
hardcodedVariables.clear();
76+
},
77+
78+
ImportDeclaration(node: estree.ImportDeclaration) {
79+
if (BLOCKCHAIN_MODULES.includes(node.source.value as string)) {
80+
isBlockchainModuleImported = true;
81+
}
82+
},
83+
84+
VariableDeclarator(node: estree.VariableDeclarator) {
85+
if (
86+
isBlockchainModuleImported &&
87+
node.id.type === 'Identifier' &&
88+
node.init &&
89+
((node.init.type === 'Literal' && typeof node.init.value === 'string') ||
90+
(node.init.type === 'TemplateLiteral' && node.init.expressions.length === 0))
91+
) {
92+
hardcodedVariables.set(node.id.name, node.init);
93+
}
94+
},
95+
96+
CallExpression(node: estree.CallExpression) {
97+
if (isRequireModule(node, ...BLOCKCHAIN_MODULES)) {
98+
isBlockchainModuleImported = true;
99+
return;
100+
}
101+
102+
if (
103+
isBlockchainModuleImported &&
104+
isMnemonicFunction(node.callee) &&
105+
node.arguments.length > 0 &&
106+
node.arguments[0].type !== 'SpreadElement' &&
107+
isHardcodedString(node.arguments[0])
108+
) {
109+
context.report({
110+
messageId: 'reviewBlockchainSeedPhrase',
111+
node: getReportNode(node.arguments[0]),
112+
});
113+
}
114+
},
115+
};
116+
},
117+
};

0 commit comments

Comments
 (0)