2323import org .sonar .check .Rule ;
2424import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2525import org .sonar .plugins .python .api .SubscriptionContext ;
26+ import org .sonar .plugins .python .api .tree .AssignmentStatement ;
2627import org .sonar .plugins .python .api .tree .CallExpression ;
2728import org .sonar .plugins .python .api .tree .Expression ;
2829import org .sonar .plugins .python .api .tree .Name ;
2930import org .sonar .plugins .python .api .tree .QualifiedExpression ;
3031import org .sonar .plugins .python .api .tree .RegularArgument ;
3132import org .sonar .plugins .python .api .tree .StringLiteral ;
3233import org .sonar .plugins .python .api .tree .Tree ;
34+ import org .sonar .plugins .python .api .types .v2 .TriBool ;
3335import org .sonar .python .checks .hotspots .CommonValidationUtils .ArgumentValidator ;
3436import org .sonar .python .checks .hotspots .CommonValidationUtils .CallValidator ;
3537import org .sonar .python .checks .utils .Expressions ;
3638import org .sonar .python .tree .TreeUtils ;
39+ import org .sonar .python .types .v2 .TypeCheckBuilder ;
3740
41+ import static org .sonar .python .checks .hotspots .CommonValidationUtils .isEqualTo ;
3842import static org .sonar .python .checks .hotspots .CommonValidationUtils .isLessThan ;
3943import static org .sonar .python .semantic .SymbolUtils .qualifiedNameOrEmpty ;
44+ import static org .sonar .python .tree .TreeUtils .nthArgumentOrKeyword ;
4045import static org .sonar .python .tree .TreeUtils .nthArgumentOrKeywordOptional ;
4146
4247@ Rule (key = "S5344" )
4348public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
4449
4550 private static final String SCRYPT_PARAMETERS_MESSAGE = "Use strong scrypt parameters." ;
4651 private static final String PBKDF2_MESSAGE = "Use at least 100 000 iterations." ;
52+ private static final String ARGON2_MESSAGE = "Use secure Argon2 parameters." ;
4753 private static final Set <String > PBKDF2_ALGOS = Set .of (
4854 "sha1" ,
4955 "sha256" ,
@@ -136,13 +142,18 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
136142 );
137143
138144
139- private static final Map <String , Collection <CallValidator >> CALL_EXPRESSION_VALIDATORS = Map .of (
140- "scrypt.hash" , List .of (SCRYPT_R , SCRYPT_BUFLEN , SCRYPT_N ),
141- "hashlib.scrypt" , List .of (HASHLIB_R , HASHLIB_N , HASHLIB_DKLEN ),
142- "hashlib.pbkdf2_hmac" , List .of (HASHLIB_PBKDF2 ),
143- "cryptography.hazmat.primitives.kdf.scrypt.Scrypt" , List .of (CRYPTOGRAPHY_R , CRYPTOGRAPHY_N , CRYPTOGRAPHY_LENGTH ),
144- "cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC" , List .of (CRYPTOGRAPHY_PBKDF2 ),
145- "passlib.hash.scrypt.using" , List .of (PASSLIB_BLOCK_SIZE , PASSLIB_ROUNDS )
145+ private static final Map <String , Collection <CallValidator >> CALL_EXPRESSION_VALIDATORS = Map .ofEntries (
146+ Map .entry ("scrypt.hash" , List .of (SCRYPT_R , SCRYPT_BUFLEN , SCRYPT_N )),
147+ Map .entry ("hashlib.scrypt" , List .of (HASHLIB_R , HASHLIB_N , HASHLIB_DKLEN )),
148+ Map .entry ("hashlib.pbkdf2_hmac" , List .of (HASHLIB_PBKDF2 )),
149+ Map .entry ("cryptography.hazmat.primitives.kdf.scrypt.Scrypt" , List .of (CRYPTOGRAPHY_R , CRYPTOGRAPHY_N , CRYPTOGRAPHY_LENGTH )),
150+ Map .entry ("cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC" , List .of (CRYPTOGRAPHY_PBKDF2 )),
151+ Map .entry ("passlib.hash.scrypt.using" , List .of (PASSLIB_BLOCK_SIZE , PASSLIB_ROUNDS )),
152+ Map .entry ("argon2.PasswordHasher" , List .of (new Argon2PasswordHasherValidator (0 , 1 , 2 ))),
153+ Map .entry ("argon2.Parameters" , List .of (new Argon2PasswordHasherValidator (4 , 5 , 6 ))),
154+ Map .entry ("argon2.low_level.hash_secret" , List .of (new Argon2PasswordHasherValidator (2 , 3 , 4 ))),
155+ 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 )))
146157 );
147158
148159 private static final Map <String , Collection <CallValidator >> QUALIFIED_EXPR_VALIDATOR = Map .of (
@@ -152,9 +163,32 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
152163 );
153164
154165
166+ private TypeCheckBuilder argon2CheapestProfileTypeChecker = null ;
155167 @ Override
156168 public void initialize (Context context ) {
169+ context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , this ::registerTypeCheckers );
157170 context .registerSyntaxNodeConsumer (Tree .Kind .CALL_EXPR , FastHashingOrPlainTextCheck ::checkCallExpr );
171+ context .registerSyntaxNodeConsumer (Tree .Kind .NAME , this ::checkName );
172+ }
173+
174+ private void registerTypeCheckers (SubscriptionContext subscriptionContext ) {
175+ argon2CheapestProfileTypeChecker = subscriptionContext .typeChecker ().typeCheckBuilder ().isTypeWithFqn ("argon2.profiles.CHEAPEST" );
176+ }
177+
178+ private void checkName (SubscriptionContext subscriptionContext ) {
179+ var name = (Name ) subscriptionContext .syntaxNode ();
180+ if (argon2CheapestProfileTypeChecker .check (name .typeV2 ()) != TriBool .TRUE ) {
181+ return ;
182+ }
183+ var ancestorAssign = ((AssignmentStatement ) TreeUtils .firstAncestorOfKind (name , Tree .Kind .ASSIGNMENT_STMT ));
184+ if (ancestorAssign != null && isChildOf (ancestorAssign , name )) {
185+ return ;
186+ }
187+ subscriptionContext .addIssue (name , "Use a secure Argon2 profile." );
188+ }
189+
190+ private static boolean isChildOf (AssignmentStatement ancestorAssign , Name name ) {
191+ return ancestorAssign .lhsExpressions ().stream ().flatMap (expressionList -> expressionList .children ().stream ()).anyMatch (tree -> tree == name );
158192 }
159193
160194 private static void checkCallExpr (SubscriptionContext subscriptionContext ) {
@@ -204,12 +238,36 @@ public void validate(SubscriptionContext ctx, CallExpression callExpression) {
204238 .ifPresent (arg -> ctx .addIssue (arg , PBKDF2_MESSAGE ));
205239 }
206240 }
207-
208241 record MissingArgumentValidator (int position , String keywordName , String message ) implements CallValidator {
209242 @ Override
210243 public void validate (SubscriptionContext ctx , CallExpression callExpression ) {
211244 nthArgumentOrKeywordOptional (position , keywordName , callExpression .arguments ()).ifPresentOrElse (regularArgument -> {
212245 }, () -> ctx .addIssue (callExpression .callee (), message ));
213246 }
214247 }
248+
249+ record Argon2PasswordHasherValidator (
250+ int timeCostPosition ,
251+ int memoryCostPosition ,
252+ int parallelismPosition
253+ ) implements CallValidator {
254+
255+ @ Override
256+ public void validate (SubscriptionContext ctx , CallExpression callExpression ) {
257+ var timeCostArgument =
258+ nthArgumentOrKeyword (timeCostPosition , "time_cost" , callExpression .arguments ());
259+ var memoryCostArgument =
260+ nthArgumentOrKeyword (memoryCostPosition , "memory_cost" , callExpression .arguments ());
261+ var parallelismArgument =
262+ nthArgumentOrKeyword (parallelismPosition , "parallelism" , callExpression .arguments ());
263+
264+ var isTimeCostNOk = timeCostArgument != null && isLessThan (timeCostArgument .expression (), 5 );
265+ var isMemoryCostNOk = memoryCostArgument != null && isLessThan (memoryCostArgument .expression (), 7168 );
266+ var isParallelismNOk = parallelismArgument != null && isEqualTo (parallelismArgument .expression (), 1 );
267+
268+ if (isMemoryCostNOk && isTimeCostNOk && isParallelismNOk ) {
269+ ctx .addIssue (callExpression .callee (), ARGON2_MESSAGE );
270+ }
271+ }
272+ }
215273}
0 commit comments