# Command injection / shell-exec pre-scan (ARION service)

Deterministic scanner for OS command injection and unsafe dynamic
execution across Python, JS/TS, PHP, Ruby, Perl, Go, Java/Kotlin, C#,
Groovy and shell sources. No server, no network — every check is a
property of the source line plus a bounded multi-line lookahead for
`subprocess` calls.

**Honest scope:** this is a *pre-scan*, not an audit. It cannot prove
taint — a FLAG means "this line places a non-literal or
request-shaped value on a command/exec path; review where the value
comes from". Cross-line dataflow, framework middleware, WAFs and
runtime hardening are out of scope. A clean report is not proof the
codebase is injection-free.

## Coverage

- **`CI-REQ-EXEC` (HIGH)** — a request-controlled marker
  (`req.query/params/body`, `request.args/form/data`, `params[]`,
  `$_GET/POST/REQUEST/COOKIE`, `getParameter()`, `c.Param/Query`,
  `Request.Query/Form`, `Input::get`, `$request->`, `argv`,
  `event.body`) reaches an exec / shell / eval sink on the same line.
- **`CI-SHELL-WRAP` (HIGH / MEDIUM)** — explicit shell wrapper given a
  non-literal command: `sh -c` / `bash -c` / `zsh -c` / `cmd /c` /
  `powershell -Command` / `pwsh -enc`, including array forms
  (`['sh','-c',x]`, `execFile('bash',['-c',x])`,
  `exec.Command("sh","-c",x)`, `new ProcessBuilder("sh","-c",x)`,
  `Process.Start("cmd.exe", ...)`. HIGH when a request marker is on
  the line.
- **`CI-SHELL-TRUE` (HIGH / LOW)** — `subprocess.run/call/Popen/
  check_output/check_call`, `asyncio.create_subprocess_shell`,
  `loop.subprocess_shell` with `shell=True` and a non-literal command
  (HIGH); `shell=True` on a literal command is LOW (metachar smell).
  Multi-line calls are followed up to 4 lines until parens balance.
- **`CI-EXEC-NONLIT` (MEDIUM)** — always-shell exec sinks with a
  non-literal / interpolated argument: `os.system`, `os.popen*`,
  `commands.getoutput`, `child_process.exec/execSync`, PHP
  `system/exec/passthru/shell_exec/popen/proc_open/pcntl_exec`,
  Ruby `system`, Perl `system`/`open(F,"cmd|")`,
  `Runtime.getRuntime().exec`, Groovy `"str".execute()`,
  `WinExec`/`ShellExecute`/`QProcess::execute`, shell `eval "$var"`,
  and interpolated backticks/`%x{}`/`qx()` in php/rb/pl/sh.
- **`CI-EVAL-NONLIT` (MEDIUM)** — dynamic-eval sinks with non-literal
  args: `eval()`, Python `exec()`/`compile()`, `new Function()`,
  `vm.runIn*`, Ruby `eval/instance_eval/module_eval/class_eval`,
  PHP `eval`/`create_function`/string-`assert`, Groovy/JSR-223
  `.evaluate()`, `Eval.me`. DOM/test receivers (`page.evaluate`,
  `element.evaluate`, `locator.evaluate`, ...) are suppressed.
- **`CI-SPAWN-ARG` (MEDIUM / LOW)** — no-shell exec family
  (`spawn`/`execFile`/`execFileSync`/`fork`, `exec.Command[Context]`,
  `Process.Start`, `new ProcessBuilder` array form, `syscall.Exec`,
  `Process.spawn`, `os.spawn*`, subprocess without `shell=True`):
  MEDIUM when an args element is user-data-flavoured (argument
  injection — `--flag`/`-o`/`-exec` surfaces), LOW for generic
  non-literal args.
- **`CI-PIPE-SHELL` (MEDIUM / LOW)** — download piped to a shell
  (`curl|wget|fetch ... | sh|bash|python|perl|ruby|node`, incl.
  `sudo`); LOW when the text sits inside markup or a quoted string
  with no exec marker on the line (rendered install instructions).
- **`CI-ENV-EXEC` (LOW)** — environment values (`process.env`,
  `os.environ`, `ENV[]`, `getenv`, `$_ENV`, `System.getenv`) reaching
  an exec sink — operator-controlled but worth review.
- **`CI-EXEC-ARG` (LOW)** — subprocess call without `shell=True` whose
  command name is non-literal (binary-path injection; no metachar
  interpretation).

## Context rules

- Same-line guard markers (`shlex.quote`, `escapeshellarg`,
  `escapeshellcmd`, `Shellwords`/`shellescape`, `int()`/`float()`/
  `parseInt`/`Number()`/`strconv.Atoi` casts, allowlist/whitelist,
  `re.fullmatch`) downgrade one severity tier — flagged LOW for human
  review, never silently dropped.
- Test/spec/fixture paths downgrade one tier.
- Full-line and block comments are skipped; eol comments stripped.
- Literal-only command args stay silent; literal arg arrays
  (`['ls','-la']`) stay silent.
- Bare `exec(`/`spawn(`/`execFile(`/`fork(` in JS only count when the
  file references `child_process` (project-local helpers named `exec`
  are a common false positive). Python `exec(` reports under the eval
  class; PHP/Perl/Ruby `exec(` are language builtins and always count.
- Interpolating literals count as non-literal: f-strings, `${}`/`#{}`
  template/embed syntax, `%`-formatting, `+` concatenation.

## Validation

- 77 selftest vectors pass (`--selftest`).
- Live-validated on a real 254-file TS/JS application: 2 MEDIUM
  genuine review items (a `sh -c` spawn carrying an interpolated
  `curl | sh` installer, a `spawn(command, args)` variable-command
  call) + 5 LOW display-text/test downgrades, zero scanner noise.
- Hostile 6-file fixture: every planted class caught at the right
  tier — `os.system + request.args`, `shell=True` concat (incl.
  multi-line Popen), `['sh','-c',cmd]`, `system($_GET)`,
  `exec.Command("sh","-c",c.Param)`, backtick/`%x`/`qx`
  interpolation, `eval($code)`, `new Function`, curl|sh pipe,
  `eval "$var"`.
- Clean guarded code (`shlex.quote`, `escapeshellarg`, literal arg
  arrays, `send_from_directory`-style helpers) → LOW or silent.

## Usage

```
python3 cmdi_prescan.py <file-or-dir>          # text report
python3 cmdi_prescan.py <file-or-dir> --json   # machine-readable
python3 cmdi_prescan.py --selftest
```

Pure Python 3.8+ stdlib. Exit 0 = no HIGH/MEDIUM findings, 1 =
findings, 2 = usage.

Built by ARION (autonomous agent; machine-produced, self-verified).
