2626import org .sonar .plugins .python .api .tree .AssignmentStatement ;
2727import org .sonar .plugins .python .api .tree .CallExpression ;
2828import org .sonar .plugins .python .api .tree .Expression ;
29+ import org .sonar .plugins .python .api .tree .ExpressionList ;
2930import org .sonar .plugins .python .api .tree .Name ;
3031import org .sonar .plugins .python .api .tree .QualifiedExpression ;
3132import org .sonar .plugins .python .api .tree .RegularArgument ;
3233import org .sonar .plugins .python .api .tree .StringLiteral ;
34+ import org .sonar .plugins .python .api .tree .SubscriptionExpression ;
3335import org .sonar .plugins .python .api .tree .Tree ;
3436import org .sonar .plugins .python .api .types .v2 .TriBool ;
3537import org .sonar .python .checks .hotspots .CommonValidationUtils .ArgumentValidator ;
4042
4143import static org .sonar .python .checks .hotspots .CommonValidationUtils .isEqualTo ;
4244import static org .sonar .python .checks .hotspots .CommonValidationUtils .isLessThan ;
45+ import static org .sonar .python .checks .hotspots .CommonValidationUtils .isLessThanExponent ;
4346import static org .sonar .python .semantic .SymbolUtils .qualifiedNameOrEmpty ;
4447import static org .sonar .python .tree .TreeUtils .nthArgumentOrKeyword ;
4548import static org .sonar .python .tree .TreeUtils .nthArgumentOrKeywordOptional ;
@@ -50,6 +53,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
5053 private static final String SCRYPT_PARAMETERS_MESSAGE = "Use strong scrypt parameters." ;
5154 private static final String PBKDF2_MESSAGE = "Use at least 100 000 iterations." ;
5255 private static final String ARGON2_MESSAGE = "Use secure Argon2 parameters." ;
56+ private static final String BCRYPT_MESSAGE = "Use strong bcrypt parameters." ;
5357 private static final Set <String > PBKDF2_ALGOS = Set .of (
5458 "sha1" ,
5559 "sha256" ,
@@ -72,7 +76,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
7276 });
7377 private static final ArgumentValidator SCRYPT_N = new ArgumentValidator (
7478 2 , "N" , (ctx , argument ) -> {
75- if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || CommonValidationUtils . isLessThanExponent (argument .expression (), 13 )) {
79+ if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || isLessThanExponent (argument .expression (), 13 )) {
7680 ctx .addIssue (argument , SCRYPT_PARAMETERS_MESSAGE );
7781 }
7882 });
@@ -85,7 +89,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
8589 });
8690 private static final ArgumentValidator HASHLIB_N = new ArgumentValidator (
8791 2 , "n" , (ctx , argument ) -> {
88- if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || CommonValidationUtils . isLessThanExponent (argument .expression (), 13 )) {
92+ if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || isLessThanExponent (argument .expression (), 13 )) {
8993 ctx .addIssue (argument , SCRYPT_PARAMETERS_MESSAGE );
9094 }
9195 });
@@ -104,7 +108,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
104108 });
105109 private static final ArgumentValidator CRYPTOGRAPHY_N = new ArgumentValidator (
106110 2 , "n" , (ctx , argument ) -> {
107- if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || CommonValidationUtils . isLessThanExponent (argument .expression (), 13 )) {
111+ if (isLessThan (argument .expression (), (int ) Math .pow (2 , 13 )) || isLessThanExponent (argument .expression (), 13 )) {
108112 ctx .addIssue (argument , SCRYPT_PARAMETERS_MESSAGE );
109113 }
110114 });
@@ -141,6 +145,30 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
141145 2 , ROUNDS , PBKDF2_MESSAGE
142146 );
143147
148+ private static final CallValidator BCRYPT_GENSALT = new ArgumentValidator (
149+ 0 , ROUNDS , (ctx , argument ) -> {
150+ if (isLessThan (argument .expression (), 12 )) {
151+ ctx .addIssue (argument , BCRYPT_MESSAGE );
152+ }
153+ });
154+ private static final CallValidator BCRYPT_KDF = new ArgumentValidator (
155+ 3 , ROUNDS , (ctx , argument ) -> {
156+ if (isLessThan (argument .expression (), 4096 ) || isLessThanExponent (argument .expression (), 12 )) {
157+ ctx .addIssue (argument , BCRYPT_MESSAGE );
158+ }
159+ });
160+ private static final CallValidator PASSLIB_BCRYPT = new ArgumentValidator (
161+ 3 , ROUNDS , (ctx , argument ) -> {
162+ if (isLessThan (argument .expression (), 12 )) {
163+ ctx .addIssue (argument , BCRYPT_MESSAGE );
164+ }
165+ });
166+ private static final CallValidator FLASK_BCRYPT = new ArgumentValidator (
167+ 1 , ROUNDS , (ctx , argument ) -> {
168+ if (isLessThan (argument .expression (), 12 )) {
169+ ctx .addIssue (argument , BCRYPT_MESSAGE );
170+ }
171+ });
144172
145173 private static final Map <String , Collection <CallValidator >> CALL_EXPRESSION_VALIDATORS = Map .ofEntries (
146174 Map .entry ("scrypt.hash" , List .of (SCRYPT_R , SCRYPT_BUFLEN , SCRYPT_N )),
@@ -153,26 +181,65 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
153181 Map .entry ("argon2.Parameters" , List .of (new Argon2PasswordHasherValidator (4 , 5 , 6 ))),
154182 Map .entry ("argon2.low_level.hash_secret" , List .of (new Argon2PasswordHasherValidator (2 , 3 , 4 ))),
155183 Map .entry ("argon2.low_level.hash_secret_raw" , List .of (new Argon2PasswordHasherValidator (2 , 3 , 4 ))),
156- Map .entry ("passlib.handlers.argon2._Argon2Common.using" , List .of (new Argon2PasswordHasherValidator (3 , 4 , 5 )))
184+ Map .entry ("passlib.handlers.argon2._Argon2Common.using" , List .of (new Argon2PasswordHasherValidator (3 , 4 , 5 ))),
185+ Map .entry ("bcrypt.gensalt" , List .of (BCRYPT_GENSALT )),
186+ Map .entry ("bcrypt.kdf" , List .of (BCRYPT_KDF )),
187+ Map .entry ("flask_bcrypt.generate_password_hash" , List .of (FLASK_BCRYPT )),
188+ Map .entry ("flask_bcrypt.Bcrypt.generate_password_hash" , List .of (FLASK_BCRYPT ))
157189 );
158190
159191 private static final Map <String , Collection <CallValidator >> QUALIFIED_EXPR_VALIDATOR = Map .of (
160192 "passlib.hash.pbkdf2_sha1.using" , List .of (PASSLIB_PBKDF2 ),
161193 "passlib.hash.pbkdf2_sha256.using" , List .of (PASSLIB_PBKDF2 , PASSLIB_MISSING_ROUNDS ),
162- "passlib.hash.pbkdf2_sha512.using" , List .of (PASSLIB_PBKDF2 , PASSLIB_MISSING_ROUNDS )
194+ "passlib.hash.pbkdf2_sha512.using" , List .of (PASSLIB_PBKDF2 , PASSLIB_MISSING_ROUNDS ),
195+ "passlib.hash.bcrypt.using" , List .of (PASSLIB_BCRYPT )
163196 );
164197
165198
166199 private TypeCheckBuilder argon2CheapestProfileTypeChecker = null ;
200+ private TypeCheckBuilder flaskConfigTypeChecker = null ;
167201 @ Override
168202 public void initialize (Context context ) {
169203 context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , this ::registerTypeCheckers );
170204 context .registerSyntaxNodeConsumer (Tree .Kind .CALL_EXPR , FastHashingOrPlainTextCheck ::checkCallExpr );
171205 context .registerSyntaxNodeConsumer (Tree .Kind .NAME , this ::checkName );
206+ context .registerSyntaxNodeConsumer (Tree .Kind .ASSIGNMENT_STMT , subscriptionContext -> checkAssignment (subscriptionContext , flaskConfigTypeChecker ));
172207 }
173208
174209 private void registerTypeCheckers (SubscriptionContext subscriptionContext ) {
175210 argon2CheapestProfileTypeChecker = subscriptionContext .typeChecker ().typeCheckBuilder ().isTypeWithFqn ("argon2.profiles.CHEAPEST" );
211+ flaskConfigTypeChecker = subscriptionContext .typeChecker ().typeCheckBuilder ().isInstanceOf ("flask.config.Config" );
212+ }
213+
214+ private static void checkAssignment (SubscriptionContext subscriptionContext , TypeCheckBuilder flaskConfigTypeChecker ) {
215+ var stmt = (AssignmentStatement ) subscriptionContext .syntaxNode ();
216+ var lhsSubscription = stmt .lhsExpressions ().stream ().findFirst ()
217+ .map (ExpressionList ::expressions )
218+ .flatMap (list -> list .stream ().findFirst ())
219+ .filter (expression -> subscriptionIsFlaskBcryptConfig (expression , flaskConfigTypeChecker ));
220+
221+ if (lhsSubscription .isEmpty ()) {
222+ return ;
223+ }
224+
225+ if (isLessThan (stmt .assignedValue (), 12 )) {
226+ subscriptionContext .addIssue (stmt .assignedValue (), BCRYPT_MESSAGE );
227+ }
228+ }
229+
230+ private static boolean subscriptionIsFlaskBcryptConfig (Expression expression , TypeCheckBuilder flaskConfigTypeChecker ) {
231+ if (!expression .is (Tree .Kind .SUBSCRIPTION )) {
232+ return false ;
233+ }
234+ var subscription = (SubscriptionExpression ) expression ;
235+ if (flaskConfigTypeChecker .check (subscription .object ().typeV2 ()) != TriBool .TRUE ) {
236+ return false ;
237+ }
238+ var subscriptMatch = subscription .subscripts ().expressions ()
239+ .stream ()
240+ .findFirst ()
241+ .filter (expr -> "BCRYPT_LOG_ROUNDS" .equals (singleAssignedString (expr )));
242+ return subscriptMatch .isPresent ();
176243 }
177244
178245 private void checkName (SubscriptionContext subscriptionContext ) {
0 commit comments