Skip to content

Commit 3cba41d

Browse files
GabinL21sonartech
authored andcommitted
SONARPY-2787 [S6437] Support additional libraries (#184)
GitOrigin-RevId: 67c5e689dcad634f3bcba83f8f052f1a8d2502f1
1 parent 227f509 commit 3cba41d

5 files changed

Lines changed: 133 additions & 32 deletions

File tree

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

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import java.io.InputStreamReader;
2222
import java.util.List;
2323
import java.util.Map;
24+
import java.util.Objects;
2425
import java.util.Optional;
2526
import java.util.function.Function;
2627
import java.util.function.Predicate;
2728
import java.util.stream.Collectors;
2829
import java.util.stream.Stream;
30+
import javax.annotation.Nullable;
2931
import org.sonar.check.Rule;
3032
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
3133
import org.sonar.plugins.python.api.SubscriptionContext;
@@ -64,15 +66,18 @@ private void processCallExpression(SubscriptionContext ctx) {
6466
}
6567

6668
private void checkCallArguments(SubscriptionContext ctx, CallExpression call) {
67-
getMethod(call)
68-
.ifPresent(method -> method.indices()
69-
.forEach(argumentIndex -> {
70-
var argumentName = method.args().get(argumentIndex);
71-
var argument = TreeUtils.nthArgumentOrKeyword(argumentIndex, argumentName, call.arguments());
72-
if (argument != null) {
73-
checkArgument(ctx, argument);
74-
}
75-
}));
69+
getMethod(call).ifPresent(method ->
70+
getArgumentsToCheck(call, method)
71+
.forEach(argument -> checkArgument(ctx, argument))
72+
);
73+
}
74+
75+
private Stream<RegularArgument> getArgumentsToCheck(CallExpression call, CredentialMethod method) {
76+
return method.sensitiveArguments().stream()
77+
.map(sensitiveArgument -> sensitiveArgument.index() != null
78+
? TreeUtils.nthArgumentOrKeyword(sensitiveArgument.index(), sensitiveArgument.name(), call.arguments())
79+
: TreeUtils.argumentByKeyword(sensitiveArgument.name(), call.arguments()))
80+
.filter(Objects::nonNull);
7681
}
7782

7883
private static void checkArgument(SubscriptionContext ctx, RegularArgument argument) {
@@ -142,42 +147,41 @@ private Optional<CredentialMethod> getMethod(CallExpression call) {
142147
.map(methods::get);
143148
}
144149

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-
}
150+
public record CredentialMethod(
151+
String name,
152+
List<MethodArgument> sensitiveArguments) {
153+
}
157154

158-
public List<Integer> indices() {
159-
return indices;
160-
}
155+
public record MethodArgument(
156+
String name,
157+
@Nullable Integer index) {
161158
}
162159

163160
private static class CredentialMethodsLoader {
164-
private static final String METHODS_RESOURCE_PATH = "/org/sonar/python/checks/hardcoded_credentials_call_check_meta.json";
161+
private static final String CHECKS_DIR = "/org/sonar/python/checks";
162+
private static final String GENERATED_METHODS_RESOURCE_PATH = CHECKS_DIR + "/generated_hardcoded_credentials_call_check_meta.json";
163+
private static final String MANUAL_METHODS_RESOURCE_PATH = CHECKS_DIR + "/manual_hardcoded_credentials_call_check_meta.json";
165164
private final Gson gson;
166165

167166
private CredentialMethodsLoader() {
168167
gson = new Gson();
169168
}
170169

171170
private Map<String, CredentialMethod> load() {
172-
try (var is = HardcodedCredentialsCallCheck.class.getResourceAsStream(METHODS_RESOURCE_PATH)) {
171+
var generatedCredentialMethods = loadMethodsFromResource(GENERATED_METHODS_RESOURCE_PATH);
172+
var manualCredentialMethods = loadMethodsFromResource(MANUAL_METHODS_RESOURCE_PATH);
173+
return Stream.concat(Stream.of(generatedCredentialMethods), Stream.of(manualCredentialMethods))
174+
.collect(Collectors.toMap(CredentialMethod::name, Function.identity())); // Will throw an exception if there are duplicates
175+
}
176+
177+
private CredentialMethod[] loadMethodsFromResource(String resourcePath) {
178+
try (var is = HardcodedCredentialsCallCheck.class.getResourceAsStream(resourcePath)) {
173179
return Optional.ofNullable(is)
174180
.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()));
181+
.map(reader -> gson.fromJson(reader, CredentialMethod[].class))
182+
.orElseThrow(() -> new IllegalStateException("Unable to open resource: " + resourcePath));
179183
} catch (IOException e) {
180-
throw new IllegalStateException("Unable to read methods metadata from " + METHODS_RESOURCE_PATH, e);
184+
throw new IllegalStateException("Unable to read methods metadata from " + resourcePath, e);
181185
}
182186
}
183187
}

