Repository: PyCQA/bandit (python)

Issue:
Django's `RawSQL` raises error if you use kwargs rather than args
### Describe the bug

An `IndexError: list index out of range` error is raised if you try to run bandit `RawSQL` with keyword arguments:
```python
RawSQL(sql='{}secure'.format('no'), params=[])
```

but runs as expected on positional args:
```python
RawSQL('{}secure'.format('no'), [])
```

### Reproduction steps

```bash
1. `echo -e "from django.db.models.expressions import RawSQL\nRawSQL(sql='{}secure'.format('no'), params=[])" > /tmp/bad-bandit.py`
2. `bandit /tmp/bad-bandit.py`
3. Bandit internal error gets printed out and the security issue isn't flagged
...
```


### Expected behavior

Positional args and keyword args should both be supported

### Bandit version

1.7.1 (Default)

### Python version

3.9 (Default)

### Additional context

_No response_

Pull request:
django_rawsql_used: support keyword arguments used in `RawSQL`
Fix rule B611: django_rawsql_used breaking when a user passes in keyword arguments to Django's `RawSQL`.

Closes #764

Patch:
diff --git a/bandit/plugins/django_sql_injection.py b/bandit/plugins/django_sql_injection.py
index 4d5df1a..a57ff46 100644
--- a/bandit/plugins/django_sql_injection.py
+++ b/bandit/plugins/django_sql_injection.py
@@ -129,7 +129,12 @@ def django_rawsql_used(context):
     description = "Use of RawSQL potential SQL attack vector."
     if context.is_module_imported_like("django.db.models"):
         if context.call_function_name == "RawSQL":
-            sql = context.node.args[0]
+            if context.node.args:
+                sql = context.node.args[0]
+            else:
+                kwargs = keywords2dict(context.node.keywords)
+                sql = kwargs["sql"]
+
             if not isinstance(sql, ast.Str):
                 return bandit.Issue(
                     severity=bandit.MEDIUM,
diff --git a/examples/django_sql_injection_raw.py b/examples/django_sql_injection_raw.py
index adc7aff..ee42302 100644
--- a/examples/django_sql_injection_raw.py
+++ b/examples/django_sql_injection_raw.py
@@ -9,3 +9,5 @@ User.objects.annotate(val=RawSQL(raw, []))
 raw = '"username") AS "val" FROM "auth_user"' \
       ' WHERE "username"="admin" OR 1=%s --'
 User.objects.annotate(val=RawSQL(raw, [0]))
+User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
+User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index aba300e..7835e74 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -527,8 +527,8 @@ class FunctionalTests(testtools.TestCase):
         """Test insecure raw functions on Django."""
 
         expect = {
-            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0},
-            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0},
+            "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 6, "HIGH": 0},
+            "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 6, "HIGH": 0},
         }
         self.check_example("django_sql_injection_raw.py", expect)