Skip to content

Commit c7f3022

Browse files
committed
Initial implementation of transformation
1 parent 054d4b0 commit c7f3022

File tree

3 files changed

+194
-2
lines changed

3 files changed

+194
-2
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Communication using clear-text protocols allows an attacker to sniff or tamper with the transported data.
2+
3+
This codemod will replace any detected clear text protocol with their cryptographic enabled version.
4+
5+
Our changes look like the following:
6+
```diff
7+
- url = "http://example.com"
8+
+ url = "https://example.com"
9+
10+
- ftp_con = ftplib.FTP("ftp.example.com")
11+
+ ftp_con = ftplib.FTP_TLS("ftp.example.com")
12+
+ smtp_context = ssl.create_default_context()
13+
+ smtp_context.verify_mode = ssl.CERT_REQUIRED
14+
+ smtp_context.check_hostname = True
15+
smtp_con = smtplib.SMTP("smtp.example.com", port=587)
16+
+ smtp.starttls(context=smtp_context)
17+
18+
19+
+ smtp_context_1 = ssl.create_default_context()
20+
+ smtp_context_1.verify_mode = ssl.CERT_REQUIRED
21+
+ smtp_context_1.check_hostname = True
22+
- smtp_con_2 = smtplib.SMTP("smtp.gmail.com")
23+
+ smtp_con_2 = smtplib.SMTP_SSL("smtp.gmail.com", context=smtp_context_1)
24+
```

src/core_codemods/sonar/api.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@
1010

1111

1212
class SonarCodemod(SASTCodemod):
13+
14+
def __init__(
15+
self,
16+
*,
17+
metadata: Metadata,
18+
transformer: BaseTransformerPipeline,
19+
default_extensions: list[str] | None = None,
20+
requested_rules: list[str] | None = None,
21+
provider: str | None = None,
22+
):
23+
super().__init__(
24+
metadata=metadata,
25+
detector=SonarDetector(),
26+
transformer=transformer,
27+
default_extensions=default_extensions,
28+
requested_rules=requested_rules,
29+
provider=provider,
30+
)
31+
1332
@property
1433
def origin(self):
1534
return "sonar"
@@ -38,7 +57,6 @@ def from_core_codemod_with_multiple_rules(
3857
),
3958
),
4059
transformer=transformer if transformer else other.transformer,
41-
detector=SonarDetector(),
4260
default_extensions=other.default_extensions,
4361
requested_rules=[tr.id for tr in rules],
4462
)

src/core_codemods/sonar/sonar_use_secure_protocols.py

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import libcst as cst
2+
from libcst.codemod import CodemodContext
3+
14
from codemodder.codemods.base_codemod import Metadata, ReviewGuidance, ToolRule
25
from codemodder.codemods.libcst_transformer import (
36
LibcstResultTransformer,
47
LibcstTransformerPipeline,
58
)
9+
from codemodder.codemods.utils_mixin import NameAndAncestorResolutionMixin
610
from codemodder.codetf import Reference
11+
from codemodder.file_context import FileContext
12+
from codemodder.result import Result
713
from core_codemods.sonar.api import SonarCodemod
814

915
rules = [
@@ -15,10 +21,154 @@
1521
]
1622

1723

18-
class SonarUseSecureProtocolsTransformer(LibcstResultTransformer):
24+
class SonarUseSecureProtocolsTransformer(
25+
LibcstResultTransformer, NameAndAncestorResolutionMixin
26+
):
1927
change_description = "Modified URLs or calls to use secure protocols"
2028

