code-node-graph 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. code_node/__init__.py +40 -0
  2. code_node/__main__.py +7 -0
  3. code_node/analysis/__init__.py +3 -0
  4. code_node/analysis/aws_model.py +197 -0
  5. code_node/analysis/findings.py +462 -0
  6. code_node/analysis/graph.py +780 -0
  7. code_node/analysis/mapping.py +114 -0
  8. code_node/analysis/model.py +289 -0
  9. code_node/analysis/reader.py +1926 -0
  10. code_node/analysis/syntax.py +220 -0
  11. code_node/analysis/terms.py +505 -0
  12. code_node/cli.py +316 -0
  13. code_node/deploys/__init__.py +321 -0
  14. code_node/deploys/sam.py +371 -0
  15. code_node/deploys/serverless.py +117 -0
  16. code_node/kit/__init__.py +4 -0
  17. code_node/kit/blocks.py +184 -0
  18. code_node/kit/fmt.py +389 -0
  19. code_node/kit/highlight.py +131 -0
  20. code_node/kit/render.py +1066 -0
  21. code_node/kit/text.py +81 -0
  22. code_node/libraries/__init__.py +333 -0
  23. code_node/libraries/llm.py +124 -0
  24. code_node/libraries/mongo.py +169 -0
  25. code_node/libraries/redis.py +253 -0
  26. code_node/libraries/sql.py +232 -0
  27. code_node/pages.py +92 -0
  28. code_node/platforms/__init__.py +203 -0
  29. code_node/platforms/aws_lambda.py +290 -0
  30. code_node/platforms/web.py +301 -0
  31. code_node/project.py +402 -0
  32. code_node/report/__init__.py +3 -0
  33. code_node/report/brands.py +244 -0
  34. code_node/report/build.py +119 -0
  35. code_node/report/editor.py +40 -0
  36. code_node/report/graph.py +1411 -0
  37. code_node/report/logos/README.md +8 -0
  38. code_node/report/logos/anthropic-icon.svg +1 -0
  39. code_node/report/logos/aws-amplify.svg +1 -0
  40. code_node/report/logos/aws-api-gateway.svg +1 -0
  41. code_node/report/logos/aws-appflow.svg +1 -0
  42. code_node/report/logos/aws-appsync.svg +1 -0
  43. code_node/report/logos/aws-athena.svg +1 -0
  44. code_node/report/logos/aws-backup.svg +1 -0
  45. code_node/report/logos/aws-batch.svg +1 -0
  46. code_node/report/logos/aws-certificate-manager.svg +1 -0
  47. code_node/report/logos/aws-cloudformation.svg +1 -0
  48. code_node/report/logos/aws-cloudfront.svg +1 -0
  49. code_node/report/logos/aws-cloudtrail.svg +1 -0
  50. code_node/report/logos/aws-cloudwatch.svg +1 -0
  51. code_node/report/logos/aws-codebuild.svg +1 -0
  52. code_node/report/logos/aws-codecommit.svg +1 -0
  53. code_node/report/logos/aws-codedeploy.svg +1 -0
  54. code_node/report/logos/aws-codepipeline.svg +1 -0
  55. code_node/report/logos/aws-cognito.svg +1 -0
  56. code_node/report/logos/aws-config.svg +1 -0
  57. code_node/report/logos/aws-documentdb.svg +1 -0
  58. code_node/report/logos/aws-dynamodb.svg +1 -0
  59. code_node/report/logos/aws-ec2.svg +1 -0
  60. code_node/report/logos/aws-ecs.svg +1 -0
  61. code_node/report/logos/aws-eks.svg +1 -0
  62. code_node/report/logos/aws-elasticache.svg +1 -0
  63. code_node/report/logos/aws-elb.svg +1 -0
  64. code_node/report/logos/aws-eventbridge.svg +1 -0
  65. code_node/report/logos/aws-glacier.svg +1 -0
  66. code_node/report/logos/aws-glue.svg +1 -0
  67. code_node/report/logos/aws-iam.svg +1 -0
  68. code_node/report/logos/aws-keyspaces.svg +1 -0
  69. code_node/report/logos/aws-kinesis.svg +1 -0
  70. code_node/report/logos/aws-kms.svg +1 -0
  71. code_node/report/logos/aws-lake-formation.svg +1 -0
  72. code_node/report/logos/aws-lambda.svg +1 -0
  73. code_node/report/logos/aws-lightsail.svg +1 -0
  74. code_node/report/logos/aws-mq.svg +1 -0
  75. code_node/report/logos/aws-msk.svg +1 -0
  76. code_node/report/logos/aws-neptune.svg +1 -0
  77. code_node/report/logos/aws-open-search.svg +1 -0
  78. code_node/report/logos/aws-quicksight.svg +1 -0
  79. code_node/report/logos/aws-rds.svg +1 -0
  80. code_node/report/logos/aws-redshift.svg +1 -0
  81. code_node/report/logos/aws-route53.svg +1 -0
  82. code_node/report/logos/aws-s3.svg +1 -0
  83. code_node/report/logos/aws-secrets-manager.svg +1 -0
  84. code_node/report/logos/aws-ses.svg +1 -0
  85. code_node/report/logos/aws-shield.svg +1 -0
  86. code_node/report/logos/aws-sns.svg +1 -0
  87. code_node/report/logos/aws-sqs.svg +1 -0
  88. code_node/report/logos/aws-step-functions.svg +1 -0
  89. code_node/report/logos/aws-systems-manager.svg +1 -0
  90. code_node/report/logos/aws-timestream.svg +1 -0
  91. code_node/report/logos/aws-waf.svg +1 -0
  92. code_node/report/logos/aws-xray.svg +1 -0
  93. code_node/report/logos/aws.svg +1 -0
  94. code_node/report/logos/azure-icon.svg +1 -0
  95. code_node/report/logos/django-icon.svg +1 -0
  96. code_node/report/logos/elasticsearch.svg +1 -0
  97. code_node/report/logos/fastapi-icon.svg +1 -0
  98. code_node/report/logos/firebase-icon.svg +1 -0
  99. code_node/report/logos/flask.svg +1 -0
  100. code_node/report/logos/google-cloud.svg +1 -0
  101. code_node/report/logos/kafka-icon.svg +1 -0
  102. code_node/report/logos/mariadb-icon.svg +1 -0
  103. code_node/report/logos/mongodb-icon.svg +1 -0
  104. code_node/report/logos/mysql-icon.svg +1 -0
  105. code_node/report/logos/openai-icon.svg +1 -0
  106. code_node/report/logos/postgresql.svg +1 -0
  107. code_node/report/logos/python.svg +1 -0
  108. code_node/report/logos/rabbitmq-icon.svg +1 -0
  109. code_node/report/logos/redis.svg +1 -0
  110. code_node/report/logos/serverless.svg +1 -0
  111. code_node/report/logos/slack-icon.svg +1 -0
  112. code_node/report/logos/small/aws-api-gateway.svg +1 -0
  113. code_node/report/logos/small/aws-appflow.svg +1 -0
  114. code_node/report/logos/small/aws-appsync.svg +1 -0
  115. code_node/report/logos/small/aws-athena.svg +1 -0
  116. code_node/report/logos/small/aws-backup.svg +1 -0
  117. code_node/report/logos/small/aws-batch.svg +1 -0
  118. code_node/report/logos/small/aws-certificate-manager.svg +1 -0
  119. code_node/report/logos/small/aws-cloudformation.svg +1 -0
  120. code_node/report/logos/small/aws-cloudfront.svg +1 -0
  121. code_node/report/logos/small/aws-cloudtrail.svg +1 -0
  122. code_node/report/logos/small/aws-cloudwatch.svg +1 -0
  123. code_node/report/logos/small/aws-codebuild.svg +1 -0
  124. code_node/report/logos/small/aws-codecommit.svg +1 -0
  125. code_node/report/logos/small/aws-codedeploy.svg +1 -0
  126. code_node/report/logos/small/aws-codepipeline.svg +1 -0
  127. code_node/report/logos/small/aws-cognito.svg +1 -0
  128. code_node/report/logos/small/aws-config.svg +1 -0
  129. code_node/report/logos/small/aws-documentdb.svg +1 -0
  130. code_node/report/logos/small/aws-dynamodb.svg +1 -0
  131. code_node/report/logos/small/aws-ec2.svg +1 -0
  132. code_node/report/logos/small/aws-ecs.svg +1 -0
  133. code_node/report/logos/small/aws-eks.svg +1 -0
  134. code_node/report/logos/small/aws-elasticache.svg +1 -0
  135. code_node/report/logos/small/aws-elb.svg +1 -0
  136. code_node/report/logos/small/aws-eventbridge.svg +1 -0
  137. code_node/report/logos/small/aws-glacier.svg +1 -0
  138. code_node/report/logos/small/aws-glue.svg +1 -0
  139. code_node/report/logos/small/aws-iam.svg +1 -0
  140. code_node/report/logos/small/aws-keyspaces.svg +1 -0
  141. code_node/report/logos/small/aws-kinesis.svg +1 -0
  142. code_node/report/logos/small/aws-kms.svg +1 -0
  143. code_node/report/logos/small/aws-lake-formation.svg +1 -0
  144. code_node/report/logos/small/aws-lambda.svg +1 -0
  145. code_node/report/logos/small/aws-mq.svg +1 -0
  146. code_node/report/logos/small/aws-msk.svg +1 -0
  147. code_node/report/logos/small/aws-neptune.svg +1 -0
  148. code_node/report/logos/small/aws-open-search.svg +1 -0
  149. code_node/report/logos/small/aws-quicksight.svg +1 -0
  150. code_node/report/logos/small/aws-rds.svg +1 -0
  151. code_node/report/logos/small/aws-redshift.svg +1 -0
  152. code_node/report/logos/small/aws-route53.svg +1 -0
  153. code_node/report/logos/small/aws-s3.svg +1 -0
  154. code_node/report/logos/small/aws-secrets-manager.svg +1 -0
  155. code_node/report/logos/small/aws-ses.svg +1 -0
  156. code_node/report/logos/small/aws-shield.svg +1 -0
  157. code_node/report/logos/small/aws-sns.svg +1 -0
  158. code_node/report/logos/small/aws-sqs.svg +1 -0
  159. code_node/report/logos/small/aws-step-functions.svg +1 -0
  160. code_node/report/logos/small/aws-systems-manager.svg +1 -0
  161. code_node/report/logos/small/aws-timestream.svg +1 -0
  162. code_node/report/logos/small/aws-waf.svg +1 -0
  163. code_node/report/logos/small/aws-xray.svg +1 -0
  164. code_node/report/logos/snowflake-icon.svg +1 -0
  165. code_node/report/logos/sqlite.svg +1 -0
  166. code_node/report/logos/stripe.svg +1 -0
  167. code_node/report/logos/supabase-icon.svg +1 -0
  168. code_node/report/logos/twilio-icon.svg +1 -0
  169. code_node/report/outline.py +219 -0
  170. code_node/report/style.py +358 -0
  171. code_node/report/tabs.py +73 -0
  172. code_node/report/walk.py +624 -0
  173. code_node/serve.py +277 -0
  174. code_node_graph-0.1.0.dist-info/METADATA +165 -0
  175. code_node_graph-0.1.0.dist-info/RECORD +178 -0
  176. code_node_graph-0.1.0.dist-info/WHEEL +4 -0
  177. code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
  178. code_node_graph-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,169 @@
