Repository: securesauce/precli (language: python)

Issue:
Add rules to catch cases of hardcoded secrets in code
**Is your feature request related to a problem? Please describe.**
Find secrets such as passwords, keys, credentials, etc

**Describe the solution you'd like**
Use regex patterns to find strings that match types of private credentials.

**Describe alternatives you've considered**
n/a

**Additional context**


Love this idea? Give it a 👍. We prioritize fulfilling features with the most 👍.



Discussion / hints:

Fix patch:
diff --git a/precli/rules/python/stdlib/http_url_secret.py b/precli/rules/python/stdlib/http_url_secret.py
new file mode 100644
index 00000000..8ae0b255
--- /dev/null
+++ b/precli/rules/python/stdlib/http_url_secret.py
@@ -0,0 +1,108 @@
+# Copyright 2024 Secure Saurce LLC
+r"""
+=======================================================
+Use of HTTP Request Method With Sensitive Query Strings
+=======================================================
+
+The inclusion of sensitive information, such as a username, password, or API
+key, directly within a URL is considered a security risk because URLs can be
+logged in various places, such as web server logs, browser history, and network
+monitoring tools, making the sensitive information vulnerable to unauthorized
+access.
+
+-------
+Example
+-------
+
+.. code-block:: python
+   :linenos:
+   :emphasize-lines: 6
+
+    import http.client
+
+
+    host = "example.com"
+    conn = http.client.HTTPSConnection(host)
+    conn.request("GET", "/path?apiKey=value&otherParam=123", headers={})
+    response = conn.getresponse()
+
+-----------
+Remediation
+-----------
+
+To avoid this vulnerability, put sensitive information in the request as
+headers, rather than a parameter of the URL.
+
+.. code-block:: python
+   :linenos:
+   :emphasize-lines: 5-7, 9
+
+    import http.client
+
+
+    host = "example.com"
+    headers = {
+        "X-FullContact-APIKey": "value"
+    }
+    conn = http.client.HTTPSConnection(host)
+    conn.request("GET", "/path?otherParam=123", headers=headers)
+    response = conn.getresponse()
+
+.. seealso::
+
+ - `http.client — HTTP protocol client <https://docs.python.org/3/library/http.client.html#http.client.HTTPConnection.request>`_
+ - `CWE-598: Use of GET Request Method With Sensitive Query Strings <https://cwe.mitre.org/data/definitions/598.html>`_
+ - `Never Put Secrets in URLs and Query Parameters <https://www.fullcontact.com/blog/2016/04/29/never-put-secrets-urls-query-parameters/>`_
+
+.. versionadded:: 0.3.4
+
+"""  # noqa: E501
+from urllib.parse import parse_qs
+from urllib.parse import urlsplit
+
+from precli.core.level import Level
+from precli.core.location import Location
+from precli.core.result import Result
+from precli.rules import Rule
+
+
+SENSITIVE_PARAMS = ("apiKey", "password", "username")
+
+
+class HttpUrlSecret(Rule):
+    def __init__(self, id: str):
+        super().__init__(
+            id=id,
+            name="sensitive_query_strings",
+            full_descr=__doc__,
+            cwe_id=598,
+            message="Secrets in URLs are vulnerable to unauthorized access.",
+            targets=("call"),
+            wildcards={
+                "http.client": [
+                    "HTTPConnection",
+                    "HTTPSConnection",
+                ]
+            },
+        )
+
+    def analyze(self, context: dict, **kwargs: dict) -> Result:
+        call = kwargs.get("call")
+
+        if call.name_qualified in [
+            "http.client.HTTPConnection.request",
+            "http.client.HTTPSConnection.request",
+        ]:
+            argument = call.get_argument(position=1, name="url")
+            url = argument.value
+
+            query = urlsplit(url).query
+            params = parse_qs(query)
+
+            if any(key in params for key in SENSITIVE_PARAMS):
+                return Result(
+                    rule_id=self.id,
+                    location=Location(node=argument.node),
+                    level=Level.ERROR,
+                    message=self.message.format(call.name_qualified),
+                )
diff --git a/setup.cfg b/setup.cfg
index 3ff571be..19469a75 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -63,47 +63,50 @@ precli.rules.python =
     # precli/rules/python/stdlib/hmac_weak_hash.py
     PY006 = precli.rules.python.stdlib.hmac_weak_hash:HmacWeakHash
 
+    # precli/rules/python/stdlib/http_url_secret.py
+    PY007 = precli.rules.python.stdlib.http_url_secret:HttpUrlSecret
+
     # precli/rules/python/stdlib/imaplib_cleartext.py
