Skip to content

Commit 66323c2

Browse files
SONARPY-1297 Rule S6437: Credentials should not be hard-coded (#1462)
1 parent 46c3754 commit 66323c2

File tree

13 files changed

+632
-3
lines changed

13 files changed

+632
-3
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
{
2+
'project:buildbot-0.8.6p1/buildbot/clients/tryclient.py':[
3+
600,
4+
],
5+
'project:buildbot-0.8.6p1/buildbot/test/unit/test_pbmanager.py':[
6+
54,
7+
],
8+
'project:buildbot-0.8.6p1/contrib/bb_applet.py':[
9+
184,
10+
],
11+
'project:buildbot-0.8.6p1/contrib/bitbucket_buildbot.py':[
12+
104,
13+
],
14+
'project:buildbot-0.8.6p1/contrib/bk_buildbot.py':[
15+
118,
16+
],
17+
'project:buildbot-0.8.6p1/contrib/fakechange.py':[
18+
77,
19+
],
20+
'project:buildbot-0.8.6p1/contrib/github_buildbot.py':[
21+
114,
22+
],
23+
'project:buildbot-0.8.6p1/contrib/viewcvspoll.py':[
24+
94,
25+
],
26+
'project:twisted-12.1.0/doc/core/examples/pb_exceptions.py':[
27+
27,
28+
],
29+
'project:twisted-12.1.0/doc/core/examples/pbbenchclient.py':[
30+
33,
31+
],
32+
'project:twisted-12.1.0/doc/core/examples/pbbenchserver.py':[
33+
48,
34+
],
35+
'project:twisted-12.1.0/doc/core/examples/pbecho.py':[
36+
48,
37+
],
38+
'project:twisted-12.1.0/doc/core/examples/pbechoclient.py':[
39+
30,
40+
],
41+
'project:twisted-12.1.0/doc/core/howto/listings/pb/chatclient.py':[
42+
17,
43+
],
44+
'project:twisted-12.1.0/doc/core/howto/listings/pb/pb5client.py':[
45+
13,
46+
],
47+
'project:twisted-12.1.0/doc/core/howto/listings/pb/pb6client1.py':[
48+
13,
49+
],
50+
'project:twisted-12.1.0/doc/core/howto/listings/pb/pb6client2.py':[
51+
16,
52+
],
53+
'project:twisted-12.1.0/doc/core/howto/listings/pb/pbAnonClient.py':[
54+
59,
55+
],
56+
'project:twisted-12.1.0/doc/words/examples/jabber_client.py':[
57+
26,
58+
],
59+
'project:twisted-12.1.0/twisted/conch/test/test_checkers.py':[
60+
106,
61+
126,
62+
147,
63+
199,
64+
206,
65+
255,
66+
385,
67+
387,
68+
405,
69+
407,
70+
417,
71+
468,
72+
471,
73+
473,
74+
474,
75+
487,
76+
524,
77+
540,
78+
552,
79+
567,
80+
586,
81+
597,
82+
598,
83+
605,
84+
608,
85+
],
86+
'project:twisted-12.1.0/twisted/conch/test/test_keys.py':[
87+
194,
88+
],
89+
'project:twisted-12.1.0/twisted/conch/test/test_tap.py':[
90+
119,
91+
],
92+
'project:twisted-12.1.0/twisted/mail/test/test_mail.py':[
93+
618,
94+
624,
95+
],
96+
'project:twisted-12.1.0/twisted/mail/test/test_smtp.py':[
97+
599,
98+
621,
99+
645,
100+
819,
101+
],
102+
'project:twisted-12.1.0/twisted/test/test_ftp_options.py':[
103+
62,
104+
],
105+
'project:twisted-12.1.0/twisted/test/test_newcred.py':[
106+
100,
107+
142,
108+
150,
109+
269,
110+
],
111+
'project:twisted-12.1.0/twisted/test/test_pb.py':[
112+
1196,
113+
1355,
114+
1384,
115+
1433,
116+
1461,
117+
1463,
118+
1491,
119+
1493,
120+
1589,
121+
1613,
122+
1675,
123+
],
124+
'project:twisted-12.1.0/twisted/test/test_strcred.py':[
125+
95,
126+
96,
127+
97,
128+
98,
129+
186,
130+
187,
131+
188,
132+
189,
133+
267,
134+
268,
135+
269,
136+
270,
137+
],
138+
'project:twisted-12.1.0/twisted/words/test/test_basechat.py':[
139+
19,
140+
],
141+
'project:twisted-12.1.0/twisted/words/test/test_jabberclient.py':[
142+
396,
143+
],
144+
'project:twisted-12.1.0/twisted/words/test/test_jabbercomponent.py':[
145+
70,
146+
110,
147+
216,
148+
363,
149+
],
150+
'project:twisted-12.1.0/twisted/words/test/test_jabbersaslmechanisms.py':[
151+
17,
152+
38,
153+
],
154+
'project:twisted-12.1.0/twisted/words/test/test_jabberxmlstream.py':[
155+
36,
156+
60,
157+
],
158+
'project:twisted-12.1.0/twisted/words/test/test_tap.py':[
159+
16,
160+
17,
161+
],
162+
}

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
<sslr.version>1.23</sslr.version>
9999
<protobuf.version>3.21.7</protobuf.version>
100100
<woodstox.version>6.2.7</woodstox.version>
101+
<gson.version>2.8.9</gson.version>
101102

102103
<!-- Advertise minimal required JRE version -->
103104
<jre.min.version>11</jre.min.version>
@@ -147,6 +148,12 @@
147148
<artifactId>sonar-regex-parsing</artifactId>
148149
<version>${sonar-analyzer-commons.version}</version>
149150
</dependency>
151+
152+
<dependency>
153+
<groupId>com.google.code.gson</groupId>
154+
<artifactId>gson</artifactId>
155+
<version>${gson.version}</version>
156+
</dependency>
150157
<!-- used by StaxParser, CoberturaParser and TestSuiteParser -->
151158
<dependency>
152159
<groupId>org.codehaus.staxmate</groupId>

python-checks/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<groupId>commons-io</groupId>
2727
<artifactId>commons-io</artifactId>
2828
</dependency>
29+
<dependency>
30+
<groupId>com.google.code.gson</groupId>
31+
<artifactId>gson</artifactId>
32+
</dependency>
2933

3034
<!-- test dependencies -->
3135
<dependency>

python-checks/src/main/java/org/sonar/python/checks/CheckList.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ public static Iterable<Class> getChecks() {
358358
DjangoModelFormFieldsCheck.class,
359359
DjangoReceiverDecoratorCheck.class,
360360
DjangoModelStringFieldCheck.class,
361-
DjangoModelStrMethodCheck.class
361+
DjangoModelStrMethodCheck.class,
362+
HardcodedCredentialsCallCheck.class
362363
)));
363364
}
364365