1
+ """
2
+ MongoDB through PyMongo (and Motor, its async twin): a client, its databases and their collections, by attribute
3
+ or by name, and what each collection method does.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from . import Call, Handle, Library, Use
11
+
12
+ _CLIENTS = ("MongoClient", "AsyncMongoClient", "AsyncIOMotorClient", "MotorClient")
13
+
14
+ _CLIENT_METHODS = frozenset(
15
+ {
16
+ "get_database",
17
+ "get_default_database",
18
+ "list_database_names",
19
+ "list_databases",
20
+ "drop_database",
21
+ "close",
22
+ "start_session",
23
+ "server_info",
24
+ "watch",
25
+ "address",
26
+ "options",
27
+ "admin",
28
+ "bulk_write",
29
+ "with_options",
30
+ }
31
+ )
32
+
33
+ _DATABASE_METHODS = frozenset(
34
+ {
35
+ "get_collection",
36
+ "list_collection_names",
37
+ "list_collections",
38
+ "create_collection",
39
+ "drop_collection",
40
+ "command",
41
+ "aggregate",
42
+ "watch",
43
+ "client",
44
+ "name",
45
+ "with_options",
46
+ "validate_collection",
47
+ "dereference",
48
+ "cursor_command",
49
+ }
50
+ )
51
+
52
+ _READS = frozenset(
53
+ {
54
+ "find",
55
+ "find_one",
56
+ "count_documents",
57
+ "estimated_document_count",
58
+ "distinct",
59
+ "aggregate",
60
+ "watch",
61
+ "find_raw_batches",
62
+ "aggregate_raw_batches",
63
+ "list_indexes",
64
+ "index_information",
65
+ "options",
66
+ }
67
+ )
68
+
69
+ _WRITES = frozenset(
70
+ {
71
+ "insert_one",
72
+ "insert_many",
73
+ "update_one",
74
+ "update_many",
75
+ "replace_one",
76
+ "delete_one",
77
+ "delete_many",
78
+ "find_one_and_update",
79
+ "find_one_and_replace",
80
+ "find_one_and_delete",
81
+ "bulk_write",
82
+ "create_index",
83
+ "create_indexes",
84
+ "drop_index",
85
+ "drop_indexes",
86
+ "drop",
87
+ "rename",
88
+ }
89
+ )
90
+
91
+ _COLLECTION_METHODS = _READS | _WRITES | {"with_options", "name", "full_name", "database"}
92
+
93
+ _MANY = {
94
+ "insert_one": "insert_many()",
95
+ "update_one": "bulk_write() with UpdateOne",
96
+ "delete_one": "delete_many()",
97
+ "replace_one": "bulk_write() with ReplaceOne",
98
+ "find_one": "one find() with $in",
99
+ }
100
+
101
+
102
+ class MongoLibrary(Library):
103
+ """
104
+ PyMongo and Motor: MongoClient(...), client.shop.orders (or client['shop']['orders']), and its methods.
105
+ """
106
+
107
+ name = "mongodb"
108
+ title = "MongoDB"
109
+ modules = ("pymongo", "motor")
110
+ icon = "database"
111
+
112
+ def make(self, name: str, call: Call) -> Handle | Use | None:
113
+ if name.rsplit(".", 1)[-1] in _CLIENTS:
114
+ return Handle("client", "MongoDB", opens=True)
115
+ return None
116
+
117
+ def attribute(self, handle: Handle, name: str) -> Handle | None:
118
+ if name.startswith("_"):
119
+ return None
120
+ if handle.kind == "client" and name not in _CLIENT_METHODS:
121
+ return handle.but("database", database=name)
122
+ if handle.kind == "database" and name not in _DATABASE_METHODS:
123
+ return handle.but("collection", collection=name)
124
+ if handle.kind == "collection" and name not in _COLLECTION_METHODS: # db.orders.archive: a sub-collection
125
+ return handle.but("collection", collection=f"{handle.fact('collection')}.{name}")
126
+ return None
127
+
128
+ def item(self, handle: Handle, key: Any) -> Handle | None:
129
+ name = key if isinstance(key, str) else "?"
130
+ if handle.kind == "client":
131
+ return handle.but("database", database=name)
132
+ if handle.kind == "database":
133
+ return handle.but("collection", collection=name)
134
+ return None
135
+
136
+ def call(self, handle: Handle, method: str, call: Call) -> Handle | Use | None:
137
+ if handle.kind == "client" and method in ("get_database", "get_default_database"):
138
+ return handle.but("database", database=str(call.value(0, "name") or "?"))
139
+ if handle.kind == "database" and method in ("get_collection", "create_collection"):
140
+ return handle.but("collection", collection=str(call.value(0, "name") or "?"))
141
+ if handle.kind != "collection":
142
+ return None
143
+ collection = handle.fact("collection")
144
+ flag = why = ""
145
+ where = call.inputs(0, "filter")
146
+ if (
147
+ method in ("find", "find_one", "delete_many", "update_many", "count_documents")
148
+ and where
149
+ and "$where" in call.code(0, "filter", most=400)
150
+ ):
151
+ flag, why = (
152
+ "$where from the input",
153
+ (
154
+ f"runs JavaScript built from {where[0]} on the server ($where): whoever sends it can run their own. "
155
+ "Match the fields with a plain filter instead."
156
+ ),
157
+ )
158
+ if method in _READS:
159
+ return Use(method, "reads", collection, flag=flag, why=why, batch=_MANY.get(method, ""))
160
+ if method in _WRITES:
161
+ return Use(method, "writes", collection, flag=flag, why=why, batch=_MANY.get(method, ""))
162
+ return None
163
+
164
+ def describe(self, handle: Handle) -> str:
165
+ if handle.kind == "database":
166
+ return f"the MongoDB database {handle.fact('database')}"
167
+ if handle.kind == "collection":
168
+ return f"the MongoDB collection {handle.fact('collection')}"
169
+ return "a MongoDB client"
@@ -0,0 +1,253 @@
1
+ """
2
+ Redis through redis-py: the client (or a cluster's), its pipelines and pub/sub, and what each command does.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from . import Call, Handle, Library, Use
8
+
9
+ _CLIENTS = ("Redis", "StrictRedis", "RedisCluster", "from_url")
10
+
11
+ _READS = frozenset(
12
+ {
13
+ "get",
14
+ "mget",
15
+ "getrange",
16
+ "strlen",
17
+ "exists",
18
+ "type",
19
+ "ttl",
20
+ "pttl",
21
+ "hget",
22
+ "hgetall",
23
+ "hmget",
24
+ "hkeys",
25
+ "hvals",
26
+ "hlen",
27
+ "hexists",
28
+ "hscan",
29
+ "hscan_iter",
30
+ "lrange",
31
+ "llen",
32
+ "lindex",
33
+ "lpos",
34
+ "smembers",
35
+ "sismember",
36
+ "smismember",
37
+ "scard",
38
+ "srandmember",
39
+ "sscan",
40
+ "sscan_iter",
41
+ "sinter",
42
+ "sunion",
43
+ "sdiff",
44
+ "zrange",
45
+ "zrevrange",
46
+ "zrangebyscore",
47
+ "zrevrangebyscore",
48
+ "zscore",
49
+ "zmscore",
50
+ "zcard",
51
+ "zcount",
52
+ "zrank",
53
+ "zrevrank",
54
+ "zscan",
55
+ "zscan_iter",
56
+ "scan",
57
+ "scan_iter",
58
+ "keys",
59
+ "xread",
60
+ "xreadgroup",
61
+ "xrange",
62
+ "xrevrange",
63
+ "xlen",
64
+ "blpop",
65
+ "brpop",
66
+ "brpoplpush",
67
+ "blmove",
68
+ "bzpopmin",
69
+ "bzpopmax",
70
+ "pfcount",
71
+ "getbit",
72
+ "bitcount",
73
+ "geopos",
74
+ "geodist",
75
+ "geosearch",
76
+ "dbsize",
77
+ "json",
78
+ "ping",
79
+ "info",
80
+ }
81
+ )
82
+
83
+ _WRITES = frozenset(
84
+ {
85
+ "set",
86
+ "setex",
87
+ "psetex",
88
+ "setnx",
89
+ "mset",
90
+ "msetnx",
91
+ "getset",
92
+ "getdel",
93
+ "getex",
94
+ "delete",
95
+ "unlink",
96
+ "expire",
97
+ "pexpire",
98
+ "expireat",
99
+ "pexpireat",
100
+ "persist",
101
+ "rename",
102
+ "renamenx",
103
+ "incr",
104
+ "incrby",
105
+ "incrbyfloat",
106
+ "decr",
107
+ "decrby",
108
+ "append",
109
+ "setrange",
110
+ "hset",
111
+ "hmset",
112
+ "hsetnx",
113
+ "hdel",
114
+ "hincrby",
115
+ "hincrbyfloat",
116
+ "lpush",
117
+ "rpush",
118
+ "lpushx",
119
+ "rpushx",
120
+ "lpop",
121
+ "rpop",
122
+ "lrem",
123
+ "lset",
124
+ "ltrim",
125
+ "linsert",
126
+ "lmove",
127
+ "rpoplpush",
128
+ "sadd",
129
+ "srem",
130
+ "spop",
131
+ "smove",
132
+ "sinterstore",
133
+ "sunionstore",
134
+ "sdiffstore",
135
+ "zadd",
136
+ "zrem",
137
+ "zincrby",
138
+ "zpopmin",
139
+ "zpopmax",
140
+ "zremrangebyscore",
141
+ "zremrangebyrank",
142
+ "xack",
143
+ "xdel",
144
+ "xtrim",
145
+ "xgroup_create",
146
+ "pfadd",
147
+ "setbit",
148
+ "geoadd",
149
+ "copy",
150
+ "flushdb",
151
+ "flushall",
152
+ }
153
+ )
154
+
155
+ _SENDS = frozenset({"publish", "spublish", "xadd"})
156
+
157
+ _RUNS = frozenset({"eval", "evalsha", "fcall", "script_load", "register_script"})
158
+
159
+ _MANY = {"get": "mget()", "set": "mset()", "hget": "hmget()", "delete": "one delete() with every key"}
160
+
161
+
162
+ class RedisLibrary(Library):
163
+ """
164
+ redis-py: redis.Redis(...) and redis.from_url(...), its pipelines and pub/sub.
165
+ """
166
+
167
+ name = "redis"
168
+ title = "Redis"
169
+ modules = ("redis",)
170
+ icon = "database"
171
+
172
+ def make(self, name: str, call: Call) -> Handle | Use | None:
173
+ last = name.rsplit(".", 1)[-1]
174
+ if last in _CLIENTS:
175
+ given = call.given("socket_timeout") or "socket_timeout=" in call.code(0, most=200)
176
+ return Handle(
177
+ "client",
178
+ "Redis",
179
+ opens=True,
180
+ waits="" if given else "as long as Redis takes",
181
+ fix=f"{name.rsplit('.', 1)[-1]}(..., socket_timeout=5)",
182
+ )
183
+ if last in ("ConnectionPool", "BlockingConnectionPool"):
184
+ given = call.given("socket_timeout")
185
+ return Handle(
186
+ "pool", "Redis", waits="" if given else "as long as Redis takes", fix=f"{last}(..., socket_timeout=5)"
187
+ )
188
+ return None
189
+
190
+ def call(self, handle: Handle, method: str, call: Call) -> Handle | Use | None:
191
+ if method == "pipeline":
192
+ return handle.but("pipeline")
193
+ if method == "pubsub":
194
+ return handle.but("pubsub")
195
+ if method == "lock":
196
+ return Use("LOCK", "writes", call.shown(0, "name"), gives=handle.but("lock"))
197
+ if handle.kind == "pubsub":
198
+ if method in ("subscribe", "psubscribe"):
199
+ return Use(method.upper(), "reads", call.shown(0))
200
+ if method in ("listen", "get_message"):
201
+ return None
202
+ if handle.kind == "lock" or method in ("execute", "close", "reset", "watch", "multi"):
203
+ return None
204
+ target = call.shown(0, "name") if call.node.args or call.given("name", "keys", "channel") else ""
205
+ operation = {
206
+ "scan_iter": "SCAN",
207
+ "hscan_iter": "HSCAN",
208
+ "sscan_iter": "SSCAN",
209
+ "zscan_iter": "ZSCAN",
210
+ "register_script": "SCRIPT LOAD",
211
+ "script_load": "SCRIPT LOAD",
212
+ }.get(method, method.upper())
213
+ batch = "" if handle.kind == "pipeline" else (_MANY.get(method, "") or "a pipeline()")
214
+ if method in _READS:
215
+ if method == "keys":
216
+ return Use(
217
+ operation,
218
+ "reads",
219
+ target,
220
+ flag="KEYS blocks Redis",
221
+ batch=batch,
222
+ why=(
223
+ "walks every key in the database, and Redis answers nothing else until it's done: on a big "
224
+ "database that stalls every client. Use scan_iter(match=...) instead."
225
+ ),
226
+ )
227
+ return Use(operation, "reads", target, batch=batch)
228
+ if method in _WRITES:
229
+ if method in ("flushdb", "flushall"):
230
+ return Use(
231
+ operation,
232
+ "writes",
233
+ flag="empties the database",
234
+ why=(
235
+ f"deletes every key{' in every database' if method == 'flushall' else ''}: make sure this can only "
236
+ "run where that's meant."
237
+ ),
238
+ )
239
+ return Use(operation, "writes", target, batch=batch)
240
+ if method in _SENDS:
241
+ return Use(operation, "sends", target, icon="queue", batch=batch if method == "xadd" else "")
242
+ if method in _RUNS:
243
+ return Use("EVAL" if method == "eval" else operation, "runs", "a Lua script")
244
+ return None
245
+
246
+ def describe(self, handle: Handle) -> str:
247
+ return {
248
+ "client": "a Redis client",
249
+ "pool": "a Redis connection pool",
250
+ "pipeline": "a Redis pipeline",
251
+ "pubsub": "a Redis pub/sub",
252
+ "lock": "a Redis lock",
253
+ }.get(handle.kind, "a Redis " + handle.kind)
@@ -0,0 +1,232 @@
1
+ """
2
+ SQL databases through their drivers: sqlite3, psycopg2 and psycopg (PostgreSQL), PyMySQL and mysql-connector
3
+ (MySQL), asyncpg, SQLAlchemy's engines and text(), and Django's connection.cursor(). Each statement is read for what it does to which table, and a
4
+ statement put together from the entry point's input is flagged.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from . import Call, Handle, Library, Use
12
+
13
+ _CONNECT = { # a driver's function that opens a connection -> the database, as people name it
14
+ "sqlite3.connect": "SQLite",
15
+ "psycopg2.connect": "PostgreSQL",
16
+ "psycopg.connect": "PostgreSQL",
17
+ "psycopg.Connection.connect": "PostgreSQL",
18
+ "psycopg.AsyncConnection.connect": "PostgreSQL",
19
+ "pymysql.connect": "MySQL",
20
+ "pymysql.connections.Connection": "MySQL",
21
+ "MySQLdb.connect": "MySQL",
22
+ "mysql.connector.connect": "MySQL",
23
+ "asyncpg.connect": "PostgreSQL",
24
+ }
25
+
26
+ _POOLS = {
27
+ "asyncpg.create_pool": "PostgreSQL",
28
+ "psycopg2.pool.SimpleConnectionPool": "PostgreSQL",
29
+ "psycopg2.pool.ThreadedConnectionPool": "PostgreSQL",
30
+ "psycopg_pool.ConnectionPool": "PostgreSQL",
31
+ }
32
+
33
+ _SCHEMES = (
34
+ ("postgres", "PostgreSQL"),
35
+ ("mysql", "MySQL"),
36
+ ("mariadb", "MySQL"),
37
+ ("sqlite", "SQLite"),
38
+ ("mssql", "SQL Server"),
39
+ ("oracle", "Oracle"),
40
+ ("snowflake", "Snowflake"),
41
+ ("redshift", "Redshift"),
42
+ )
43
+
44
+ _TIMEOUTS = ("connect_timeout", "timeout", "command_timeout", "connection_timeout", "read_timeout")
45
+
46
+ _RUN = (
47
+ "execute",
48
+ "executemany",
49
+ "executescript",
50
+ "fetch",
51
+ "fetchrow",
52
+ "fetchval",
53
+ "exec_driver_sql",
54
+ "scalar",
55
+ "scalars",
56
+ "copy_records_to_table",
57
+ "copy_from",
58
+ "copy_expert",
59
+ )
60
+
61
+ _STATEMENT = re.compile(
62
+ r"^\s*(?:\(\s*)?(WITH|SELECT|INSERT|UPDATE|DELETE|MERGE|REPLACE|UPSERT|CREATE|DROP|ALTER|"
63
+ r"TRUNCATE|CALL|COPY|GRANT|VACUUM|PRAGMA|SHOW|EXPLAIN|BEGIN|COMMIT|ROLLBACK|SET)\b",
64
+ re.I,
65
+ )
66
+
67
+ _TABLE = { # a statement -> where its table is named
68
+ "SELECT": r"\bFROM\s+([\w.\"`\[\]{}]+)",
69
+ "WITH": r"\bFROM\s+([\w.\"`\[\]{}]+)",
70
+ "INSERT": r"\bINTO\s+([\w.\"`\[\]{}]+)",
71
+ "REPLACE": r"\bINTO\s+([\w.\"`\[\]{}]+)",
72
+ "UPSERT": r"\bINTO\s+([\w.\"`\[\]{}]+)",
73
+ "MERGE": r"\bINTO\s+([\w.\"`\[\]{}]+)",
74
+ "UPDATE": r"^\s*UPDATE\s+(?:ONLY\s+)?([\w.\"`\[\]{}]+)",
75
+ "DELETE": r"\bFROM\s+([\w.\"`\[\]{}]+)",
76
+ "CREATE": r"\b(?:TABLE|INDEX|VIEW)\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w.\"`\[\]{}]+)",
77
+ "DROP": r"\b(?:TABLE|INDEX|VIEW)\s+(?:IF\s+EXISTS\s+)?([\w.\"`\[\]{}]+)",
78
+ "ALTER": r"\bTABLE\s+([\w.\"`\[\]{}]+)",
79
+ "TRUNCATE": r"^\s*TRUNCATE\s+(?:TABLE\s+)?([\w.\"`\[\]{}]+)",
80
+ "COPY": r"^\s*COPY\s+([\w.\"`\[\]{}]+)",
81
+ }
82
+
83
+ _MARKERS = {"SQLite": "?", "PostgreSQL": "%s", "MySQL": "%s"} # how a driver's parameters are written
84
+
85
+
86
+ def _statement(text: str) -> tuple[str, str]:
87
+ """
88
+ What a SQL statement does and to which table, from its text (a literal or an f-string, as code):
89
+ "'SELECT id FROM orders WHERE ...'" -> ('SELECT', 'orders'). ('', '') when it isn't SQL it can read.
90
+ """
91
+ body = re.sub(r"^[rbuf]*(['\"]{1,3})", "", text.strip(), flags=re.I)
92
+ found = _STATEMENT.match(body)
93
+ if not found:
94
+ return "", ""
95
+ verb = found.group(1).upper()
96
+ if verb == "WITH": # WITH ... AS (...) then the statement that does the work
97
+ main = re.search(r"\)\s*(SELECT|INSERT|UPDATE|DELETE|MERGE)\b", body, re.I)
98
+ verb = main.group(1).upper() if main else "SELECT"
99
+ table = re.search(_TABLE.get(verb, r"$^"), body, re.I | re.S)
100
+ return verb, table.group(1).strip('"`[]') if table else ""
101
+
102
+
103
+ def _access(verb: str) -> str:
104
+ if verb in ("SELECT", "SHOW", "EXPLAIN", "PRAGMA"):
105
+ return "reads"
106
+ if verb == "CALL":
107
+ return "runs"
108
+ return "writes"
109
+
110
+
111
+ class SqlLibrary(Library):
112
+ """
113
+ SQL through a DB-API driver (connect(), cursor(), execute()) or SQLAlchemy (create_engine(), connect(),
114
+ execute(text(...))).
115
+ """
116
+
117
+ name = "sql"
118
+ title = "SQL"
119
+ modules = (
120
+ "sqlite3",
121
+ "psycopg2",
122
+ "psycopg",
123
+ "psycopg_pool",
124
+ "pymysql",
125
+ "MySQLdb",
126
+ "mysql",
127
+ "asyncpg",
128
+ "sqlalchemy",
129
+ "django",
130
+ )
131
+ icon = "database"
132
+
133
+ def make(self, name: str, call: Call) -> Handle | Use | None:
134
+ if name in _CONNECT or name in _POOLS:
135
+ service = _CONNECT.get(name) or _POOLS[name]
136
+ target = call.shown(0, "database") if service == "SQLite" else ""
137
+ waits = (
138
+ ""
139
+ if service == "SQLite" or call.given(*_TIMEOUTS) or "timeout" in call.code(0, most=300)
140
+ else f"as long as {service} takes to answer"
141
+ )
142
+ kind = "connection" if name in _CONNECT else "pool"
143
+ facts = (("database", target),) if target else ()
144
+ fix = f"{name}(..., {'timeout' if name.startswith('asyncpg') else 'connect_timeout'}=5)"
145
+ return Handle(
146
+ kind,
147
+ service,
148
+ facts + (("driver", name.split(".")[0]),),
149
+ opens=service != "SQLite",
150
+ waits=waits,
151
+ fix=fix,
152
+ ) # SQLite opens a file: nothing to keep a connection to
153
+ if name == "django.db.connection.cursor": # Django opens and keeps the connection itself
154
+ return Handle("cursor", "", (("driver", "django"),))
155
+ last = name.rsplit(".", 1)[-1]
156
+ if name.startswith("sqlalchemy") and last in ("create_engine", "create_async_engine", "engine_from_config"):
157
+ url = str(call.value(0, "url") or "")
158
+ service = next((label for scheme, label in _SCHEMES if url.startswith(scheme)), "SQL database")
159
+ return Handle("engine", service, (("driver", "sqlalchemy"),), opens=True)
160
+ if name.startswith("sqlalchemy") and last == "text":
161
+ return Handle(
162
+ "statement",
163
+ "",
164
+ (
165
+ ("sql", call.text(0)),
166
+ ("built", "yes" if call.built(0) else ""),
167
+ ("inputs", ", ".join(call.inputs(0))),
168
+ ),
169
+ )
170
+ return None
171
+
172
+ def call(self, handle: Handle, method: str, call: Call) -> Handle | Use | None:
173
+ kind = handle.kind
174
+ if kind == "pool" and method in ("getconn", "acquire", "connection"):
175
+ return handle.but("connection")
176
+ if kind == "engine" and method in ("connect", "begin", "raw_connection"):
177
+ return handle.but("connection")
178
+ if kind == "connection" and method == "cursor":
179
+ return handle.but("cursor")
180
+ if kind == "connection" and method in ("transaction", "begin"):
181
+ return handle.but("connection")
182
+ if method not in _RUN:
183
+ return None
184
+ return self._statement(handle, method, call)
185
+
186
+ def _statement(self, handle: Handle, method: str, call: Call) -> Use:
187
+ service = handle.service or "SQL database"
188
+ held = call.held(0, "statement") or call.held(0, "query") or call.held(0, "sql")
189
+ if held is not None and held.kind == "statement":
190
+ text, built = held.fact("sql"), bool(held.fact("built"))
191
+ inputs = [part for part in held.fact("inputs").split(", ") if part]
192
+ else:
193
+ text = call.text(0, "sql") or call.text(0, "query") or call.text(0, "operation")
194
+ built = call.built(0, "sql") or call.built(0, "query") or call.built(0, "operation")
195
+ inputs = call.inputs(0, "sql") + call.inputs(0, "query") + call.inputs(0, "operation")
196
+ verb, table = _statement(text) if text else ("", "")
197
+ if method == "executescript":
198
+ verb, table = verb or "SCRIPT", table
199
+ operation = verb or "a query"
200
+ access = _access(verb) if verb else "runs"
201
+ flag = why = ""
202
+ if built and inputs:
203
+ marker = "$1" if handle.fact("driver") == "asyncpg" else _MARKERS.get(service, "%s")
204
+ flag = "SQL injection"
205
+ why = (
206
+ f"puts {', '.join(dict.fromkeys(inputs))} into the SQL text itself, so whoever sends it can change "
207
+ f"the statement (SQL injection). Pass it as a parameter instead: "
208
+ f'execute("... WHERE id = {marker}", (value,)).'
209
+ )
210
+ batch = ""
211
+ if method == "execute" and access == "writes" and verb in ("INSERT", "UPDATE", "DELETE", "REPLACE", "UPSERT"):
212
+ batch = "one execute() with a list of rows" if handle.fact("driver") == "sqlalchemy" else "executemany()"
213
+ elif access == "reads":
214
+ batch = "one query for all of them (WHERE ... IN (...), or a join)"
215
+ return Use(
216
+ operation,
217
+ access,
218
+ table,
219
+ service,
220
+ flag=flag,
221
+ why=why,
222
+ batch=batch,
223
+ gives=handle.but("cursor") if handle.kind == "connection" else None,
224
+ )
225
+
226
+ def describe(self, handle: Handle) -> str:
227
+ service = handle.service or "SQL"
228
+ return {
229
+ "statement": "a SQL statement",
230
+ "engine": f"a {service} engine",
231
+ "pool": f"a {service} connection pool",
232
+ }.get(handle.kind, f"a {service} {handle.kind}")