4242import org .sonar .python .checks .utils .Expressions ;
4343import org .sonar .python .tree .TreeUtils ;
4444import org .sonar .python .types .v2 .TypeCheckBuilder ;
45+ import org .sonar .python .types .v2 .TypeCheckMap ;
4546
4647import static org .sonar .python .checks .hotspots .CommonValidationUtils .isEqualTo ;
4748import static org .sonar .python .checks .hotspots .CommonValidationUtils .isLessThan ;
@@ -188,15 +189,18 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
188189 Map .entry ("hashlib.pbkdf2_hmac" , List .of (HASHLIB_PBKDF2 )),
189190 Map .entry ("cryptography.hazmat.primitives.kdf.scrypt.Scrypt" , List .of (CRYPTOGRAPHY_R , CRYPTOGRAPHY_N , CRYPTOGRAPHY_LENGTH )),
190191 Map .entry ("cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC" , List .of (CRYPTOGRAPHY_PBKDF2 )),
191- Map .entry ("passlib.hash .scrypt.using" , List .of (PASSLIB_BLOCK_SIZE , PASSLIB_ROUNDS )),
192+ Map .entry ("passlib.handlers.scrypt .scrypt.using" , List .of (PASSLIB_BLOCK_SIZE , PASSLIB_ROUNDS )),
192193 Map .entry ("argon2.PasswordHasher" , List .of (new Argon2PasswordHasherValidator (0 , 1 , 2 ))),
193194 Map .entry ("argon2.Parameters" , List .of (new Argon2PasswordHasherValidator (4 , 5 , 6 ))),
194195 Map .entry ("argon2.low_level.hash_secret" , List .of (new Argon2PasswordHasherValidator (2 , 3 , 4 ))),
195196 Map .entry ("argon2.low_level.hash_secret_raw" , List .of (new Argon2PasswordHasherValidator (2 , 3 , 4 ))),
196197 Map .entry ("passlib.handlers.argon2._Argon2Common.using" , List .of (new Argon2PasswordHasherValidator (3 , 4 , 5 ))),
197198 Map .entry ("bcrypt.gensalt" , List .of (BCRYPT_GENSALT )),
198199 Map .entry ("bcrypt.kdf" , List .of (BCRYPT_KDF )),
199- Map .entry ("flask_bcrypt.generate_password_hash" , List .of (FLASK_BCRYPT )),
200+ Map .entry ("flask_bcrypt.generate_password_hash" , List .of (FLASK_BCRYPT ))
201+ );
202+
203+ private static final Map <String , Collection <CallValidator >> CALL_EXPRESSION_VALIDATORS_V1 = Map .ofEntries (
200204 Map .entry ("flask_bcrypt.Bcrypt.generate_password_hash" , List .of (FLASK_BCRYPT ))
201205 );
202206
@@ -210,6 +214,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
210214
211215 private TypeCheckBuilder argon2CheapestProfileTypeChecker = null ;
212216 private TypeCheckBuilder flaskConfigTypeChecker = null ;
217+ private TypeCheckMap <Collection <CallValidator >> typeCheckMap = new TypeCheckMap <>();
213218 @ Override
214219 public void initialize (Context context ) {
215220 context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , this ::registerTypeCheckers );
@@ -219,7 +224,7 @@ public void initialize(Context context) {
219224 }
220225 context .registerSyntaxNodeConsumer (Tree .Kind .ASSIGNMENT_STMT , FastHashingOrPlainTextCheck ::checkDjangoHasher );
221226 });
222- context .registerSyntaxNodeConsumer (Tree .Kind .CALL_EXPR , FastHashingOrPlainTextCheck ::checkCallExpr );
227+ context .registerSyntaxNodeConsumer (Tree .Kind .CALL_EXPR , this ::checkCallExpr );
223228 context .registerSyntaxNodeConsumer (Tree .Kind .NAME , this ::checkName );
224229 context .registerSyntaxNodeConsumer (Tree .Kind .ASSIGNMENT_STMT , subscriptionContext -> checkAssignment (subscriptionContext , flaskConfigTypeChecker ));
225230 }
@@ -249,6 +254,11 @@ private static void checkDjangoHasher(SubscriptionContext subscriptionContext) {
249254 private void registerTypeCheckers (SubscriptionContext subscriptionContext ) {
250255 argon2CheapestProfileTypeChecker = subscriptionContext .typeChecker ().typeCheckBuilder ().isTypeWithFqn ("argon2.profiles.CHEAPEST" );
251256 flaskConfigTypeChecker = subscriptionContext .typeChecker ().typeCheckBuilder ().isInstanceOf ("flask.config.Config" );
257+ typeCheckMap = new TypeCheckMap <>();
258+ CALL_EXPRESSION_VALIDATORS .forEach ((key , value ) -> {
259+ var typeCheckBuilder = subscriptionContext .typeChecker ().typeCheckBuilder ().isTypeWithFqn (key );
260+ typeCheckMap .put (typeCheckBuilder , value );
261+ });
252262 }
253263
254264 private static void checkAssignment (SubscriptionContext subscriptionContext , TypeCheckBuilder flaskConfigTypeChecker ) {
@@ -298,18 +308,22 @@ private static boolean isChildOf(AssignmentStatement ancestorAssign, Name name)
298308 return ancestorAssign .lhsExpressions ().stream ().flatMap (expressionList -> expressionList .children ().stream ()).anyMatch (tree -> tree == name );
299309 }
300310
301- private static void checkCallExpr (SubscriptionContext subscriptionContext ) {
311+ private void checkCallExpr (SubscriptionContext subscriptionContext ) {
302312 CallExpression callExpression = (CallExpression ) subscriptionContext .syntaxNode ();
303313 var qualifiedName = qualifiedNameOrEmpty (callExpression );
304- var configs = CALL_EXPRESSION_VALIDATORS .getOrDefault (qualifiedName , List .of ());
305-
306- configs .forEach (config -> config .validate (subscriptionContext , callExpression ));
307-
308- // We can't directly type check against the callee type (or FQN) for the passlib PBKDF2 because both type inference engine resolve passlib.utils.handlers.HasRounds.using
309- // which will result in false positives
314+ var configsV2 = typeCheckMap .getOptionalForType (callExpression .callee ().typeV2 ()).orElse (List .of ());
315+ configsV2 .forEach (config -> config .validate (subscriptionContext , callExpression ));
316+ // We need to keep some V1 checks because of SONARPY-2268.
317+ // When resolving members, we can get an UnknownType for the callee. This looses all type information and makes typechecking impossible
318+ // We could use the qualified expression mechanism to bypass this limitation, however handling it at the type inference level is better
319+ var configsV1 = CALL_EXPRESSION_VALIDATORS_V1 .getOrDefault (qualifiedName , List .of ());
320+ configsV1 .forEach (config -> config .validate (subscriptionContext , callExpression ));
321+
322+ // We can't directly type check against the callee type (or FQN) for the passlib PBKDF2 because both type inference engine resolve passlib.utils.handlers.HasRounds.using or
323+ // passlib.ifc.PasswordHash.using which will result in false positives
310324 if (callExpression .callee ().is (Tree .Kind .QUALIFIED_EXPR )) {
311325 var fqn = TreeUtils .fullyQualifiedNameFromQualifiedExpression (((QualifiedExpression ) callExpression .callee ())).orElse ("" );
312- configs = QUALIFIED_EXPR_VALIDATOR .getOrDefault (fqn , List .of ());
326+ var configs = QUALIFIED_EXPR_VALIDATOR .getOrDefault (fqn , List .of ());
313327 configs .forEach (config -> config .validate (subscriptionContext , callExpression ));
314328 }
315329 }
0 commit comments