-    PY007 = precli.rules.python.stdlib.imaplib_cleartext:ImapCleartext
+    PY008 = precli.rules.python.stdlib.imaplib_cleartext:ImapCleartext
 
     # precli/rules/python/stdlib/json_load.py
-    PY008 = precli.rules.python.stdlib.json_load:JsonLoad
+    PY009 = precli.rules.python.stdlib.json_load:JsonLoad
 
     # precli/rules/python/stdlib/logging_insecure_listen_config.py
-    PY009 = precli.rules.python.stdlib.logging_insecure_listen_config:InsecureListenConfig
+    PY010 = precli.rules.python.stdlib.logging_insecure_listen_config:InsecureListenConfig
 
     # precli/rules/python/stdlib/marshal_load.py
-    PY010 = precli.rules.python.stdlib.marshal_load:MarshalLoad
+    PY011 = precli.rules.python.stdlib.marshal_load:MarshalLoad
 
     # precli/rules/python/stdlib/nntplib_cleartext.py
-    PY011 = precli.rules.python.stdlib.nntplib_cleartext:NntpCleartext
+    PY012 = precli.rules.python.stdlib.nntplib_cleartext:NntpCleartext
 
     # precli/rules/python/stdlib/pickle_load.py
-    PY012 = precli.rules.python.stdlib.pickle_load:PickleLoad
+    PY013 = precli.rules.python.stdlib.pickle_load:PickleLoad
 
     # precli/rules/python/stdlib/pop_cleartext.py
-    PY013 = precli.rules.python.stdlib.poplib_cleartext:PopCleartext
+    PY014 = precli.rules.python.stdlib.poplib_cleartext:PopCleartext
 
     # precli/rules/python/stdlib/shelve_open.py
-    PY014 = precli.rules.python.stdlib.shelve_open:ShelveOpen
+    PY015 = precli.rules.python.stdlib.shelve_open:ShelveOpen
 
     # precli/rules/python/stdlib/smtplib_cleartext.py
-    PY015 = precli.rules.python.stdlib.smtplib_cleartext:SmtpCleartext
+    PY016 = precli.rules.python.stdlib.smtplib_cleartext:SmtpCleartext
 
     # precli/rules/python/stdlib/ssl_create_unverified_context.py
-    PY016 = precli.rules.python.stdlib.ssl_create_unverified_context:CreateUnverifiedContext
+    PY017 = precli.rules.python.stdlib.ssl_create_unverified_context:CreateUnverifiedContext
 
     # precli/rules/python/stdlib/ssl_insecure_tls_version.py
-    PY017 = precli.rules.python.stdlib.ssl_insecure_tls_version:InsecureTlsVersion
+    PY018 = precli.rules.python.stdlib.ssl_insecure_tls_version:InsecureTlsVersion
 
     # precli/rules/python/stdlib/ssl_context_weak_key.py
-    PY018 = precli.rules.python.stdlib.ssl_context_weak_key:SslContextWeakKey
+    PY019 = precli.rules.python.stdlib.ssl_context_weak_key:SslContextWeakKey
 
     # precli/rules/python/stdlib/telnetlib_cleartext.py
-    PY019 = precli.rules.python.stdlib.telnetlib_cleartext:TelnetlibCleartext
+    PY020 = precli.rules.python.stdlib.telnetlib_cleartext:TelnetlibCleartext
 
     # precli/rules/python/stdlib/tempfile_mktemp_race_condition.py
-    PY020 = precli.rules.python.stdlib.tempfile_mktemp_race_condition:MktempRaceCondition
+    PY021 = precli.rules.python.stdlib.tempfile_mktemp_race_condition:MktempRaceCondition
 
 [build_sphinx]
 all_files = 1


Test patch:
diff --git a/tests/unit/rules/python/stdlib/examples/http_url_secret_apikey.py b/tests/unit/rules/python/stdlib/examples/http_url_secret_apikey.py
new file mode 100644
index 00000000..c1d3ecf1
--- /dev/null
+++ b/tests/unit/rules/python/stdlib/examples/http_url_secret_apikey.py
@@ -0,0 +1,14 @@
+# level: ERROR
+# start_line: 12
+# end_line: 12
+# start_column: 11
+# end_column: 46
+import http.client
+
+
+host = "example.com"
+conn = http.client.HTTPSConnection(host)
+conn.request(
+    "GET", "/path?apiKey=value&otherParam=123", headers={"Host": host}
+)
+response = conn.getresponse()
diff --git a/tests/unit/rules/python/stdlib/examples/ssl_context_set_ecdh_curve_unverified_context.py b/tests/unit/rules/python/stdlib/examples/ssl_context_set_ecdh_curve_unverified_context.py
index 6c19faa5..409d9cdf 100644
--- a/tests/unit/rules/python/stdlib/examples/ssl_context_set_ecdh_curve_unverified_context.py
+++ b/tests/unit/rules/python/stdlib/examples/ssl_context_set_ecdh_curve_unverified_context.py
@@ -6,5 +6,5 @@
 import ssl
 
 
