npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ecdsa-scan

v0.1.1

Published

Static analysis for digital-signature code: JWT algorithm confusion, curve mix-ups, signature-encoding bugs, weak nonces, hardcoded keys and disabled certificate checks. Part of the ecdsa.com signature toolkit — https://ecdsa.com

Readme

ecdsa-scan

npm version license: MIT node >=20

Made by ecdsa.com — free, local-first tools to verify, debug and understand digital signatures. Scanner docs and demo: ecdsa.com/scanner.

Static analysis for code that creates and checks digital signatures.

Secret scanners already find committed keys. This tool looks for the layer above that: the places where signature code is subtly wrong — a JWT verified without pinning the algorithm, a P-256 key generated in a file that derives Ethereum addresses, an r‖s signature built by hand without padding, a verification whose boolean result is thrown away.

It is read-only (it never rewrites your files), has zero dependencies, needs no build step, and runs on Node 20+.

$ ecdsa scan ./src

src/auth/session.ts
  42:18  confirmed  jwt-verify-missing-algorithms  JWT verified without an explicit algorithm allow-list
      │ const claims = jwt.verify(token, publicKey);
      `jwt.verify(token, publicKey)` does not pass `algorithms`, so the token header
      decides how the signature is checked.
      Why it matters: A JWT names its own algorithm in the header, so a verifier that
      does not pin the accepted algorithms lets the token choose how it is checked …
      Fix:
        jwt.verify(token, publicKey, { algorithms: ["ES256"] })
      Reference: https://datatracker.ietf.org/doc/html/rfc8725#section-3.1

Summary 214 files scanned, 3 findings (1 confirmed, 2 advisory)
Exit code 1: 1 confirmed finding.

Install and run

# no install
npx ecdsa-scan .

# from a clone of this repository
node cli/src/index.js scan .

# global (installs two equivalent binaries: `ecdsa-scan` and `ecdsa`)
npm install -g ecdsa-scan && ecdsa scan .

Usage

ecdsa scan [path]            scan a directory or a single file (default: .)
ecdsa rules                  list the rules and their default confidence
ecdsa --help | --version

--json                       machine-readable report on stdout
--sarif [file]               write SARIF 2.1.0 (default: ecdsa-scan.sarif, "-" for stdout)
--min-confidence <level>     confirmed | suspected | advisory   (default: advisory)
--ignore <glob>              skip paths matching a glob; repeatable
--no-color                   plain output (also honours NO_COLOR)

Examples:

ecdsa scan ./src --min-confidence suspected
ecdsa scan . --ignore "**/*.test.ts" --ignore examples
ecdsa scan . --sarif results.sarif        # upload to GitHub Code Scanning
ecdsa scan . --json | jq '.inventory'     # the CBOM seed
ecdsa scan . --json | jq '.findings[] | select(.confidence=="confirmed")'

Languages: JavaScript, TypeScript, Python, Go (.js .mjs .cjs .jsx .ts .tsx .mts .cts .py .go), plus key files (.pem .key .p8 .p12 .pfx .jks, id_rsa, id_ecdsa, …). Skipped automatically: node_modules, .git, dist, build, out, vendor, .next, .venv, __pycache__, target, coverage and similar.

Exit codes

| Code | Meaning | |---|---| | 0 | no confirmed findings (suspected/advisory findings do not fail a build) | | 1 | at least one confirmed finding — use this to gate CI | | 2 | usage error, or the path could not be read |

Three levels of confidence

Findings are graded, and the grade is the contract with you:

| Level | Meaning | What to do | |---|---|---| | confirmed | The pattern is a defect regardless of surrounding code. | Fix it. | | suspected | Very likely wrong; the surrounding code decides. | Read the finding, then fix or dismiss. | | advisory | Worth a look. Legitimate code matches here. | Treat as a question, not a verdict. |

Some rules move a finding between levels based on context — a private key in a test/fixtures/ path is reported as advisory rather than confirmed, and a jwt.decode() whose result feeds a role check is upgraded from advisory to suspected.

