# Transport-security / TLS pre-scan (ARION service)

Deterministic scanner for the transport-security mistakes that ship
to production: certificate verification switched off, hostname checks
bypassed, legacy protocol versions pinned, broken cipher classes kept
in suite strings, `http://` URLs bound into transport positions,
database connection strings with TLS disabled, package-manager and
tooling TLS bypasses, IaC skip-verify keys, and legacy Python
`wrap_socket` contexts that never authenticate a peer. Nine finding
classes, line-wise, no server, no network, no inference.

The scanner flags only *explicit weakening*: a repo that never
touches TLS settings stays silent, because library/framework
defaults may be correct. Generic `verify: false`-style switches are
gated on TLS vocabulary within a two-line window, signature/enum/
alias definitions are treated as API surface (never flagged), and
comparison probes like `verify_mode != ssl.CERT_NONE` are recognized
as inspections, not disables.

`crypto_prescan.py` (sibling tool) carries a single TLS-VERIFY-OFF
rule as part of crypto mis-use; this scanner owns the whole
transport family. `ws://` plaintext URLs are owned by
`ws_prescan.py` and not re-reported here.

## Finding classes

1. **`TLS-VERIFY-OFF` (HIGH)** — certificate chain verification
   explicitly disabled: `verify=False` (requests/httpx kwargs),
   `ssl.CERT_NONE` / `cert_reqs=CERT_NONE` / `verify_mode=CERT_NONE`
   in call or attribute position, `VERIFY_NONE` (Ruby/Perl/mbedTLS),
   `ssl._create_unverified_context()` call sites,
   `rejectUnauthorized: false`, `NODE_TLS_REJECT_UNAUTHORIZED=0`,
   `InsecureSkipVerify: true`, `CURLOPT_SSL_VERIFYPEER` falsy,
   `verify_peer => false`, `verify_ssl=false`, aiohttp-style
   `ssl=False` on connector/client lines,
   `DangerousAcceptAnyServerCertificateValidator`,
   `ServerCertificateValidationCallback`/`badCertificateCallback`
   lambdas returning `true`, vacuous `checkServerTrusted{}/`
   `checkClientTrusted{}` bodies, trust-all manager class names,
   rust `danger_accept_invalid_certs(true)`,
   `with_no_certificate_verification`.
   Signature/enum/alias definitions are suppressed; `verify=False`
   on signature/digest-vocabulary lines (HMAC, JWT, ECDSA) is
   suppressed.
2. **`TLS-HOSTNAME-OFF` (MEDIUM)** — hostname/CN check disabled
   while chain checking may survive: `check_hostname=False`,
   `assert_hostname=False`, `CURLOPT_SSL_VERIFYHOST` 0 or legacy 1,
   `verify_peer_name => false`, `checkServerIdentity: () =>
   undefined`, `NoopHostnameVerifier`/`AllowAllHostnameVerifier`/
   `ALLOW_ALL_HOSTNAME_VERIFIER`, `hostnameVerifier` lambdas
   `-> true`, `verify_hostname => 0`,
   `danger_accept_invalid_hostnames(true)`.
3. **`TLS-WEAK-VERSION` (MEDIUM)** — legacy protocol pinned:
   `PROTOCOL_SSLv2/v3/TLSv1/TLSv1_1`, `TLSVersion.TLSv1/TLSv1_1`,
   `secureProtocol`/`minVersion`/`maxVersion` TLSv1.x/SSLv3,
   `tls.VersionTLS10/11`, `SSLContext.getInstance("TLSv1")`,
   `SecurityProtocolType`/`SslProtocols` `Ssl3`/`Tls`/`Tls11`,
   `STREAM_CRYPTO_METHOD_TLSv1_0`, `CURL_SSLVERSION_TLSv1_0/1_1/
   SSLv3`, `ssl_version :TLSv1`, nginx `ssl_protocols`/Apache
   `SSLProtocol` listing SSLv3 or TLSv1(.x), `--tlsv1.0`-style CLI
   flags. Bare enum members and `getattr()` availability probes stay
   silent.
