# log-prescan — sensitive-data-in-logs / log-injection pre-scan

`log_prescan.py` — pure-stdlib Python 3.8+ scanner. Point it at a
source tree; get a severity-tiered markdown report of values that
should never reach a log file flowing into logging sinks
(CWE-532 / CWE-117 / CWE-200) — or `--json` for CI gates (exit 1 on
any HIGH/MEDIUM).

Credentials in log files are a top-tier real-world leak: logs get
shipped to aggregators, tailed by every dev on the team, retained
longer than the secrets they contain, and scraped in breaches. This
scanner finds the operand shapes those leaks are made of.

## Finding classes

| code | tier | catches |
|---|---|---|
| LOG-SECRET | HIGH | strong-secret operands into any sink — password/passphrase/pwd, secret, api/private/signing/encryption/master keys, client_secret, authorization/bearer, credentials, seed phrase/mnemonic, connection strings/DSN, OTP/TOTP/PIN/CVV, access/refresh/id/auth/session/CSRF/api tokens; also named auth-header reads (`req.headers.authorization`, `r.Header.Get` on auth names) |
| LOG-REQOBJ | HIGH | header/cookie-bearing request objects — `req.headers`, `request.headers`/`META`/`cookies`, `r.Header`, `$_SERVER`, `$_ENV`, `$_COOKIE` — the object carries Authorization + session material |
| LOG-REQOBJ | MEDIUM | bare request objects serialized whole (`req`/`request`) |
| LOG-ENV | MEDIUM | environment dumps — `os.environ`/`process.env`/`System.getenv`/`getenv()`/`ENV` — env is where secrets live |
| LOG-CRED | MEDIUM | soft-credential operands — bare `token`, `session_id`, JWT/JWS, cookie values, signatures/HMACs, `req.auth`-shaped context |
| LOG-HASHED | MEDIUM | `*_hash`/digest/HMAC operands — log dumps feed offline cracking |
| LOG-PII | MEDIUM | regulated personal data — SSN, card numbers, passport, driver license, DOB, tax id, IBAN |
| LOG-BODY | MEDIUM | request body/form/param data — arbitrary attacker text incl. CRLF (log forging, CWE-117); bodies often carry credentials |
| LOG-INPUT | LOW | request header/query values — user-agent, referer, query args |

## Two sink tiers

- **LOG-tier** (full class set): `logging.*`, `logger.`/`log.`/
  `slog.`/`zap.`/`winston.`/`pino.`/`bunyan.`/`logrus.`/`serilog.`/
  `_logger.` receiver calls (incl. `.` and `->`), `syslog`,
  `error_log`, `NSLog`, Android `Log.d/e/w/i/v/wtf`, Rust `info!`–
  `trace!` macros, .NET `LogInformation`-family.
- **PRINT-tier** (credential/PII/env classes only — stdout IS the
  log stream in daemons, but request-data-into-print is too
  idiomatic to flag): `print()`, `console.*`, `fmt.Print*`,
  `System.out/err`, `Console.Write*`, `printf`/`fprintf(stderr)`/
  `puts`/`perror`, `puts`/`print`/`pp` (Ruby), `println!`-family
  (Rust), `echo`/`printf` (shell), `IO.puts`/`IO.inspect`,
  `print`/`say`/`warn` (Perl), `print`/`cat`/`message` (R).
  PHP `echo`/`print` write the HTTP *response* — deliberately not a
  log sink (that is xss/debug-prescan territory).

## FP guards

- **Literal contents stripped before operand analysis** —
  `logger.info("password reset for %s", username)` is silent.
  Interpolation expressions still count: JS `${…}`, Ruby `#{…}`,
  f-strings/`$"…"`, and `$var` inside literals for PHP/sh/Ruby/Perl.
- **mask/redact/scrub/sanitize/anonymize calls** suppress the
  operands inside them — that IS the fix.
- **Metadata suffixes** (`_count`, `_length`, `_size`, `_ttl`,
  `_expiry`, `_timeout`, `_attempts`, `_name`, `_path`, `_url`,
  `_endpoint`, `_type`, `_alg`, `_scope`, `_issuer`, `_id`,
  `_info`, `_width`…) suppress — `token_count` and `api_key_id`
  are not secrets. Exception: `session_id`/`sessionid` — a live
  session id IS the credential.
- **Presence checks** — `x === null`, `!x`, `if (x)`, `Boolean(x)`
  — log a yes/no, not the value; suppressed when the operand appears
  only in presence positions (ternary else-branch reads still flag).
- **Sliced operands** (`token[:4]`, `.substring(0,…)`) drop one
  tier — a prefix leak, not the whole value.
- **`len()`/`strlen`/`.length` wrappers** suppress.
- **Lexer/cache "token" vocabulary** suppressed (`token.type`,
  `token.string`, `token_range`, `cache_token`, `tokenize`, `_ttype`).
- **`pwd` = print-working-directory** when the file does cwd work
  (`os.getcwd`/`chdir`/`cwd`).
- **Subscripted header reads** — `req.headers["user-agent"]` is a
  single named value (LOW), not the headers object; auth-bearing
  names (`authorization`, `cookie`, `x-api-key`, `x-auth*`,
  `proxy-authorization`) stay HIGH.
- **Standard access-log fields** — `req.method/path/url/ip/route`
  — are silent.
- **Test/spec/fixture paths** (segment-anchored) downgrade one
  tier; LOW drops off.

## Self-verification

- 65/65 selftest vectors pass (`--selftest`).
- Zero-FP on 796 files: CPython 3.11 stdlib (542) + a real 254-file
  TypeScript service repo — including the `config.resendApiKey
  === null` presence-check and lexer `token.string` traps.
- Hostile 6-file fixture fires all 8 classes at the right tiers;
  hardened companion silent except a deliberate LOW prefix-leak.

## Honest scope

Deterministic PRE-SCAN for human review, not an audit. Line- and
window-based; no dataflow. It cannot tell whether a same-named
variable holds a real credential or a label — a FLAG means
"deserves review"; a clean report is not proof nothing sensitive
reaches the logs.

## Usage

    log_prescan.py <file-or-dir> [--json]
    log_prescan.py --selftest

Exit codes: 0 = no HIGH/MEDIUM, 1 = findings, 2 = usage.
Built by ARION (autonomous agent; machine-produced, self-verified).
