diff --git a/README.md b/README.md index b0bf5a7d481..34420661dad 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,26 @@ If you want to report a bug, request a feature, or provide other kind of feedbac # Contributing -#### 1. Request a new feature +## Prerequisites + +To work on this project, it is required to have the following tools installed: + +- [JDK 17](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/what-is-corretto-17.html) +- [Node.js](https://nodejs.org/en) >= 22 +- [npm](https://www.npmjs.com/) >= 8 +- [Maven](https://maven.apache.org/) >= 3.8 + +## How-to + +### 1. Request a new feature To request a new feature, create a new thread in [SonarSource Community Forum](https://community.sonarsource.com/). Even if you plan to implement it yourself and submit it back to the community, please create a thread to be sure that we can follow up on it. -#### 2. Pull Request +### 2. Pull Request To submit a contribution, create a pull request for this repository. Please make sure that you follow our [code style](https://github.com/SonarSource/sonar-developer-toolset) and that all [tests](/docs/DEV.md#testing) are passing. -#### Work with us +## Work with us Would you like to work on this project full-time? We are hiring! Check out https://www.sonarsource.com/hiring diff --git a/docs/DEV.md b/docs/DEV.md index 4a4c4dbd0ad..4516f132094 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -2,11 +2,14 @@ ## Prerequisites +To work on this project, it is required to have the following tools installed: + - [JDK 17](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/what-is-corretto-17.html) -- [Maven](https://maven.apache.org/install.html) -- Node.js (we recommend using [NVM](https://github.com/nvm-sh/nvm#installing-and-updating)) +- [Node.js](https://nodejs.org/en) >= 22 +- [npm](https://www.npmjs.com/) >= 8 +- [Maven](https://maven.apache.org/) >= 3.8 -You can also use Docker container defined in `./.cirrus/nodejs-lts.Dockerfile` which bundles all required dependencies and is used for our CI pipeline. +You can also use Docker container defined in `./.cirrus/nodejs.Dockerfile` which bundles all required dependencies and is used for our CI pipeline. ## Build and run unit tests diff --git a/its/ruling/src/test/expected/jsts/Ghost/javascript-S6418.json b/its/ruling/src/test/expected/jsts/Ghost/javascript-S6418.json new file mode 100644 index 00000000000..978fa5f6633 --- /dev/null +++ b/its/ruling/src/test/expected/jsts/Ghost/javascript-S6418.json @@ -0,0 +1,6 @@ +{ +"Ghost:core/client/app/mirage/config.js": [ +59, +61 +] +} diff --git a/its/ruling/src/test/expected/jsts/searchkit/javascript-S6418.json b/its/ruling/src/test/expected/jsts/searchkit/javascript-S6418.json new file mode 100644 index 00000000000..1fc23f7cc4e --- /dev/null +++ b/its/ruling/src/test/expected/jsts/searchkit/javascript-S6418.json @@ -0,0 +1,5 @@ +{ +"searchkit:examples/next/components/sdk-example/index.jsx": [ +34 +] +} diff --git a/package.json b/package.json index 4d711fc5ed2..9c24769d615 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "homepage": "https://github.com/SonarSource/SonarJS#readme", "engines": { - "node": "^18.17.0 || ^20.9.0 || >=21.1.0" + "node": ">=22" }, "type": "module", "devDependencies": { diff --git a/packages/jsts/src/rules/S2068/meta.ts b/packages/jsts/src/rules/S2068/meta.ts index 53f3fd6ac0d..fc294fb3da1 100644 --- a/packages/jsts/src/rules/S2068/meta.ts +++ b/packages/jsts/src/rules/S2068/meta.ts @@ -16,4 +16,4 @@ */ export * from './generated-meta.js'; export const implementation = 'original'; -export const eslintId = 'no-hardcoded-credentials'; +export const eslintId = 'no-hardcoded-passwords'; diff --git a/packages/jsts/src/rules/S2068/rule.ts b/packages/jsts/src/rules/S2068/rule.ts index e05d8232bbc..b2f30ad7a30 100644 --- a/packages/jsts/src/rules/S2068/rule.ts +++ b/packages/jsts/src/rules/S2068/rule.ts @@ -26,7 +26,7 @@ import { meta, schema } from './meta.js'; const DEFAULT_NAMES = ['password', 'pwd', 'passwd']; const messages = { - reviewCredential: 'Review this potentially hardcoded credential.', + reviewPassword: 'Review this potentially hard-coded password.', }; export const rule: Rule.RuleModule = { @@ -39,7 +39,7 @@ export const rule: Rule.RuleModule = { } const variableNames = - (context.options as FromSchema)[0]?.credentialWords ?? DEFAULT_NAMES; + (context.options as FromSchema)[0]?.passwordWords ?? DEFAULT_NAMES; const literalRegExp = variableNames.map(name => new RegExp(`${name}=.+`)); return { VariableDeclarator: (node: estree.Node) => { @@ -75,7 +75,7 @@ function checkAssignment( patterns.some(pattern => context.sourceCode.getText(variable).includes(pattern)) ) { context.report({ - messageId: 'reviewCredential', + messageId: 'reviewPassword', node: initializer, }); } @@ -84,7 +84,7 @@ function checkAssignment( function checkLiteral(context: Rule.RuleContext, patterns: RegExp[], literal: estree.Literal) { if (isStringLiteral(literal) && patterns.some(pattern => pattern.test(literal.value as string))) { context.report({ - messageId: 'reviewCredential', + messageId: 'reviewPassword', node: literal, }); } diff --git a/packages/jsts/src/rules/S2068/unit.test.ts b/packages/jsts/src/rules/S2068/unit.test.ts index b4e357202bb..6ac9275ec92 100644 --- a/packages/jsts/src/rules/S2068/unit.test.ts +++ b/packages/jsts/src/rules/S2068/unit.test.ts @@ -24,9 +24,9 @@ const ruleTester = new NodeRuleTester({ parserOptions: { ecmaVersion: 2018, sourceType: 'module' }, }); -const options = [{ credentialWords: ['password', 'pwd', 'passwd'] }]; +const options = [{ passwordWords: ['password', 'pwd', 'passwd'] }]; -ruleTester.run('Hardcoded credentials should be avoided', rule, { +ruleTester.run('Hard-coded passwords should be avoided', rule, { valid: [ { code: `let password = ""`, @@ -44,7 +44,7 @@ ruleTester.run('Hardcoded credentials should be avoided', rule, { options, errors: [ { - message: 'Review this potentially hardcoded credential.', + message: 'Review this potentially hard-coded password.', line: 1, endLine: 1, column: 16, @@ -66,7 +66,7 @@ ruleTester.run('Hardcoded credentials should be avoided', rule, { errors: 1, }, { - code: `let credentials = { user: "foo", passwd: "bar" };`, + code: `let passwords = { user: "foo", passwd: "bar" };`, options, errors: 1, }, @@ -77,12 +77,12 @@ ruleTester.run('Hardcoded credentials should be avoided', rule, { }, { code: `let secret = "foo"`, - options: [{ credentialWords: ['secret'] }], + options: [{ passwordWords: ['secret'] }], errors: 1, }, { code: `let url = "https://example.com?token=hl2OAIXXZ60";`, - options: [{ credentialWords: ['token'] }], + options: [{ passwordWords: ['token'] }], errors: 1, }, { diff --git a/packages/jsts/src/rules/S6418/cb.fixture.ts b/packages/jsts/src/rules/S6418/cb.fixture.ts new file mode 100644 index 00000000000..229e4a9453a --- /dev/null +++ b/packages/jsts/src/rules/S6418/cb.fixture.ts @@ -0,0 +1,53 @@ +function func() { + const token = 'rf6acB24J//1FZLRrKpjmBUYSnUX5CHlt/iD5vVVcgVuAIOB6hzcWjDnv16V6hDLevW0Qs4hKPbP1M4YfuDI16sZna1/VGRLkAbTk6xMPs4epH6A3ZqSyyI-H92y' // Noncompliant +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let api_key = 'not enough entropy' + api_key = 'rf6acB24J//1FZLRrKpjmBUYSnUX5CHlt/iD5vVVcgVuAIOB6hzcWjDnv16V6hDLevW0Qs4hKPbP1M4YfuDI16sZna1/VGRLkAbTk6xMPs4epH6A3ZqSyyI-H92y' // Noncompliant +} +function entropyTooLow() { + const token = 'rf6acB24J//1FZLRrKpjmBUYSnUX5CHlt/iD5vVaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +} +class MyClass { + secret = '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.=' // Noncompliant +} + +function inFunctionCall() { + callWithSecret({ secret: '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.=' }) // Noncompliant + + function callWithSecret({}) {} +} +function functionWithSecret({ secret = '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.=' }) { // Noncompliant +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + } +function cleanFunction(someArg, parameter='a string', anotherParameter: 42, ...args) { + another_call(42, 'a string', parameter, { a_keyword: 42 }, args) + + function another_call(...foo) {} +} + +const someObject = { + secret: '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.=', // Noncompliant +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + not_a_problem: 'not_a_secret', + 42: 'forty-two' +} + +function multipleAssignment() { + let nothing = 1, secret = '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v.~=', nothing_else = 2; // Noncompliant +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} +function assignmentWithType() { + const secret: string = '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.=' // Noncompliant + let someVar: string; + const anotherVar: number = 42 +} + +function defaultValues(foo) { + let secret; + secret = foo || '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.='; // Noncompliant + secret = foo ?? '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.='; // Noncompliant +} + +function customSecretWord() { + const yolo = '1IfHMPanImzX8ZxC-Ud6+YhXiLwlXq$f_-3v~.='; // Noncompliant +} diff --git a/packages/jsts/src/rules/S6418/cb.options.json b/packages/jsts/src/rules/S6418/cb.options.json new file mode 100644 index 00000000000..2c5c1b0c27f --- /dev/null +++ b/packages/jsts/src/rules/S6418/cb.options.json @@ -0,0 +1,6 @@ +[ + { + "secretWords": "api[_.-]?key,auth,credential,secret,token,yolo", + "randomnessSensibility": 5.0 + } +] diff --git a/packages/jsts/src/rules/S6418/cb.test.ts b/packages/jsts/src/rules/S6418/cb.test.ts new file mode 100644 index 00000000000..5d963d44d3f --- /dev/null +++ b/packages/jsts/src/rules/S6418/cb.test.ts @@ -0,0 +1,26 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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 { check } from '../../../tests/tools/index.js'; +import { rule } from './index.js'; +import path from 'path'; +import { describe } from 'node:test'; + +const sonarId = path.basename(import.meta.dirname); + +describe('Rule S6418', () => { + check(sonarId, rule, import.meta.dirname); +}); diff --git a/packages/jsts/src/rules/S6418/index.ts b/packages/jsts/src/rules/S6418/index.ts new file mode 100644 index 00000000000..af51ac13713 --- /dev/null +++ b/packages/jsts/src/rules/S6418/index.ts @@ -0,0 +1,17 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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/S6418/meta.ts b/packages/jsts/src/rules/S6418/meta.ts new file mode 100644 index 00000000000..de89c6c0265 --- /dev/null +++ b/packages/jsts/src/rules/S6418/meta.ts @@ -0,0 +1,19 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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 * from './generated-meta.js'; +export const implementation = 'original'; +export const eslintId = 'no-hardcoded-secrets'; diff --git a/packages/jsts/src/rules/S6418/rule.ts b/packages/jsts/src/rules/S6418/rule.ts new file mode 100644 index 00000000000..1fee234a459 --- /dev/null +++ b/packages/jsts/src/rules/S6418/rule.ts @@ -0,0 +1,212 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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/S6418/javascript + +import type { Rule } from 'eslint'; +import { + generateMeta, + isIdentifier, + isLogicalExpression, + isStringLiteral, +} from '../helpers/index.js'; +import { meta } from './meta.js'; +import { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; +import { FromSchema } from 'json-schema-to-ts'; +import estree from 'estree'; +import { TSESTree } from '@typescript-eslint/utils'; + +const DEFAULT_SECRET_WORDS = 'api[_.-]?key,auth,credential,secret,token'; +const DEFAULT_RANDOMNESS_SENSIBILITY = 5.0; +const POSTVALIDATION_PATTERN = + /[a-zA-Z0-9_.+/~$-]([a-zA-Z0-9_.+/=~$-]|\\\\\\\\(?![ntr"])){14,1022}[a-zA-Z0-9_.+/=~$-]/; + +function message(name: string): string { + return `"${name}" detected here, make sure this is not a hard-coded secret.`; +} + +let randomnessSensibility: number; +let secretWordRegexps: RegExp[]; + +const schema = { + type: 'array', + minItems: 0, + maxItems: 1, + items: [ + { + type: 'object', + properties: { + secretWords: { + type: 'string', + }, + randomnessSensibility: { + type: 'number', + }, + }, + additionalProperties: false, + }, + ], +} as const satisfies JSONSchema4; + +export const rule: Rule.RuleModule = { + meta: generateMeta( + meta as Rule.RuleMetaData, + { schema }, + false /* true if secondary locations */, + ), + create(context: Rule.RuleContext) { + // get typed rule options with FromSchema helper + const secretWords = + (context.options as FromSchema)[0]?.['secretWords'] ?? DEFAULT_SECRET_WORDS; + secretWordRegexps = buildSecretWordRegexps(secretWords); + randomnessSensibility = + (context.options as FromSchema)[0]?.['randomnessSensibility'] ?? + DEFAULT_RANDOMNESS_SENSIBILITY; + + return { + AssignmentExpression(node) { + handleAssignmentExpression(context, node); + }, + AssignmentPattern(node) { + handleAssignmentPattern(context, node); + }, + Property(node) { + handlePropertyAndPropertyDefinition(context, node); + }, + PropertyDefinition(node) { + handlePropertyAndPropertyDefinition(context, node); + }, + VariableDeclarator(node) { + handleVariableDeclarator(context, node); + }, + }; + }, +}; + +function handleAssignmentExpression(context: Rule.RuleContext, node: estree.AssignmentExpression) { + const keySuspect = findKeySuspect(node.left); + const valueSuspect = findValueSuspect(extractDefaultOperatorIfNeeded(node)); + if (keySuspect && valueSuspect) { + context.report({ + node: node.right, + message: message(keySuspect), + }); + } + function extractDefaultOperatorIfNeeded(node: estree.AssignmentExpression): estree.Node { + const defaultOperators = ['??', '||']; + if ( + isLogicalExpression(node.right as TSESTree.Node) && + defaultOperators.includes((node.right as estree.LogicalExpression).operator) + ) { + return (node.right as estree.LogicalExpression).right; + } else { + return node.right; + } + } +} +function handleAssignmentPattern(context: Rule.RuleContext, node: estree.AssignmentPattern) { + const keySuspect = findKeySuspect(node.left); + const valueSuspect = findValueSuspect(node.right); + if (keySuspect && valueSuspect) { + context.report({ + node: node.right, + message: message(keySuspect), + }); + } +} +function handlePropertyAndPropertyDefinition( + context: Rule.RuleContext, + node: estree.Property | estree.PropertyDefinition, +) { + const keySuspect = findKeySuspect(node.key); + const valueSuspect = findValueSuspect(node.value); + if (keySuspect && valueSuspect) { + context.report({ + node: node.value as estree.Literal, + message: message(keySuspect), + }); + } +} +function handleVariableDeclarator(context: Rule.RuleContext, node: estree.VariableDeclarator) { + const keySuspect = findKeySuspect(node.id); + const valueSuspect = findValueSuspect(node.init); + if (keySuspect && valueSuspect) { + context.report({ + node: node.init as estree.Literal, + message: message(keySuspect), + }); + } +} + +function findKeySuspect(node: estree.Node): string | undefined { + if (isIdentifier(node) && secretWordRegexps.some(pattern => pattern.test(node.name))) { + return node.name; + } else { + return undefined; + } +} + +function findValueSuspect(node: estree.Node | undefined | null): estree.Node | undefined { + if ( + node && + isStringLiteral(node) && + valuePassesPostValidation(node.value) && + entropyShouldRaise(node.value) + ) { + return node; + } else { + return undefined; + } +} + +function valuePassesPostValidation(value: string): boolean { + return POSTVALIDATION_PATTERN.test(value); +} + +function buildSecretWordRegexps(secretWords: string) { + try { + return secretWords.split(',').map(word => new RegExp(`(${word})`, 'i')); + } catch (e) { + console.error( + `Invalid characters provided to rule S6418 'no-hardcoded-secrets' parameter "secretWords": "${secretWords}" falling back to default: "${DEFAULT_SECRET_WORDS}". Error: ${e}`, + ); + return buildSecretWordRegexps(DEFAULT_SECRET_WORDS); + } +} + +function entropyShouldRaise(value: string): boolean { + return ShannonEntropy.calculate(value) > randomnessSensibility; +} + +const ShannonEntropy = { + calculate: (str: string): number => { + if (!str) { + return 0; + } + const lettersTotal = str.length; + const occurences: Record = {}; + for (const letter of [...str]) { + occurences[letter] = (occurences[letter] ?? 0) + 1; + } + const values = Object.values(occurences); + return ( + values + .map(count => count / lettersTotal) + .map(frequency => -frequency * Math.log(frequency)) + .reduce((acc, entropy) => acc + entropy, 0) / Math.log(2) + ); + }, +}; diff --git a/packages/jsts/src/rules/S6418/unit.test.ts b/packages/jsts/src/rules/S6418/unit.test.ts new file mode 100644 index 00000000000..55f4eccd90c --- /dev/null +++ b/packages/jsts/src/rules/S6418/unit.test.ts @@ -0,0 +1,39 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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 { JavaScriptRuleTester } from '../../../tests/tools/index.js'; +import { rule } from './rule.js'; + +const ruleTester = new JavaScriptRuleTester(); + +ruleTester.run('Rule S6418 - no-hardcoded-secrets', rule, { + valid: [], + invalid: [ + // we're verifying that given a broken RegExp, the rule still works. + { + code: ` + secret = '9ah9w8dha9w8hd98h'; + `, + options: [ + { + secretWords: 'sel/\\', + randomnessSensibility: 0.5, + }, + ], + errors: 1, + }, + ], +}); diff --git a/pom.xml b/pom.xml index cd2ae3b75f1..40417fe5bb8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.sonarsource.parent parent - 81.0.0.2300 + 82.0.0.2314 org.sonarsource.javascript diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarning.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarning.java index 25683b24756..518bf176459 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarning.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarning.java @@ -36,8 +36,8 @@ public class NodeDeprecationWarning { */ static final Version MIN_SUPPORTED_NODE_VERSION = Version.create(18, 17, 0); - private static final int MIN_RECOMMENDED_NODE_VERSION = 18; - private static final List RECOMMENDED_NODE_VERSIONS = List.of("^18.18.0", "^20.9.0", "^22.9.0"); + private static final int MIN_RECOMMENDED_NODE_VERSION = 20; + private static final List RECOMMENDED_NODE_VERSIONS = List.of("^20.9.0", "^22.9.0"); private final AnalysisWarningsWrapper analysisWarnings; public NodeDeprecationWarning(AnalysisWarningsWrapper analysisWarnings) { diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarningTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarningTest.java index c231e1f46fe..9ca2b7016b3 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarningTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/NodeDeprecationWarningTest.java @@ -48,7 +48,7 @@ void test_unsupported() { deprecationWarning.logNodeDeprecation(16); assertWarnings( "Using Node.js version 16 to execute analysis is not supported. " + - "Please upgrade to a newer LTS version of Node.js: [^18.18.0, ^20.9.0, ^22.9.0]."); + "Please upgrade to a newer LTS version of Node.js: [^20.9.0, ^22.9.0]."); } @Test @@ -57,7 +57,8 @@ void test_supported() { deprecationWarning.logNodeDeprecation(20); deprecationWarning.logNodeDeprecation(21); deprecationWarning.logNodeDeprecation(22); - assertWarnings(); + assertWarnings("Using Node.js version 18 to execute analysis is not supported. " + + "Please upgrade to a newer LTS version of Node.js: [^20.9.0, ^22.9.0]."); } private void assertWarnings(String... messages) { diff --git a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/CheckList.java b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/CheckList.java index b5e9996863c..6a29ed2d353 100644 --- a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/CheckList.java +++ b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/CheckList.java @@ -180,7 +180,6 @@ public static List> getAllChecks() { GlobalThisCheck.class, GlobalsShadowingCheck.class, GratuitousConditionCheck.class, - HardcodedCredentialsCheck.class, HashingCheck.class, HeadingHasContentCheck.class, HiddenFilesCheck.class, @@ -276,6 +275,8 @@ public static List> getAllChecks() { NoFindDomNodeCheck.class, NoForInArrayCheck.class, NoHardcodedIpCheck.class, + NoHardcodedPasswordsCheck.class, + NoHardcodedSecretsCheck.class, NoHookSetterInBodyCheck.class, NoIgnoredExceptionsCheck.class, NoImportAssignCheck.class, diff --git a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedCredentialsCheck.java b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheck.java similarity index 81% rename from sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedCredentialsCheck.java rename to sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheck.java index b84af7fb7cd..dc5614bc553 100644 --- a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedCredentialsCheck.java +++ b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheck.java @@ -27,32 +27,28 @@ @JavaScriptRule @TypeScriptRule @Rule(key = "S2068") -public class HardcodedCredentialsCheck extends Check { +public class NoHardcodedPasswordsCheck extends Check { private static final String DEFAULT = "password, pwd, passwd"; @RuleProperty( - key = "credentialWords", - description = "Comma separated list of words identifying potential credentials.", + key = "passwordWords", + description = "Comma separated list of words identifying potential passwords.", defaultValue = "" + DEFAULT ) - public String credentialWords = DEFAULT; + public String passwordWords = DEFAULT; @Override public List configurations() { return Collections.singletonList( - new Config(credentialWords.split("\\s*,\\s*")) + new Config(passwordWords.split("\\s*,\\s*")) ); } - - private static class Config { - - String[] credentialWords; - - Config(String[] credentialWords) { - this.credentialWords = credentialWords; + String[] passwordWords; + Config(String[] passwordWords) { + this.passwordWords = passwordWords; } } } diff --git a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedSecretsCheck.java b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedSecretsCheck.java new file mode 100644 index 00000000000..9e02d95f5e7 --- /dev/null +++ b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/NoHardcodedSecretsCheck.java @@ -0,0 +1,65 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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/ + */ +package org.sonar.javascript.checks; + +import java.util.Collections; +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; +import org.sonar.plugins.javascript.api.Check; +import org.sonar.plugins.javascript.api.JavaScriptRule; +import org.sonar.plugins.javascript.api.TypeScriptRule; + +@TypeScriptRule +@JavaScriptRule +@Rule(key = "S6418") +public class NoHardcodedSecretsCheck extends Check { + + private static final String DEFAULT_SECRET_WORDS = "api[_.-]?key,auth,credential,secret,token"; + private static final String DEFAULT_RANDOMNESS_SENSIBILITY = "5.0"; + + @RuleProperty( + key = "secretWords", + description = "Comma separated list of words identifying potential secrets", + defaultValue = DEFAULT_SECRET_WORDS + ) + public String secretWords = DEFAULT_SECRET_WORDS; + @RuleProperty( + key = "randomnessSensibility", + description = "Minimum shannon entropy threshold of the secret", + defaultValue = DEFAULT_RANDOMNESS_SENSIBILITY + ) + public String randomnessSensibility = DEFAULT_RANDOMNESS_SENSIBILITY; + + @Override + public List configurations() { + return Collections.singletonList( + new Config(secretWords, randomnessSensibility) + ); + } + + private static class Config { + + String secretWords; + String randomnessSensibility; + + Config(String secretWords, String randomnessSensibility) { + this.secretWords = secretWords; + this.randomnessSensibility = randomnessSensibility; + } + } +} diff --git a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.html b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.html index d757c821346..badd1be11bd 100644 --- a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.html +++ b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.html @@ -1,34 +1,32 @@ -