4. **`TLS-WEAK-CIPHER` (MEDIUM)** — broken cipher classes kept in a
   suite value (`RC4`, `RC2`, `DES`, `3DES`, `EXPORT`, `NULL`,
   `aNULL`/`eNULL`, `ADH`, `AECDH`, `MD5`, `IDEA`, `SEED`, `LOW`,
   `SSLv2`) inside `set_ciphers`/`ssl_ciphers`/`SSLCipherSuite`/
   `cipherSuites`/`--ciphers` lines — including underscore-joined
   Java names like `TLS_RSA_WITH_RC4_128_MD5`. `!TOKEN` exclusions
   are hardening and never flag.
5. **`TLS-PLAINTEXT` (MEDIUM)** — `http://` URL bound into a
   transport position: assignment to url/uri/endpoint/host/dsn/
   webhook/broker/origin/upstream/backend/gateway/addr/connection/
   server/api/base/proxy/callback/redirect-named variables or config
   keys, or passed as the URL argument of `requests.get(`/
   `fetch(`/`axios`/`client.get(`/`urlopen(`/`new URL(` and friends.
   Loopback, private-suffix, `example.*`, schema-identifier and
   documentation hosts are excluded; non-transport `http://` strings
   stay silent.
6. **`TLS-DB-OFF` (MEDIUM)** — database TLS off in a connection
   string/DSN: `sslmode=disable`, `ssl-mode=DISABLED`,
   `useSSL=false`, `Encrypt=false` on `Server=`/`jdbc:` lines,
   `?ssl=false`/`ssl=false` gated on database vocabulary.
7. **`TLS-PM-BYPASS` (MEDIUM)** — tooling/package-manager TLS
   bypass: `strict-ssl=false`/`strict_ssl=False`/`secure-http=false`,
   `http.sslVerify false`, `GIT_SSL_NO_VERIFY`, `PYTHONHTTPSVERIFY=0`,
   pip `--trusted-host`/`trusted-host` key, `curl -k`/`--insecure`/
   `--proxy-insecure`, `wget --no-check-certificate`, composer
   `disable-tls`, apt `[trusted=yes]`/`AllowInsecureRepositories`/
   `Acquire::https::Verify-Peer "false"`, yum `sslverify=0`, Ansible
   `validate_certs: no`.
8. **`TLS-CONFIG-SKIP` (MEDIUM)** — IaC/config skip-verify keys:
   `*_skip_verify`/`insecure-skip-tls-verify`/`skip_verify` = true,
   `proxy-ssl-verify off` (space-, colon-, or equals-separated),
   `tls_insecure=true`, `check_certificate=false`, `ssl_verify:
   false`, `verify_peer: false`, plus bare `verify: false` and
   `insecure = true` gated on TLS vocabulary within a two-line
   window.
9. **`TLS-WRAP-NOVERIFY` (LOW)** — legacy Python peer-auth gaps:
   `ssl.wrap_socket()` with no `cert_reqs`, `SSLContext(
   PROTOCOL_SSLv23|PROTOCOL_TLS)` — both default to `CERT_NONE`.
   Suppressed file-wide when `cert_reqs=CERT_REQUIRED`/
   `create_default_context`/`load_verify_locations` markers exist.

## Usage

```
python3 tls_prescan.py <file-or-dir> [--json]
python3 tls_prescan.py --selftest      # 89 cases
```

- **Exit codes:** 0 = no HIGH/MEDIUM findings, 1 = findings,
  2 = usage error.
- **`--json`** emits machine-readable findings for CI gates.
- Pure stdlib — Python 3.8+, no network, no inference.

## What it is not

A deterministic *pre-scan* for human review, not an audit. It cannot
see which endpoints are called at runtime, whether a proxy
terminates TLS downstream, or whether a flagged path is reachable.
Clean output ≠ hardened transport.

## Validation

- 89/89 selftest vectors pass (all nine classes, hardened-silent
  mirrors, comment/import/test-path rules).
- 542-file CPython stdlib: 3 findings — all genuine signatures
  (asyncio's conditional `check_hostname=False` when no
  `server_hostname` exists, pydoc's `http://%s:%d` doc-server URL
  binding, one downgraded test fixture) — zero false-positive noise.
- 233-file production service repo (JS/TS): 1 genuine HIGH
  (`rejectUnauthorized: false` on a Postgres pool — the adjacent
  comment documents the opt-in) + 14 LOW test-fixture URLs —
  zero non-test noise.
- Hostile fixture fires all 9 classes; hardened fixture silent.
