diff --git a/plugins/connectors/testssl-inspector/README.md b/plugins/connectors/testssl-inspector/README.md index d9c1ded..8c3ce35 100644 --- a/plugins/connectors/testssl-inspector/README.md +++ b/plugins/connectors/testssl-inspector/README.md @@ -47,4 +47,15 @@ See `commands/scan.md` for the full option set, and `skills/testssl-inspector-ex ## Scope -Only HTTPS endpoints in the current wrapper. `testssl.sh` itself supports `--starttls` for SMTP/IMAP/POP/FTP/etc.; if you need any of those, open an issue. +HTTPS endpoints by default. STARTTLS services are supported through the `--starttls=` flag for the following protocols with their default ports: + +- `smtp` (25) — Simple Mail Transfer Protocol +- `imap` (143) — Internet Message Access Protocol +- `pop3` (110) — Post Office Protocol +- `ftp` (21) — File Transfer Protocol +- `ldap` (389) — Lightweight Directory Access Protocol +- `postgres` (5432) — PostgreSQL database +- `mysql` (3306) — MySQL database +- `smtps` (465) — SMTP Secure + +When `--starttls=` is specified without an explicit port in the target, the target is automatically appended with the protocol's default port. If an explicit port is already present in the target, it is preserved. See `commands/scan.md` for examples. diff --git a/plugins/connectors/testssl-inspector/commands/scan.md b/plugins/connectors/testssl-inspector/commands/scan.md index 4ff2c18..96c1bf3 100644 --- a/plugins/connectors/testssl-inspector/commands/scan.md +++ b/plugins/connectors/testssl-inspector/commands/scan.md @@ -15,7 +15,8 @@ node plugins/connectors/testssl-inspector/scripts/scan.js [options] ## Options -- `--target=host[:port]` — repeatable; or pass targets positionally. Port defaults to 443. +- `--target=host[:port]` — repeatable; or pass targets positionally. Port defaults to 443 for HTTPS, or the protocol's default port when `--starttls=` is used. +- `--starttls=` — enable STARTTLS for the specified protocol. Supported protocols: `smtp` (25), `imap` (143), `pop3` (110), `ftp` (21), `ldap` (389), `postgres` (5432), `mysql` (3306), `smtps` (465). When `--starttls=` is specified without an explicit port in the target, the target is automatically appended with the protocol's default port. For example, `--target=mail.example.com --starttls=smtp` becomes `mail.example.com:25`. If the target already includes an explicit port (e.g., `--target=mail.example.com:587 --starttls=smtp`), the explicit port is preserved. - `--fast` — `testssl.sh --fast` (~3× faster, drops vulnerability checks). - `--full` — full check set (default). Slower; includes CVE checks (Heartbleed, ROBOT, POODLE, BEAST, BREACH, SWEET32, FREAK, LOGJAM, DROWN, …). - `--docker` / `--no-docker` — override the runner choice from the config. @@ -69,18 +70,27 @@ Severity translation: testssl `FATAL`/`CRITICAL` → `critical`; `HIGH` → `hig ## Examples ```bash -# Fast scan of one endpoint +# Fast scan of one HTTPS endpoint /testssl-inspector:scan --target=example.com --fast -# Full vulnerability scan against two endpoints, Docker runner +# Full vulnerability scan against two HTTPS endpoints, Docker runner /testssl-inspector:scan --target=example.com --target=api.example.com:8443 --docker +# STARTTLS scan of SMTP service with default port +/testssl-inspector:scan --target=mail.example.com --starttls=smtp --output=json + +# STARTTLS scan of SMTP service with explicit port +/testssl-inspector:scan --target=mail.example.com:587 --starttls=smtp --output=json + +# STARTTLS scan of IMAP service +/testssl-inspector:scan --target=mail.example.com --starttls=imap --output=json + # Pipe summary into another step /testssl-inspector:scan --target=example.com --output=json | jq '.counters' ``` ## Targets and scope -- Only HTTPS endpoints. testssl supports `--starttls` for SMTP/IMAP/etc.; not currently wired into this wrapper — open an issue if you need it. +- HTTPS endpoints by default. STARTTLS services are supported through the `--starttls=` flag for protocols including SMTP, IMAP, POP3, FTP, LDAP, PostgreSQL, MySQL, and SMTPS. - Scanning runs on the local machine and requires outbound network reach to the target. Not suitable for endpoints behind a private network unless the runner has access. - Be courteous: testssl makes a couple hundred connections per target. Don't point it at infrastructure you don't own without authorization. diff --git a/plugins/connectors/testssl-inspector/scripts/scan.js b/plugins/connectors/testssl-inspector/scripts/scan.js index 74d842f..79631c7 100755 --- a/plugins/connectors/testssl-inspector/scripts/scan.js +++ b/plugins/connectors/testssl-inspector/scripts/scan.js @@ -32,6 +32,19 @@ const SCF_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; const EXIT = { OK: 0, USAGE: 2, TOOL_UNAVAILABLE: 3, PARTIAL: 4, NOT_CONFIGURED: 5 }; +const STARTTLS_PORTS = Object.freeze({ + smtp: 25, + imap: 143, + pop3: 110, + ftp: 21, + ldap: 389, + postgres: 5432, + mysql: 3306, + smtps: 465, +}); + +const SUPPORTED_STARTTLS = Object.keys(STARTTLS_PORTS).join(', '); + async function main(argv) { const args = parseArgs(argv); const log = args.quiet ? () => {} : (m) => process.stderr.write(`[${SOURCE}] ${m}\n`); @@ -47,7 +60,7 @@ async function main(argv) { const mode = args.fast ? 'fast' : 'full'; const useDocker = args.docker ?? (config.use_docker === true); - const runner = await resolveRunner({ useDocker, configBinary: config.testssl_path }); + const runner = await resolveRunner({ useDocker, configBinary: config.testssl_path, starttls: args.starttls }); log(`runner=${runner.label} mode=${mode} targets=${targets.length}`); // Pre-fetch SCF crosswalks for the frameworks we expand to. Done once per run. @@ -73,8 +86,9 @@ async function main(argv) { for (const target of targets) { try { - const raw = await runTestssl(runner, target, mode, log); - const doc = normalizeTargetFindings(raw, target, runId, runner.version, expansion); + const finalTarget = withDefaultStarttlsPort(target, args.starttls); + const raw = await runTestssl(runner, finalTarget, mode, args.starttls, log); + const doc = normalizeTargetFindings(raw, finalTarget, runId, runner.version, expansion); findings.push(doc); } catch (err) { errors.push({ target, error: err.message }); @@ -121,7 +135,7 @@ async function main(argv) { } function parseArgs(argv) { - const args = { targets: [], fast: false, docker: undefined, quiet: false, output: 'summary', scfOnly: false, offline: false }; + const args = { targets: [], fast: false, docker: undefined, quiet: false, output: 'summary', scfOnly: false, offline: false, starttls: null }; for (const a of argv) { if (a === '--fast') args.fast = true; else if (a === '--full') args.fast = false; @@ -132,9 +146,21 @@ function parseArgs(argv) { else if (a === '--offline') args.offline = true; else if (a.startsWith('--target=')) args.targets.push(a.slice(9)); else if (a.startsWith('--output=')) args.output = a.slice(9); + else if (a.startsWith('--starttls=')) { + const proto = a.slice('--starttls='.length).toLowerCase(); + + if (!Object.prototype.hasOwnProperty.call(STARTTLS_PORTS, proto)) { + fail( + EXIT.USAGE, + `unknown STARTTLS protocol: ${proto}. Supported protocols: ${SUPPORTED_STARTTLS}` + ); + } + + args.starttls = proto; + } else if (a === '-h' || a === '--help') { process.stdout.write( - `Usage: scan.js --target=host[:port] [--target=...] [--fast|--full] [--docker] [--scf-only] [--offline] [--output=summary|silent|json] [--quiet]\n` + `Usage: scan.js --target=host[:port] [--target=...] [--starttls=protocol] [--fast|--full] [--docker] [--scf-only] [--offline] [--output=summary|silent|json] [--quiet]\n` ); process.exit(0); } else if (!a.startsWith('-')) args.targets.push(a); @@ -144,7 +170,7 @@ function parseArgs(argv) { } /** Resolve which testssl invocation we'll use. */ -async function resolveRunner({ useDocker, configBinary }) { +async function resolveRunner({ useDocker, configBinary, starttls }) { if (useDocker) { if (!await commandExists('docker')) fail(EXIT.TOOL_UNAVAILABLE, 'docker not on PATH — drop --docker or install Docker.'); return { @@ -154,7 +180,7 @@ async function resolveRunner({ useDocker, configBinary }) { 'run', '--rm', '--network', 'host', '-v', `${CACHE_DIR}:/tmp/scan-out`, 'drwetter/testssl.sh:latest', - ...testsslArgs(target, mode, '/tmp/scan-out'), + ...testsslArgs(target, mode, starttls, '/tmp/scan-out'), ], cmd: 'docker', }; @@ -171,7 +197,7 @@ async function resolveRunner({ useDocker, configBinary }) { label: binary, version, cmd: binary, - argv: (target, mode) => testsslArgs(target, mode), + argv: (target, mode) => testsslArgs(target, mode, starttls), }; } @@ -201,7 +227,14 @@ function parseTestsslVersion(text) { return m ? m[1] : 'unknown'; } -function testsslArgs(target, mode, outDir = null) { +function withDefaultStarttlsPort(target, starttls) { + if (!starttls) return target; + if (target.includes(':')) return target; + + return `${target}:${STARTTLS_PORTS[starttls]}`; +} + +function testsslArgs(target, mode, starttls = null, outDir = null) { const out = outDir || CACHE_DIR; const jsonPath = path.join(out, `testssl-raw-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`); const base = [ @@ -209,12 +242,15 @@ function testsslArgs(target, mode, outDir = null) { '--jsonfile-pretty', jsonPath, ]; if (mode === 'fast') base.push('--fast'); + if (starttls) { + base.push('--starttls', starttls); + } base.push(target); base.__jsonPath = jsonPath; return base; } -async function runTestssl(runner, target, mode, log) { +async function runTestssl(runner, target, mode, starttls, log) { const argv = runner.argv(target, mode); const jsonPath = argv.__jsonPath; log(`scanning ${target}`);