|
19 | 19 | import java.util.Collection; |
20 | 20 | import java.util.List; |
21 | 21 | import java.util.Map; |
| 22 | +import java.util.Optional; |
22 | 23 | import java.util.Set; |
| 24 | +import java.util.stream.Stream; |
23 | 25 | import org.sonar.check.Rule; |
24 | 26 | import org.sonar.plugins.python.api.PythonSubscriptionCheck; |
25 | 27 | import org.sonar.plugins.python.api.SubscriptionContext; |
26 | 28 | import org.sonar.plugins.python.api.tree.AssignmentStatement; |
27 | 29 | import org.sonar.plugins.python.api.tree.CallExpression; |
28 | 30 | import org.sonar.plugins.python.api.tree.Expression; |
29 | 31 | import org.sonar.plugins.python.api.tree.ExpressionList; |
| 32 | +import org.sonar.plugins.python.api.tree.ListLiteral; |
30 | 33 | import org.sonar.plugins.python.api.tree.Name; |
31 | 34 | import org.sonar.plugins.python.api.tree.QualifiedExpression; |
32 | 35 | import org.sonar.plugins.python.api.tree.RegularArgument; |
@@ -54,12 +57,21 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck { |
54 | 57 | private static final String PBKDF2_MESSAGE = "Use at least 100 000 iterations."; |
55 | 58 | private static final String ARGON2_MESSAGE = "Use secure Argon2 parameters."; |
56 | 59 | private static final String BCRYPT_MESSAGE = "Use strong bcrypt parameters."; |
| 60 | + private static final String DJANGO_MESSAGE = "Use a secure hashing algorithm to store passwords."; |
| 61 | + |
57 | 62 | private static final Set<String> PBKDF2_ALGOS = Set.of( |
58 | 63 | "sha1", |
59 | 64 | "sha256", |
60 | 65 | "sha512" |
61 | 66 | ); |
62 | 67 | private static final String ROUNDS = "rounds"; |
| 68 | + private static final Set<String> DJANGO_FIRST_FORBIDDEN_HASHERS = Set.of( |
| 69 | + "django.contrib.auth.hashers.SHA1PasswordHasher", |
| 70 | + "django.contrib.auth.hashers.MD5PasswordHasher", |
| 71 | + "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher", |
| 72 | + "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher", |
| 73 | + "django.contrib.auth.hashers.CryptPasswordHasher" |
| 74 | + ); |
63 | 75 |
|
64 | 76 |
|
65 | 77 | private static final ArgumentValidator SCRYPT_R = new ArgumentValidator( |
@@ -201,11 +213,39 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck { |
201 | 213 | @Override |
202 | 214 | public void initialize(Context context) { |
203 | 215 | context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::registerTypeCheckers); |
| 216 | + context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, subscriptionContext1 -> { |
| 217 | + if (!"settings.py".equals(subscriptionContext1.pythonFile().fileName())) { |
| 218 | + return; |
| 219 | + } |
| 220 | + context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, FastHashingOrPlainTextCheck::checkDjangoHasher); |
| 221 | + }); |
204 | 222 | context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, FastHashingOrPlainTextCheck::checkCallExpr); |
205 | 223 | context.registerSyntaxNodeConsumer(Tree.Kind.NAME, this::checkName); |
206 | 224 | context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, subscriptionContext -> checkAssignment(subscriptionContext, flaskConfigTypeChecker)); |
207 | 225 | } |
208 | 226 |
|
| 227 | + private static void checkDjangoHasher(SubscriptionContext subscriptionContext) { |
| 228 | + var stmt = (AssignmentStatement) subscriptionContext.syntaxNode(); |
| 229 | + var lhsIsConfig = stmt.lhsExpressions().stream().findFirst() |
| 230 | + .map(ExpressionList::expressions) |
| 231 | + .flatMap(list -> list.stream().findFirst()) |
| 232 | + .filter(expression -> expression.is(Tree.Kind.NAME)) |
| 233 | + .filter(name -> "PASSWORD_HASHERS".equals(((Name) name).name())); |
| 234 | + |
| 235 | + var firstRhsString = Optional.of(stmt.assignedValue()) |
| 236 | + .flatMap(TreeUtils.toOptionalInstanceOfMapper(ListLiteral.class)) |
| 237 | + .map(ListLiteral::elements) |
| 238 | + .map(ExpressionList::expressions) |
| 239 | + .map(List::stream) |
| 240 | + .flatMap(Stream::findFirst) |
| 241 | + .flatMap(TreeUtils.toOptionalInstanceOfMapper(StringLiteral.class)) |
| 242 | + .filter(stringLiteral -> DJANGO_FIRST_FORBIDDEN_HASHERS.contains(stringLiteral.trimmedQuotesValue())); |
| 243 | + |
| 244 | + if (lhsIsConfig.isPresent() && firstRhsString.isPresent()) { |
| 245 | + subscriptionContext.addIssue(firstRhsString.get(), DJANGO_MESSAGE); |
| 246 | + } |
| 247 | + } |
| 248 | + |
209 | 249 | private void registerTypeCheckers(SubscriptionContext subscriptionContext) { |
210 | 250 | argon2CheapestProfileTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isTypeWithFqn("argon2.profiles.CHEAPEST"); |
211 | 251 | flaskConfigTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isInstanceOf("flask.config.Config"); |
|
0 commit comments