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,371 @@
1
+ """
2
+ AWS SAM and CloudFormation templates: each AWS::Serverless::Function (and AWS::Lambda::Function) on a Python
3
+ runtime, with Globals applied, its environment variables, and what its role allows: SAM's policy templates
4
+ (DynamoDBCrudPolicy), the managed policies it knows, inline statements, a role in the same template, and the
5
+ permissions its event sources add.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import posixpath
12
+ from typing import Any
13
+
14
+ from ..analysis.model import Deployment
15
+ from . import MANAGED_POLICIES, Deploy, allowed, as_list, line_of, load_yaml, resolve_handler, statement_actions
16
+
17
+ _D = "dynamodb:"
18
+
19
+ POLICY_TEMPLATES: dict[str, set[str]] = { # SAM's policy templates -> the actions they allow
20
+ "DynamoDBCrudPolicy": {
21
+ _D + a
22
+ for a in (
23
+ "GetItem",
24
+ "DeleteItem",
25
+ "PutItem",
26
+ "Scan",
27
+ "Query",
28
+ "UpdateItem",
29
+ "BatchWriteItem",
30
+ "BatchGetItem",
31
+ "DescribeTable",
32
+ "ConditionCheckItem",
33
+ )
34
+ },
35
+ "DynamoDBReadPolicy": {_D + a for a in ("GetItem", "Scan", "Query", "BatchGetItem", "DescribeTable")},
36
+ "DynamoDBWritePolicy": {_D + a for a in ("PutItem", "UpdateItem", "BatchWriteItem")},
37
+ "DynamoDBReconfigurePolicy": {_D + "DescribeTable", _D + "UpdateTable"},
38
+ "DynamoDBStreamReadPolicy": {_D + a for a in ("DescribeStream", "GetRecords", "GetShardIterator", "ListStreams")},
39
+ "S3ReadPolicy": {
40
+ "s3:GetObject",
41
+ "s3:ListBucket",
42
+ "s3:GetBucketLocation",
43
+ "s3:GetObjectVersion",
44
+ "s3:GetLifecycleConfiguration",
45
+ },
46
+ "S3WritePolicy": {"s3:PutObject", "s3:PutObjectAcl", "s3:PutLifecycleConfiguration"},
47
+ "S3CrudPolicy": {
48
+ "s3:GetObject",
49
+ "s3:ListBucket",
50
+ "s3:GetBucketLocation",
51
+ "s3:GetObjectVersion",
52
+ "s3:PutObject",
53
+ "s3:PutObjectAcl",
54
+ "s3:GetLifecycleConfiguration",
55
+ "s3:PutLifecycleConfiguration",
56
+ "s3:DeleteObject",
57
+ },
58
+ "S3FullAccessPolicy": {
59
+ "s3:GetObject",
60
+ "s3:GetObjectAcl",
61
+ "s3:GetObjectVersion",
62
+ "s3:PutObject",
63
+ "s3:PutObjectAcl",
64
+ "s3:DeleteObject",
65
+ "s3:DeleteObjectTagging",
66
+ "s3:DeleteObjectVersionTagging",
67
+ "s3:GetObjectTagging",
68
+ "s3:GetObjectVersionTagging",
69
+ "s3:PutObjectTagging",
70
+ "s3:PutObjectVersionTagging",
71
+ "s3:ListBucket",
72
+ "s3:GetBucketLocation",
73
+ "s3:GetLifecycleConfiguration",
74
+ "s3:PutLifecycleConfiguration",
75
+ },
76
+ "SQSSendMessagePolicy": {"sqs:SendMessage*"},
77
+ "SQSPollerPolicy": {
78
+ "sqs:ChangeMessageVisibility",
79
+ "sqs:ChangeMessageVisibilityBatch",
80
+ "sqs:DeleteMessage",
81
+ "sqs:DeleteMessageBatch",
82
+ "sqs:GetQueueAttributes",
83
+ "sqs:ReceiveMessage",
84
+ },
85
+ "SNSPublishMessagePolicy": {"sns:Publish"},
86
+ "SNSCrudPolicy": {
87
+ "sns:ListSubscriptionsByTopic",
88
+ "sns:CreateTopic",
89
+ "sns:SetTopicAttributes",
90
+ "sns:Subscribe",
91
+ "sns:Publish",
92
+ },
93
+ "LambdaInvokePolicy": {"lambda:InvokeFunction"},
94
+ "StepFunctionsExecutionPolicy": {"states:StartExecution"},
95
+ "StepFunctionsCallbackPolicy": {"states:SendTaskSuccess", "states:SendTaskFailure", "states:SendTaskHeartbeat"},
96
+ "AWSSecretsManagerGetSecretValuePolicy": {"secretsmanager:GetSecretValue"},
97
+ "SSMParameterReadPolicy": {
98
+ "ssm:DescribeParameters",
99
+ "ssm:GetParameters",
100
+ "ssm:GetParameter",
101
+ "ssm:GetParametersByPath",
102
+ },
103
+ "SSMParameterWithSlashPrefixReadPolicy": {
104
+ "ssm:DescribeParameters",
105
+ "ssm:GetParameters",
106
+ "ssm:GetParameter",
107
+ "ssm:GetParametersByPath",
108
+ },
109
+ "KMSDecryptPolicy": {"kms:Decrypt"},
110
+ "KMSEncryptPolicy": {"kms:Encrypt"},
111
+ "EventBridgePutEventsPolicy": {"events:PutEvents"},
112
+ "FirehoseWritePolicy": {"firehose:PutRecord", "firehose:PutRecordBatch"},
113
+ "SESCrudPolicy": {
114
+ "ses:GetIdentityVerificationAttributes",
115
+ "ses:SendEmail",
116
+ "ses:SendRawEmail",
117
+ "ses:VerifyEmailIdentity",
118
+ },
119
+ "SESSendBouncePolicy": {"ses:SendBounce"},
120
+ "SESBulkTemplatedCrudPolicy": {
121
+ "ses:GetIdentityVerificationAttributes",
122
+ "ses:SendEmail",
123
+ "ses:SendRawEmail",
124
+ "ses:SendTemplatedEmail",
125
+ "ses:SendBulkTemplatedEmail",
126
+ "ses:VerifyEmailIdentity",
127
+ },
128
+ "CloudWatchPutMetricPolicy": {"cloudwatch:PutMetricData"},
129
+ "RekognitionDetectOnlyPolicy": {
130
+ "rekognition:DetectFaces",
131
+ "rekognition:DetectLabels",
132
+ "rekognition:DetectModerationLabels",
133
+ "rekognition:DetectText",
134
+ },
135
+ "TextractDetectAnalyzePolicy": {
136
+ "textract:DetectDocumentText",
137
+ "textract:StartDocumentTextDetection",
138
+ "textract:StartDocumentAnalysis",
139
+ "textract:AnalyzeDocument",
140
+ },
141
+ "TextractGetResultPolicy": {"textract:GetDocumentTextDetection", "textract:GetDocumentAnalysis"},
142
+ "ComprehendBasicAccessPolicy": {
143
+ "comprehend:BatchDetectKeyPhrases",
144
+ "comprehend:DetectDominantLanguage",
145
+ "comprehend:DetectEntities",
146
+ "comprehend:BatchDetectEntities",
147
+ "comprehend:DetectKeyPhrases",
148
+ "comprehend:DetectSentiment",
149
+ "comprehend:BatchDetectDominantLanguage",
150
+ "comprehend:BatchDetectSentiment",
151
+ },
152
+ "EKSDescribePolicy": {"eks:DescribeCluster", "eks:ListClusters"},
153
+ "EC2DescribePolicy": {"ec2:DescribeRegions", "ec2:DescribeInstances"},
154
+ "AMIDescribePolicy": {"ec2:DescribeImages"},
155
+ "CloudFormationDescribeStacksPolicy": {"cloudformation:DescribeStacks"},
156
+ "ElasticsearchHttpPostPolicy": {"es:ESHttpPost", "es:ESHttpPut"},
157
+ }
158
+
159
+ _EVENT_GRANTS = { # an event source's type -> what SAM lets the function do to read it
160
+ "SQS": MANAGED_POLICIES["AWSLambdaSQSQueueExecutionRole"],
161
+ "Kinesis": MANAGED_POLICIES["AWSLambdaKinesisExecutionRole"],
162
+ "DynamoDB": MANAGED_POLICIES["AWSLambdaDynamoDBExecutionRole"],
163
+ }
164
+
165
+ _PYTHON_DEFAULT_TIMEOUT = 3 # seconds: CloudFormation's (and SAM's) Lambda timeout when none is set
166
+
167
+
168
+ def _managed(name: Any) -> set[str] | None:
169
+ if not isinstance(name, str):
170
+ return None
171
+ return MANAGED_POLICIES.get(name.rsplit("/", 1)[-1])
172
+
173
+
174
+ def _sam_policies(policies: Any) -> tuple[set[str] | None, list[str]]:
175
+ """
176
+ What a function's Policies allow, and how the template names each: ({'sns:Publish'}, ['SNSPublishMessagePolicy']).
177
+ None for the actions when one of them can't be told.
178
+ """
179
+ actions: set[str] | None = set()
180
+ names: list[str] = []
181
+ for policy in as_list(policies):
182
+ found: set[str] | None = None
183
+ if isinstance(policy, str):
184
+ found = _managed(policy)
185
+ names.append(policy.rsplit("/", 1)[-1])
186
+ elif isinstance(policy, dict) and ("Statement" in policy or "PolicyDocument" in policy):
187
+ found = statement_actions(policy.get("PolicyDocument", policy))
188
+ names.append("an inline statement")
189
+ elif isinstance(policy, dict) and len(policy) == 1:
190
+ template = next(iter(policy))
191
+ found = POLICY_TEMPLATES.get(template)
192
+ names.append(template)
193
+ if found is None:
194
+ actions = None
195
+ elif actions is not None:
196
+ actions |= found
197
+ return actions, names
198
+
199
+
200
+ def _role_actions(role: dict[str, Any]) -> set[str] | None:
201
+ """
202
+ What an AWS::IAM::Role in the template allows, from its inline policies and the managed ones it knows.
203
+ """
204
+ props = role.get("Properties") or {}
205
+ actions: set[str] = set()
206
+ for policy in as_list(props.get("Policies")):
207
+ found = statement_actions((policy or {}).get("PolicyDocument")) if isinstance(policy, dict) else None
208
+ if found is None:
209
+ return None
210
+ actions |= found
211
+ for arn in as_list(props.get("ManagedPolicyArns")):
212
+ found = _managed(arn) if isinstance(arn, str) else None
213
+ if found is None:
214
+ return None
215
+ actions |= found
216
+ return actions
217
+
218
+
219
+ def _logical(ref: Any) -> str | None:
220
+ """
221
+ The resource a !Ref or !GetAtt names: {'Fn::GetAtt': ['CartRole', 'Arn']} -> 'CartRole'.
222
+ """
223
+ if isinstance(ref, dict) and "Ref" in ref and isinstance(ref["Ref"], str):
224
+ return ref["Ref"]
225
+ if isinstance(ref, dict) and "Fn::GetAtt" in ref:
226
+ value = ref["Fn::GetAtt"]
227
+ return value[0] if isinstance(value, list) and value and isinstance(value[0], str) else None
228
+ return None
229
+
230
+
231
+ class SamTemplate(Deploy):
232
+ """
233
+ template.yaml (or .yml, or .json): SAM and plain CloudFormation.
234
+ """
235
+
236
+ name = "sam"
237
+ title = "AWS SAM"
238
+ files = ("template.yaml", "template.yml", "template.json")
239
+
240
+ def deployments(self, files: dict[str, str], code: set[str]) -> list[Deployment]:
241
+ found: list[Deployment] = []
242
+ for path, text in files.items():
243
+ data = json.loads(text) if path.endswith(".json") else load_yaml(text)
244
+ if not isinstance(data, dict) or not isinstance(data.get("Resources"), dict):
245
+ continue
246
+ found += self._functions(path, text, data, code)
247
+ return found
248
+
249
+ def _functions(self, path: str, text: str, data: dict[str, Any], code: set[str]) -> list[Deployment]:
250
+ resources: dict[str, Any] = data["Resources"]
251
+ base = posixpath.dirname(path)
252
+ defaults = (data.get("Globals") or {}).get("Function") or {}
253
+ connected = any(
254
+ isinstance(r, dict) and r.get("Type") == "AWS::Serverless::Connector" for r in resources.values()
255
+ )
256
+ found = []
257
+ for logical_id, resource in resources.items():
258
+ if not isinstance(resource, dict):
259
+ continue
260
+ kind = resource.get("Type")
261
+ props = resource.get("Properties") or {}
262
+ if (
263
+ kind not in ("AWS::Serverless::Function", "AWS::Lambda::Function")
264
+ or props.get("PackageType") == "Image"
265
+ ):
266
+ continue
267
+ merged = {**defaults, **props} if kind == "AWS::Serverless::Function" else dict(props)
268
+ runtime = merged.get("Runtime")
269
+ if isinstance(runtime, str) and not runtime.startswith("python"):
270
+ continue
271
+ code_dir = merged.get("CodeUri") if kind == "AWS::Serverless::Function" else merged.get("Code")
272
+ where = resolve_handler(
273
+ base, code_dir if isinstance(code_dir, str) else "", merged.get("Handler", ""), code
274
+ )
275
+ if where is None:
276
+ continue
277
+ env_names, env_error = set(), None
278
+ for source in ((defaults if kind == "AWS::Serverless::Function" else {}), props):
279
+ variables = ((source.get("Environment") or {}).get("Variables")) if isinstance(source, dict) else None
280
+ if isinstance(variables, dict):
281
+ env_names |= {str(name) for name in variables}
282
+ elif variables is not None:
283
+ env_error = "its Variables aren't written out"
284
+ actions, names = self._permissions(logical_id, props, kind, resources)
285
+ if connected: # a Connector grants what it joins, which this doesn't read
286
+ actions = None
287
+ layers = [str(_logical(layer) or layer) for layer in as_list(merged.get("Layers"))]
288
+ timeout = merged.get("Timeout", _PYTHON_DEFAULT_TIMEOUT)
289
+ memory = merged.get("MemorySize", 128)
290
+ found.append(
291
+ Deployment(
292
+ logical_id,
293
+ path,
294
+ self.name,
295
+ where[0],
296
+ where[1],
297
+ "lambda",
298
+ line_of(text, logical_id),
299
+ runtime if isinstance(runtime, str) else None,
300
+ timeout if isinstance(timeout, int) else None,
301
+ memory if isinstance(memory, int) else None,
302
+ env_names,
303
+ env_error,
304
+ layers,
305
+ actions,
306
+ names,
307
+ )
308
+ )
309
+ return found
310
+
311
+ def _permissions(
312
+ self, logical_id: str, props: dict[str, Any], kind: str, resources: dict[str, Any]
313
+ ) -> tuple[set[str] | None, list[str]]:
314
+ """
315
+ What a function's role allows: SAM's role from its Policies and event sources, or a role in the template,
316
+ plus any AWS::IAM::Policy attached to that role. None for the actions when it can't be told.
317
+ """
318
+ role_id = None
319
+ names: list[str] = []
320
+ if "Role" in props:
321
+ role_id = _logical(props["Role"])
322
+ role = resources.get(role_id) if role_id else None
323
+ if not isinstance(role, dict) or role.get("Type") != "AWS::IAM::Role":
324
+ return None, []
325
+ actions = _role_actions(role)
326
+ names.append(role_id or "")
327
+ elif kind == "AWS::Serverless::Function":
328
+ actions, names = _sam_policies(props.get("Policies"))
329
+ role_id = f"{logical_id}Role"
330
+ if actions is not None:
331
+ actions |= MANAGED_POLICIES["AWSLambdaBasicExecutionRole"]
332
+ for event in (props.get("Events") or {}).values():
333
+ if isinstance(event, dict):
334
+ actions |= _EVENT_GRANTS.get(event.get("Type", ""), set())
335
+ else:
336
+ return None, []
337
+ for resource in resources.values():
338
+ if not isinstance(resource, dict) or resource.get("Type") not in (
339
+ "AWS::IAM::Policy",
340
+ "AWS::IAM::ManagedPolicy",
341
+ ):
342
+ continue
343
+ policy = resource.get("Properties") or {}
344
+ if any(_logical(role) == role_id for role in as_list(policy.get("Roles"))):
345
+ more = statement_actions(policy.get("PolicyDocument"))
346
+ actions = None if more is None or actions is None else actions | more
347
+ return actions, names
348
+
349
+ def grant(self, deployment: Deployment, actions: list[str]) -> str:
350
+ """
351
+ The smallest SAM policy template for each service's missing actions ('add DynamoDBWritePolicy and
352
+ SNSPublishMessagePolicy'), and a statement for what none covers.
353
+ """
354
+ by_service: dict[str, list[str]] = {}
355
+ for action in actions:
356
+ by_service.setdefault(action.split(":")[0], []).append(action)
357
+ templates, rest = [], []
358
+ for wanted in by_service.values():
359
+ fits = [
360
+ (name, granted)
361
+ for name, granted in POLICY_TEMPLATES.items()
362
+ if all(allowed(action, granted) for action in wanted)
363
+ ]
364
+ if fits:
365
+ templates.append(min(fits, key=lambda fit: (len(fit[1]), fit[0]))[0])
366
+ else:
367
+ rest += wanted
368
+ where = f"{deployment.name}'s Policies in {deployment.source}"
369
+ said = [" and ".join(templates)] if templates else []
370
+ said += [f"a statement that allows {', '.join(rest)}"] if rest else []
371
+ return f"add {' and '.join(said)} to {where}"
@@ -0,0 +1,117 @@
1
+ """
2
+ The Serverless Framework's serverless.yml: each function on a Python runtime, with the provider's settings applied,
3
+ its environment variables (names only), and what its role's statements allow.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import posixpath
9
+ from typing import Any
10
+
11
+ from ..analysis.model import Deployment
12
+ from . import MANAGED_POLICIES, Deploy, as_list, line_of, load_yaml, resolve_handler, statement_actions
13
+
14
+ _DEFAULT_TIMEOUT = 6 # seconds: the Serverless Framework's default
15
+
16
+ _DEFAULT_MEMORY = 1024
17
+
18
+
19
+ def _statements(provider: dict[str, Any]) -> tuple[Any, Any]:
20
+ """
21
+ The provider's role statements and managed policies, from v3's iam.role or v2's iamRoleStatements. ('string',
22
+ None) when the role is one made elsewhere (an ARN or a name).
23
+ """
24
+ iam = provider.get("iam") or {}
25
+ role = iam.get("role") if isinstance(iam, dict) else None
26
+ if isinstance(role, str):
27
+ return "elsewhere", None
28
+ if isinstance(role, dict):
29
+ return role.get("statements") or [], role.get("managedPolicies")
30
+ return provider.get("iamRoleStatements") or [], provider.get("iamManagedPolicies")
31
+
32
+
33
+ class ServerlessFile(Deploy):
34
+ """
35
+ serverless.yml (or .yaml) with provider.name aws.
36
+ """
37
+
38
+ name = "serverless"
39
+ title = "Serverless Framework"
40
+ files = ("serverless.yml", "serverless.yaml")
41
+
42
+ def deployments(self, files: dict[str, str], code: set[str]) -> list[Deployment]:
43
+ found: list[Deployment] = []
44
+ for path, text in files.items():
45
+ data = load_yaml(text)
46
+ if not isinstance(data, dict) or not isinstance(data.get("provider"), dict):
47
+ continue
48
+ provider = data["provider"]
49
+ if provider.get("name", "aws") != "aws":
50
+ continue
51
+ functions = data.get("functions") or {}
52
+ if isinstance(functions, list): # an old file: a list of {name: function}
53
+ functions = {k: v for item in functions if isinstance(item, dict) for k, v in item.items()}
54
+ if not isinstance(functions, dict):
55
+ continue
56
+ statements, managed = _statements(provider)
57
+ for name, function in functions.items():
58
+ if not isinstance(function, dict) or "image" in function:
59
+ continue
60
+ runtime = function.get("runtime", provider.get("runtime"))
61
+ if isinstance(runtime, str) and not runtime.startswith("python"):
62
+ continue
63
+ where = resolve_handler(posixpath.dirname(path), "", function.get("handler", ""), code)
64
+ if where is None:
65
+ continue
66
+ env_names, env_error = set(), None
67
+ for source in (provider.get("environment"), function.get("environment")):
68
+ if isinstance(source, dict):
69
+ env_names |= {str(key) for key in source}
70
+ elif source is not None:
71
+ env_error = "its environment comes from a file or a variable"
72
+ actions = None
73
+ if statements != "elsewhere":
74
+ actions = statement_actions(as_list(statements) + as_list(function.get("iamRoleStatements")))
75
+ for policy in as_list(managed):
76
+ granted = (
77
+ MANAGED_POLICIES.get(str(policy).rsplit("/", 1)[-1]) if isinstance(policy, str) else None
78
+ )
79
+ actions = None if granted is None or actions is None else actions | granted
80
+ if actions is not None:
81
+ actions |= MANAGED_POLICIES["AWSLambdaBasicExecutionRole"]
82
+ for event in as_list(function.get("events")):
83
+ if isinstance(event, dict) and "sqs" in event:
84
+ actions |= MANAGED_POLICIES["AWSLambdaSQSQueueExecutionRole"]
85
+ if isinstance(event, dict) and "stream" in event:
86
+ actions |= (
87
+ MANAGED_POLICIES["AWSLambdaDynamoDBExecutionRole"]
88
+ | MANAGED_POLICIES["AWSLambdaKinesisExecutionRole"]
89
+ )
90
+ timeout = function.get("timeout", provider.get("timeout", _DEFAULT_TIMEOUT))
91
+ memory = function.get("memorySize", provider.get("memorySize", _DEFAULT_MEMORY))
92
+ layers = [str(layer) for layer in as_list(function.get("layers", provider.get("layers")))]
93
+ found.append(
94
+ Deployment(
95
+ str(name),
96
+ path,
97
+ self.name,
98
+ where[0],
99
+ where[1],
100
+ "lambda",
101
+ line_of(text, str(name), 2),
102
+ runtime if isinstance(runtime, str) else None,
103
+ timeout if isinstance(timeout, int) else None,
104
+ memory if isinstance(memory, int) else None,
105
+ env_names,
106
+ env_error,
107
+ layers,
108
+ actions,
109
+ ["provider.iam.role.statements"],
110
+ )
111
+ )
112
+ return found
113
+
114
+ def grant(self, deployment: Deployment, actions: list[str]) -> str:
115
+ return (
116
+ f"add a statement that allows {', '.join(actions)} to provider.iam.role.statements in {deployment.source}"
117
+ )
@@ -0,0 +1,4 @@
1
+ """
2
+ Shared rendering helpers, copied from aws-analyzer's _kit: the report blocks, their HTML and text drawings,
3
+ formatting and code highlighting.
4
+ """
@@ -0,0 +1,184 @@
1
+ """
2
+ The blocks a report is made of, and what draws each kind of block.
3
+
4
+ A View command builds a list of blocks (a _Title, _Cards, a _Table, ...) and hands it to the renderer, which draws
5
+ each block by what's registered for its type: its HTML, or its lines of text. These are the blocks every service
6
+ uses (render.py draws them); a service's own blocks (_Passage, _Steps, _Wiring, ...) are defined in its module and
7
+ register how they're drawn with @_draws when it's imported.
8
+
9
+ The registries @_draws fills: _HTML (block type -> its HTML), _TEXT (block type -> its lines in text mode) and
10
+ _CELLS (table cell type -> its HTML, given the cell's text).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Callable
17
+
18
+
19
+ @dataclass
20
+ class _Title:
21
+ """
22
+ The report's title, with a line under it (sub).
23
+ """
24
+
25
+ text: str
26
+ sub: str = ""
27
+
28
+
29
+ @dataclass
30
+ class _Cards:
31
+ """
32
+ The few numbers that matter, as cards. Each item is (label, value), or (label, value, tone) with tone 'warn',
33
+ 'bad' or 'ok'.
34
+ """
35
+
36
+ items: list[tuple[str, ...]]
37
+
38
+
39
+ @dataclass
40
+ class _Table:
41
+ """
42
+ A table of rows under its headers.
43
+
44
+ rows cells are text, or _Tone for a coloured status (or another type registered as a cell)
45
+ bars 0..1 per row, drawn as an extra column headed bar_label
46
+ tree the first column holds indented tree labels
47
+ max_rows None for the view's default, 0 for no cap
48
+ code_cols columns holding calls to copy, shown as code
49
+ prose_cols columns of sentences this tool wrote (findings): the calls in them shown as code
50
+ collapsed a secondary view: folded under its title in HTML
51
+ path_cols columns of keys and paths: the file name always shows, a long folder is shortened
52
+ sortable in HTML a click on a column's header sorts by it (tree tables never sort)
53
+ """
54
+
55
+ headers: list[str]
56
+ rows: list[list[Any]]
57
+ title: str = ""
58
+ bars: list[float] | None = None
59
+ bar_label: str = "Share"
60
+ tree: bool = False
61
+ max_rows: int | None = None
62
+ code_cols: tuple[int, ...] = ()
63
+ prose_cols: tuple[int, ...] = ()
64
+ collapsed: bool = False
65
+ path_cols: tuple[int, ...] = ()
66
+ sortable: bool = True
67
+
68
+
69
+ @dataclass
70
+ class _Note:
71
+ """
72
+ A sentence on its own, at a level: 'info', 'warn' or 'ok'.
73
+ """
74
+
75
+ text: str
76
+ level: str = "info"
77
+
78
+
79
+ @dataclass
80
+ class _Text:
81
+ """
82
+ Text shown as it is.
83
+
84
+ wrap prose: wrap long lines instead of scrolling sideways
85
+ code a snippet to copy: in HTML one click selects all of it
86
+ collapsed a secondary view (raw JSON): folded under its title in HTML
87
+ """
88
+
89
+ text: str
90
+ title: str = ""
91
+ wrap: bool = False
92
+ code: bool = False
93
+ collapsed: bool = False
94
+
95
+
96
+ @dataclass
97
+ class _Findings:
98
+ """
99
+ The (level, message) pairs from a *_findings function, and what to say (as an ok note) when there are none;
100
+ nothing when `empty` is blank.
101
+ """
102
+
103
+ items: list[tuple[str, str]]
104
+ empty: str = ""
105
+
106
+
107
+ @dataclass
108
+ class _Next:
109
+ """
110
+ The commands worth running next, as (call, what it shows), with their arguments filled in.
111
+ """
112
+
113
+ items: list[tuple[str, str]]
114
+ title: str = "Next"
115
+
116
+
117
+ @dataclass
118
+ class _Tone:
119
+ """
120
+ A table cell with a status colour (tone: 'warn', 'bad' or 'ok'): a pill in HTML, plain text elsewhere.
121
+ """
122
+
123
+ text: str
124
+ tone: str = "warn"
125
+
126
+ def __str__(self) -> str:
127
+ return self.text
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class _Report:
132
+ """
133
+ One report being drawn: what each block's drawing function gets besides the block.
134
+
135
+ max_rows the view's cap on table rows; 0 for no cap
136
+ badge the chip before each title (HTML), so reports from different services are easy to tell apart
137
+ root the service's root class, which scopes the CSS rules
138
+ blocks the whole report, for a block that needs to know what else is in it
139
+ rules CSS rules the blocks need beyond the stylesheet (a table's filters and columns): a <style> after it
140
+ parts what blocks share while the report is drawn (see shared)
141
+ """
142
+
143
+ max_rows: int
144
+ badge: str = ""
145
+ root: str = ""
146
+ blocks: tuple[Any, ...] = ()
147
+ rules: set[str] = field(default_factory=set)
148
+ parts: dict[Any, Any] = field(default_factory=dict)
149
+
150
+ def shared(self, key: Any, make: Callable[[], Any]) -> Any:
151
+ """
152
+ What the report's blocks share while it's drawn, made by make() the first time one asks for it: S3's PDF
153
+ pages share one full-size view, so its ‹ › step through every page in the report.
154
+ """
155
+ if key not in self.parts:
156
+ self.parts[key] = make()
157
+ return self.parts[key]
158
+
159
+
160
+ _HTML: dict[type, Callable[[Any, _Report], str]] = {}
161
+ _TEXT: dict[type, Callable[[Any, _Report], list[str]]] = {}
162
+ _CELLS: dict[type, Callable[[Any, str], str]] = {}
163
+
164
+
165
+ def _draws(kind: type, mode: str) -> Callable[[Callable], Callable]:
166
+ """
167
+ @_draws(_Passage, "html") makes the function under it the way a _Passage block is drawn in HTML:
168
+ f(block, report) -> str. "text" registers its lines in text mode, f(block, report) -> list[str], and "cell" how
169
+ a table cell of that type shows in HTML, f(cell, text) -> str (text mode shows str(cell)).
170
+ """
171
+ registry: dict[type, Callable] = {"html": _HTML, "text": _TEXT, "cell": _CELLS}[mode]
172
+
173
+ def register(draw: Callable) -> Callable:
174
+ registry[kind] = draw
175
+ return draw
176
+
177
+ return register
178
+
179
+
180
+ def _drawer(registry: dict[type, Callable], value: Any) -> Callable | None:
181
+ """
182
+ What draws value: the function registered for its type, or for the closest type it's made from.
183
+ """
184
+ return next((registry[cls] for cls in type(value).__mro__ if cls in registry), None)