python-checks/src/main/java/org/sonar/python/checks/DbNoPasswordCheck.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public class DbNoPasswordCheck extends PythonSubscriptionCheck {
5656
"mysql.connector.connect",
5757
"mysql.connector.connection.MySQLConnection",
5858
"pymysql.connect",
59-
"pymysql.connections.Connection",
6059
"psycopg2.connect",
6160
"pgdb.connect",
6261
"pg.DB",
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2023 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks;
21+
22+
import com.google.gson.Gson;
23+
import java.io.IOException;
24+
import java.io.InputStreamReader;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.function.Function;
29+
import java.util.function.Predicate;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
32+
import org.sonar.check.Rule;
33+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
34+
import org.sonar.plugins.python.api.SubscriptionContext;
35+
import org.sonar.plugins.python.api.symbols.Symbol;
36+
import org.sonar.plugins.python.api.tree.CallExpression;
37+
import org.sonar.plugins.python.api.tree.Expression;
38+
import org.sonar.plugins.python.api.tree.Name;
39+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
40+
import org.sonar.plugins.python.api.tree.RegularArgument;
41+
import org.sonar.plugins.python.api.tree.StringLiteral;
42+
import org.sonar.plugins.python.api.tree.Tree;
43+
import org.sonar.python.tree.TreeUtils;
44+
45+
@Rule(key = "S6437")
46+
public class HardcodedCredentialsCallCheck extends PythonSubscriptionCheck {
47+
48+
private static final String MESSAGE = "Revoke and change this password, as it is compromised.";
49+
private final Map<String, CredentialMethod> methods;
50+
51+
public HardcodedCredentialsCallCheck() {
52+
methods = new CredentialMethodsLoader().load();
53+
}
54+
55+
@Override
56+
public void initialize(Context context) {
57+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::processCallExpression);
58+
}
59+
60+
private void processCallExpression(SubscriptionContext ctx) {
61+
Optional.of(ctx.syntaxNode())
62+
.filter(CallExpression.class::isInstance)
63+
.map(CallExpression.class::cast)
64+
.filter(this::callHasToBeChecked)
65+
.ifPresent(call -> checkCallArguments(ctx, call));
66+
}
67+
68+
private void checkCallArguments(SubscriptionContext ctx, CallExpression call) {
69+
getMethod(call)
70+
.ifPresent(method -> method.indices()
71+
.forEach(argumentIndex -> {
72+
var argumentName = method.args().get(argumentIndex);
73+
var argument = TreeUtils.nthArgumentOrKeyword(argumentIndex, argumentName, call.arguments());
74+
if (argument != null) {
75+
checkArgument(ctx, argument);
76+
}
77+
}));
78+
}
79+
80+
private static void checkArgument(SubscriptionContext ctx, RegularArgument argument) {
81+
var argExp = argument.expression();
82+
if (argExp.is(Tree.Kind.STRING_LITERAL)) {
83+
Optional.of(argExp)
84+
.filter(StringLiteral.class::isInstance)
85+
.map(StringLiteral.class::cast)
86+
.filter(HardcodedCredentialsCallCheck::isNotEmpty)
87+
.ifPresent(string -> ctx.addIssue(argument, MESSAGE));
88+
} else if (argExp.is(Tree.Kind.NAME)) {
89+
findAssignment((Name) argExp, 0)
90+
.filter(StringLiteral.class::isInstance)
91+
.map(StringLiteral.class::cast)
92+
.filter(HardcodedCredentialsCallCheck::isNotEmpty)
93+
.ifPresent(assignedValue -> ctx.addIssue(argument, MESSAGE).secondary(assignedValue, MESSAGE));
94+
}
95+
}
96+
97+
private static boolean isNotEmpty(StringLiteral stringLiteral) {
98+
return Optional.of(stringLiteral)
99+
.map(StringLiteral::trimmedQuotesValue)
100+
.filter(Predicate.not(String::isEmpty))
101+
.isPresent();
102+
}
103+
104+
private static Optional<Tree> findAssignment(Name name, int depth) {
105+
if (depth > 99) {
106+
return Optional.empty();
107+
}
108+
return Optional.of(name)
109+
.map(Expressions::singleAssignedValue)
110+
.map(v -> findValue(v, depth));
111+
}
112+
113+
private static Tree findValue(Expression assignedValue, int depth) {
114+
if (assignedValue.is(Tree.Kind.NAME)) {
115+
return findAssignment((Name) assignedValue, depth + 1).orElse(null);
116+
} else if (assignedValue.is(Tree.Kind.CALL_EXPR)) {
117+
return Optional.of(assignedValue)
118+
.filter(CallExpression.class::isInstance)
119+
.map(CallExpression.class::cast)
120+
.map(CallExpression::callee)
121+
.filter(QualifiedExpression.class::isInstance)
122+
.map(QualifiedExpression.class::cast)
123+
.map(QualifiedExpression::qualifier)
124+
.map(v -> findValue(v, depth + 1))
125+
.orElse(assignedValue);
126+
}
127+
return assignedValue;
128+
}
129+
130+
private Boolean callHasToBeChecked(CallExpression call) {
131+
return Optional.of(call)
132+
.map(CallExpression::calleeSymbol)
133+
.map(Symbol::fullyQualifiedName)
134+
.map(methods::containsKey)
135+
.orElse(false);
136+
}
137+
138+
private Optional<CredentialMethod> getMethod(CallExpression call) {
139+
return Optional.of(call)
140+
.map(CallExpression::calleeSymbol)
141+
.map(Symbol::fullyQualifiedName)
142+
.map(methods::get);
143+
}
144+
145+
public static class CredentialMethod {
146+
private String name;
147+
private List<String> args;
148+
private List<Integer> indices;
149+
150+
public String name() {
151+
return name;
152+
}
153+
154+
public List<String> args() {
155+
return args;
156+
}
157+
158+
public List<Integer> indices() {
159+
return indices;
160+
}
161+
}
162+
163+
private static class CredentialMethodsLoader {
164+
private static final String METHODS_RESOURCE_PATH = "/org/sonar/python/checks/hardcoded_credentials_call_check_meta.json";
165+
private final Gson gson;
166+
167+
private CredentialMethodsLoader() {
168+
gson = new Gson();
169+
}
170+
171+
private Map<String, CredentialMethod> load() {
172+
try (var is = HardcodedCredentialsCallCheck.class.getResourceAsStream(METHODS_RESOURCE_PATH)) {
173+
return Optional.ofNullable(is)
174+
.map(InputStreamReader::new)
175+
.map(r -> gson.fromJson(r, CredentialMethod[].class))
176+
.stream()
177+
.flatMap(Stream::of)
178+
.collect(Collectors.toMap(CredentialMethod::name, Function.identity()));
179+
} catch (IOException e) {
180+
throw new IllegalStateException("Unable to read methods metadata from " + METHODS_RESOURCE_PATH, e);
181+
}
182+
}
183+
}
184+
185+
}

0 commit comments

Comments
 (0)