Skip to content

Commit 1c9acfb

Browse files
joke1196sonartech
authored andcommitted
SONARPY-3244: S7608 Should not raise on upload_file and download_file (#438)
GitOrigin-RevId: a0b1281af38b1e0a38b3e4513122c7562e76c2b3
1 parent 39a0e6d commit 1c9acfb

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

python-checks/src/main/java/org/sonar/python/checks/AwsExpectedBucketOwnerCheck.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
public class AwsExpectedBucketOwnerCheck extends PythonSubscriptionCheck {
3131

3232
private static final String MESSAGE = "Add the 'ExpectedBucketOwner' parameter to verify S3 bucket ownership.";
33+
private static final String MESSAGE_EXTRA_ARGS = "Add the 'ExpectedBucketOwner' to the 'ExtraArgs' parameter to verify S3 bucket ownership.";
3334
private static final List<String> S3_CLIENT_FQN_PREFIX = List.of("botocore.client.BaseClient", "aiobotocore.client.AioBaseClient");
3435
private static final List<String> S3_METHODS_REQUIRING_EXPECTED_BUCKET_OWNER = List.of(
3536
"copy_object",
@@ -56,8 +57,6 @@ public class AwsExpectedBucketOwnerCheck extends PythonSubscriptionCheck {
5657
"delete_object_tagging",
5758
"delete_objects",
5859
"delete_public_access_block",
59-
"download_file",
60-
"download_fileobj",
6160
"get_bucket_accelerate_configuration",
6261
"get_bucket_acl",
6362
"get_bucket_analytics_configuration",
@@ -134,16 +133,25 @@ public class AwsExpectedBucketOwnerCheck extends PythonSubscriptionCheck {
134133
"select_object_content",
135134
"update_bucket_metadata_inventory_table_configuration",
136135
"update_bucket_metadata_journal_table_configuration",
137-
"upload_file",
138-
"upload_fileobj",
139136
"upload_part",
140137
"upload_part_copy");
141138

139+
private static final List<String> UPLOAD_DOWNLOAD_FILE_FQN = List.of(
140+
"upload_file",
141+
"upload_fileobj",
142+
"download_file",
143+
"download_fileobj");
144+
142145
private static final List<String> FQN_TO_CHECK = S3_CLIENT_FQN_PREFIX.stream()
143146
.flatMap(prefix -> S3_METHODS_REQUIRING_EXPECTED_BUCKET_OWNER.stream().map(method -> prefix + "." + method))
144147
.toList();
145148

149+
private static final List<String> UPLOAD_DOWNLOAD_FQN_TO_CHECK = S3_CLIENT_FQN_PREFIX.stream()
150+
.flatMap(prefix -> UPLOAD_DOWNLOAD_FILE_FQN.stream().map(method -> prefix + "." + method))
151+
.toList();
152+
146153
private TypeCheckMap<Object> isS3MethodCall;
154+
private TypeCheckMap<Object> isUploadDownloadFileMethodCall;
147155

148156
@Override
149157
public void initialize(Context context) {
@@ -154,17 +162,28 @@ public void initialize(Context context) {
154162
private void setupTypeChecker(SubscriptionContext ctx) {
155163
Object object = new Object();
156164
isS3MethodCall = new TypeCheckMap<>();
165+
isUploadDownloadFileMethodCall = new TypeCheckMap<>();
157166

158167
for (var fqn : FQN_TO_CHECK) {
159168
var typeChecker = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn(fqn);
160169
isS3MethodCall.put(typeChecker, object);
161170
}
171+
for (var fqn : UPLOAD_DOWNLOAD_FQN_TO_CHECK) {
172+
var typeChecker = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn(fqn);
173+
isUploadDownloadFileMethodCall.put(typeChecker, object);
174+
}
162175
}
163176

164177
private void checkCall(SubscriptionContext ctx) {
165178
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
166179

167180
PythonType type = TreeUtils.inferSingleAssignedExpressionType(callExpression.callee());
181+
182+
if(isUploadDownloadFileMethodCallWithoutExtraArgs(type, callExpression)){
183+
ctx.addIssue(callExpression.callee(), MESSAGE_EXTRA_ARGS);
184+
return;
185+
}
186+
168187
if (isS3MethodCall.getOptionalForType(type).isEmpty()) {
169188
return;
170189
}
@@ -179,10 +198,21 @@ private void checkCall(SubscriptionContext ctx) {
179198

180199
ctx.addIssue(callExpression.callee(), MESSAGE);
181200
}
201+
202+
private boolean isUploadDownloadFileMethodCallWithoutExtraArgs(PythonType type, CallExpression callExpression){
203+
return isUploadDownloadFileMethodCall.getOptionalForType(type)
204+
.filter(obj -> !hasExtraArgsParameter(callExpression)).isPresent();
205+
}
206+
207+
private static boolean hasExtraArgsParameter(CallExpression callExpression){
208+
return TreeUtils.argumentByKeyword("ExtraArgs", callExpression.arguments()) != null;
209+
}
210+
182211
private static boolean hasArgsOrKwargsParams(CallExpression callExpression){
183212
return callExpression.arguments().stream().anyMatch(arg -> arg.is(Tree.Kind.UNPACKING_EXPR));
184213
}
185214

215+
186216
private static boolean hasExpectedBucketOwnerParameter(CallExpression callExpression) {
187217
return TreeUtils.argumentByKeyword("ExpectedBucketOwner", callExpression.arguments()) != null;
188218
}

python-checks/src/test/resources/checks/awsExpectedBucketOwner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ def compliant_when_wrapped(*args, **kwargs):
7878
s3_client.get_object(**kwargs)
7979
s3_client.get_object(*args, **kwargs)
8080

81+
def upload_download_file():
82+
bucket_name = "my-production-bucket"
83+
s3_client.upload_file("name", bucket_name, "1") # Noncompliant {{Add the 'ExpectedBucketOwner' to the 'ExtraArgs' parameter to verify S3 bucket ownership.}}
84+
s3_client.upload_fileobj("name", bucket_name, "1") # Noncompliant
85+
s3_client.download_file("name", bucket_name, "1") # Noncompliant
86+
s3_client.download_fileobj("name", bucket_name, "1") # Noncompliant
87+
88+
# ExpectedBucketOwner is not an argument download_fileobj
89+
s3_client.download_fileobj("name", bucket_name, "1", ExpectedBucketOwner="") # Noncompliant
90+
91+
s3_client.upload_file("name", bucket_name, "1", ExtraArgs={}) # Compliant
92+
s3_client.upload_fileobj("name", bucket_name, "1", ExtraArgs=unknown()) # Compliant
93+
dict_config = {}
94+
s3_client.download_file("name", bucket_name, "1", ExtraArgs=dict_config) # Compliant
95+
s3_client.download_fileobj("name", bucket_name, "1", ExtraArgs={"ExpectedBucketOwner":None}) # Compliant
96+
97+
# We should still raise on the other FQN
98+
s3_client.get_object(Bucket=bucket_name, ExtraArgs={}) # Noncompliant
99+
81100
async def async_function():
82101
bucket_name = "my-production-bucket"
83102
expected_owner = "123456789012"

0 commit comments

Comments
 (0)