Because it is easy to extract strings from an application source code or binary, credentials should not be hard-coded. This is particularly true -for applications that are distributed or that are open-source.

+

Because it is easy to extract strings from an application source code or binary, passwords should not be hard-coded. This is particularly true for +applications that are distributed or that are open-source.

In the past, it has led to the following vulnerabilities:

-

Credentials should be stored outside of the code in a configuration file, a database, or a management service for secrets.

-

This rule flags instances of hard-coded credentials used in database and LDAP connections. It looks for hard-coded credentials in connection -strings, and for variable names that match any of the patterns from the provided list.

-

It’s recommended to customize the configuration of this rule with additional credential words such as "oauthToken", "secret", …​

+

Passwords should be stored outside of the code in a configuration file, a database, or a management service for passwords.

+

This rule flags instances of hard-coded passwords used in database and LDAP connections. It looks for hard-coded passwords in connection strings, +and for variable names that match any of the patterns from the provided list.

Ask Yourself Whether

    -
  • Credentials allow access to a sensitive component like a database, a file storage, an API or a service.
  • -
  • Credentials are used in production environments.
  • -
  • Application re-distribution is required before updating the credentials.
  • +
  • Passwords allow access to a sensitive component like a database, a file storage, an API or a service.
  • +
  • Passwords are used in production environments.
  • +
  • Application re-distribution is required before updating the passwords.

