All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
EXPOSED_SECRETSno longer echoes detected secrets in its own output. The rule'sevidencepreviously included the matched credential verbatim (only truncated past 60 chars), so a scan report — stdout/JSON, and any artifact or CI log derived from it — re-exposed the very secrets it was flagging. Findings now report each match by type and length only (e.g.OpenAI API key (29 chars, redacted)), never the value.
- New
PLACEHOLDER_CREDENTIALrule (HIGH). Flagstransport.auth.apiKey/transport.auth.tokenvalues that are placeholders (your-api-key-here,<REPLACE_ME>,{{API_KEY}}, filler likexxxxxxxx) or well-known default/weak secrets (changeme,password,admin, …). Previously a long placeholder (≥ 32 chars, not JWT-shaped) satisfiedNO_AUTHand passedWEAK_API_KEY, so a config shipped with an unfilled credential went green. Matching is anchored to the whole normalized value (defaults) or high-signal phrases (placeholders) to keep false positives near zero — a real random key and a JWT are not flagged.
MISSING_TLS/INSECURE_TRANSPORTno longer fire a false HIGH on loopback hosts (#48). Both rules flagged anyhttp:///ws://URL as a cleartext network-eavesdropping risk, including loopback addresses (localhost,127.0.0.0/8,::1) where traffic never leaves the machine. This failed the default gate on the most common local-development setup (e.g.http://localhost:3000). Loopback cleartext is now suppressed; non-loopback cleartext endpoints are still flagged exactly as before.
- Default CI/CD gate now fails on HIGH findings, not just CRITICAL (#47).
The pass decision was
score < 50 && no CRITICAL; because HIGH findings are weighted 15 and the threshold is 50, a config could accumulate three HIGH findings and still reportPASSED/ exit 0 (e.g. tools grantedshell:exec,network:*, andfilesystem:*at once). HIGH findings are real compromise vectors, so a single CRITICAL or HIGH finding now vetoes the default gate:passed = score < 50 && no CRITICAL && no HIGH. This is a deliberate tightening — pipelines that were green because of HIGH findings will now turn red. To keep the old behaviour explicitly, gate on the score or use--fail-on=critical. Sub-HIGH findings (MEDIUM/LOW) still pass by default; use--fail-on=medium/--exit-codefor stricter gating.
NO_AUTHnow fires when a credential-requiringauth.typeis declared with no credential (#54). The check treated any non-disabledauth.typeas proof of authentication, so a config likeauth: { type: "bearer" }with noapiKey/token— e.g. a deploy-time secret that never populated — sailed through the flagship CRITICAL gate with 0 findings /PASSED. Credential- bearing types (bearer,token,apikey/api_key/api-key,basic) now require a non-empty in-config secret; their absence reports CRITICALNO_AUTH. Mechanisms whose credential lives outside the config (mtls,oauth,oauth2,oidc,cert) still pass on the type alone — no regression to the #24 carve-out. Empty-string credentials (apiKey: ""/token: "") are now treated as absent, so they surface asNO_AUTHrather than being mislabelled a "weak" (length-0)WEAK_API_KEYHIGH that still passed the default gate.- URL/endpoint mode no longer produces false findings (#27). Scanning a URL
previously ran every config rule against an effectively empty config, so any
endpoint always emitted a CRITICAL
NO_AUTHand a MEDIUMMISSING_RATE_LIMITand always failed — even for a secure, authenticated server. URL mode now runs only the rules a URL can actually answer (MISSING_TLS,INSECURE_TRANSPORT) and emits an INFOURL_SCAN_LIMITEDnote explaining that live introspection is not performed and that a config file is required for the full rule set. WEAK_API_KEY: now also length-checksauth.token, not justauth.apiKey(#26). A short (< 32 char) opaque bearer token previously passed the gate even though an identical value inapiKeywas flagged HIGH. JWT-shaped tokens are exempt (structured, effectively always > 32 chars); finding evidence now names the offending field (apiKeyvstoken).- YAML configs are now parsed correctly (#19). The previous hand-rolled parser
only handled flat scalar keys, silently dropping nested
transport.auth(a falseNO_AUTH) and thetools:sequence-of-maps (all tool rules skipped). A YAML config and its JSON equivalent now produce identical results. Still zero-dependency — the parser is indentation-driven. MISSING_TLS: scoped to the HTTP family so a securewss://transport is no longer flagged "uses plain HTTP", andws://is no longer double-counted (it remains covered byINSECURE_TRANSPORT).- CLI:
--rulenow rejects unknown/typo'd rule IDs with exit code 2 (#11). Previously an unrecognized rule filtered the rule set to nothing, producing a silent false "PASSED" — a security gate bypassed by a typo.--helpnow lists the valid rule IDs. NO_AUTH: fixed a CRITICAL false negative where declaringtransport.auth.type: "none"(ordisabled/off/false/anonymous) satisfied the auth check, so an explicitly unauthenticated server passed the gate. An authtypenow only counts as authentication when it names a real mechanism; placeholder/disabled values are treated as no auth.- Restored the test suite / green CI:
import.meta.urlinsrc/sarif.tscould not compile under the Jestmodule: CommonJStransform, so thesarifandscannertest suites silently failed to run (and the test gate had been effectively disabled). Jest now runs ts-jest in ESM mode, matching the package's published ESM build. Also fixed a stale SARIF version assertion that was hard-coded to0.0.1; it now reads the version frompackage.json. - CLI: accept
--output=as an alias for--format=. The README and dev.to launch post documented--output=sarif, but the CLI rejected it as an unknown flag (exit 2), so the documented quick-start command failed.
- Extracted CLI argument parsing into a pure, side-effect-free
parseArgs()(src/args.ts) with direct unit-test coverage (src/__tests__/args.test.ts).
- Initial scaffold: project structure, TypeScript configuration, and package metadata.
- Added real scanner implementation with 8 security rules, CLI, Jest test suite, ESLint config.
- Auth rules: NO_AUTH, WEAK_API_KEY, MISSING_TLS
- Injection rules: TOOL_DESC_INJECTION, UNSAFE_TOOL_OUTPUT_PATH
- Config rules: WILDCARD_CORS, VERBOSE_ERRORS, OVERPRIVILEGED_TOOL
- CLI entry point (
mcp-security-scanner <config-path-or-url>) - Jest test suite with 35 tests across scanner and scorer
- ESLint + @typescript-eslint wired up with zero warnings
- Added 5 new runtime security rules (
src/rules/runtime-rules.ts):INSECURE_TRANSPORT(HIGH): fires whentransport.urluses unencryptedws://MISSING_RATE_LIMIT(MEDIUM): fires whenrateLimitis absent orenabled: falseDEBUG_MODE_ENABLED(LOW): fires whendebug: trueis setEXPOSED_SECRETS(CRITICAL): scans all raw string values for common secret patterns (OpenAI API key, GitHub PAT, AWS access key, password assignments)UNRESTRICTED_FILE_ACCESS(HIGH): fires when a tool hasfilesystem:*or bothfilesystem:readandfilesystem:writepermissions
- Added SARIF 2.1.0 output support (
src/sarif.ts):toSarif(report, configPath?)converts aSecurityReportto a minimal SARIF 2.1.0 document- Severity mapping: critical/high →
error, medium →warning, low/info →note - Includes tool driver metadata and per-result artifact locations
- Enhanced CLI with rich flag parsing (
src/cli.ts):--format=json|sarif|table(default: json)--exit-code— exit 1 on any finding regardless of score--fail-on=critical|high|medium|low— force fail at a minimum severity level--rule=RULE_ID— filter to specific rules (repeatable)--help/-h— display usage information--format=tablerenders an ASCII table sorted by severity with a summary line
- Implemented
failOninscan()(src/scanner.ts): forcespassed=falsewhen any finding meets or exceeds the configured severity threshold - Updated
ParsedMcpConfigwithrawStrings?: string[]field populated from all string values in the raw config object (used byEXPOSED_SECRETSrule) - Added example configuration files:
examples/secure-config.json— a passing config with auth, HTTPS/WSS, scoped CORS, rate limiting, no debugexamples/vulnerable-config.json— a deliberately failing config triggering 8+ rulesexamples/minimal-config.json— bare minimum valid config with just aserverUrl
- Expanded test suite to 70 tests (added
sarif.test.tsand new cases inscanner.test.ts)