Rules

14 defect rules and one inventory collector. ecdsa rules prints the same list.

| Rule | Default | Severity | What it finds | |---|---|---|---| | jwt-verify-missing-algorithms | confirmed | high | jwt.verify / jwtVerify without algorithms:, PyJWT decode without algorithms=, jwt.Parse without WithValidMethods; also empty or "none" lists | | jwt-decode-without-verification | suspected | high | jwt.decode, decodeJwt, jwtDecode, PyJWT verify_signature: False — upgraded when role/user/permission identifiers follow the call | | jwt-alg-from-token | confirmed | high | The algorithm list is built from the token's own header (algorithms: [header.alg], get_unverified_header) | | curve-mixing | advisory | medium | A P-256 key created in Ethereum/Bitcoin code (suspected); secp256k1 and P-256 handled in one module (advisory) | | signature-encoding | suspected | medium | r/s concatenated without zero-padding to the field size; Node crypto.sign/verify without dsaEncoding in JWS code (advisory) | | secp256k1-low-s | advisory | low | secp256k1 signing/verification with no mention of low-S canonicalisation | | insecure-nonce-source | confirmed | high | Math.random() / Python random / math/rand next to key or nonce material (confirmed); caller-supplied k, extraEntropy, deterministic: false (suspected) | | hardcoded-private-key | confirmed | high | PEM private key blocks in source; 64-hex literals assigned to privateKey/secret/signingKey-style names (suspected) | | key-file-outside-tests | suspected | high | .pem/.key/.p12/id_ecdsa files with private key material outside test/, fixtures/, examples/ | | non-constant-time-comparison | suspected | medium | Signatures/MACs/digests compared with ==, ===, .equals(), bytes.Equal instead of a constant-time helper | | unchecked-verification-result | suspected | high | Boolean-returning verification (crypto.verify, ecdsa.VerifyASN1, …) called as a bare statement | | weak-signature-hash | confirmed | high | SHA-1/MD5 in a signing path (createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA); SHA-1 digests elsewhere in signing code (advisory) | | unvalidated-public-key-point | advisory | medium | Public keys built from raw x/y coordinates with no on-curve check; hand-rolled curve arithmetic | | tls-verification-disabled | confirmed | high | rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False on HTTP clients, ssl.CERT_NONE, InsecureSkipVerify: true | | crypto-inventory | — | — | Not a defect rule: collects libraries, algorithms, curves and signing operations per file |

Every finding carries a plain-English explanation, a fix example and a link to the relevant RFC or guidance.

Inventory (the CBOM seed)

--json includes an inventory section: which crypto libraries, algorithms, curves and signing operations appear in which files. This is the raw material for a cryptographic bill of materials, and for answering "where do we depend on ECDSA?" before the NIST 2030/2035 deadlines.

{
  "inventory": {
    "libraries": [{ "name": "jose", "detail": "npm", "files": ["src/auth/jwt.ts"] }],
    "algorithms": [{ "name": "ES256", "files": ["src/auth/jwt.ts"] }],
    "curves": [{ "name": "P-256", "files": ["src/auth/jwt.ts"] }],
    "operations": [{ "name": "verify", "files": ["src/auth/jwt.ts"] }]
  }
}

GitHub Action

A complete workflow: scan on every push and pull request, publish the findings to GitHub Code Scanning as pull-request annotations.

# .github/workflows/ecdsa-scan.yml
name: ecdsa-scan

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  security-events: write           # required by upload-sarif

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: ECDSA signature scan
        run: npx ecdsa-scan . --sarif ecdsa.sarif
        continue-on-error: true    # keep the upload step running on findings

      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: ecdsa.sarif

Confidence maps to SARIF levels: confirmed → error, suspected → warning, advisory → note. Each rule ships its explanation, fix and reference in the SARIF help field, so the annotation in a pull request is self-contained.