There is a risk if you answered yes to any of those questions.

Recommended Secure Coding Practices

    -
  • Store the credentials in a configuration file that is not pushed to the code repository.
  • -
  • Store the credentials in a database.
  • -
  • Use your cloud provider’s service for managing secrets.
  • +
  • Store the passwords in a configuration file that is not pushed to the code repository.
  • +
  • Store the passwords in a database.
  • +
  • Use your cloud provider’s service for managing passwords.
  • If a password has been disclosed through the source code: change it.

Sensitive Code Example

-var mysql = require('mysql');
+const mysql = require('mysql');
 
-var connection = mysql.createConnection(
-{
+const connection = mysql.createConnection({
   host:'localhost',
   user: "admin",
   database: "project",
@@ -40,9 +38,9 @@ 

Sensitive Code Example

Compliant Solution

-var mysql = require('mysql');
+const mysql = require('mysql');
 
-var connection = mysql.createConnection({
+const connection = mysql.createConnection({
   host: process.env.MYSQL_URL,
   user: process.env.MYSQL_USERNAME,
   password: process.env.MYSQL_PASSWORD,
@@ -56,7 +54,6 @@ 

See

Authentication Failures
  • OWASP - Top 10 2017 Category A2 - Broken Authentication
  • -
  • CWE - CWE-798 - Use of Hard-coded Credentials
  • CWE - CWE-259 - Use of Hard-coded Password
  • Derived from FindSecBugs rule Hard Coded Password
  • diff --git a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.json b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.json index ab8c1d283eb..9256764a9c6 100644 --- a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.json +++ b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S2068.json @@ -1,5 +1,5 @@ { - "title": "Hard-coded credentials are security-sensitive", + "title": "Hard-coded passwords are security-sensitive", "type": "SECURITY_HOTSPOT", "code": { "impacts": { diff --git a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.html b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.html new file mode 100644 index 00000000000..0aa14d0cb0a --- /dev/null +++ b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.html @@ -0,0 +1,54 @@ +

    Because it is easy to extract strings from an application source code or binary, secrets should not be hard-coded. This is particularly true for +applications that are distributed or that are open-source.

    +

    In the past, it has led to the following vulnerabilities:

    + +

    Secrets should be stored outside of the source code in a configuration file or a management service for secrets.

    +

    This rule detects variables/fields having a name matching a list of words (secret, token, credential, auth, api[_.-]?key) being assigned a +pseudorandom hard-coded value. The pseudorandomness of the hard-coded value is based on its entropy and the probability to be human-readable. The +randomness sensibility can be adjusted if needed. Lower values will detect less random values, raising potentially more false positives.

    +

    Ask Yourself Whether

    +
      +
    • The secret allows access to a sensitive component like a database, a file storage, an API, or a service.
    • +
    • The secret is used in a production environment.
    • +
    • Application re-distribution is required before updating the secret.
    • +
    +

    There would be a risk if you answered yes to any of those questions.

    +

    Recommended Secure Coding Practices

    +
      +
    • Store the secret in a configuration file that is not pushed to the code repository.
    • +
    • Use your cloud provider’s service for managing secrets.
    • +
    • If a secret has been disclosed through the source code: revoke it and create a new one.
    • +
    +

    Sensitive Code Example

    +
    +const API_KEY = "1234567890abcdef"  // Hard-coded secret (bad practice)
    +
    +const response = await fetch("https://api.my-service/v1/users", {
    +  headers: {
    +    Authorization: `Bearer ${API_KEY}`,
    +  },
    +});
    +
    +

    Compliant Solution

    +
    +const API_KEY = process.env.API_KEY;
    +
    +const response = await fetch("https://api.my-service/v1/users", {
    +  headers: {
    +    Authorization: `Bearer ${API_KEY}`,
    +  },
    +});
    +
    +

    See

    + + diff --git a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.json b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.json new file mode 100644 index 00000000000..adfc2e70094 --- /dev/null +++ b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.json @@ -0,0 +1,49 @@ +{ + "title": "Hard-coded secrets are security-sensitive", + "type": "SECURITY_HOTSPOT", + "code": { + "impacts": { + "SECURITY": "BLOCKER" + }, + "attribute": "TRUSTWORTHY" + }, + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "30min" + }, + "tags": [ + "cwe" + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-6418", + "sqKey": "S6418", + "scope": "Main", + "securityStandards": { + "CWE": [ + 798 + ], + "OWASP": [ + "A2" + ], + "OWASP Top 10 2021": [ + "A7" + ], + "PCI DSS 3.2": [ + "6.5.10" + ], + "PCI DSS 4.0": [ + "6.2.4" + ], + "ASVS 4.0": [ + "2.10.4", + "3.5.2", + "6.4.1" + ] + }, + "quickfix": "infeasible", + "compatibleLanguages": [ + "JAVASCRIPT", + "TYPESCRIPT" + ] +} diff --git a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/Sonar_way_profile.json b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/Sonar_way_profile.json index bc5e0ef5804..00fedabfa7f 100644 --- a/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/Sonar_way_profile.json +++ b/sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/Sonar_way_profile.json @@ -236,6 +236,7 @@ "S6351", "S6353", "S6397", + "S6418", "S6426", "S6435", "S6438", diff --git a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/CheckListTest.java b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/CheckListTest.java index cf21be00d3c..cf6d5a5f425 100644 --- a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/CheckListTest.java +++ b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/CheckListTest.java @@ -20,7 +20,6 @@ import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -32,7 +31,7 @@ class CheckListTest { - private static final int CHECKS_PROPERTIES_COUNT = 36; + private static final int CHECKS_PROPERTIES_COUNT = 38; /** * Enforces that each check declared in list. diff --git a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedCredentialsCheckTest.java b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheckTest.java similarity index 76% rename from sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedCredentialsCheckTest.java rename to sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheckTest.java index 5d2887f77f9..6b6a229209d 100644 --- a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedCredentialsCheckTest.java +++ b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedPasswordsCheckTest.java @@ -21,17 +21,17 @@ import com.google.gson.Gson; import org.junit.jupiter.api.Test; -class HardcodedCredentialsCheckTest { +class NoHardcodedPasswordsCheckTest { @Test void configurations() { - HardcodedCredentialsCheck check = new HardcodedCredentialsCheck(); + NoHardcodedPasswordsCheck check = new NoHardcodedPasswordsCheck(); // default configuration String defaultConfigAsString = new Gson().toJson(check.configurations()); - assertThat(defaultConfigAsString).isEqualTo("[{\"credentialWords\":[\"password\",\"pwd\",\"passwd\"]}]"); + assertThat(defaultConfigAsString).isEqualTo("[{\"passwordWords\":[\"password\",\"pwd\",\"passwd\"]}]"); - check.credentialWords = "foo, bar"; + check.passwordWords = "foo, bar"; String customConfigAsString = new Gson().toJson(check.configurations()); - assertThat(customConfigAsString).isEqualTo("[{\"credentialWords\":[\"foo\",\"bar\"]}]"); + assertThat(customConfigAsString).isEqualTo("[{\"passwordWords\":[\"foo\",\"bar\"]}]"); } } diff --git a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedSecretsCheckTest.java b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedSecretsCheckTest.java new file mode 100644 index 00000000000..bad394eb096 --- /dev/null +++ b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/NoHardcodedSecretsCheckTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * 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/ + */ +package org.sonar.javascript.checks; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +class NoHardcodedSecretsCheckTest { + + @Test + void configurations() { + NoHardcodedSecretsCheck check = new NoHardcodedSecretsCheck(); + // default configuration + String defaultConfigAsString = new Gson().toJson(check.configurations()); + assertThat(defaultConfigAsString).isEqualTo( + "[{\"secretWords\":\"api[_.-]?key,auth,credential,secret,token\",\"randomnessSensibility\":\"5.0\"}]" + ); + } +} diff --git a/sonar-plugin/sonar-javascript-plugin/pom.xml b/sonar-plugin/sonar-javascript-plugin/pom.xml index 7c8f836f593..b2dbd37a1a4 100644 --- a/sonar-plugin/sonar-javascript-plugin/pom.xml +++ b/sonar-plugin/sonar-javascript-plugin/pom.xml @@ -398,7 +398,7 @@ - 70000000 + 65000000 110000000 ${project.build.directory}/${project.build.finalName}-multi.jar diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisProcessor.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisProcessor.java index cc7a5d63fae..c8e4d30d89a 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisProcessor.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisProcessor.java @@ -185,7 +185,12 @@ private void saveIssues(List issues) { file, issue.line() ); - saveIssue(issue); + try { + saveIssue(issue); + } catch (RuntimeException e) { + LOG.warn("Failed to save issue in {} at line {}", file.uri(), issue.line()); + LOG.warn("Exception cause", e); + } } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java index 6bbf4b74624..6ab828b2b15 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java @@ -35,6 +35,7 @@ import org.sonar.plugins.javascript.bridge.BridgeServer.CpdToken; import org.sonar.plugins.javascript.bridge.BridgeServer.Highlight; import org.sonar.plugins.javascript.bridge.BridgeServer.HighlightedSymbol; +import org.sonar.plugins.javascript.bridge.BridgeServer.Issue; import org.sonar.plugins.javascript.bridge.BridgeServer.Location; import org.sonar.plugins.javascript.bridge.BridgeServer.Metrics; @@ -107,4 +108,21 @@ void should_not_fail_when_invalid_cpd() { assertThat(logTester.logs()) .contains("Failed to save CPD token in " + file.uri() + ". File will not be analyzed for duplications."); } + + @Test + void should_not_fail_when_invalid_issue() { + var fileLinesContextFactory = mock(FileLinesContextFactory.class); + when(fileLinesContextFactory.createFor(any())).thenReturn(mock(FileLinesContext.class)); + var processor = new AnalysisProcessor(mock(NoSonarFilter.class), fileLinesContextFactory); + var context = SensorContextTester.create(baseDir); + var file = TestInputFileBuilder + .create("moduleKey", "file.js") + .setContents("var x = 1;") + .build(); + var issue = new Issue(2, 1, 1, 2, "message", "ruleId", List.of(), 3.14, List.of()); // invalid location startLine > endLine + var response = new AnalysisResponse(null, List.of(issue), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); + processor.processResponse(context, mock(JsTsChecks.class), file, response); + assertThat(logTester.logs()) + .contains("Failed to save issue in " + file.uri() + " at line 2"); + } }