29+
def __init__(
30+
self,
31+
context: CodemodContext,
32+
results: list[Result] | None,
33+
file_context: FileContext,
34+
_transformer: bool = False,
35+
):
36+
self.nodes_memory_with_context_name: dict[cst.CSTNode, str] = {}
37+
super().__init__(context, results, file_context, _transformer)
38+
39+
def _match_and_handle_statement(
40+
self, possible_smtp_call, original_node_statement, updated_node_statement
41+
):
42+
maybe_name = self.find_base_name(possible_smtp_call)
43+
match possible_smtp_call:
44+
case cst.Call() if maybe_name == "smtplib.SMTP":
45+
# get the stored context_name or create a new one:
46+
if possible_smtp_call in self.nodes_memory_with_context_name:
47+
context_name = self.nodes_memory_with_context_name[
48+
possible_smtp_call
49+
]
50+
else:
51+
context_name = self.generate_available_name(
52+
original_node_statement, ["smtp_context"]
53+
)
54+
55+
new_statements = []
56+
new_statements.append(
57+
cst.parse_statement(
58+
f"{context_name} = ssl.create_default_context()"
59+
)
60+
)
61+
new_statements.append(
62+
cst.parse_statement(
63+
f"{context_name}.verify_mode = ssl.CERT_REQUIRED"
64+
)
65+
)
66+
new_statements.append(
67+
cst.parse_statement(f"{context_name}.check_hostname = True")
68+
)
69+
new_statements.append(updated_node_statement)
70+
# don't append this if we changed the call to SSL version
71+
if possible_smtp_call in self.nodes_memory_with_context_name:
72+
self.nodes_memory_with_context_name.pop(possible_smtp_call)
73+
else:
74+
new_statements.append(
75+
cst.parse_statement(f"smtplib.starttls(context={context_name})")
76+
)
77+
self.add_needed_import("smtplib")
78+
self.add_needed_import("ssl")
79+
self.report_change(possible_smtp_call)
80+
return cst.FlattenSentinel(new_statements)
81+
return updated_node_statement
82+
83+
def leave_SimpleStatementLine(self, original_node, updated_node):
84+
match original_node.body:
85+
# match the first statement that is either selected or is an assignment whose value is selected
86+
case [cst.Assign() as a, *_] if self.node_is_selected(a.value):
87+
88+
return self._match_and_handle_statement(
89+
a.value, original_node, updated_node
90+
)
91+
case [s, *_] if self.node_is_selected(s):
92+
return self._match_and_handle_statement(s, original_node, updated_node)
93+
return updated_node
94+
2195
def leave_Call(self, original_node, updated_node):
96+
if self.node_is_selected(original_node):
97+
match self.find_base_name(original_node):
98+
case "ftplib.FTP":
99+
new_func = cst.parse_expression("ftplib.FTP_TLS")
100+
self.report_change(original_node)
101+
self.add_needed_import("ftplib")
102+
return updated_node.with_changes(func=new_func)
103+
# Just using ssl.create_default_context() may not be enough for older python versions
104+
# See https://stackoverflow.com/questions/33857698/sending-email-from-python-using-starttls
105+
case "smtplib.SMTP":
106+
# port is the second positional, check that
107+
maybe_port_value = (
108+
original_node.args[1]
109+
if len(original_node.args) >= 2
110+
and original_node.args[1].keyword is None
111+
else None
112+
)
113+
# find port keyword, if any
114+
maybe_port_value = maybe_port_value or next(
115+
iter(
116+
[
117+
a
118+
for a in original_node.args
119+
if a.keyword and a.keyword.value == "port"
120+
]
121+
),
122+
None,
123+
)
124+
maybe_port_value = (
125+
maybe_port_value.value if maybe_port_value else None
126+
)
127+
match maybe_port_value:
128+
case None:
129+
self._change_to_smtp_ssl(original_node, updated_node)
130+
case cst.Integer() if maybe_port_value == "0":
131+
self._change_to_smtp_ssl(original_node, updated_node)
132+
133+
return updated_node
134+
135+
def _change_to_smtp_ssl(self, original_node, updated_node):
136+
# remember this node so we don't add the starttls
137+
new_func = cst.parse_expression("smtplib.SMTP_SSL")
138+
139+
context_name = self.generate_available_name(original_node, ["smtp_context"])
140+
self.nodes_memory_with_context_name[original_node] = context_name
141+
142+
new_args = [
143+
*original_node.args,
144+
cst.Arg(
145+
keyword=cst.Name("context"),
146+
value=cst.Name(context_name),
147+
),
148+
]
149+
return updated_node.with_changes(func=new_func, args=new_args)
150+
151+
def leave_SimpleString(
152+
self, original_node: cst.SimpleString, updated_node: cst.SimpleString
153+
) -> cst.BaseExpression:
154+
if self.node_is_selected(original_node):
155+
match original_node.raw_value:
156+
case original_node.raw_value as s if s.startswith("http"):
157+
self.report_change(original_node)
158+
return updated_node.with_changes(
159+
value=original_node.prefix
160+
+ original_node.quote
161+
+ s.replace("http", "https", 1)
162+
+ original_node.quote
163+
)
164+
case original_node.raw_value as s if s.startswith("ftp"):
165+
self.report_change(original_node)
166+
return updated_node.with_changes(
167+
value=original_node.prefix
168+
+ original_node.quote
169+
+ s.replace("ftp", "sftp", 1)
170+
+ original_node.quote
171+
)
22172
return updated_node
23173

24174

0 commit comments

Comments
 (0)