To fail the job on confirmed findings instead of only annotating, drop continue-on-error — the scanner exits 1 when a confirmed finding exists. A ready-made composite action lives in action.yml.

Pre-commit

With pre-commit:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: ecdsa-scan
        name: ecdsa-scan (signature defects)
        entry: npx --yes ecdsa-scan
        args: ["--min-confidence", "suspected"]
        language: system
        pass_filenames: false      # the scanner walks the tree itself

Or as a plain Git hook:

# .git/hooks/pre-commit
#!/bin/sh
npx --yes ecdsa-scan . || exit 1

Only confirmed findings exit non-zero, so a pre-commit hook blocks real defects without nagging about advisory matches.

How it works

  1. Walk the tree, skipping dependency and build directories.
  2. For each file build three views of the source, all with identical byte offsets: the raw text; a copy with comments blanked; and a copy with the contents of strings, template literals, regexes and JSX prose blanked too.
  3. Run every rule that applies to the file's language. Rules match structure against the masked views (so documentation and code samples never become findings) and read literal values from the raw view when the value is the point — a PEM header, createSign("sha1"), a curve name.
  4. Sort, de-duplicate and format.

There is no parser and no type information. That is a deliberate trade-off: the tool runs on any repository instantly, in any state, without installing its dependencies or compiling anything.

Limitations — read this

This is pattern matching over text, not program analysis. Specifically:

  • False positives happen. A module that legitimately supports several curves matches curve-mixing; a signature-debugging tool legitimately calls jwt.decode. That is why every rule has a confidence level, why only confirmed findings fail the build, and why advisory findings are phrased as questions.
  • False negatives happen, and they are worse. Anything indirect is invisible: a wrapper function (verifyToken() defined in another file), an algorithm read from configuration, a key type decided at runtime, a defect expressed through a library this tool has never heard of.
  • Weak randomness is only partly detectable. A biased nonce produced by a custom PRNG three modules away looks exactly like correct code. Statistical nonce problems are found by looking at signatures, not at source.
  • No cross-file analysis, no data flow, no taint tracking. Each file is judged on its own.
  • Comment and literal masking is heuristic. Unusual formatting (a regex the lexer misreads, a template literal containing real logic in ${…}) can hide a finding.
  • It does not check your cryptography is correct — only that certain well-known mistakes are absent. Passing this scan is not an audit, and no finding count implies a security level.

Treat the output as a prioritised reading list for a human reviewer.

Development

node --test test/*.test.js     # 78 tests, no dependencies
node src/index.js scan ..      # dogfood: scan the repository above

test/fixtures/bad/ holds a defective example per rule and test/fixtures/good/ the corrected version of the same code; the suite asserts that every rule fires on the first and stays silent on the second. The .pem fixtures contain placeholder text, not usable keys.

Adding a rule

Create src/rules/<id>.js exporting one object and register it in src/rules/index.js:

export default {
  id: "my-rule",              // kebab-case; becomes the SARIF ruleId
  title: "One line, human",
  severity: "high",           // high | medium | low
  confidence: "suspected",    // default level for this rule's findings
  languages: ["js", "ts", "python", "go"],
  why: "Two or three sentences: what is wrong and why it matters.",
  fix: "A short corrected snippet.",
  docs: "https://…",          // RFC or authoritative guidance
  match(ctx) {
    return [...ctx.matchAll(/pattern/g)]
      .filter((m) => !ctx.isMasked(m.index))
      .map((m) => ({ index: m.index, message: "What is wrong here." }));
  },
};

The context gives you ctx.text (raw), ctx.code (comments blanked), ctx.structure (literals blanked), ctx.isMasked(index), ctx.matchAll, ctx.findCalls, ctx.window, ctx.lineOf and ctx.isTestPath. Add fixtures under test/fixtures/bad/ and test/fixtures/good/ and a row in the CASES table in test/rules.test.js — the suite fails if a rule has no fixture.

Prefer a lower confidence over a louder rule: a scanner people mute is worth nothing.