Code rules: what shield360 scan checks in your own code
For a directory, shield360 scan checks the app’s own code as well as its packages. It reads JavaScript, TypeScript (including JSX and TSX) and Python on your machine and reports each problem as its rule, file and lines. No code is sent. Findings appear in Protect → Application security → Code security with the kind Code, ranked and triaged like the scan’s other findings.
Dependency folders (node_modules, vendor, virtual environments), build output, hidden folders, minified files, type declaration files and files over 1 MB are not checked. A file with a syntax error is still checked around the error.
shield360 scan --dry-run # print what was foundshield360 scan --rules ./security-rules # add your own rulesshield360 scan --no-code # packages, configuration and secrets onlyshield360 scan --no-cost # leave out the cost rulesCost rules
Section titled “Cost rules”Rules whose id starts with cost. or py.cost. find code that costs money to run rather than a security flaw. Their findings appear in Costs → Cost of code, not in Protect → Application security → Code security. They are left out of the scan’s security counts and alerts, and never fail --fail-on.
| Rule | Language | Usual impact | Finds |
|---|---|---|---|
cost.db-query-in-loop, py.cost.db-query-in-loop | JS/TS, Python | Higher | An awaited database query (Prisma, SQL clients, cursor.execute, Django objects.get and filter, SQLAlchemy query) inside a loop, forEach or map: an N+1 |
cost.remote-call-in-loop, py.cost.remote-call-in-loop | JS/TS, Python | Higher | fetch, axios, an AWS SDK send, requests, httpx or a cloud client call inside a loop. Each one is a billed request; most APIs have a batch call |
cost.llm-call-in-loop, py.cost.llm-call-in-loop | JS/TS, Python | Higher | A model call (chat completions, responses, messages, embeddings) inside a loop. Batch APIs usually cost half |
cost.select-star, py.cost.select-star | JS/TS, Python | Lower | SELECT * FROM in a query: every column is read and sent, and column stores bill by column read |
cost.offset-pagination | JS/TS | Lower | OFFSET with a variable: every skipped row is read again on each page. Keyset pagination (WHERE id > $1) is not |
cost.unbounded-find-many | JS/TS | Lower | Prisma findMany with neither where nor take, which reads the whole table as it grows |
cost.tight-polling, py.cost.tight-polling | JS/TS, Python | Lower | setInterval under 10 seconds, or time.sleep under 10 seconds in a while loop |
cost.regex-built-in-loop | JS/TS | Lower | new RegExp(...) compiled on every pass of a loop |
cost.linear-search-in-loop | JS/TS | Lower | find or findIndex with a callback on an array inside another loop or map: quadratic. A Map lookup is not, and a search over an upper-case constant list is left out |
cost.log-whole-body | JS/TS | Lower | A whole request or response body (or its JSON) written to the logs, which are billed by the gigabyte |
A pattern only costs money when it runs often. Costs → Cost of code ranks each finding by what its code cost at runtime, from your traces. The usual impact is a guide for findings not yet seen running.
Built-in rules
Section titled “Built-in rules”| Rule | Language | Severity | Finds |
|---|---|---|---|
ai.llm-output-eval, py.ai.llm-output-eval | JS/TS, Python | Critical | A model’s reply reaching eval, new Function, vm or exec |
ai.llm-output-shell, py.ai.llm-output-shell | JS/TS, Python | Critical | A model’s reply reaching a shell command |
ai.llm-output-sql, py.ai.llm-output-sql | JS/TS, Python | High | A model’s reply reaching a SQL query |
ai.llm-output-html | JS/TS | High | A model’s reply written as HTML (innerHTML, insertAdjacentHTML, document.write) |
cmd.request-to-shell, py.cmd.request-to-shell | JS/TS, Python | Critical | Request input reaching a shell command |
code.request-to-eval, py.code.request-to-eval | JS/TS, Python | Critical | Request input reaching eval or exec |
sql.request-to-query, py.sql.request-to-query | JS/TS, Python | High | Request input reaching a SQL query, not as a parameter |
path.request-to-file, py.path.request-to-file | JS/TS, Python | High | Request input choosing a file path, unless it passes basename or secure_filename |
ssrf.request-to-fetch, py.ssrf.request-to-fetch | JS/TS, Python | High | Request input choosing the host of an outbound request (fetch, axios, http, requests, httpx, urllib). A fixed host or a relative path is fine |
xss.request-to-html | JS/TS | High | Request input written as HTML |
py.ai.langchain-dangerous | Python | Critical | allow_dangerous_code=True or allow_dangerous_requests=True |
ai.system-prompt-request-input, py.ai.system-prompt-request-input | JS/TS, Python | High | Request body, query, parameters or cookies reaching a system prompt |
ai.provider-key-hardcoded, py.ai.provider-key-hardcoded | JS/TS, Python | High | An OpenAI, Anthropic, Azure OpenAI, Groq, Mistral, Cohere, Together or Gemini key written in the code |
py.ai.torch-load-unsafe | Python | High | torch.load without weights_only=True |
py.ai.trust-remote-code | Python | High | Model loading with trust_remote_code=True |
cmd.exec-dynamic, py.cmd.os-system-dynamic | JS/TS, Python | High | A shell command built from a variable |
cmd.spawn-shell-true, py.cmd.subprocess-shell-true | JS/TS, Python | High | A process started through a shell |
sql.string-built-query, py.sql.string-built-query | JS/TS, Python | High | SQL built by interpolation or concatenation instead of parameters |
py.deserialize.yaml-load | Python | High | yaml.load with a loader that can build any object |
tls.verification-disabled, py.tls.verification-disabled | JS/TS, Python | High | TLS certificate checks turned off |
jwt.none-algorithm | JS/TS | High | jwt.verify that accepts unsigned tokens |
xss.inner-html | JS/TS | Medium | innerHTML or outerHTML set from a variable |
py.deserialize.pickle | Python | Medium | Pickle, dill or joblib data loaded |
py.flask.debug-on | Python | Medium | An app run with debug=True |
crypto.weak-hash, py.crypto.weak-hash | JS/TS, Python | Low | MD4, MD5 or SHA-1. Fine for IDs and caches, not for passwords or signatures |
Following data
Section titled “Following data”The rules above that say “reaching” follow data within each function, not just one line. Request input (req.body, req.query, req.params, request.json(), searchParams.get(), Flask’s request.args, request.form, and so on) and a model’s reply (the result of chat.completions.create, messages.create, responses.create, generateText, generate_content) are untrusted. They stay untrusted through variables, fields, destructuring, string building, loops and nested functions, until a sanitizer cleans them or a clean value replaces them. A value that is untrusted on one branch of an if stays untrusted after it. Each finding names the line where the untrusted data came from.
Data passed into another function is not followed inside it. A model’s reply to a prompt that held request input is reported once, as model output.
When one of these rules and a broader pattern rule (cmd.exec-dynamic, sql.string-built-query, xss.inner-html and their Python versions) find the same line, only the more specific finding is kept.
Names are matched after imports, so a rule for child_process.exec also finds cp.exec(...) after import * as cp from "node:child_process", and exec(...) after const { exec } = require("child_process"). Optional chaining, parentheses, comments and formatting make no difference.
Write your own rules
Section titled “Write your own rules”A rule file is YAML with a rules list. Each rule’s match is a pattern written as code in the rule’s language:
rules: - id: acme.no-raw-payments title: Payment call outside the payments client severity: high cwe: [CWE-284] languages: [typescript] match: all: - pattern: stripe.charges.create(...) - not: inside: pattern: | class PaymentsClient { ... }| Field | Required | What it is |
|---|---|---|
id | Yes | 3–80 lower-case letters, digits, dots, dashes or underscores, unique in the file |
title | Yes | What the finding means, up to 200 characters |
severity | Yes | critical, high, medium or low |
cwe | No | CWE identifiers |
languages | Yes | javascript or typescript (both mean JavaScript, TypeScript, JSX and TSX), or python |
match | Yes | The formula below |
Patterns
Section titled “Patterns”| In a pattern | Matches |
|---|---|
| Code | The same code, ignoring formatting, comments and parentheses |
$NAME | Any one expression, name or statement. Used twice, both must be the same code. $_ matches without remembering |
... | Any number of arguments, items, object properties or statements, including none |
"..." | Any plain string literal (not an f-string or a template with substitutions) |
Several lines are a run of statements, found anywhere in a block. Quote a pattern in YAML when it contains : , for example pattern: '{..., rejectUnauthorized: false, ...}'.
Formulas
Section titled “Formulas”| Key | What it does |
|---|---|
pattern | Places that match the pattern |
any | Places that match any of the formulas listed |
all | Places every positive formula listed matches, kept only when inside every inside, minus every not |
not | Inside all: drop places that match. With not: {inside: ...}, drop places within a match |
inside | Inside all: keep places within a match |
where | A list of conditions on what placeholders matched, on any formula |
Each where condition names a placeholder and checks the code it matched:
| Check | Passes when |
|---|---|
regex | The code’s text matches the regular expression |
not-regex | The code’s text does not match |
kind | The code is one of these syntax node kinds, such as string, template_string, identifier or binary_expression |
pattern | The pattern matches somewhere inside the code |
match: pattern: $DB.$Q($SQL, ...) where: - placeholder: $Q regex: ^(query|execute)$ - placeholder: $SQL kind: [template_string]Taint rules
Section titled “Taint rules”A rule can use taint instead of match to follow data from sources to sinks within a function:
rules: - id: acme.ticket-to-shell title: Support ticket text reaches a shell command severity: critical languages: [typescript] taint: sources: - pattern: tickets.get(...) sanitizers: - pattern: shellEscape(...) sinks: - pattern: child_process.exec($CMD, ...) focus: $CMD| Key | What it is |
|---|---|
sources | Formulas for where untrusted data comes from |
sanitizers | Optional. Formulas for code that makes data safe; nothing inside a match is untrusted |
sinks | Formulas for where untrusted data must not reach, each with an optional focus: the placeholder whose code is checked. Without it, every placeholder the sink matched is checked |
A rule can also list rule ids in supersedes: on a line where this rule finds something, findings of those rules are dropped.
A file can share lists between rules with YAML anchors under a top-level definitions key.
A rule file with a mistake is refused with the rule’s id and the problem, before anything is scanned.
Next steps
Section titled “Next steps”- Code security: vulnerabilities, configuration and secrets from the same scan
- shield360 scan: flags and CI setup