1- from typing import List , Optional , Union
1+ from functools import cached_property
22
3- import libcst as cst
4- from libcst import CSTNode , matchers
5- from libcst .codemod import CodemodContext , ContextAwareVisitor
6- from libcst .codemod .visitors import AddImportsVisitor , ImportItem
7- from libcst .metadata import PositionProvider , ScopeProvider
8-
9- from codemodder .codemods .base_visitor import UtilsMixin
10- from codemodder .codemods .libcst_transformer import (
11- LibcstResultTransformer ,
12- LibcstTransformerPipeline ,
13- )
3+ from codemodder .codemods .import_modifier_codemod import SecurityImportModifierCodemod
4+ from codemodder .codemods .libcst_transformer import LibcstTransformerPipeline
145from codemodder .codemods .semgrep import SemgrepRuleDetector
15- from codemodder .codemods .transformations .remove_unused_imports import (
16- RemoveUnusedImportsCodemod ,
17- )
18- from codemodder .codemods .utils import ReplaceNodes
19- from codemodder .codetf import Change
20- from codemodder .dependency import Security
21- from codemodder .file_context import FileContext
6+ from codemodder .dependency import Dependency , Security
227from core_codemods .api import CoreCodemod , Metadata , Reference , ReviewGuidance
238
24- replacement_import = "safe_requests"
25-
269
27- class UrlSandboxTransformer (LibcstResultTransformer ):
10+ class UrlSandboxTransformer (SecurityImportModifierCodemod ):
2811 change_description = "Switch use of requests for security.safe_requests"
29- METADATA_DEPENDENCIES = (PositionProvider , ScopeProvider )
30- adds_dependency = True
31-
32- def transform_module_impl (self , tree : cst .Module ) -> cst .Module :
33- # we first gather all the nodes we want to change together with their replacements
34- find_requests_visitor = FindRequestCallsAndImports (
35- self .context ,
36- self .file_context ,
37- self .file_context .results ,
38- )
39- tree .visit (find_requests_visitor )
40- if find_requests_visitor .nodes_to_change :
41- self .file_context .codemod_changes .extend (
42- find_requests_visitor .changes_in_file
43- )
44- new_tree = tree .visit (ReplaceNodes (find_requests_visitor .nodes_to_change ))
45- self .add_dependency (Security )
46- # if it finds any request.get(...), try to remove the imports
47- if any (
48- (
49- matchers .matches (n , matchers .Call ())
50- for n in find_requests_visitor .nodes_to_change
51- )
52- ):
53- new_tree = AddImportsVisitor (
54- self .context ,
55- [ImportItem (Security .name , replacement_import , None , 0 )],
56- ).transform_module (new_tree )
57- new_tree = RemoveUnusedImportsCodemod (self .context ).transform_module (
58- new_tree
59- )
60- return new_tree
61- return tree
62-
63-
64- class FindRequestCallsAndImports (ContextAwareVisitor , UtilsMixin ):
65- METADATA_DEPENDENCIES = (ScopeProvider ,)
66-
67- def __init__ (
68- self , codemod_context : CodemodContext , file_context : FileContext , results
69- ):
70- self .nodes_to_change : dict [
71- cst .CSTNode , Union [cst .CSTNode , cst .FlattenSentinel , cst .RemovalSentinel ]
72- ] = {}
73- self .changes_in_file : List [Change ] = []
74- self .file_context = file_context
75- ContextAwareVisitor .__init__ (self , codemod_context )
76- UtilsMixin .__init__ (
77- self ,
78- results = results ,
79- line_include = file_context .line_include ,
80- line_exclude = file_context .line_exclude ,
81- )
8212
83- def leave_Call (self , original_node : cst .Call ):
84- if not self .node_is_selected (original_node ):
85- return
13+ @cached_property
14+ def mapping (self ) -> dict [str , str ]:
15+ """Build a mapping of functions to their safe_requests imports"""
16+ _matching_functions : dict [str , str ] = {
17+ "requests.get" : "safe_requests" ,
18+ }
19+ return _matching_functions
8620
87- line_number = self .node_position (original_node ).start .line
88- match original_node .args [0 ].value :
89- case cst .SimpleString ():
90- return
91-
92- match original_node :
93- # case get(...)
94- case cst .Call (func = cst .Name ()):
95- # find if get(...) comes from an from requests import get
96- match self .find_single_assignment (original_node ):
97- case cst .ImportFrom () as node :
98- self .nodes_to_change .update (
99- {
100- node : cst .ImportFrom (
101- module = cst .Attribute (
102- value = cst .Name (Security .name ),
103- attr = cst .Name (replacement_import ),
104- ),
105- names = node .names ,
106- )
107- }
108- )
109- self .changes_in_file .append (
110- Change (
111- lineNumber = line_number ,
112- description = UrlSandboxTransformer .change_description ,
113- findings = self .file_context .get_findings_for_location (
114- line_number
115- ),
116- )
117- )
118-
119- # case req.get(...)
120- case _:
121- self .nodes_to_change .update (
122- {
123- original_node : cst .Call (
124- func = cst .parse_expression (replacement_import + ".get" ),
125- args = original_node .args ,
126- )
127- }
128- )
129- self .changes_in_file .append (
130- Change (
131- lineNumber = line_number ,
132- description = UrlSandboxTransformer .change_description ,
133- findings = self .file_context .get_findings_for_location (
134- line_number
135- ),
136- )
137- )
138-
139- def _find_assignments (self , node : CSTNode ):
140- """
141- Given a MetadataWrapper and a CSTNode representing an access, find all the possible assignments that it refers.
142- """
143- scope = self .get_metadata (ScopeProvider , node )
144- return next (iter (scope .accesses [node ]))._Access__assignments
145-
146- def find_single_assignment (self , node : CSTNode ) -> Optional [CSTNode ]:
147- """
148- Given a MetadataWrapper and a CSTNode representing an access, find if there is a single assignment that it refers to.
149- """
150- assignments = self ._find_assignments (node )
151- if len (assignments ) == 1 :
152- return next (iter (assignments )).node
153- return None
21+ @property
22+ def dependency (self ) -> Dependency :
23+ return Security
15424
15525
15626UrlSandbox = CoreCodemod (
@@ -174,20 +44,20 @@ def find_single_assignment(self, node: CSTNode) -> Optional[CSTNode]:
17444 ),
17545 detector = SemgrepRuleDetector (
17646 """
177- rules:
178- - id: url-sandbox
179- message: Unbounded URL creation
180- severity: WARNING
181- languages:
182- - python
183- pattern-either:
184- - patterns:
185- - pattern: requests.get(...)
186- - pattern-not: requests.get("...")
187- - pattern-inside: |
188- import requests
189- ...
190- """
47+ rules:
48+ - id: url-sandbox
49+ message: Unbounded URL creation
50+ severity: WARNING
51+ languages:
52+ - python
53+ pattern-either:
54+ - patterns:
55+ - pattern: requests.get(...)
56+ - pattern-not: requests.get("...")
57+ - pattern-inside: |
58+ import requests
59+ ...
60+ """
19161 ),
19262 transformer = LibcstTransformerPipeline (UrlSandboxTransformer ),
19363)
0 commit comments