Skip to content

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.

Terminal window
shield360 scan --dry-run # print what was found
shield360 scan --rules ./security-rules # add your own rules
shield360 scan --no-code # packages, configuration and secrets only
shield360 scan --no-cost # leave out the 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.

RuleLanguageUsual impactFinds
cost.db-query-in-loop, py.cost.db-query-in-loopJS/TS, PythonHigherAn 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-loopJS/TS, PythonHigherfetch, 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-loopJS/TS, PythonHigherA model call (chat completions, responses, messages, embeddings) inside a loop. Batch APIs usually cost half
cost.select-star, py.cost.select-starJS/TS, PythonLowerSELECT * FROM in a query: every column is read and sent, and column stores bill by column read
cost.offset-paginationJS/TSLowerOFFSET with a variable: every skipped row is read again on each page. Keyset pagination (WHERE id > $1) is not
cost.unbounded-find-manyJS/TSLowerPrisma findMany with neither where nor take, which reads the whole table as it grows
cost.tight-polling, py.cost.tight-pollingJS/TS, PythonLowersetInterval under 10 seconds, or time.sleep under 10 seconds in a while loop
cost.regex-built-in-loopJS/TSLowernew RegExp(...) compiled on every pass of a loop
cost.linear-search-in-loopJS/TSLowerfind 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-bodyJS/TSLowerA 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.

RuleLanguageSeverityFinds
ai.llm-output-eval, py.ai.llm-output-evalJS/TS, PythonCriticalA model’s reply reaching eval, new Function, vm or exec
ai.llm-output-shell, py.ai.llm-output-shellJS/TS, PythonCriticalA model’s reply reaching a shell command
ai.llm-output-sql, py.ai.llm-output-sqlJS/TS, PythonHighA model’s reply reaching a SQL query
ai.llm-output-htmlJS/TSHighA model’s reply written as HTML (innerHTML, insertAdjacentHTML, document.write)
cmd.request-to-shell, py.cmd.request-to-shellJS/TS, PythonCriticalRequest input reaching a shell command
code.request-to-eval, py.code.request-to-evalJS/TS, PythonCriticalRequest input reaching eval or exec
sql.request-to-query, py.sql.request-to-queryJS/TS, PythonHighRequest input reaching a SQL query, not as a parameter
path.request-to-file, py.path.request-to-fileJS/TS, PythonHighRequest input choosing a file path, unless it passes basename or secure_filename
ssrf.request-to-fetch, py.ssrf.request-to-fetchJS/TS, PythonHighRequest 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-htmlJS/TSHighRequest input written as HTML
py.ai.langchain-dangerousPythonCriticalallow_dangerous_code=True or allow_dangerous_requests=True
ai.system-prompt-request-input, py.ai.system-prompt-request-inputJS/TS, PythonHighRequest body, query, parameters or cookies reaching a system prompt
ai.provider-key-hardcoded, py.ai.provider-key-hardcodedJS/TS, PythonHighAn OpenAI, Anthropic, Azure OpenAI, Groq, Mistral, Cohere, Together or Gemini key written in the code
py.ai.torch-load-unsafePythonHightorch.load without weights_only=True
py.ai.trust-remote-codePythonHighModel loading with trust_remote_code=True
cmd.exec-dynamic, py.cmd.os-system-dynamicJS/TS, PythonHighA shell command built from a variable
cmd.spawn-shell-true, py.cmd.subprocess-shell-trueJS/TS, PythonHighA process started through a shell
sql.string-built-query, py.sql.string-built-queryJS/TS, PythonHighSQL built by interpolation or concatenation instead of parameters
py.deserialize.yaml-loadPythonHighyaml.load with a loader that can build any object
tls.verification-disabled, py.tls.verification-disabledJS/TS, PythonHighTLS certificate checks turned off
jwt.none-algorithmJS/TSHighjwt.verify that accepts unsigned tokens
xss.inner-htmlJS/TSMediuminnerHTML or outerHTML set from a variable
py.deserialize.picklePythonMediumPickle, dill or joblib data loaded
py.flask.debug-onPythonMediumAn app run with debug=True
crypto.weak-hash, py.crypto.weak-hashJS/TS, PythonLowMD4, MD5 or SHA-1. Fine for IDs and caches, not for passwords or signatures

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.

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 {
...
}
FieldRequiredWhat it is
idYes3–80 lower-case letters, digits, dots, dashes or underscores, unique in the file
titleYesWhat the finding means, up to 200 characters
severityYescritical, high, medium or low
cweNoCWE identifiers
languagesYesjavascript or typescript (both mean JavaScript, TypeScript, JSX and TSX), or python
matchYesThe formula below
In a patternMatches
CodeThe same code, ignoring formatting, comments and parentheses
$NAMEAny 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, ...}'.

KeyWhat it does
patternPlaces that match the pattern
anyPlaces that match any of the formulas listed
allPlaces every positive formula listed matches, kept only when inside every inside, minus every not
notInside all: drop places that match. With not: {inside: ...}, drop places within a match
insideInside all: keep places within a match
whereA list of conditions on what placeholders matched, on any formula

Each where condition names a placeholder and checks the code it matched:

CheckPasses when
regexThe code’s text matches the regular expression
not-regexThe code’s text does not match
kindThe code is one of these syntax node kinds, such as string, template_string, identifier or binary_expression
patternThe pattern matches somewhere inside the code
match:
pattern: $DB.$Q($SQL, ...)
where:
- placeholder: $Q
regex: ^(query|execute)$
- placeholder: $SQL
kind: [template_string]

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
KeyWhat it is
sourcesFormulas for where untrusted data comes from
sanitizersOptional. Formulas for code that makes data safe; nothing inside a match is untrusted
sinksFormulas 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.