-context = ssl._create_unverified_context()  # suppress: PY016
+context = ssl._create_unverified_context()  # suppress: PY017
 context.set_ecdh_curve("prime192v1")
diff --git a/tests/unit/rules/python/stdlib/test_http_url_secret.py b/tests/unit/rules/python/stdlib/test_http_url_secret.py
new file mode 100644
index 00000000..5d6666db
--- /dev/null
+++ b/tests/unit/rules/python/stdlib/test_http_url_secret.py
@@ -0,0 +1,44 @@
+# Copyright 2024 Secure Saurce LLC
+import os
+
+from parameterized import parameterized
+
+from precli.core.level import Level
+from precli.parsers import python
+from precli.rules import Rule
+from tests.unit.rules import test_case
+
+
+class HttpUrlSecretTests(test_case.TestCase):
+    def setUp(self):
+        super().setUp()
+        self.rule_id = "PY007"
+        self.parser = python.Python()
+        self.base_path = os.path.join(
+            "tests",
+            "unit",
+            "rules",
+            "python",
+            "stdlib",
+            "examples",
+        )
+
+    def test_rule_meta(self):
+        rule = Rule.get_by_id(self.rule_id)
+        self.assertEqual(self.rule_id, rule.id)
+        self.assertEqual("sensitive_query_strings", rule.name)
+        self.assertEqual(
+            f"https://docs.securesauce.dev/rules/{self.rule_id}", rule.help_url
+        )
+        self.assertEqual(True, rule.default_config.enabled)
+        self.assertEqual(Level.WARNING, rule.default_config.level)
+        self.assertEqual(-1.0, rule.default_config.rank)
+        self.assertEqual("598", rule.cwe.cwe_id)
+
+    @parameterized.expand(
+        [
+            "http_url_secret_apikey.py",
+        ]
+    )
+    def test(self, filename):
+        self.check(filename)
diff --git a/tests/unit/rules/python/stdlib/test_imaplib_cleartext.py b/tests/unit/rules/python/stdlib/test_imaplib_cleartext.py
index 90a69969..911c24a7 100644
--- a/tests/unit/rules/python/stdlib/test_imaplib_cleartext.py
+++ b/tests/unit/rules/python/stdlib/test_imaplib_cleartext.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class ImapCleartextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY007"
+        self.rule_id = "PY008"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_json_load.py b/tests/unit/rules/python/stdlib/test_json_load.py
index e7fd42c2..4f4e93a1 100644
--- a/tests/unit/rules/python/stdlib/test_json_load.py
+++ b/tests/unit/rules/python/stdlib/test_json_load.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class JsonLoadTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY008"
+        self.rule_id = "PY009"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_logging_insecure_listen_config.py b/tests/unit/rules/python/stdlib/test_logging_insecure_listen_config.py
index 97f19351..ce39c1e4 100644
--- a/tests/unit/rules/python/stdlib/test_logging_insecure_listen_config.py
+++ b/tests/unit/rules/python/stdlib/test_logging_insecure_listen_config.py
@@ -12,7 +12,7 @@
 class InsecureListenConfigTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY009"
+        self.rule_id = "PY010"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_marshal_load.py b/tests/unit/rules/python/stdlib/test_marshal_load.py
index a0246d1e..a1829e1a 100644
--- a/tests/unit/rules/python/stdlib/test_marshal_load.py
+++ b/tests/unit/rules/python/stdlib/test_marshal_load.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class MarshalLoadTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY010"
+        self.rule_id = "PY011"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_nntplib_cleartext.py b/tests/unit/rules/python/stdlib/test_nntplib_cleartext.py
index c134f5b8..3ce91085 100644
--- a/tests/unit/rules/python/stdlib/test_nntplib_cleartext.py
+++ b/tests/unit/rules/python/stdlib/test_nntplib_cleartext.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class NntpCleartextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY011"
+        self.rule_id = "PY012"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_pickle_load.py b/tests/unit/rules/python/stdlib/test_pickle_load.py
index 51add0dd..e0f56fdd 100644
--- a/tests/unit/rules/python/stdlib/test_pickle_load.py
+++ b/tests/unit/rules/python/stdlib/test_pickle_load.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class PickleLoadTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY012"
+        self.rule_id = "PY013"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_poplib_cleartext.py b/tests/unit/rules/python/stdlib/test_poplib_cleartext.py
index 8922387a..9f07ac9b 100644
--- a/tests/unit/rules/python/stdlib/test_poplib_cleartext.py
+++ b/tests/unit/rules/python/stdlib/test_poplib_cleartext.py
@@ -12,7 +12,7 @@
 class PopCleartextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY013"
