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.
- code_node/__init__.py +40 -0
- code_node/__main__.py +7 -0
- code_node/analysis/__init__.py +3 -0
- code_node/analysis/aws_model.py +197 -0
- code_node/analysis/findings.py +462 -0
- code_node/analysis/graph.py +780 -0
- code_node/analysis/mapping.py +114 -0
- code_node/analysis/model.py +289 -0
- code_node/analysis/reader.py +1926 -0
- code_node/analysis/syntax.py +220 -0
- code_node/analysis/terms.py +505 -0
- code_node/cli.py +316 -0
- code_node/deploys/__init__.py +321 -0
- code_node/deploys/sam.py +371 -0
- code_node/deploys/serverless.py +117 -0
- code_node/kit/__init__.py +4 -0
- code_node/kit/blocks.py +184 -0
- code_node/kit/fmt.py +389 -0
- code_node/kit/highlight.py +131 -0
- code_node/kit/render.py +1066 -0
- code_node/kit/text.py +81 -0
- code_node/libraries/__init__.py +333 -0
- code_node/libraries/llm.py +124 -0
- code_node/libraries/mongo.py +169 -0
- code_node/libraries/redis.py +253 -0
- code_node/libraries/sql.py +232 -0
- code_node/pages.py +92 -0
- code_node/platforms/__init__.py +203 -0
- code_node/platforms/aws_lambda.py +290 -0
- code_node/platforms/web.py +301 -0
- code_node/project.py +402 -0
- code_node/report/__init__.py +3 -0
- code_node/report/brands.py +244 -0
- code_node/report/build.py +119 -0
- code_node/report/editor.py +40 -0
- code_node/report/graph.py +1411 -0
- code_node/report/logos/README.md +8 -0
- code_node/report/logos/anthropic-icon.svg +1 -0
- code_node/report/logos/aws-amplify.svg +1 -0
- code_node/report/logos/aws-api-gateway.svg +1 -0
- code_node/report/logos/aws-appflow.svg +1 -0
- code_node/report/logos/aws-appsync.svg +1 -0
- code_node/report/logos/aws-athena.svg +1 -0
- code_node/report/logos/aws-backup.svg +1 -0
- code_node/report/logos/aws-batch.svg +1 -0
- code_node/report/logos/aws-certificate-manager.svg +1 -0
- code_node/report/logos/aws-cloudformation.svg +1 -0
- code_node/report/logos/aws-cloudfront.svg +1 -0
- code_node/report/logos/aws-cloudtrail.svg +1 -0
- code_node/report/logos/aws-cloudwatch.svg +1 -0
- code_node/report/logos/aws-codebuild.svg +1 -0
- code_node/report/logos/aws-codecommit.svg +1 -0
- code_node/report/logos/aws-codedeploy.svg +1 -0
- code_node/report/logos/aws-codepipeline.svg +1 -0
- code_node/report/logos/aws-cognito.svg +1 -0
- code_node/report/logos/aws-config.svg +1 -0
- code_node/report/logos/aws-documentdb.svg +1 -0
- code_node/report/logos/aws-dynamodb.svg +1 -0
- code_node/report/logos/aws-ec2.svg +1 -0
- code_node/report/logos/aws-ecs.svg +1 -0
- code_node/report/logos/aws-eks.svg +1 -0
- code_node/report/logos/aws-elasticache.svg +1 -0
- code_node/report/logos/aws-elb.svg +1 -0
- code_node/report/logos/aws-eventbridge.svg +1 -0
- code_node/report/logos/aws-glacier.svg +1 -0
- code_node/report/logos/aws-glue.svg +1 -0
- code_node/report/logos/aws-iam.svg +1 -0
- code_node/report/logos/aws-keyspaces.svg +1 -0
- code_node/report/logos/aws-kinesis.svg +1 -0
- code_node/report/logos/aws-kms.svg +1 -0
- code_node/report/logos/aws-lake-formation.svg +1 -0
- code_node/report/logos/aws-lambda.svg +1 -0
- code_node/report/logos/aws-lightsail.svg +1 -0
- code_node/report/logos/aws-mq.svg +1 -0
- code_node/report/logos/aws-msk.svg +1 -0
- code_node/report/logos/aws-neptune.svg +1 -0
- code_node/report/logos/aws-open-search.svg +1 -0
- code_node/report/logos/aws-quicksight.svg +1 -0
- code_node/report/logos/aws-rds.svg +1 -0
- code_node/report/logos/aws-redshift.svg +1 -0
- code_node/report/logos/aws-route53.svg +1 -0
- code_node/report/logos/aws-s3.svg +1 -0
- code_node/report/logos/aws-secrets-manager.svg +1 -0
- code_node/report/logos/aws-ses.svg +1 -0
- code_node/report/logos/aws-shield.svg +1 -0
- code_node/report/logos/aws-sns.svg +1 -0
- code_node/report/logos/aws-sqs.svg +1 -0
- code_node/report/logos/aws-step-functions.svg +1 -0
- code_node/report/logos/aws-systems-manager.svg +1 -0
- code_node/report/logos/aws-timestream.svg +1 -0
- code_node/report/logos/aws-waf.svg +1 -0
- code_node/report/logos/aws-xray.svg +1 -0
- code_node/report/logos/aws.svg +1 -0
- code_node/report/logos/azure-icon.svg +1 -0
- code_node/report/logos/django-icon.svg +1 -0
- code_node/report/logos/elasticsearch.svg +1 -0
- code_node/report/logos/fastapi-icon.svg +1 -0
- code_node/report/logos/firebase-icon.svg +1 -0
- code_node/report/logos/flask.svg +1 -0
- code_node/report/logos/google-cloud.svg +1 -0
- code_node/report/logos/kafka-icon.svg +1 -0
- code_node/report/logos/mariadb-icon.svg +1 -0
- code_node/report/logos/mongodb-icon.svg +1 -0
- code_node/report/logos/mysql-icon.svg +1 -0
- code_node/report/logos/openai-icon.svg +1 -0
- code_node/report/logos/postgresql.svg +1 -0
- code_node/report/logos/python.svg +1 -0
- code_node/report/logos/rabbitmq-icon.svg +1 -0
- code_node/report/logos/redis.svg +1 -0
- code_node/report/logos/serverless.svg +1 -0
- code_node/report/logos/slack-icon.svg +1 -0
- code_node/report/logos/small/aws-api-gateway.svg +1 -0
- code_node/report/logos/small/aws-appflow.svg +1 -0
- code_node/report/logos/small/aws-appsync.svg +1 -0
- code_node/report/logos/small/aws-athena.svg +1 -0
- code_node/report/logos/small/aws-backup.svg +1 -0
- code_node/report/logos/small/aws-batch.svg +1 -0
- code_node/report/logos/small/aws-certificate-manager.svg +1 -0
- code_node/report/logos/small/aws-cloudformation.svg +1 -0
- code_node/report/logos/small/aws-cloudfront.svg +1 -0
- code_node/report/logos/small/aws-cloudtrail.svg +1 -0
- code_node/report/logos/small/aws-cloudwatch.svg +1 -0
- code_node/report/logos/small/aws-codebuild.svg +1 -0
- code_node/report/logos/small/aws-codecommit.svg +1 -0
- code_node/report/logos/small/aws-codedeploy.svg +1 -0
- code_node/report/logos/small/aws-codepipeline.svg +1 -0
- code_node/report/logos/small/aws-cognito.svg +1 -0
- code_node/report/logos/small/aws-config.svg +1 -0
- code_node/report/logos/small/aws-documentdb.svg +1 -0
- code_node/report/logos/small/aws-dynamodb.svg +1 -0
- code_node/report/logos/small/aws-ec2.svg +1 -0
- code_node/report/logos/small/aws-ecs.svg +1 -0
- code_node/report/logos/small/aws-eks.svg +1 -0
- code_node/report/logos/small/aws-elasticache.svg +1 -0
- code_node/report/logos/small/aws-elb.svg +1 -0
- code_node/report/logos/small/aws-eventbridge.svg +1 -0
- code_node/report/logos/small/aws-glacier.svg +1 -0
- code_node/report/logos/small/aws-glue.svg +1 -0
- code_node/report/logos/small/aws-iam.svg +1 -0
- code_node/report/logos/small/aws-keyspaces.svg +1 -0
- code_node/report/logos/small/aws-kinesis.svg +1 -0
- code_node/report/logos/small/aws-kms.svg +1 -0
- code_node/report/logos/small/aws-lake-formation.svg +1 -0
- code_node/report/logos/small/aws-lambda.svg +1 -0
- code_node/report/logos/small/aws-mq.svg +1 -0
- code_node/report/logos/small/aws-msk.svg +1 -0
- code_node/report/logos/small/aws-neptune.svg +1 -0
- code_node/report/logos/small/aws-open-search.svg +1 -0
- code_node/report/logos/small/aws-quicksight.svg +1 -0
- code_node/report/logos/small/aws-rds.svg +1 -0
- code_node/report/logos/small/aws-redshift.svg +1 -0
- code_node/report/logos/small/aws-route53.svg +1 -0
- code_node/report/logos/small/aws-s3.svg +1 -0
- code_node/report/logos/small/aws-secrets-manager.svg +1 -0
- code_node/report/logos/small/aws-ses.svg +1 -0
- code_node/report/logos/small/aws-shield.svg +1 -0
- code_node/report/logos/small/aws-sns.svg +1 -0
- code_node/report/logos/small/aws-sqs.svg +1 -0
- code_node/report/logos/small/aws-step-functions.svg +1 -0
- code_node/report/logos/small/aws-systems-manager.svg +1 -0
- code_node/report/logos/small/aws-timestream.svg +1 -0
- code_node/report/logos/small/aws-waf.svg +1 -0
- code_node/report/logos/small/aws-xray.svg +1 -0
- code_node/report/logos/snowflake-icon.svg +1 -0
- code_node/report/logos/sqlite.svg +1 -0
- code_node/report/logos/stripe.svg +1 -0
- code_node/report/logos/supabase-icon.svg +1 -0
- code_node/report/logos/twilio-icon.svg +1 -0
- code_node/report/outline.py +219 -0
- code_node/report/style.py +358 -0
- code_node/report/tabs.py +73 -0
- code_node/report/walk.py +624 -0
- code_node/serve.py +277 -0
- code_node_graph-0.1.0.dist-info/METADATA +165 -0
- code_node_graph-0.1.0.dist-info/RECORD +178 -0
- code_node_graph-0.1.0.dist-info/WHEEL +4 -0
- code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
- code_node_graph-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small readings of Python syntax the code reader shares: names, constants, docstrings,
|
|
3
|
+
the lines of a definition, and secrets written into the code.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import copy
|
|
10
|
+
import re
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Any, Iterable
|
|
13
|
+
|
|
14
|
+
from ..kit.text import _clip
|
|
15
|
+
|
|
16
|
+
from .terms import _AWS_KEY_RE, _NOT_SECRET_NAME_RE, _PLACEHOLDERS, _SECRET_NAME_RE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse(text: str, filename: str = "<unknown>", mode: str = "exec") -> Any:
|
|
20
|
+
"""
|
|
21
|
+
ast.parse, with the warnings Python gives while parsing (a SyntaxWarning for '\\d' in a plain string) kept quiet:
|
|
22
|
+
they're about the code read, and would only clutter the terminal.
|
|
23
|
+
"""
|
|
24
|
+
with warnings.catch_warnings():
|
|
25
|
+
warnings.simplefilter("ignore")
|
|
26
|
+
return ast.parse(text, filename=filename, mode=mode)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_OPPOSITES = {
|
|
30
|
+
ast.Is: ast.IsNot,
|
|
31
|
+
ast.IsNot: ast.Is,
|
|
32
|
+
ast.In: ast.NotIn,
|
|
33
|
+
ast.NotIn: ast.In,
|
|
34
|
+
ast.Eq: ast.NotEq,
|
|
35
|
+
ast.NotEq: ast.Eq,
|
|
36
|
+
} # a comparison's operator -> the one that's true when it isn't
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _negated(test: Any) -> str:
|
|
40
|
+
"""
|
|
41
|
+
The opposite of an if's test, as code: 'check(order)' for `not check(order)`, 'x is not None' for `x is None`,
|
|
42
|
+
'not (a or b)' for `a or b`.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not):
|
|
45
|
+
return _unparse(test.operand, 110)
|
|
46
|
+
if isinstance(test, ast.Compare) and len(test.ops) == 1 and type(test.ops[0]) in _OPPOSITES:
|
|
47
|
+
flipped = ast.Compare(test.left, [_OPPOSITES[type(test.ops[0])]()], test.comparators)
|
|
48
|
+
return _unparse(flipped, 110)
|
|
49
|
+
code = _unparse(test, 110)
|
|
50
|
+
return f"not ({code})" if isinstance(test, (ast.BoolOp, ast.IfExp, ast.NamedExpr, ast.Lambda)) else f"not {code}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _ends_early(body: list[Any]) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Whether a block always leaves where it is: its last line returns, raises, continues or breaks.
|
|
56
|
+
"""
|
|
57
|
+
return bool(body) and isinstance(body[-1], (ast.Return, ast.Raise, ast.Continue, ast.Break))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _secret_spots(node: Any) -> list[tuple[str, Any]]:
|
|
61
|
+
"""
|
|
62
|
+
The secret-looking literals in a piece of code, with the name each is given to: a password=, token= or key given
|
|
63
|
+
as text (in an assignment, a call or a dict), and any AWS access key.
|
|
64
|
+
"""
|
|
65
|
+
found: list[tuple[str, Any]] = []
|
|
66
|
+
for n in ast.walk(node):
|
|
67
|
+
pairs: list[tuple[Any, Any]] = []
|
|
68
|
+
if isinstance(n, (ast.Assign, ast.AnnAssign)) and n.value is not None:
|
|
69
|
+
for target in n.targets if isinstance(n, ast.Assign) else [n.target]:
|
|
70
|
+
pairs.append(
|
|
71
|
+
(
|
|
72
|
+
target.id
|
|
73
|
+
if isinstance(target, ast.Name)
|
|
74
|
+
else target.attr
|
|
75
|
+
if isinstance(target, ast.Attribute)
|
|
76
|
+
else _const(target.slice)
|
|
77
|
+
if isinstance(target, ast.Subscript)
|
|
78
|
+
else None,
|
|
79
|
+
n.value,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
elif isinstance(n, ast.Call):
|
|
83
|
+
pairs += [(kw.arg, kw.value) for kw in n.keywords if kw.arg]
|
|
84
|
+
elif isinstance(n, ast.Dict):
|
|
85
|
+
pairs += [(_const(k), v) for k, v in zip(n.keys, n.values) if k is not None]
|
|
86
|
+
for name, value in pairs:
|
|
87
|
+
text = _const(value)
|
|
88
|
+
if (
|
|
89
|
+
isinstance(name, str)
|
|
90
|
+
and isinstance(text, str)
|
|
91
|
+
and _SECRET_NAME_RE.search(name)
|
|
92
|
+
and not _NOT_SECRET_NAME_RE.search(name)
|
|
93
|
+
and _secret_value(text)
|
|
94
|
+
):
|
|
95
|
+
found.append((name, value))
|
|
96
|
+
if isinstance(n, ast.Constant) and isinstance(n.value, str) and _AWS_KEY_RE.search(n.value):
|
|
97
|
+
found.append(("an AWS access key", n))
|
|
98
|
+
return found
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _Hide(ast.NodeTransformer):
|
|
102
|
+
"""
|
|
103
|
+
Puts '…' in place of the literals at these places (line, column).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, places: set[tuple[int, int]]):
|
|
107
|
+
self.places = places
|
|
108
|
+
|
|
109
|
+
def visit_Constant(self, node: ast.Constant) -> Any:
|
|
110
|
+
if (getattr(node, "lineno", 0), getattr(node, "col_offset", 0)) in self.places:
|
|
111
|
+
return ast.copy_location(ast.Constant("…"), node)
|
|
112
|
+
return node
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _unparse(node: Any, width: int = 140) -> str:
|
|
116
|
+
"""
|
|
117
|
+
A piece of code on one line, as Python writes it back from the tree: "s3.get_object(Bucket=b, Key=k)". A
|
|
118
|
+
secret-looking literal in it shows as '…'.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
if isinstance(node, ast.AST) and not isinstance(node, (ast.arguments, ast.pattern)):
|
|
122
|
+
places = {(getattr(v, "lineno", 0), getattr(v, "col_offset", 0)) for _, v in _secret_spots(node)}
|
|
123
|
+
if places:
|
|
124
|
+
node = _Hide(places).visit(copy.deepcopy(node))
|
|
125
|
+
text = ast.unparse(node)
|
|
126
|
+
except (AttributeError, ValueError, TypeError, RecursionError):
|
|
127
|
+
return "…"
|
|
128
|
+
return _clip(" ".join(text.split()), width)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _target(node: Any) -> str:
|
|
132
|
+
"""
|
|
133
|
+
A loop's target as written: 'i, record', not '(i, record)'.
|
|
134
|
+
"""
|
|
135
|
+
if isinstance(node, ast.Tuple) and node.elts:
|
|
136
|
+
return ", ".join(_unparse(elt, 30) for elt in node.elts)
|
|
137
|
+
return _unparse(node, 40)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _a(name: str) -> str:
|
|
141
|
+
"""
|
|
142
|
+
'a' or 'an' before a service's name, as it's said: an S3, an SQS, a DynamoDB, an EventBridge, a KMS.
|
|
143
|
+
"""
|
|
144
|
+
if re.match(r"[A-Z][A-Z0-9]", name): # said letter by letter
|
|
145
|
+
return "an" if name[0] in "AEFHILMNORSX" else "a"
|
|
146
|
+
return "an" if name[:1].lower() in "aeiou" else "a"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _const(node: Any) -> Any:
|
|
150
|
+
return node.value if isinstance(node, ast.Constant) else None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _names_in(node: Any, names: Iterable[str]) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Whether an `except` catches one of these exceptions: except ImportError, except (KeyError, Exception).
|
|
156
|
+
"""
|
|
157
|
+
if node is None:
|
|
158
|
+
return False
|
|
159
|
+
if isinstance(node, ast.Tuple):
|
|
160
|
+
return any(_names_in(elt, names) for elt in node.elts)
|
|
161
|
+
return (node.id if isinstance(node, ast.Name) else getattr(node, "attr", "")) in set(names)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _swallows(handler: ast.ExceptHandler) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
An `except` that catches every error and does nothing with it: no log line, no raise, nothing returned.
|
|
167
|
+
"""
|
|
168
|
+
if not (handler.type is None or _names_in(handler.type, ("Exception", "BaseException"))):
|
|
169
|
+
return False
|
|
170
|
+
for stmt in handler.body:
|
|
171
|
+
if isinstance(stmt, (ast.Pass, ast.Continue, ast.Break)):
|
|
172
|
+
continue
|
|
173
|
+
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
|
|
174
|
+
continue
|
|
175
|
+
if isinstance(stmt, ast.Return) and (stmt.value is None or isinstance(stmt.value, ast.Constant)):
|
|
176
|
+
continue
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _is_main_guard(test: Any) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
`if __name__ == "__main__":`, which never runs when the code is imported.
|
|
184
|
+
"""
|
|
185
|
+
return (
|
|
186
|
+
isinstance(test, ast.Compare)
|
|
187
|
+
and isinstance(test.left, ast.Name)
|
|
188
|
+
and test.left.id == "__name__"
|
|
189
|
+
and any(_const(c) == "__main__" for c in test.comparators)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _doc_line(node: Any) -> str:
|
|
194
|
+
doc = ast.get_docstring(node) or ""
|
|
195
|
+
return _clip(doc.strip().split("\n\n")[0].replace("\n", " ").strip(), 160)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _def_line(node: Any) -> str:
|
|
199
|
+
"""
|
|
200
|
+
'def connect(host, password):'.
|
|
201
|
+
"""
|
|
202
|
+
prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
|
|
203
|
+
return f"{prefix} {node.name}({_unparse(node.args, 100)}):"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _secret_value(value: str) -> bool:
|
|
207
|
+
"""
|
|
208
|
+
Whether a literal looks like a real secret rather than a name, a path or a placeholder.
|
|
209
|
+
"""
|
|
210
|
+
text = value.strip()
|
|
211
|
+
return (
|
|
212
|
+
len(text) >= 6
|
|
213
|
+
and not re.search(r"\s", text)
|
|
214
|
+
and not re.fullmatch(r"[A-Z0-9_]+", text)
|
|
215
|
+
and not text.startswith(("arn:", "/", "{", "$", "<", "%", "http", "s3://", "projects/"))
|
|
216
|
+
and text.lower() not in _PLACEHOLDERS
|
|
217
|
+
and "xxx" not in text.lower()
|
|
218
|
+
and len(set(text)) > 2
|
|
219
|
+
and not text.lower().startswith("your")
|
|
220
|
+
)
|