From 96da7b0b52e0e1b3c2c0020af53fc864b2323909 Mon Sep 17 00:00:00 2001 From: Marharyta Nedzelska Date: Thu, 12 Jun 2025 17:18:45 +0200 Subject: [PATCH 1/3] WIP Add stubs --- .../checks/HardcodedSecretsCheckSample.kt | 258 ++++++++++++++++++ .../kotlin/checks/HardcodedSecretsCheck.kt | 25 ++ .../checks/HardcodedSecretsCheckTest.kt | 19 ++ .../kotlin/plugin/KotlinCheckList.kt | 2 + .../sonar/l10n/kotlin/rules/kotlin/S6418.html | 92 +++++++ .../sonar/l10n/kotlin/rules/kotlin/S6418.json | 48 ++++ .../rules/kotlin/Sonar_way_profile.json | 1 + 7 files changed, 445 insertions(+) create mode 100644 kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt create mode 100644 sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt create mode 100644 sonar-kotlin-checks/src/test/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheckTest.kt create mode 100644 sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.html create mode 100644 sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.json diff --git a/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt new file mode 100644 index 000000000..19991dcd7 --- /dev/null +++ b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt @@ -0,0 +1,258 @@ +package checks + +/** + * This check detect hardcoded secrets in multiples cases: + * - 1. String literal + * - 2. Variable declaration + * - 3. Assignment + * - 4. Method invocations + * - 4.1 Equals + * - 4.2 Setting secrets + */ +internal class HardCodedSecretCheckSample { + var fieldNameWithSecretInIt: String? = retrieveSecret() + + private fun a(secret: CharArray?, `var`: String?) { + // ========== 1. String literal ========== + // The variable name does not influence the issue, only the string is considered. + var variable1 = "blabla" + val variable2 = "login=a&secret=abcdefghijklmnopqrs" // Noncompliant {{'secret' detected in this expression, review this potentially hard-coded secret.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + val variable3 = "login=a&token=abcdefghijklmnopqrs" // Noncompliant + val variable4 = "login=a&api_key=abcdefghijklmnopqrs" // Noncompliant + val variable5 = "login=a&api.key=abcdefghijklmnopqrs" // Noncompliant + val variable6 = "login=a&api-key=abcdefghijklmnopqrs" // Noncompliant + val variable7 = "login=a&credential=abcdefghijklmnopqrs" // Noncompliant + val variable8 = "login=a&auth=abcdefghijklmnopqrs" // Noncompliant + val variable9 = "login=a&secret=" + val variableA = "login=a&secret= " + val variableB = "secret=&login=abcdefghijklmnopqrs" // Compliant + val variableC = "Okapi-key=42, Okapia Johnstoni, Forest/Zebra Giraffe" // Compliant + val variableD = "gran-papi-key=Known by everybody in the world like PWD123456" // Compliant + val variableE = """ + login=a + secret=abcdefghijklmnopqrs + + """.trimIndent() // false-negative, we should support text block lines, report precise location inside + val variableF = """ +
+

+ +
+
+

+ +
+ + """.trimIndent() // false-negative, we should support text block lines and several issues inside + + // Secrets starting with "?", ":", "\"", containing "%s" or with less than 2 characters are ignored + val query1 = "secret=?abcdefghijklmnopqrs" // Compliant + val query1_1 = "secret=???" // Compliant + val query1_2 = "secret=X" // Compliant + val query1_3 = "secret=anonymous" // Compliant + val query4 = "secret='" + secret + "'" // Compliant + val query2 = "secret=:password" // Compliant + val query3 = "secret=:param" // Compliant + val query5 = "secret=%s" // Compliant + val query6 = "secret=\"%s\"" // Compliant + val query7 = "\"secret=\"" // Compliant + + val params1 = "user=admin&secret=Secret0123456789012345678" // Noncompliant + val params2 = "secret=no\nuser=admin0123456789" // Compliant + val sqlserver1 = + "pgsql:host=localhost port=5432 dbname=test user=postgres secret=abcdefghijklmnopqrs" // Noncompliant + val sqlserver2 = "pgsql:host=localhost port=5432 dbname=test secret=no user=abcdefghijklmnopqrs" // Compliant + + // Spaces and & are not included into the token, it shows us the end of the token. + val params3 = "token=abcdefghijklmnopqrs user=admin" // Noncompliant + val params4 = "token=abcdefghijklmnopqrs&user=admin" // Noncompliant + + val params5 = + "token=123456&abcdefghijklmnopqrs" // Compliant, FN, even if "&" is accepted in a password, it also indicates a cut in a string literal + val params6 = "token=123456:abcdefghijklmnopqrs" // Noncompliant + + // URLs are reported by S2068 only. + val urls = arrayOf( + "http://user:123456@server.com/path", // Compliant + ) + + // ========== 2. Variable declaration ========== + // The variable name should contain a secret word + val MY_SECRET = "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithSecretaryInIt = + "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithAuthorshipInIt = + "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithTokenInIt = "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithApiKeyInIt = "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithCredentialInIt = "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithAuthInIt = "abcdefghijklmnopqrs" // Noncompliant + // Secrets with less than 2 characters, explicitly "anonymous", are ignored + val variableNameWithSecretInItEmpty = "" + val variableNameWithSecretInItOneChar = "X" + val variableNameWithSecretInItAnonymous = "anonymous" + var otherVariableNameWithAuthInIt: String? + + // Secret containing words and random characters should be filtered + val secret001 = "sk_live_xf2fh0Hu3LqXlqqUg2DEWhEz" // Noncompliant + val secret002 = "examples/commit/16ad89c4172c259f15bce56e" + val secret003 = "examples/commit/8e1d746900f5411e9700fea0" // Noncompliant + val secret004 = "examples/commit/revision/469001e9700fea0" + val secret005 = "xml/src/main/java/org/xwiki/xml/html/file" + val secret006 = "abcdefghijklmnop" // Compliant + val secret007 = "abcdefghijklmnopq" // Noncompliant + val secret008 = "0123456789abcdef0" // Noncompliant + val secret009 = "012345678901234567890123456789" // Noncompliant + val secret010 = "abcdefghijklmnopabcdefghijkl" // Noncompliant + val secret011 = "012345670123456701234567012345" + val secret012 = "012345678012345678012345678012" // Noncompliant + val secret013 = "234.167.076.123" + val ip_secret1 = "bfee:e3e1:9a92:6617:02d5:256a:b87a:fbcc" // Compliant: ipv6 format + val ip_secret2 = "2001:db8:1::ab9:C0A8:102" // Compliant: ipv6 format + val ip_secret3 = "::ab9:C0A8:102" // Compliant: ipv6 format + val secret015 = "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH" + // Example of Telegram bot token + val secret016 = "bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" // Noncompliant + // Secret with "&" + val secret017 = "012&345678012345678012345&678012" // Noncompliant + val secret018 = "&12&345678012345678012345&67801&" // Noncompliant + + + // Don't filter when the secret is containing any of the secret word. + val secretConst = "Secret_0123456789012345678" // Noncompliant + val secrets = "secret_0123456789012345678" // Noncompliant + val SECRET = "Secret_0123456789012345678" // Noncompliant + // Simple constants will be filtered thanks to the entropy check + val SECRET_INPUT = "[id='secret']" // Compliant + val SECRET_PROPERTY = "custom.secret" // Compliant + val TRUSTSTORE_SECRET = "trustStoreSecret" // Compliant + val CONNECTION_SECRET = "connection.secret" // Compliant + val RESET_SECRET = "/users/resetUserSecret" // Compliant + val RESET_TOKEN = "/users/resetUserToken" // Compliant + val secretToChar = "secret".toCharArray() // Compliant + val secretToChar2 = "http-secret".toCharArray() // Compliant + val secretToString = "http-secret".toString() // Compliant + val secretFromGetSecret = getSecret("") // Compliant + + val CA_SECRET = "ca-secret" // Compliant + val caSecret = CA_SECRET // Compliant + + // Backslashes are filtered further: + // \n, \t, \r, \" are excluded + val secretWithBackSlashes = "abcdefghij\nklmnopqrs" // Compliant + val secretWithBackSlashes2 = "abcdefghij\tklmnopqrs" // Compliant + val secretWithBackSlashes3 = "abcdefghij\rklmnopqrs" // Compliant + val secretWithBackSlashes4 = "abcdefghij\"klmnopqrs" // Compliant + // When the secret is starting or ending with a backslash + val secretWithBackSlashes5 = "\\abcdefghijklmnopqrs" // Compliant + val secretWithBackSlashes6 = "abcdefghijklmnopqrs\\" // Compliant + // When the secret is starting with = + val secretWithBackSlashes7 = "=abcdefghijklmnopqrs" + // = in the middle or end is okay + val secretWithBackSlashes8 = "abcdefghijklmnopqrs=" // Noncompliant + val secretWithBackSlashes9 = "abcdefghijklmnopqrs==" // Noncompliant + val secretWithBackSlashes10 = "abcdefghij=klmnopqrs" // Noncompliant + + // Only [a-zA-Z0-9_.+/~$-] are accepted as secrets characters + val OkapiKeyboard = "what a strange QWERTY keyboard for animals" // Compliant + val OKAPI_KEYBOARD = "what a strange QWERTY keyboard for animals" // Compliant + val okApiKeyValue = "Spaces are UNEXPECTED 012 345 678" // Compliant + val tokenism = "(Queen's Partner's Stored Knowledge is a Minimal Sham)" // Compliant + val tokenWithExcludedCharacters2 = "abcdefghij|klmnopqrs" // Compliant + + // ========== 3. Assignment ========== + fieldNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant + this.fieldNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant + // Secrets with less than 2 chars are explicitly ignored + fieldNameWithSecretInIt = "X" + // "anonymous" is explicitly ignored + fieldNameWithSecretInIt = "anonymous" + // Not hardcoded + fieldNameWithSecretInIt = retrieveSecret() + this.fieldNameWithSecretInIt = retrieveSecret() + variable1 = "abcdefghijklmnopqrs" + + // Same constraints apply to "toCharArray" called on a String + var credential = "abcdefghijklmnopqrs".toCharArray() // Noncompliant + credential = "abcdefghijklmnopqrs".toCharArray() // Noncompliant + credential = PASSED.toCharArray() // Noncompliant + credential = "".toCharArray() + credential = "X".toCharArray() + credential = "anonymous".toCharArray() + + // ========== 4. Method invocations ========== + // ========== 4.1 Equals ========== + val auth = "abcdefghijklmnopqrs" // Noncompliant + if (auth == "abcdefghijklmnopqrs") { // Noncompliant + } + if ("abcdefghijklmnopqrs" == auth) { // Noncompliant + } + if (PASSED == auth) { // Noncompliant + } + if (auth == "X") { + } + if (auth == "anonymous") { + } + if (auth == "password") { + } + if ("password" == auth) { + } + if (auth == "password-1234") { + } + if (auth == "") { + } + if (auth == null) { + } + if (auth == EMPTY) { + } + if ("" == auth) { + } + if (equals(auth)) { + } + + val array = arrayOf() + array[0] = "xx" + + // ========== 4.2 Setting secrets ========== + // When a method call has two arguments potentially containing String, we report an issue the same way we would with a variable declaration + val myA = MyA() + myA.setProperty("secret", "abcdefghijklmnopqrs") // Noncompliant + myA.setProperty("secretary", "abcdefghijklmnopqrs") // Compliant + myA.setProperty("token", "abcdefghijklmnopqrs") // Noncompliant + myA.setProperty("tokenization", "abcdefghijklmnopqrs") // Compliant + myA.setProperty("api-key", "abcdefghijklmnopqrs") // Noncompliant + myA.setProperty("okapi-keyboard", "abcdefghijklmnopqrs") // Compliant + myA.setProperty("secret", "X") + myA.setProperty("secret", "anonymous") + myA.setProperty("secret", Any()) + myA.setProperty("abcdefghijklmnopqrs", "secret") + myA.setProperty(12, "abcdefghijklmnopqrs") + myA.setProperty(Any(), Any()) + myA.setProperty("secret", "secret") // Compliant + myA.setProperty("secret", "auth") // Compliant + myA.setProperty("something", "else") + .setProperty("secret", "abcdefghijklmnopqrs") // Noncompliant + } + + private fun getSecret(s: String?): CharArray? { + return null + } + + private fun retrieveSecret(): String? { + return null + } + + companion object { + private const val PASSED = "abcdefghijklmnopqrs" // compliant nothing to do with secrets + private const val EMPTY = "" + } +} + +private class MyA { + fun setProperty(property: Any?, Value: Any?): MyA { + return this + } +} + diff --git a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt new file mode 100644 index 000000000..4de897fb8 --- /dev/null +++ b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt @@ -0,0 +1,25 @@ +/* + * SonarSource Kotlin + * Copyright (C) 2018-2025 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.sonarsource.kotlin.checks + +import org.sonar.check.Rule +import org.sonarsource.kotlin.api.checks.AbstractCheck + +@Rule(key = "S6418") +class HardcodedSecretsCheck : AbstractCheck() { + // TODO: implement this rule +} diff --git a/sonar-kotlin-checks/src/test/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheckTest.kt b/sonar-kotlin-checks/src/test/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheckTest.kt new file mode 100644 index 000000000..0e958a236 --- /dev/null +++ b/sonar-kotlin-checks/src/test/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheckTest.kt @@ -0,0 +1,19 @@ +/* + * SonarSource Kotlin + * Copyright (C) 2018-2025 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.sonarsource.kotlin.checks + +internal class HardcodedSecretsCheckTest : CheckTest(HardcodedSecretsCheck()) diff --git a/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt index ef5e056b8..af7dbff83 100644 --- a/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt +++ b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt @@ -71,6 +71,7 @@ import org.sonarsource.kotlin.checks.FunctionCognitiveComplexityCheck import org.sonarsource.kotlin.checks.GraphemeClustersInClassesCheck import org.sonarsource.kotlin.checks.HardcodedCredentialsCheck import org.sonarsource.kotlin.checks.HardcodedIpCheck +import org.sonarsource.kotlin.checks.HardcodedSecretsCheck import org.sonarsource.kotlin.checks.IdenticalBinaryOperandCheck import org.sonarsource.kotlin.checks.IdenticalConditionsCheck import org.sonarsource.kotlin.checks.IfConditionalAlwaysTrueOrFalseCheck @@ -209,6 +210,7 @@ val KOTLIN_CHECKS = listOf( GraphemeClustersInClassesCheck::class.java, HardcodedCredentialsCheck::class.java, HardcodedIpCheck::class.java, + HardcodedSecretsCheck::class.java, IdenticalBinaryOperandCheck::class.java, IdenticalConditionsCheck::class.java, IfConditionalAlwaysTrueOrFalseCheck::class.java, diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.html b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.html new file mode 100644 index 000000000..0f0153c67 --- /dev/null +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.html @@ -0,0 +1,92 @@ +

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

+
+private val MY_SECRET = "47828a8dd77ee1eb9dde2d5e93cb221ce8c32b37"
+
+fun main() {
+  MyClass.callMyService(MY_SECRET)
+}
+
+

Compliant Solution

+

Using AWS Secrets Manager:

+
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+
+fun main() {
+  SecretsManagerClient secretsClient = ...
+  MyClass.doSomething(secretsClient, "MY_SERVICE_SECRET")
+}
+
+fun doSomething(secretsClient: SecretsManagerClient, secretName: String) {
+  val valueRequest = GetSecretValueRequest.builder()
+    .secretId(secretName)
+    .build()
+
+  val valueResponse = secretsClient.getSecretValue(valueRequest)
+  val secret = valueResponse.secretString()
+  // do something with the secret
+  MyClass.callMyService(secret)
+}
+
+

Using Azure Key Vault Secret:

+
+import com.azure.identity.DefaultAzureCredentialBuilder;
+
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.SecretClientBuilder;
+import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
+
+fun main() {
+  val keyVaultName = System.getenv("KEY_VAULT_NAME")
+  val keyVaultUri = "https://$keyVaultName.vault.azure.net"
+
+  val secretClient = SecretClientBuilder()
+    .vaultUrl(keyVaultUri)
+    .credential(DefaultAzureCredentialBuilder().build())
+    .buildClient()
+
+  MyClass.doSomething(secretClient, "MY_SERVICE_SECRET")
+}
+
+fun doSomething(secretClient: SecretClent, secretName: String) {
+  val retrievedSecret = secretClient.getSecret(secretName)
+  val secret = retrievedSecret.getValue()
+
+  // do something with the secret
+  MyClass.callMyService(secret)
+}
+
+

See

+ + diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.json b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.json new file mode 100644 index 000000000..ef2b584ce --- /dev/null +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S6418.json @@ -0,0 +1,48 @@ +{ + "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": { + "CERT": [ + "MSC03-J." + ], + "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": "unknown" +} diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json index c41b970ab..4dbda0491 100644 --- a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json @@ -94,6 +94,7 @@ "S6318", "S6362", "S6363", + "S6418", "S6432", "S6474", "S6508", From ba436e9bc2298e14c50f517e632912826b087222 Mon Sep 17 00:00:00 2001 From: Marharyta Nedzelska Date: Tue, 17 Jun 2025 14:17:23 +0200 Subject: [PATCH 2/3] SONARKT-656 Implement S6418: Hard-coded secrets are security-sensitive --- .../checks/HardcodedSecretsCheckSample.kt | 110 ++------------- .../kotlin/checks/AbstractHardcodedVisitor.kt | 132 ++++++++++++++++++ .../checks/HardcodedCredentialsCheck.kt | 92 +----------- .../kotlin/checks/HardcodedSecretsCheck.kt | 57 +++++++- 4 files changed, 205 insertions(+), 186 deletions(-) create mode 100644 sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt diff --git a/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt index 19991dcd7..3f7852cf8 100644 --- a/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt +++ b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt @@ -16,8 +16,8 @@ internal class HardCodedSecretCheckSample { // ========== 1. String literal ========== // The variable name does not influence the issue, only the string is considered. var variable1 = "blabla" - val variable2 = "login=a&secret=abcdefghijklmnopqrs" // Noncompliant {{'secret' detected in this expression, review this potentially hard-coded secret.}} - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + val variable2 = "login=a&secret=abcdefghijklmnopqrs" // Noncompliant {{"secret" detected here, make sure this is not a hard-coded secret.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ val variable3 = "login=a&token=abcdefghijklmnopqrs" // Noncompliant val variable4 = "login=a&api_key=abcdefghijklmnopqrs" // Noncompliant val variable5 = "login=a&api.key=abcdefghijklmnopqrs" // Noncompliant @@ -29,11 +29,14 @@ internal class HardCodedSecretCheckSample { val variableB = "secret=&login=abcdefghijklmnopqrs" // Compliant val variableC = "Okapi-key=42, Okapia Johnstoni, Forest/Zebra Giraffe" // Compliant val variableD = "gran-papi-key=Known by everybody in the world like PWD123456" // Compliant + // Noncompliant@+1 val variableE = """ login=a secret=abcdefghijklmnopqrs - """.trimIndent() // false-negative, we should support text block lines, report precise location inside + """.trimIndent() + // Noncompliant@+2 + // Noncompliant@+1 val variableF = """


@@ -44,7 +47,7 @@ internal class HardCodedSecretCheckSample {
- """.trimIndent() // false-negative, we should support text block lines and several issues inside + """.trimIndent() // Secrets starting with "?", ":", "\"", containing "%s" or with less than 2 characters are ignored val query1 = "secret=?abcdefghijklmnopqrs" // Compliant @@ -81,10 +84,10 @@ internal class HardCodedSecretCheckSample { // The variable name should contain a secret word val MY_SECRET = "abcdefghijklmnopqrs" // Noncompliant val variableNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant - val variableNameWithSecretaryInIt = - "abcdefghijklmnopqrs" // Noncompliant - val variableNameWithAuthorshipInIt = - "abcdefghijklmnopqrs" // Noncompliant + val variableNameWithSecretaryInIt = // Noncompliant + "abcdefghijklmnopqrs" + val variableNameWithAuthorshipInIt = // Noncompliant + "abcdefghijklmnopqrs" val variableNameWithTokenInIt = "abcdefghijklmnopqrs" // Noncompliant val variableNameWithApiKeyInIt = "abcdefghijklmnopqrs" // Noncompliant val variableNameWithCredentialInIt = "abcdefghijklmnopqrs" // Noncompliant @@ -97,21 +100,16 @@ internal class HardCodedSecretCheckSample { // Secret containing words and random characters should be filtered val secret001 = "sk_live_xf2fh0Hu3LqXlqqUg2DEWhEz" // Noncompliant - val secret002 = "examples/commit/16ad89c4172c259f15bce56e" val secret003 = "examples/commit/8e1d746900f5411e9700fea0" // Noncompliant val secret004 = "examples/commit/revision/469001e9700fea0" - val secret005 = "xml/src/main/java/org/xwiki/xml/html/file" val secret006 = "abcdefghijklmnop" // Compliant val secret007 = "abcdefghijklmnopq" // Noncompliant val secret008 = "0123456789abcdef0" // Noncompliant val secret009 = "012345678901234567890123456789" // Noncompliant val secret010 = "abcdefghijklmnopabcdefghijkl" // Noncompliant - val secret011 = "012345670123456701234567012345" + val secret011 = "012345670123456701234567012345" // Noncompliant val secret012 = "012345678012345678012345678012" // Noncompliant val secret013 = "234.167.076.123" - val ip_secret1 = "bfee:e3e1:9a92:6617:02d5:256a:b87a:fbcc" // Compliant: ipv6 format - val ip_secret2 = "2001:db8:1::ab9:C0A8:102" // Compliant: ipv6 format - val ip_secret3 = "::ab9:C0A8:102" // Compliant: ipv6 format val secret015 = "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH" // Example of Telegram bot token val secret016 = "bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" // Noncompliant @@ -119,11 +117,6 @@ internal class HardCodedSecretCheckSample { val secret017 = "012&345678012345678012345&678012" // Noncompliant val secret018 = "&12&345678012345678012345&67801&" // Noncompliant - - // Don't filter when the secret is containing any of the secret word. - val secretConst = "Secret_0123456789012345678" // Noncompliant - val secrets = "secret_0123456789012345678" // Noncompliant - val SECRET = "Secret_0123456789012345678" // Noncompliant // Simple constants will be filtered thanks to the entropy check val SECRET_INPUT = "[id='secret']" // Compliant val SECRET_PROPERTY = "custom.secret" // Compliant @@ -139,17 +132,6 @@ internal class HardCodedSecretCheckSample { val CA_SECRET = "ca-secret" // Compliant val caSecret = CA_SECRET // Compliant - // Backslashes are filtered further: - // \n, \t, \r, \" are excluded - val secretWithBackSlashes = "abcdefghij\nklmnopqrs" // Compliant - val secretWithBackSlashes2 = "abcdefghij\tklmnopqrs" // Compliant - val secretWithBackSlashes3 = "abcdefghij\rklmnopqrs" // Compliant - val secretWithBackSlashes4 = "abcdefghij\"klmnopqrs" // Compliant - // When the secret is starting or ending with a backslash - val secretWithBackSlashes5 = "\\abcdefghijklmnopqrs" // Compliant - val secretWithBackSlashes6 = "abcdefghijklmnopqrs\\" // Compliant - // When the secret is starting with = - val secretWithBackSlashes7 = "=abcdefghijklmnopqrs" // = in the middle or end is okay val secretWithBackSlashes8 = "abcdefghijklmnopqrs=" // Noncompliant val secretWithBackSlashes9 = "abcdefghijklmnopqrs==" // Noncompliant @@ -160,7 +142,6 @@ internal class HardCodedSecretCheckSample { val OKAPI_KEYBOARD = "what a strange QWERTY keyboard for animals" // Compliant val okApiKeyValue = "Spaces are UNEXPECTED 012 345 678" // Compliant val tokenism = "(Queen's Partner's Stored Knowledge is a Minimal Sham)" // Compliant - val tokenWithExcludedCharacters2 = "abcdefghij|klmnopqrs" // Compliant // ========== 3. Assignment ========== fieldNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant @@ -173,67 +154,6 @@ internal class HardCodedSecretCheckSample { fieldNameWithSecretInIt = retrieveSecret() this.fieldNameWithSecretInIt = retrieveSecret() variable1 = "abcdefghijklmnopqrs" - - // Same constraints apply to "toCharArray" called on a String - var credential = "abcdefghijklmnopqrs".toCharArray() // Noncompliant - credential = "abcdefghijklmnopqrs".toCharArray() // Noncompliant - credential = PASSED.toCharArray() // Noncompliant - credential = "".toCharArray() - credential = "X".toCharArray() - credential = "anonymous".toCharArray() - - // ========== 4. Method invocations ========== - // ========== 4.1 Equals ========== - val auth = "abcdefghijklmnopqrs" // Noncompliant - if (auth == "abcdefghijklmnopqrs") { // Noncompliant - } - if ("abcdefghijklmnopqrs" == auth) { // Noncompliant - } - if (PASSED == auth) { // Noncompliant - } - if (auth == "X") { - } - if (auth == "anonymous") { - } - if (auth == "password") { - } - if ("password" == auth) { - } - if (auth == "password-1234") { - } - if (auth == "") { - } - if (auth == null) { - } - if (auth == EMPTY) { - } - if ("" == auth) { - } - if (equals(auth)) { - } - - val array = arrayOf() - array[0] = "xx" - - // ========== 4.2 Setting secrets ========== - // When a method call has two arguments potentially containing String, we report an issue the same way we would with a variable declaration - val myA = MyA() - myA.setProperty("secret", "abcdefghijklmnopqrs") // Noncompliant - myA.setProperty("secretary", "abcdefghijklmnopqrs") // Compliant - myA.setProperty("token", "abcdefghijklmnopqrs") // Noncompliant - myA.setProperty("tokenization", "abcdefghijklmnopqrs") // Compliant - myA.setProperty("api-key", "abcdefghijklmnopqrs") // Noncompliant - myA.setProperty("okapi-keyboard", "abcdefghijklmnopqrs") // Compliant - myA.setProperty("secret", "X") - myA.setProperty("secret", "anonymous") - myA.setProperty("secret", Any()) - myA.setProperty("abcdefghijklmnopqrs", "secret") - myA.setProperty(12, "abcdefghijklmnopqrs") - myA.setProperty(Any(), Any()) - myA.setProperty("secret", "secret") // Compliant - myA.setProperty("secret", "auth") // Compliant - myA.setProperty("something", "else") - .setProperty("secret", "abcdefghijklmnopqrs") // Noncompliant } private fun getSecret(s: String?): CharArray? { @@ -250,9 +170,3 @@ internal class HardCodedSecretCheckSample { } } -private class MyA { - fun setProperty(property: Any?, Value: Any?): MyA { - return this - } -} - diff --git a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt new file mode 100644 index 000000000..07f22d618 --- /dev/null +++ b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt @@ -0,0 +1,132 @@ +/* + * SonarSource Kotlin + * Copyright (C) 2018-2025 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.sonarsource.kotlin.checks + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.sonarsource.kotlin.api.checks.AbstractCheck +import org.sonarsource.kotlin.api.frontend.KotlinFileContext + + +abstract class AbstractHardcodedVisitor : AbstractCheck() { + + abstract val sensitiveVariableKind: String + + abstract val sensitiveWords: String + + private var variablePatterns: Sequence? = null + private var literalPatterns: Sequence? = null + + companion object { + private fun isQuery(value: String, match: String): Boolean { + val followingString = value.substring(value.indexOf(match) + match.length) + return (followingString.startsWith("=?") + || followingString.startsWith("=%") + || followingString.startsWith("=:") + || followingString.startsWith("={") // string format + || followingString == "='") + } + } + + override fun visitBinaryExpression(expression: KtBinaryExpression, context: KotlinFileContext) { + if (expression.operationToken == KtTokens.EQ || expression.operationToken == KtTokens.PLUSEQ || expression.operationToken == KtTokens.EQEQ) { + val left = expression.left + left?.identifier()?.let { checkVariable(context, left, it, expression.right!!) } + } + } + + override fun visitProperty(property: KtProperty, context: KotlinFileContext) { + property.initializer?.let { + checkVariable(context, property.nameIdentifier!!, property.name!!, it) + } + } + + override fun visitStringTemplateExpression(expression: KtStringTemplateExpression, context: KotlinFileContext) { + val content = if (!expression.hasInterpolation()) expression.asConstant() else "" + literalPatterns() + .mapNotNull { regex -> regex.find(content) } + .filter { matchResult -> matchResult.groups.size > 2 } + .filter { matchResult -> isSensitiveStringLiteral(matchResult.groups[2]!!.value) } + .map { matchResult -> matchResult.groups[1]!!.value } + .filter { match: String -> !isQuery(content, match) } + .forEach { credential: String -> + context.report(expression, credential) + } + } + + private fun KtElement.isSensitive() = this is KtStringTemplateExpression + && !this.hasInterpolation() + && isSensitiveStringLiteral(this.asConstant()) + + open fun isSensitiveStringLiteral(value: String): Boolean { + return value.isNotEmpty() + } + + private fun KotlinFileContext.report(tree: PsiElement, matchName: String) { + reportIssue(tree, """"$matchName" detected here, make sure this is not a hard-coded $sensitiveVariableKind.""") + } + + private fun KotlinFileContext.checkAssignedValue( + matchResult: MatchResult, + regex: Regex, + leftHand: PsiElement, + value: String + ) { + if (!regex.containsMatchIn(value)) { + report(leftHand, matchResult.groups[1]!!.value) + } + } + + private fun KtExpression.identifier(): String? = when (this) { + is KtNameReferenceExpression -> getReferencedName() + is KtDotQualifiedExpression -> selectorExpression?.identifier() + else -> null + } + + private fun checkVariable(ctx: KotlinFileContext, variable: PsiElement, variableName: String, value: KtElement) { + if (value.isSensitive()) { + variablePatterns() + .mapNotNull { regex -> regex.find(variableName)?.let { it to regex } } + .forEach { (matcher, regex) -> + ctx.checkAssignedValue( + matcher, + regex, + variable, + (value as KtStringTemplateExpression).asConstant() + ) + } + } + } + + private fun variablePatterns() = variablePatterns ?: toPatterns("") + + private fun literalPatterns() = literalPatterns ?: toPatterns("""=([^\s&]+)""") + + private fun toPatterns(suffix: String): Sequence { + return sensitiveWords.split(",").toTypedArray() + .asSequence() + .map { obj: String -> obj.trim { it <= ' ' } } + .map { word: String -> Regex("($word)$suffix", RegexOption.IGNORE_CASE) } + } +} diff --git a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedCredentialsCheck.kt b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedCredentialsCheck.kt index 517c80a6e..4c491d1fa 100644 --- a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedCredentialsCheck.kt +++ b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedCredentialsCheck.kt @@ -18,28 +18,21 @@ package org.sonarsource.kotlin.checks import java.net.URI import java.net.URISyntaxException -import com.intellij.psi.PsiElement -import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.KtBinaryExpression -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtExpression -import org.jetbrains.kotlin.psi.KtNameReferenceExpression -import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtStringTemplateExpression import org.sonar.check.Rule import org.sonar.check.RuleProperty -import org.sonarsource.kotlin.api.checks.AbstractCheck import org.sonarsource.kotlin.api.frontend.KotlinFileContext @Rule(key = "S2068") -class HardcodedCredentialsCheck : AbstractCheck() { +class HardcodedCredentialsCheck : AbstractHardcodedVisitor() { @RuleProperty(key = "credentialWords", description = "Comma separated list of words identifying potential credentials", defaultValue = DEFAULT_VALUE) var credentialWords = DEFAULT_VALUE - private var variablePatterns: Sequence? = null - private var literalPatterns: Sequence? = null + override val sensitiveVariableKind: String + get() = "credential" + override val sensitiveWords: String + get() = credentialWords companion object { private const val DEFAULT_VALUE = "password,passwd,pwd,passphrase" @@ -53,34 +46,12 @@ class HardcodedCredentialsCheck : AbstractCheck() { val parts = userInfo.split(":").toTypedArray() return (parts.size > 1 && parts[0] != parts[1]) && !(parts.size == 2 && parts[1].isEmpty()) } - } catch (e: URISyntaxException) { + } catch (_: URISyntaxException) { // ignore, stringLiteral is not a valid URI } } return false } - - private fun isQuery(value: String, match: String): Boolean { - val followingString = value.substring(value.indexOf(match) + match.length) - return (followingString.startsWith("=?") - || followingString.startsWith("=%") - || followingString.startsWith("=:") - || followingString.startsWith("={") // string format - || followingString == "='") - } - } - - override fun visitBinaryExpression(expression: KtBinaryExpression, context: KotlinFileContext) { - if (expression.operationToken == KtTokens.EQ || expression.operationToken == KtTokens.PLUSEQ) { - val left = expression.left - left?.identifier()?.let { checkVariable(context, left, it, expression.right!!) } - } - } - - override fun visitProperty(property: KtProperty, context: KotlinFileContext) { - property.initializer?.let { - checkVariable(context, property.nameIdentifier!!, property.name!!, it) - } } override fun visitStringTemplateExpression(expression: KtStringTemplateExpression, context: KotlinFileContext) { @@ -88,56 +59,7 @@ class HardcodedCredentialsCheck : AbstractCheck() { if (isURIWithCredentials(content)) { context.reportIssue(expression, "Review this hard-coded URL, which may contain a credential.") } else { - literalPatterns() - .mapNotNull { regex -> regex.find(content) } - .map { matchResult -> matchResult.groups[1]!!.value } - .filter { match: String -> !isQuery(content, match) } - .forEach { credential: String -> context.report(expression, credential) } - } - } - - private fun KtElement.isNotEmptyString() = this is KtStringTemplateExpression - && !this.hasInterpolation() - && this.asConstant().isNotEmpty() - - private fun KotlinFileContext.report(tree: PsiElement, matchName: String) { - reportIssue(tree, """"$matchName" detected here, make sure this is not a hard-coded credential.""") - } - - private fun KotlinFileContext.checkAssignedValue(matchResult: MatchResult, regex: Regex, leftHand: PsiElement, value: String) { - if (!regex.containsMatchIn(value)) { - report(leftHand, matchResult.groups[1]!!.value) - } - } - - private fun KtExpression.identifier(): String? = when (this) { - is KtNameReferenceExpression -> getReferencedName() - is KtDotQualifiedExpression -> selectorExpression?.identifier() - else -> null - } - - private fun checkVariable(ctx: KotlinFileContext, variable: PsiElement, variableName: String, value: KtElement) { - if (value.isNotEmptyString()) { - variablePatterns() - .mapNotNull { regex -> regex.find(variableName)?.let { it to regex } } - .forEach { (matcher, regex) -> - ctx.checkAssignedValue( - matcher, - regex, - variable, - (value as KtStringTemplateExpression).asConstant()) - } + super.visitStringTemplateExpression(expression, context) } } - - private fun variablePatterns() = variablePatterns ?: toPatterns("") - - private fun literalPatterns() = literalPatterns ?: toPatterns("""=\S""") - - private fun toPatterns(suffix: String): Sequence { - return credentialWords.split(",").toTypedArray() - .asSequence() - .map { obj: String -> obj.trim { it <= ' ' } } - .map { word: String -> Regex("($word)$suffix", RegexOption.IGNORE_CASE) } - } } diff --git a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt index 4de897fb8..196b3a440 100644 --- a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt +++ b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/HardcodedSecretsCheck.kt @@ -17,9 +17,60 @@ package org.sonarsource.kotlin.checks import org.sonar.check.Rule -import org.sonarsource.kotlin.api.checks.AbstractCheck +import org.sonar.check.RuleProperty +import org.sonarsource.analyzer.commons.EntropyDetector +import org.sonarsource.analyzer.commons.HumanLanguageDetector + +private const val DEFAULT_SECRET_WORDS = "api[_.-]?key,auth,credential,secret,token" +private const val DEFAULT_RANDOMNESS_SENSIBILITY = "3.0" + +private const val MAX_RANDOMNESS_SENSIBILITY = 10 +private const val MINIMUM_CREDENTIAL_LENGTH = 17 +private const val LANGUAGE_SCORE_INCREMENT = 0.3 @Rule(key = "S6418") -class HardcodedSecretsCheck : AbstractCheck() { - // TODO: implement this rule +class HardcodedSecretsCheck : AbstractHardcodedVisitor() { + + @RuleProperty( + key = "secretWords", + description = "Comma separated list of words identifying potential secrets", + defaultValue = DEFAULT_SECRET_WORDS + ) + var secretWords: String = DEFAULT_SECRET_WORDS + + @RuleProperty( + key = "randomnessSensibility", + description = "Allows to tune the Randomness Sensibility (from 0 to 10)", + defaultValue = DEFAULT_RANDOMNESS_SENSIBILITY + ) + var randomnessSensibility: Double = DEFAULT_RANDOMNESS_SENSIBILITY.toDouble() + + private lateinit var entropyDetector: EntropyDetector + private var maxLanguageScore = 0.0 + + override val sensitiveVariableKind: String + get() = "secret" + override val sensitiveWords: String + get() = secretWords + + private fun getEntropyDetector(): EntropyDetector { + if (::entropyDetector.isInitialized.not()) { + entropyDetector = EntropyDetector(randomnessSensibility) + } + return entropyDetector + } + + override fun isSensitiveStringLiteral(value: String): Boolean { + return value.isNotEmpty() + && value.length >= MINIMUM_CREDENTIAL_LENGTH + && getEntropyDetector().hasEnoughEntropy(value) + && HumanLanguageDetector.humanLanguageScore(value) < maxLanguageScore() + } + + private fun maxLanguageScore(): Double { + if (maxLanguageScore == 0.0) { + maxLanguageScore = (MAX_RANDOMNESS_SENSIBILITY - randomnessSensibility) * LANGUAGE_SCORE_INCREMENT + } + return maxLanguageScore + } } From 9e53473815d9413fb253008107ec9bd147aa6a5a Mon Sep 17 00:00:00 2001 From: Marharyta Nedzelska Date: Thu, 19 Jun 2025 17:07:33 +0200 Subject: [PATCH 3/3] SONARKT-656 Fix from review --- .../src/main/kotlin/checks/HardcodedSecretsCheckSample.kt | 4 +--- .../sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt index 3f7852cf8..2710e23d9 100644 --- a/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt +++ b/kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt @@ -5,9 +5,6 @@ package checks * - 1. String literal * - 2. Variable declaration * - 3. Assignment - * - 4. Method invocations - * - 4.1 Equals - * - 4.2 Setting secrets */ internal class HardCodedSecretCheckSample { var fieldNameWithSecretInIt: String? = retrieveSecret() @@ -100,6 +97,7 @@ internal class HardCodedSecretCheckSample { // Secret containing words and random characters should be filtered val secret001 = "sk_live_xf2fh0Hu3LqXlqqUg2DEWhEz" // Noncompliant + val secret777 = "sk_live_aaaaaaaaaaaaaaaaaaaaaaaa" // Compliant, not enough entropy val secret003 = "examples/commit/8e1d746900f5411e9700fea0" // Noncompliant val secret004 = "examples/commit/revision/469001e9700fea0" val secret006 = "abcdefghijklmnop" // Compliant diff --git a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt index 07f22d618..97b869e9c 100644 --- a/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt +++ b/sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt @@ -50,7 +50,7 @@ abstract class AbstractHardcodedVisitor : AbstractCheck() { } override fun visitBinaryExpression(expression: KtBinaryExpression, context: KotlinFileContext) { - if (expression.operationToken == KtTokens.EQ || expression.operationToken == KtTokens.PLUSEQ || expression.operationToken == KtTokens.EQEQ) { + if (expression.operationToken == KtTokens.EQ || expression.operationToken == KtTokens.PLUSEQ) { val left = expression.left left?.identifier()?.let { checkVariable(context, left, it, expression.right!!) } } @@ -119,9 +119,9 @@ abstract class AbstractHardcodedVisitor : AbstractCheck() { } } - private fun variablePatterns() = variablePatterns ?: toPatterns("") + private fun variablePatterns() = variablePatterns ?: toPatterns("").also { variablePatterns = it } - private fun literalPatterns() = literalPatterns ?: toPatterns("""=([^\s&]+)""") + private fun literalPatterns() = literalPatterns ?: toPatterns("""=([^\s&]+)""").also { literalPatterns = it } private fun toPatterns(suffix: String): Sequence { return sensitiveWords.split(",").toTypedArray()