+        self.rule_id = "PY014"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_shelve_open.py b/tests/unit/rules/python/stdlib/test_shelve_open.py
index 811d2a37..0865e197 100644
--- a/tests/unit/rules/python/stdlib/test_shelve_open.py
+++ b/tests/unit/rules/python/stdlib/test_shelve_open.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class ShelveOpenTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY014"
+        self.rule_id = "PY015"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_smtplib_cleartext.py b/tests/unit/rules/python/stdlib/test_smtplib_cleartext.py
index 24fe9df0..b3b9364a 100644
--- a/tests/unit/rules/python/stdlib/test_smtplib_cleartext.py
+++ b/tests/unit/rules/python/stdlib/test_smtplib_cleartext.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class SmtpCleartextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY015"
+        self.rule_id = "PY016"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_ssl_context_tls_version.py b/tests/unit/rules/python/stdlib/test_ssl_context_tls_version.py
index e73157bc..cc4ffcb0 100644
--- a/tests/unit/rules/python/stdlib/test_ssl_context_tls_version.py
+++ b/tests/unit/rules/python/stdlib/test_ssl_context_tls_version.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class SslSocketTlsVersionTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY017"
+        self.rule_id = "PY018"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_ssl_context_weak_key.py b/tests/unit/rules/python/stdlib/test_ssl_context_weak_key.py
index d3b2abcc..72738f7c 100644
--- a/tests/unit/rules/python/stdlib/test_ssl_context_weak_key.py
+++ b/tests/unit/rules/python/stdlib/test_ssl_context_weak_key.py
@@ -12,7 +12,7 @@
 class SslSocketWeakKeyTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY018"
+        self.rule_id = "PY019"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_ssl_create_context.py b/tests/unit/rules/python/stdlib/test_ssl_create_context.py
index 76ae3cfe..1f1c3c53 100644
--- a/tests/unit/rules/python/stdlib/test_ssl_create_context.py
+++ b/tests/unit/rules/python/stdlib/test_ssl_create_context.py
@@ -1,4 +1,4 @@
-# Copyright 2023 Secure Saurce LLC
+# Copyright 2024 Secure Saurce LLC
 import os
 
 from parameterized import parameterized
@@ -12,7 +12,7 @@
 class SslCreateContextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY016"
+        self.rule_id = "PY017"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_ssl_get_server_certificate_tls_version.py b/tests/unit/rules/python/stdlib/test_ssl_get_server_certificate_tls_version.py
index 3fe82c3c..d6180608 100644
--- a/tests/unit/rules/python/stdlib/test_ssl_get_server_certificate_tls_version.py
+++ b/tests/unit/rules/python/stdlib/test_ssl_get_server_certificate_tls_version.py
@@ -12,7 +12,7 @@
 class GetServerCertificateTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY017"
+        self.rule_id = "PY018"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_ssl_wrap_socket_tls_version.py b/tests/unit/rules/python/stdlib/test_ssl_wrap_socket_tls_version.py
index 3a3e9298..a860a654 100644
--- a/tests/unit/rules/python/stdlib/test_ssl_wrap_socket_tls_version.py
+++ b/tests/unit/rules/python/stdlib/test_ssl_wrap_socket_tls_version.py
@@ -12,7 +12,7 @@
 class WrapSocketTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY017"
+        self.rule_id = "PY018"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_telnetlib_cleartext.py b/tests/unit/rules/python/stdlib/test_telnetlib_cleartext.py
index 814ab613..fedfcaa8 100644
--- a/tests/unit/rules/python/stdlib/test_telnetlib_cleartext.py
+++ b/tests/unit/rules/python/stdlib/test_telnetlib_cleartext.py
@@ -12,7 +12,7 @@
 class TelnetlibCleartextTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY019"
+        self.rule_id = "PY020"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",
diff --git a/tests/unit/rules/python/stdlib/test_tempfile_mktemp_race_condition.py b/tests/unit/rules/python/stdlib/test_tempfile_mktemp_race_condition.py
index 2d9eb828..22c543e0 100644
--- a/tests/unit/rules/python/stdlib/test_tempfile_mktemp_race_condition.py
+++ b/tests/unit/rules/python/stdlib/test_tempfile_mktemp_race_condition.py
@@ -12,7 +12,7 @@
 class MktempRaceConditionTests(test_case.TestCase):
     def setUp(self):
         super().setUp()
-        self.rule_id = "PY020"
+        self.rule_id = "PY021"
         self.parser = python.Python()
         self.base_path = os.path.join(
             "tests",