# GraphQL API security pre-scan (ARION service)

Deterministic scanner for unsafe GraphQL surface in application
source — JS/TS (Apollo, Yoga, Mercurius, express-graphql,
graphql-http, type-graphql, NestJS), Python (graphene, ariadne,
strawberry), Elixir (Absinthe) and schema files (.graphql/.gql).
Nine finding classes decided by line-level taint (name assignment
only) plus file-level server/resolver markers — no server, no
network, no inference.

A file only opens when it actually wires GraphQL — SDK imports,
server constructors (`ApolloServer`, `graphqlHTTP`, `createYoga`,
`graphqlHTTP(`), schema/resolver vocabulary (`typeDefs`,
`resolvers`, `type Query {`, `class ... Mutation`), `gql\``
tagged templates, graphene/ariadne/strawberry usage. Prose mentions
and hostnames never gate a file open.

## Finding classes

1. **`GQL-INPUT-EXEC` (HIGH)** — resolver input (`args`/`variables`/
   `input`, destructured params, `resolve_*` signature args) reaches
   `eval`/`exec`/`os.system`/`subprocess`/`execSync`/`Runtime.exec`/
   `ProcessBuilder`/`new Function`/`vm.runIn*`/`pickle.loads`/
   `System.cmd`/`Port.open`. A field argument becomes executable code.
2. **`GQL-INPUT-SQL` (HIGH)** — resolver input into `.execute`/
   `.executemany`/`.raw`/`.query`/`$queryRaw`/`knex.raw`/
   `sequelize.query`. A field argument becomes a query fragment.
3. **`GQL-INPUT-FETCH` (MEDIUM)** — resolver input into `requests.*`,
   `fetch`, `axios`, `urlopen`, `http.Get`, `curl_init`, `Net::HTTP`,
   `Faraday`, `HTTPoison` — the query chooses the outbound target:
   SSRF channel through the API.
4. **`GQL-INTROSPECTION-ON` (MEDIUM)** — introspection explicitly
   enabled (`introspection: true`, `disable_introspection: false`).
   Only the explicit-ON shape flags; an absent flag is framework
   default and silent.
5. **`GQL-DEBUG-CONSOLE` (MEDIUM)** — a dev console mounted on the
   serving path: `graphiql: true`, `playground: true`, `GraphiQL(`,
   voyager/altair, `debug: true` in server config.
6. **`GQL-BATCHING-ON` (MEDIUM)** — array request batching allowed
   (`allowBatchedHttpRequests: true`, `batching: true`,
   `graphql-batch`). One HTTP request carries N operations — rate
   limits and lockouts divide by N.
7. **`GQL-MUTATION-NO-AUTH` (MEDIUM)** — file-level: Mutation
   resolvers are defined and the file carries zero authorisation
   vocabulary (auth/permission/shield/guard/policy/role/
   `context.user`/`ctx.state.user`/`deny`). Every mutation executes
   for anonymous callers — verify guards upstream.
8. **`GQL-ERROR-STACK` (LOW)** — stacktrace/exception detail
   returned through the error channel (`formatError` passthrough,
   `extensions.stacktrace`, `exposeServerErrors`).
9. **`GQL-NO-QUERY-LIMIT` (LOW)** — file-level: the file wires a
   GraphQL endpoint and carries zero depth/complexity vocabulary
   (`depthLimit`/`maxDepth`/`queryComplexity`/`complexityLimit`/
   `costLimit`/`persistedQuery`/`validationRules`). Nested and
   widened queries burn the resolver graph unchecked.

## Method and honest limits

Line-wise taint: a name assigned from `args.`/`variables`/`input`,
a destructured resolver parameter, or a `resolve_*`/`mutate`
signature parameter is tainted; sinks on the same line or later
lines referencing the name flag. `context`/`ctx` are NOT taint
sources (server-set auth state); `parent`/`root`/`info` bare
receivers are deliberately not tainted (DOM nodes, `logging.info`,
lxml `root`).

Import lines never flag. Comments are stripped before scanning.
Test/spec/fixture paths downgrade one severity tier. Per-code caps
keep repeat signatures readable.

This is a pre-scan for human review. It cannot see framework
defaults, gateway policy (Apollo Router/GraphOS limits), WAF rules,
or cross-file resolver wiring. Clean output is not a safe API —
it is "no signature of these nine classes."

Validated: 30/30 selftest vectors, zero findings on a 253-file
application repo and the 542-file CPython stdlib (no GraphQL
surface), all nine classes on a hostile fixture, silent on a
hardened Apollo server fixture.

## Usage

    python3 graphql_prescan.py <file-or-dir>           # markdown
    python3 graphql_prescan.py <file-or-dir> --json    # machine
    python3 graphql_prescan.py --selftest

Stdlib only. Exit 0 = no HIGH/MEDIUM findings, 1 = findings,
2 = usage.

See sample-report.md for a run over the included hostile fixture.
