From 54c313c8faa1bdcbd02f4307c688ffcb6598d4be Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Tue, 26 Nov 2024 16:02:43 +0100 Subject: [PATCH] JS-359 Create rule S6418 (`hardcoded-secrets`): Hard-coded secrets are security-sensitive --- .../expected/jsts/Ghost/javascript-S6418.json | 6 + .../jsts/searchkit/javascript-S6418.json | 5 + packages/jsts/src/rules/S6418/cb.fixture.ts | 53 +++++ packages/jsts/src/rules/S6418/cb.options.json | 6 + packages/jsts/src/rules/S6418/cb.test.ts | 26 +++ packages/jsts/src/rules/S6418/index.ts | 17 ++ packages/jsts/src/rules/S6418/meta.ts | 29 +++ packages/jsts/src/rules/S6418/rule.ts | 213 ++++++++++++++++++ packages/jsts/src/rules/S6418/unit.test.ts | 39 ++++ packages/jsts/src/rules/original.ts | 1 + packages/jsts/src/rules/plugin.ts | 3 +- .../sonar/javascript/checks/CheckList.java | 3 +- ...Check.java => HardcodedPasswordCheck.java} | 6 +- .../checks/HardcodedSecretsCheck.java | 65 ++++++ .../javascript/rules/javascript/S6418.html | 54 +++++ .../javascript/rules/javascript/S6418.json | 49 ++++ .../rules/javascript/Sonar_way_profile.json | 1 + .../javascript/checks/CheckListTest.java | 3 +- ...t.java => HardcodedPasswordCheckTest.java} | 4 +- .../checks/HardcodedSecretsCheckTest.java | 35 +++ sonar-plugin/sonar-javascript-plugin/pom.xml | 2 +- 21 files changed, 608 insertions(+), 12 deletions(-) create mode 100644 its/ruling/src/test/expected/jsts/Ghost/javascript-S6418.json create mode 100644 its/ruling/src/test/expected/jsts/searchkit/javascript-S6418.json create mode 100644 packages/jsts/src/rules/S6418/cb.fixture.ts create mode 100644 packages/jsts/src/rules/S6418/cb.options.json create mode 100644 packages/jsts/src/rules/S6418/cb.test.ts create mode 100644 packages/jsts/src/rules/S6418/index.ts create mode 100644 packages/jsts/src/rules/S6418/meta.ts create mode 100644 packages/jsts/src/rules/S6418/rule.ts create mode 100644 packages/jsts/src/rules/S6418/unit.test.ts rename sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/{HardcodedCredentialsCheck.java => HardcodedPasswordCheck.java} (96%) create mode 100644 sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedSecretsCheck.java create mode 100644 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.html create mode 100644 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S6418.json rename sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/{HardcodedCredentialsCheckTest.java => HardcodedPasswordCheckTest.java} (92%) create mode 100644 sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedSecretsCheckTest.java 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/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..0b79b76d815 --- /dev/null +++ b/packages/jsts/src/rules/S6418/meta.ts @@ -0,0 +1,29 @@ +/* + * 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/ + */ +// DO NOT EDIT! This file is autogenerated by "npm run generate-meta" + +export const meta = { + type: 'problem', + docs: { + description: 'Hard-coded secrets are security-sensitive', + recommended: true, + url: 'https://sonarsource.github.io/rspec/#/rspec/S6418/javascript', + requiresTypeChecking: false, + }, +}; + +export const sonarKey = 'S6418'; diff --git a/packages/jsts/src/rules/S6418/rule.ts b/packages/jsts/src/rules/S6418/rule.ts new file mode 100644 index 00000000000..8e287233631 --- /dev/null +++ b/packages/jsts/src/rules/S6418/rule.ts @@ -0,0 +1,213 @@ +/* + * 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 { error } from '../../../../shared/src/helpers/logging.js'; +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) { + error( + `Invalid characters provided to rule S6418 '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..8165aef946a --- /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 - 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/packages/jsts/src/rules/original.ts b/packages/jsts/src/rules/original.ts index 494d669cf3f..73afcc50060 100644 --- a/packages/jsts/src/rules/original.ts +++ b/packages/jsts/src/rules/original.ts @@ -92,6 +92,7 @@ export { rule as S100 } from './S100/index.js'; // function-name export { rule as S3800 } from './S3800/index.js'; // function-return-type export { rule as S1527 } from './S1527/index.js'; // future-reserved-words export { rule as S3531 } from './S3531/index.js'; // generator-without-yield +export { rule as S6418 } from './S6418/index.js'; // hard-coded-credentials export { rule as S4790 } from './S4790/index.js'; // hashing export { rule as S5691 } from './S5691/index.js'; // hidden-files export { rule as S6754 } from './S6754/index.js'; // hook-use-state diff --git a/packages/jsts/src/rules/plugin.ts b/packages/jsts/src/rules/plugin.ts index 21f02246f77..07ca05728c2 100644 --- a/packages/jsts/src/rules/plugin.ts +++ b/packages/jsts/src/rules/plugin.ts @@ -18,7 +18,7 @@ * This is the entry point of the ESLint Plugin. * Said differently, this is the public API of the ESLint Plugin. */ -import type { Rule, Linter } from 'eslint'; +import type { Linter, Rule } from 'eslint'; import * as originalRules from './original.js'; import * as decoratedRules from './decorated.js'; @@ -102,6 +102,7 @@ export const rules: Record = { 'function-return-type': originalRules.S3800, 'future-reserved-words': originalRules.S1527, 'generator-without-yield': originalRules.S3531, + 'hardcoded-credentials': originalRules.S6418, hashing: originalRules.S4790, 'hidden-files': originalRules.S5691, 'hook-use-state': originalRules.S6754, 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..a4314e62a10 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,8 @@ public static List> getAllChecks() { GlobalThisCheck.class, GlobalsShadowingCheck.class, GratuitousConditionCheck.class, - HardcodedCredentialsCheck.class, + HardcodedPasswordCheck.class, + HardcodedSecretsCheck.class, HashingCheck.class, HeadingHasContentCheck.class, HiddenFilesCheck.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/HardcodedPasswordCheck.java similarity index 96% 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/HardcodedPasswordCheck.java index b84af7fb7cd..7bce3ccedcc 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/HardcodedPasswordCheck.java @@ -27,7 +27,7 @@ @JavaScriptRule @TypeScriptRule @Rule(key = "S2068") -public class HardcodedCredentialsCheck extends Check { +public class HardcodedPasswordCheck extends Check { private static final String DEFAULT = "password, pwd, passwd"; @@ -45,12 +45,8 @@ public List configurations() { ); } - - private static class Config { - String[] credentialWords; - Config(String[] credentialWords) { this.credentialWords = credentialWords; } diff --git a/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedSecretsCheck.java b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedSecretsCheck.java new file mode 100644 index 00000000000..908767fcf1a --- /dev/null +++ b/sonar-plugin/javascript-checks/src/main/java/org/sonar/javascript/checks/HardcodedSecretsCheck.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 HardcodedSecretsCheck 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/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/HardcodedPasswordCheckTest.java similarity index 92% 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/HardcodedPasswordCheckTest.java index 5d2887f77f9..d1c4ad0457a 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/HardcodedPasswordCheckTest.java @@ -21,11 +21,11 @@ import com.google.gson.Gson; import org.junit.jupiter.api.Test; -class HardcodedCredentialsCheckTest { +class HardcodedPasswordCheckTest { @Test void configurations() { - HardcodedCredentialsCheck check = new HardcodedCredentialsCheck(); + HardcodedPasswordCheck check = new HardcodedPasswordCheck(); // default configuration String defaultConfigAsString = new Gson().toJson(check.configurations()); assertThat(defaultConfigAsString).isEqualTo("[{\"credentialWords\":[\"password\",\"pwd\",\"passwd\"]}]"); diff --git a/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedSecretsCheckTest.java b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedSecretsCheckTest.java new file mode 100644 index 00000000000..5f37840dc11 --- /dev/null +++ b/sonar-plugin/javascript-checks/src/test/java/org/sonar/javascript/checks/HardcodedSecretsCheckTest.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 HardcodedSecretsCheckTest { + + @Test + void configurations() { + HardcodedSecretsCheck check = new HardcodedSecretsCheck(); + // 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