python-checks/src/main/resources/org/sonar/python/checks/generated_hardcoded_credentials_call_check_meta.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

python-checks/src/main/resources/org/sonar/python/checks/hardcoded_credentials_call_check_meta.json

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[
2+
{
3+
"name": "psycopg.connect",
4+
"sensitiveArguments": [
5+
{ "name": "password" }
6+
]
7+
},
8+
{
9+
"name": "psycopg2.connect",
10+
"sensitiveArguments": [
11+
{ "name": "password" }
12+
]
13+
},
14+
{
15+
"name": "mysql.connector.connect",
16+
"sensitiveArguments": [
17+
{ "name": "password" },
18+
{ "name": "passwd" },
19+
{ "name": "password1" },
20+
{ "name": "password2" },
21+
{ "name": "password3" }
22+
]
23+
},
24+
{
25+
"name": "mysql.connector.connection.MySQLConnection",
26+
"sensitiveArguments": [
27+
{ "name": "password" },
28+
{ "name": "passwd" },
29+
{ "name": "password1" },
30+
{ "name": "password2" },
31+
{ "name": "password3" }
32+
]
33+
},
34+
{
35+
"name": "pymysql.connections.connect",
36+
"sensitiveArguments": [
37+
{ "name": "password" },
38+
{ "name": "passwd" }
39+
]
40+
},
41+
{
42+
"name": "redis.Redis",
43+
"sensitiveArguments": [
44+
{ "name": "password", "index": 3 },
45+
{ "name": "ssl_password", "index": 25 }
46+
]
47+
},
48+
{
49+
"name": "pymongo.MongoClient",
50+
"sensitiveArguments": [
51+
{ "name": "password" }
52+
]
53+
},
54+
{
55+
"name": "boto3.resource",
56+
"sensitiveArguments": [
57+
{ "name": "aws_secret_access_key" }
58+
]
59+
}
60+
]

python-checks/src/test/resources/checks/hardcodedCredentialsCall.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@ def obj_creation_hardcoded_assigned_value(password):
4646
def top_level_function_call():
4747
psycopg2cffi.connect(password="abc") # Noncompliant
4848

49+
50+
# Methods manually defined in "manual_hardcoded_credentials_call_check_meta.json"
51+
import psycopg
52+
import psycopg2
53+
import mysql.connector
54+
from mysql.connector import MySQLConnection
55+
import pymysql
56+
import redis as r
57+
from pymongo import MongoClient
58+
import boto3
59+
60+
def manually_defined_method_calls_with_named_params():
61+
psycopg.connect(password="abc") # Noncompliant
62+
psycopg2.connect(password="abc") # Noncompliant
63+
mysql.connector.connect(password='abc') # Noncompliant
64+
mysql.connector.connect(passwd='abc') # Noncompliant
65+
mysql.connector.connect(password1='abc', password2='abc', password3='abc') # Noncompliant 3
66+
MySQLConnection(password='abc') # Noncompliant
67+
MySQLConnection(passwd='abc') # Noncompliant
68+
MySQLConnection(password1='abc', password2='abc', password3='abc') # Noncompliant 3
69+
pymysql.connect(password="abc") # Noncompliant
70+
pymysql.connect(passwd="abc") # Noncompliant
71+
r.Redis(password="abc") # Noncompliant
72+
MongoClient(password="abc") # Noncompliant
73+
boto3.resource(aws_secret_access_key="abc") # Noncompliant
74+
75+
def manually_defined_method_calls_with_positional_params():
76+
r.Redis("host", "port", "db", "password") # Noncompliant
77+
# The following calls are compliant because the password can't be a positional parameter
78+
psycopg.connect("abc")
79+
psycopg2.connect("abc")
80+
mysql.connector.connect("abc")
81+
MySQLConnection("abc")
82+
pymysql.connect("abc")
83+
MongoClient("abc")
84+
boto3.resource("abc")
85+
4986
# from S4433
5087
import ldap
5188
import os

0 commit comments

Comments
 (0)