1818
1919import java .util .ArrayList ;
2020import java .util .Map ;
21+ import java .util .List ;
2122import java .util .Optional ;
2223import javax .annotation .Nullable ;
2324import org .sonar .check .Rule ;
2425import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2526import org .sonar .plugins .python .api .SubscriptionContext ;
27+ import org .sonar .plugins .python .api .tree .ArgList ;
2628import org .sonar .plugins .python .api .tree .CallExpression ;
2729import org .sonar .plugins .python .api .tree .Expression ;
2830import org .sonar .plugins .python .api .tree .Name ;
31+ import org .sonar .plugins .python .api .tree .QualifiedExpression ;
2932import org .sonar .plugins .python .api .tree .RegularArgument ;
3033import org .sonar .plugins .python .api .tree .StringLiteral ;
3134import org .sonar .plugins .python .api .tree .Tree ;
3235import org .sonar .plugins .python .api .tree .UnpackingExpression ;
3336import org .sonar .python .checks .utils .Expressions ;
3437import org .sonar .python .tree .TreeUtils ;
38+ import org .sonar .plugins .python .api .types .v2 .TriBool ;
39+ import org .sonar .python .types .v2 .TypeCheckBuilder ;
3540import org .sonar .python .types .v2 .TypeCheckMap ;
3641
3742@ Rule (key = "S2053" )
3843public class PredictableSaltCheck extends PythonSubscriptionCheck {
3944
4045 private static final String MISSING_SALT_MESSAGE = "Add an unpredictable salt value to this hash." ;
4146 private static final String PREDICTABLE_SALT_MESSAGE = "Make this salt unpredictable." ;
47+ private static final String DIFFERENT_SALT_THAN_KEY_MATERIAL_MESSAGE = "Make this salt different than the derived key material." ;
48+ private static final String SALT_IS_USED_HERE_MESSAGE = "The salt is used in the derive method here." ;
4249 private static final String SALT_ARGUMENT_NAME = "salt" ;
4350 private static final String PASSWORD_ARGUMENT_NAME = "password" ;
4451
@@ -53,8 +60,11 @@ public class PredictableSaltCheck extends PythonSubscriptionCheck {
5360 Map .entry ("Cryptodome.Protocol.KDF.bcrypt" , new ArgumentInfo (2 , SALT_ARGUMENT_NAME , new ArgumentInfo (0 , PASSWORD_ARGUMENT_NAME ))),
5461 Map .entry ("Crypto.Protocol.KDF.PBKDF2" , new ArgumentInfo (1 , SALT_ARGUMENT_NAME , new ArgumentInfo (0 , PASSWORD_ARGUMENT_NAME ))),
5562 Map .entry ("Crypto.Protocol.KDF.scrypt" , new ArgumentInfo (1 , SALT_ARGUMENT_NAME , new ArgumentInfo (0 , PASSWORD_ARGUMENT_NAME ))),
56- Map .entry ("Crypto.Protocol.KDF.bcrypt" , new ArgumentInfo (2 , SALT_ARGUMENT_NAME , false , new ArgumentInfo (0 , PASSWORD_ARGUMENT_NAME )))
57- );
63+ Map .entry ("Crypto.Protocol.KDF.bcrypt" , new ArgumentInfo (2 , SALT_ARGUMENT_NAME , false , new ArgumentInfo (0 , PASSWORD_ARGUMENT_NAME ))));
64+
65+ private static final List <String > SENSITIVE_DERIVE_FUNCTIONS_FQN = List .of (
66+ "cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC.derive" ,
67+ "cryptography.hazmat.primitives.kdf.scrypt.Scrypt.derive" );
5868
5969 private static final Map <String , ArgumentInfo > SALT_FUNCTION_ARGUMENTS_TO_CHECK = Map .of (
6070 "bytes.fromhex" , new ArgumentInfo (0 , "__string" ),
@@ -64,25 +74,25 @@ public class PredictableSaltCheck extends PythonSubscriptionCheck {
6474 "base64.b32encode" , new ArgumentInfo (0 , "s" ),
6575 "base64.b32decode" , new ArgumentInfo (0 , "s" ),
6676 "base64.b16encode" , new ArgumentInfo (0 , "s" ),
67- "base64.b16decode" , new ArgumentInfo (0 , "s" )
68- );
77+ "base64.b16decode" , new ArgumentInfo (0 , "s" ));
6978
7079 @ Override
7180 public void initialize (Context context ) {
7281 var sensitiveArgumentByFqnCheck = new TypeCheckMap <ArgumentInfo >();
7382 var saltFunctionArgumentsToCheck = new TypeCheckMap <ArgumentInfo >();
83+ var deriveFunctionsToCheck = new ArrayList <TypeCheckBuilder >();
7484 context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , ctx -> initializeTypeChecks (ctx , sensitiveArgumentByFqnCheck ,
75- saltFunctionArgumentsToCheck ));
85+ saltFunctionArgumentsToCheck , deriveFunctionsToCheck ));
7686 context .registerSyntaxNodeConsumer (Tree .Kind .CALL_EXPR , ctx -> handleCallExpression ((CallExpression ) ctx .syntaxNode (),
7787 ctx ,
7888 sensitiveArgumentByFqnCheck ,
79- saltFunctionArgumentsToCheck
80- ));
89+ saltFunctionArgumentsToCheck ,
90+ deriveFunctionsToCheck ));
8191 }
8292
8393 private static void initializeTypeChecks (SubscriptionContext ctx ,
8494 TypeCheckMap <ArgumentInfo > sensitiveArgumentByFqnCheck ,
85- TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck ) {
95+ TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck , List < TypeCheckBuilder > deriveFunctionsToCheck ) {
8696 SENSITIVE_ARGUMENT_BY_FQN .forEach ((fqn , argumentNumber ) -> {
8797 var checker = ctx .typeChecker ().typeCheckBuilder ().isTypeWithFqn (fqn );
8898 sensitiveArgumentByFqnCheck .put (checker , argumentNumber );
@@ -91,43 +101,98 @@ private static void initializeTypeChecks(SubscriptionContext ctx,
91101 var checker = ctx .typeChecker ().typeCheckBuilder ().isTypeWithFqn (fqn );
92102 saltFunctionArgumentsToCheck .put (checker , argumentInfo );
93103 });
104+ SENSITIVE_DERIVE_FUNCTIONS_FQN .forEach (fqn -> {
105+ var checker = ctx .typeChecker ().typeCheckBuilder ().isTypeWithFqn (fqn );
106+ deriveFunctionsToCheck .add (checker );
107+ });
94108 }
95109
96110 private static void handleCallExpression (CallExpression callExpression , SubscriptionContext ctx ,
97- TypeCheckMap <ArgumentInfo > sensitiveArgumentByFqnCheck , TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck ) {
111+ TypeCheckMap <ArgumentInfo > sensitiveArgumentByFqnCheck , TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck , List < TypeCheckBuilder > deriveFunctionsToCheck ) {
98112 Optional .of (callExpression )
99113 .map (CallExpression ::callee )
100114 .map (Expression ::typeV2 )
101115 .map (sensitiveArgumentByFqnCheck ::getForType )
102- .ifPresent (argumentInfo -> checkArguments (callExpression , argumentInfo , ctx , saltFunctionArgumentsToCheck ));
116+ .ifPresent (argumentInfo -> checkArguments (callExpression , argumentInfo , ctx , saltFunctionArgumentsToCheck , deriveFunctionsToCheck ));
103117 }
104118
105119 private static void checkArguments (CallExpression callExpression , ArgumentInfo argumentInfo , SubscriptionContext ctx ,
106- TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck ) {
120+ TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck , List < TypeCheckBuilder > deriveFunctionsToCheck ) {
107121 var argument = TreeUtils .nthArgumentOrKeyword (argumentInfo .position (), argumentInfo .name (), callExpression .arguments ());
108122 if (argument != null ) {
109- var raised = checkSensitiveArgument (argument , ctx , saltFunctionArgumentsToCheck );
110- if (!raised ) {
111- Optional .ofNullable (argumentInfo .shouldNotBeSameAsArgument ())
112- .map (ai -> TreeUtils .nthArgumentOrKeyword (ai .position (), ai .name (), callExpression .arguments ()))
113- .ifPresent (shouldNotBeSameAsArgument -> checkForSameArguments (argument , shouldNotBeSameAsArgument , ctx ));
123+ if (hasRaisedOnSensitiveArgument (argument , ctx , saltFunctionArgumentsToCheck )) {
124+ return ;
125+ }
126+ if (hasRaisedOnSameArgument (argument , argumentInfo , callExpression , ctx )) {
127+ return ;
128+ }
129+ if (hasRaisedOnSameSaltAndDerivedKeyMaterial (argument , deriveFunctionsToCheck , ctx )) {
130+ return ;
114131 }
132+
115133 } else if (argumentInfo .required () && callExpression .arguments ().stream ().noneMatch (UnpackingExpression .class ::isInstance )) {
116134 ctx .addIssue (callExpression .callee (), MISSING_SALT_MESSAGE );
117135 }
118136 }
119137
120- private static void checkForSameArguments (RegularArgument argument , RegularArgument shouldNotBeSameAsArgument , SubscriptionContext ctx ) {
138+ private static boolean hasRaisedOnSameSaltAndDerivedKeyMaterial (RegularArgument argument , List <TypeCheckBuilder > deriveFunctionsToCheck , SubscriptionContext ctx ) {
139+ if (!argument .expression ().is (Tree .Kind .NAME )) {
140+ return false ;
141+ }
142+ var symbol = ((Name ) argument .expression ()).symbolV2 ();
143+ if (symbol == null ) {
144+ return false ;
145+ }
146+ var usagesInDeriveCall = symbol .usages ().stream ()
147+ .map (usage -> usage .tree ().parent ())
148+ .flatMap (TreeUtils .toStreamInstanceOfMapper (RegularArgument .class ))
149+ .filter (regArg -> !regArg .equals (argument ))
150+ .filter (regArg -> isUsedInDeriveCall (regArg , deriveFunctionsToCheck ))
151+ .map (RegularArgument ::expression )
152+ .toList ();
153+
154+ if (usagesInDeriveCall .isEmpty ()) {
155+ return false ;
156+ }
157+ var issue = ctx .addIssue (argument , DIFFERENT_SALT_THAN_KEY_MATERIAL_MESSAGE );
158+ usagesInDeriveCall .stream ().forEach (arg -> issue .secondary (arg , SALT_IS_USED_HERE_MESSAGE ));
159+ return true ;
160+ }
161+
162+ private static boolean isUsedInDeriveCall (RegularArgument regularArgument , List <TypeCheckBuilder > deriveFunctionsToCheck ) {
163+ return Optional .ofNullable (regularArgument .parent ())
164+ .flatMap (TreeUtils .toOptionalInstanceOfMapper (ArgList .class ))
165+ .map (ArgList ::parent )
166+ .flatMap (TreeUtils .toOptionalInstanceOfMapper (CallExpression .class ))
167+ .map (CallExpression ::callee )
168+ .flatMap (TreeUtils .toOptionalInstanceOfMapper (QualifiedExpression .class ))
169+ .map (qualifiedExpr -> deriveFunctionsToCheck .stream ()
170+ .anyMatch (deriveFunction -> deriveFunction .check (qualifiedExpr .name ().typeV2 ()) == TriBool .TRUE ))
171+ .orElse (false );
172+
173+ }
174+
175+ private static boolean hasRaisedOnSameArgument (RegularArgument argument , ArgumentInfo argumentInfo , CallExpression callExpression , SubscriptionContext ctx ) {
176+ return Optional .ofNullable (argumentInfo .shouldNotBeSameAsArgument ())
177+ .map (ai -> TreeUtils .nthArgumentOrKeyword (ai .position (), ai .name (), callExpression .arguments ()))
178+ .map (shouldNotBeSameAsArgument -> raisedOnSameArgument (argument , shouldNotBeSameAsArgument , ctx ))
179+ .orElseGet (() -> false );
180+
181+ }
182+
183+ private static boolean raisedOnSameArgument (RegularArgument argument , RegularArgument shouldNotBeSameAsArgument , SubscriptionContext ctx ) {
121184 var exp1 = argument .expression ();
122185 var exp2 = shouldNotBeSameAsArgument .expression ();
123186 if (exp1 instanceof Name n1
124- && exp2 instanceof Name n2
125- && n1 .symbolV2 () == n2 .symbolV2 ()) {
187+ && exp2 instanceof Name n2
188+ && n1 .symbolV2 () == n2 .symbolV2 ()) {
126189 ctx .addIssue (argument , PREDICTABLE_SALT_MESSAGE ).secondary (shouldNotBeSameAsArgument , "" );
190+ return true ;
127191 }
192+ return false ;
128193 }
129194
130- private static boolean checkSensitiveArgument (RegularArgument regularArgument ,
195+ private static boolean hasRaisedOnSensitiveArgument (RegularArgument regularArgument ,
131196 SubscriptionContext ctx , TypeCheckMap <ArgumentInfo > saltFunctionArgumentsToCheck ) {
132197 var secondaries = new ArrayList <Tree >();
133198 var expression = regularArgument .expression ();
@@ -163,7 +228,7 @@ private static Expression getCallExpressionArgumentValueToCheck(TypeCheckMap<Arg
163228 .map (Expression ::typeV2 )
164229 .map (saltFunctionArgumentsToCheck ::getForType )
165230 .map (argumentInfo -> Optional .ofNullable (TreeUtils .nthArgumentOrKeyword (argumentInfo .position (), argumentInfo .name (),
166- callExpression .arguments ()))
231+ callExpression .arguments ()))
167232 .map (RegularArgument ::expression )
168233 .orElse (expression ))
169234 .orElse (null );
@@ -179,5 +244,4 @@ private ArgumentInfo(int position, String name, ArgumentInfo shouldNotBeSameAsAr
179244 }
180245 }
181246
182-
183247}
0 commit comments