Plugins¶
How code-node mixes plugins: one for where the code runs, one for each library it calls through, one for each deploy file in the folder, and how to write your own.
Three kinds¶
| Kind | How many | Answers | Built in |
|---|---|---|---|
| Platform | one per entry point | where it runs: its entry points, what they're called with, its checks | AWS Lambda, web apps (FastAPI, Flask, Django) |
| Library | every one the code imports | what a library's calls reach: a box for each service, with what each call does | Redis, SQL (sqlite3, psycopg2, psycopg, PyMySQL, mysql-connector, asyncpg, SQLAlchemy, Django's connection), MongoDB (PyMongo, Motor), OpenAI and Anthropic |
| Deploy file | every one in the folder | what the deploy says: the handler, the Python, the timeout, the variables, what the role allows | AWS SAM / CloudFormation (template.yaml), Serverless Framework (serverless.yml) |
None of them needs a flag. A library plugin turns on when the code imports one of its modules, and a deploy plugin
when its file is in the folder. code-node --plugins lists what's installed.
What they find together¶
Each plugin adds to the same map, so the checks see all of it. These come from
examples/cart-service, a Lambda
function declared in template.yaml that uses PostgreSQL, Redis, OpenAI and boto3. Each finding is marked with the
plugins that found it together:
$ code-node examples/cart-service --list
1 entry point in examples/cart-service:
1. cart.app.on_checkout CheckoutFunction in template.yaml (cart/app.py:20)
$ code-node examples/cart-service --no-open
cart.app.on_checkout: 5 functions, 2 AWS calls, 4 through libraries (PostgreSQL, OpenAI, Redis)
Deployed as CheckoutFunction in template.yaml
| Plugins | What they find |
|---|---|
| SAM + Lambda | on_checkout() is an entry point, though it isn't called handler() |
| SAM + Lambda | RECEIPTS_TABLE and TOPIC_ARN aren't set on CheckoutFunction, and the code reads them with os.environ[...] |
| SAM + Lambda + boto3 | Its role has only DynamoDBReadPolicy, so DynamoDB PutItem and SNS Publish fail with AccessDenied: add DynamoDBWritePolicy and SNSPublishMessagePolicy |
| SQL + the event | The DELETE in clear_cart() puts event body (JSON).customer into the SQL text: SQL injection |
| SQL + a loop | The SELECT in price() runs once per item in the cart |
| OpenAI + SAM + Lambda | The OpenAI client has no timeout, so it waits up to 10 minutes a try, and Lambda stops the run at its 15 s timeout |
| SQL + Lambda | It opens a new PostgreSQL connection on every call, instead of once for warm calls to reuse |

The graph marks each one where it is: PutItem and Publish say "not allowed", the variables say "not set", and the
DELETE says "SQL injection". In Step by step, a name set to text built from the input says so (query holds "text
built from event body (JSON).user"), and each step it reaches is flagged.
Worth knowing
A role code-node can't read (one made elsewhere, a managed policy it doesn't know, a SAM connector) leaves permissions unchecked rather than guessed: it never says a call is denied unless the file says so.
Web apps¶
A FastAPI, Flask or Django app is an entry point of the web platform, and each route is called with the request.
What the code takes from it is followed like a Lambda handler's event: into SQL, shell commands and the calls it
reaches, and listed in the graph's first box, "The request".
examples/books-api is a FastAPI app with
one of each finding below.
| Framework | Routes from | The request is |
|---|---|---|
| FastAPI | @app.get("/orders/{order_id}"), on an app or an APIRouter |
each parameter: path.order_id, query.q, the body (a model), or where Query(), Header(), Cookie(), Form(), File() say (Annotated[...] too). Depends() isn't from the request. A Request is all of it |
| Flask | @app.route("/users/<int:user_id>"), on an app or a Blueprint |
each URL value (path.user_id), and flask.request: args.q, form.x, json.x (get_json()) |
| Django | urlpatterns in a urls.py: path(), re_path(), include(), View.as_view(), Django REST framework's router.register() |
the view's first parameter (GET.status, POST.x, body) and each URL value (path.pk) |
$ code-node examples/books-api --no-open
books.main.app: 9 functions, 1 AWS calls, 6 through libraries (Redis, PostgreSQL)
| It finds | Because |
|---|---|
| SQL injection | an f-string, + or % puts the request into the statement: search() builds its SELECT from request query.q |
| Shell injection | export() runs pandoc with shell=True and request query.fmt in the command |
| A blocked event loop | requests.get(), time.sleep() or a boto3 call in an async def route holds up every other request on that worker |
| A connection on every request | a database connection opened in the route rather than once, as a pool |
Worth knowing
The site's urls.py is offered before an app's it includes, and its routes come out under the path they're
included at (/api/orders/). Start from an app's urls.py to see only its own.
Write a library plugin¶
A library plugin says what the library's functions give the code (a Handle the reader then tracks through names,
arguments and attributes) and what each call on one does (a Use):
from code_node.libraries import Handle, Library, Use, register_library
class Kafka(Library):
name = "kafka"
title = "Kafka"
modules = ("kafka",) # turns on when the code imports kafka
icon = "queue"
def make(self, name, call): # kafka.KafkaProducer(...)
if name == "kafka.KafkaProducer":
return Handle("producer", "Kafka", opens=True)
def call(self, handle, method, call): # producer.send("orders", ...)
if method == "send":
return Use("send", "sends", call.shown(0, "topic"))
register_library(Kafka())
Every hook
| Hook | What it says |
|---|---|
make(name, call) |
What a function or class of its modules gives (Handle) or does (Use) |
attribute(handle, name), item(handle, key) |
Part of a handle: client.shop, client["shop"] |
call(handle, method, call) |
What a method does, or the handle it gives (a cursor) |
describe(handle) |
What a name holding it holds: "a Redis client" |
findings(cmap, calls) |
Its own findings |
A Use can set flag and why (a warning on the call, its box and its step), batch (what to use instead
when it runs in a loop) and gives (the handle it returns). A Handle can set opens (better made once) and
waits and fix (it has no timeout). call tells it the arguments: value() for a literal, shown() for
how a box lists it, text() and built() for SQL and other text, and inputs() for what from the entry
point's input reached that argument, through any names on the way.
Write a deploy plugin¶
A deploy plugin reads its files and returns a Deployment for each function: the Python file and function it starts
from, and what the file says about it. Its platform's checks run against that.
from code_node.analysis.model import Deployment
from code_node.deploys import Deploy, register_deploy
class Zappa(Deploy):
name = "zappa"
title = "Zappa"
files = ("zappa_settings.json",)
def deployments(self, files, code):
... # [Deployment("api", "zappa_settings.json", "zappa", "app.py", "handler", runtime="python3.12")]
register_deploy(Zappa())
Platforms are on the Python API page.
Worth knowing
A package installed next to code-node adds its plugins to the command by naming them in its pyproject.toml:
[project.entry-points."code_node.libraries"]
kafka = "my_package.kafka:Kafka"
[project.entry-points."code_node.deploys"]
zappa = "my_package.zappa:Zappa"