ecdsa-scan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ecdsa.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # `ecdsa-scan`
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ecdsa-scan.svg)](https://www.npmjs.com/package/ecdsa-scan)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-emerald.svg)](./LICENSE)
5
+ [![node >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
6
+
7
+ Static analysis for code that creates and checks **digital signatures**.
8
+
9
+ Secret scanners already find committed keys. This tool looks for the layer above
10
+ that: the places where signature code is *subtly wrong* — a JWT verified without
11
+ pinning the algorithm, a P-256 key generated in a file that derives Ethereum
12
+ addresses, an `r‖s` signature built by hand without padding, a verification whose
13
+ boolean result is thrown away.
14
+
15
+ It is **read-only** (it never rewrites your files), has **zero dependencies**,
16
+ needs **no build step**, and runs on Node 20+.
17
+
18
+ ```
19
+ $ ecdsa scan ./src
20
+
21
+ src/auth/session.ts
22
+ 42:18 confirmed jwt-verify-missing-algorithms JWT verified without an explicit algorithm allow-list
23
+ │ const claims = jwt.verify(token, publicKey);
24
+ `jwt.verify(token, publicKey)` does not pass `algorithms`, so the token header
25
+ decides how the signature is checked.
26
+ Why it matters: A JWT names its own algorithm in the header, so a verifier that
27
+ does not pin the accepted algorithms lets the token choose how it is checked …
28
+ Fix:
29
+ jwt.verify(token, publicKey, { algorithms: ["ES256"] })
30
+ Reference: https://datatracker.ietf.org/doc/html/rfc8725#section-3.1
31
+
32
+ Summary 214 files scanned, 3 findings (1 confirmed, 2 advisory)
33
+ Exit code 1: 1 confirmed finding.
34
+ ```
35
+
36
+ ## Install and run
37
+
38
+ ```bash
39
+ # no install
40
+ npx ecdsa-scan .
41
+
42
+ # from a clone of this repository
43
+ node cli/src/index.js scan .
44
+
45
+ # global (installs two equivalent binaries: `ecdsa-scan` and `ecdsa`)
46
+ npm install -g ecdsa-scan && ecdsa scan .
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```
52
+ ecdsa scan [path] scan a directory or a single file (default: .)
53
+ ecdsa rules list the rules and their default confidence
54
+ ecdsa --help | --version
55
+
56
+ --json machine-readable report on stdout
57
+ --sarif [file] write SARIF 2.1.0 (default: ecdsa-scan.sarif, "-" for stdout)
58
+ --min-confidence <level> confirmed | suspected | advisory (default: advisory)
59
+ --ignore <glob> skip paths matching a glob; repeatable
60
+ --no-color plain output (also honours NO_COLOR)
61
+ ```
62
+
63
+ Examples:
64
+
65
+ ```bash
66
+ ecdsa scan ./src --min-confidence suspected
67
+ ecdsa scan . --ignore "**/*.test.ts" --ignore examples
68
+ ecdsa scan . --sarif results.sarif # upload to GitHub Code Scanning
69
+ ecdsa scan . --json | jq '.inventory' # the CBOM seed
70
+ ecdsa scan . --json | jq '.findings[] | select(.confidence=="confirmed")'
71
+ ```
72
+
73
+ **Languages:** JavaScript, TypeScript, Python, Go (`.js .mjs .cjs .jsx .ts .tsx
74
+ .mts .cts .py .go`), plus key files (`.pem .key .p8 .p12 .pfx .jks`, `id_rsa`,
75
+ `id_ecdsa`, …).
76
+ **Skipped automatically:** `node_modules`, `.git`, `dist`, `build`, `out`,
77
+ `vendor`, `.next`, `.venv`, `__pycache__`, `target`, `coverage` and similar.
78
+
79
+ ### Exit codes
80
+
81
+ | Code | Meaning |
82
+ |---|---|
83
+ | `0` | no **confirmed** findings (suspected/advisory findings do not fail a build) |
84
+ | `1` | at least one confirmed finding — use this to gate CI |
85
+ | `2` | usage error, or the path could not be read |
86
+
87
+ ### Three levels of confidence
88
+
89
+ Findings are graded, and the grade is the contract with you:
90
+
91
+ | Level | Meaning | What to do |
92
+ |---|---|---|
93
+ | **confirmed** | The pattern is a defect regardless of surrounding code. | Fix it. |
94
+ | **suspected** | Very likely wrong; the surrounding code decides. | Read the finding, then fix or dismiss. |
95
+ | **advisory** | Worth a look. Legitimate code matches here. | Treat as a question, not a verdict. |
96
+
97
+ Some rules move a finding between levels based on context — a private key in a
98
+ `test/fixtures/` path is reported as advisory rather than confirmed, and a
99
+ `jwt.decode()` whose result feeds a role check is upgraded from advisory to
100
+ suspected.
101
+
102
+ ## Rules
103
+
104
+ 14 defect rules and one inventory collector. `ecdsa rules` prints the same list.
105
+
106
+ | Rule | Default | Severity | What it finds |
107
+ |---|---|---|---|
108
+ | `jwt-verify-missing-algorithms` | confirmed | high | `jwt.verify` / `jwtVerify` without `algorithms:`, PyJWT `decode` without `algorithms=`, `jwt.Parse` without `WithValidMethods`; also empty or `"none"` lists |
109
+ | `jwt-decode-without-verification` | suspected | high | `jwt.decode`, `decodeJwt`, `jwtDecode`, PyJWT `verify_signature: False` — upgraded when role/user/permission identifiers follow the call |
110
+ | `jwt-alg-from-token` | confirmed | high | The algorithm list is built from the token's own header (`algorithms: [header.alg]`, `get_unverified_header`) |
111
+ | `curve-mixing` | advisory | medium | A P-256 key created in Ethereum/Bitcoin code (suspected); secp256k1 and P-256 handled in one module (advisory) |
112
+ | `signature-encoding` | suspected | medium | `r`/`s` concatenated without zero-padding to the field size; Node `crypto.sign`/`verify` without `dsaEncoding` in JWS code (advisory) |
113
+ | `secp256k1-low-s` | advisory | low | secp256k1 signing/verification with no mention of low-S canonicalisation |
114
+ | `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) |
115
+ | `hardcoded-private-key` | confirmed | high | PEM private key blocks in source; 64-hex literals assigned to `privateKey`/`secret`/`signingKey`-style names (suspected) |
116
+ | `key-file-outside-tests` | suspected | high | `.pem`/`.key`/`.p12`/`id_ecdsa` files with private key material outside `test/`, `fixtures/`, `examples/` |
117
+ | `non-constant-time-comparison` | suspected | medium | Signatures/MACs/digests compared with `==`, `===`, `.equals()`, `bytes.Equal` instead of a constant-time helper |
118
+ | `unchecked-verification-result` | suspected | high | Boolean-returning verification (`crypto.verify`, `ecdsa.VerifyASN1`, …) called as a bare statement |
119
+ | `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) |
120
+ | `unvalidated-public-key-point` | advisory | medium | Public keys built from raw x/y coordinates with no on-curve check; hand-rolled curve arithmetic |
121
+ | `tls-verification-disabled` | confirmed | high | `rejectUnauthorized: false`, `NODE_TLS_REJECT_UNAUTHORIZED=0`, `verify=False` on HTTP clients, `ssl.CERT_NONE`, `InsecureSkipVerify: true` |
122
+ | `crypto-inventory` | — | — | Not a defect rule: collects libraries, algorithms, curves and signing operations per file |
123
+
124
+ Every finding carries a plain-English explanation, a fix example and a link to
125
+ the relevant RFC or guidance.
126
+
127
+ ### Inventory (the CBOM seed)
128
+
129
+ `--json` includes an `inventory` section: which crypto libraries, algorithms,
130
+ curves and signing operations appear in which files. This is the raw material
131
+ for a cryptographic bill of materials, and for answering "where do we depend on
132
+ ECDSA?" before the NIST 2030/2035 deadlines.
133
+
134
+ ```json
135
+ {
136
+ "inventory": {
137
+ "libraries": [{ "name": "jose", "detail": "npm", "files": ["src/auth/jwt.ts"] }],
138
+ "algorithms": [{ "name": "ES256", "files": ["src/auth/jwt.ts"] }],
139
+ "curves": [{ "name": "P-256", "files": ["src/auth/jwt.ts"] }],
140
+ "operations": [{ "name": "verify", "files": ["src/auth/jwt.ts"] }]
141
+ }
142
+ }
143
+ ```
144
+
145
+ ## GitHub Action
146
+
147
+ A complete workflow: scan on every push and pull request, publish the findings
148
+ to GitHub Code Scanning as pull-request annotations.
149
+
150
+ ```yaml
151
+ # .github/workflows/ecdsa-scan.yml
152
+ name: ecdsa-scan
153
+
154
+ on:
155
+ push:
156
+ branches: [main]
157
+ pull_request:
158
+
159
+ permissions:
160
+ contents: read
161
+ security-events: write # required by upload-sarif
162
+
163
+ jobs:
164
+ scan:
165
+ runs-on: ubuntu-latest
166
+ steps:
167
+ - uses: actions/checkout@v4
168
+
169
+ - uses: actions/setup-node@v4
170
+ with:
171
+ node-version: 20
172
+
173
+ - name: ECDSA signature scan
174
+ run: npx ecdsa-scan . --sarif ecdsa.sarif
175
+ continue-on-error: true # keep the upload step running on findings
176
+
177
+ - uses: github/codeql-action/upload-sarif@v3
178
+ with:
179
+ sarif_file: ecdsa.sarif
180
+ ```
181
+
182
+ Confidence maps to SARIF levels: confirmed → `error`, suspected → `warning`,
183
+ advisory → `note`. Each rule ships its explanation, fix and reference in the
184
+ SARIF `help` field, so the annotation in a pull request is self-contained.
185
+
186
+ To fail the job on confirmed findings instead of only annotating, drop
187
+ `continue-on-error` — the scanner exits `1` when a confirmed finding exists.
188
+ A ready-made composite action lives in [`action.yml`](./action.yml).
189
+
190
+ ## Pre-commit
191
+
192
+ With [pre-commit](https://pre-commit.com):
193
+
194
+ ```yaml
195
+ # .pre-commit-config.yaml
196
+ repos:
197
+ - repo: local
198
+ hooks:
199
+ - id: ecdsa-scan
200
+ name: ecdsa-scan (signature defects)
201
+ entry: npx --yes ecdsa-scan
202
+ args: ["--min-confidence", "suspected"]
203
+ language: system
204
+ pass_filenames: false # the scanner walks the tree itself
205
+ ```
206
+
207
+ Or as a plain Git hook:
208
+
209
+ ```bash
210
+ # .git/hooks/pre-commit
211
+ #!/bin/sh
212
+ npx --yes ecdsa-scan . || exit 1
213
+ ```
214
+
215
+ Only **confirmed** findings exit non-zero, so a pre-commit hook blocks real
216
+ defects without nagging about advisory matches.
217
+
218
+ ## How it works
219
+
220
+ 1. Walk the tree, skipping dependency and build directories.
221
+ 2. For each file build three views of the source, all with identical byte
222
+ offsets: the raw text; a copy with **comments blanked**; and a copy with the
223
+ **contents of strings, template literals, regexes and JSX prose blanked** too.
224
+ 3. Run every rule that applies to the file's language. Rules match structure
225
+ against the masked views (so documentation and code samples never become
226
+ findings) and read literal values from the raw view when the value is the
227
+ point — a PEM header, `createSign("sha1")`, a curve name.
228
+ 4. Sort, de-duplicate and format.
229
+
230
+ There is no parser and no type information. That is a deliberate trade-off: the
231
+ tool runs on any repository instantly, in any state, without installing its
232
+ dependencies or compiling anything.
233
+
234
+ ## Limitations — read this
235
+
236
+ This is pattern matching over text, not program analysis. Specifically:
237
+
238
+ - **False positives happen.** A module that legitimately supports several curves
239
+ matches `curve-mixing`; a signature-debugging tool legitimately calls
240
+ `jwt.decode`. That is why every rule has a confidence level, why only
241
+ *confirmed* findings fail the build, and why advisory findings are phrased as
242
+ questions.
243
+ - **False negatives happen, and they are worse.** Anything indirect is invisible:
244
+ a wrapper function (`verifyToken()` defined in another file), an algorithm read
245
+ from configuration, a key type decided at runtime, a defect expressed through
246
+ a library this tool has never heard of.
247
+ - **Weak randomness is only partly detectable.** A biased nonce produced by a
248
+ custom PRNG three modules away looks exactly like correct code. Statistical
249
+ nonce problems are found by looking at *signatures*, not at source.
250
+ - **No cross-file analysis, no data flow, no taint tracking.** Each file is
251
+ judged on its own.
252
+ - **Comment and literal masking is heuristic.** Unusual formatting (a regex the
253
+ lexer misreads, a template literal containing real logic in `${…}`) can hide a
254
+ finding.
255
+ - **It does not check your cryptography is correct** — only that certain
256
+ well-known mistakes are absent. Passing this scan is not an audit, and no
257
+ finding count implies a security level.
258
+
259
+ Treat the output as a prioritised reading list for a human reviewer.
260
+
261
+ ## Development
262
+
263
+ ```bash
264
+ node --test test/*.test.js # 78 tests, no dependencies
265
+ node src/index.js scan .. # dogfood: scan the repository above
266
+ ```
267
+
268
+ `test/fixtures/bad/` holds a defective example per rule and
269
+ `test/fixtures/good/` the corrected version of the same code; the suite asserts
270
+ that every rule fires on the first and stays silent on the second. The `.pem`
271
+ fixtures contain placeholder text, not usable keys.
272
+
273
+ ### Adding a rule
274
+
275
+ Create `src/rules/<id>.js` exporting one object and register it in
276
+ `src/rules/index.js`:
277
+
278
+ ```js
279
+ export default {
280
+ id: "my-rule", // kebab-case; becomes the SARIF ruleId
281
+ title: "One line, human",
282
+ severity: "high", // high | medium | low
283
+ confidence: "suspected", // default level for this rule's findings
284
+ languages: ["js", "ts", "python", "go"],
285
+ why: "Two or three sentences: what is wrong and why it matters.",
286
+ fix: "A short corrected snippet.",
287
+ docs: "https://…", // RFC or authoritative guidance
288
+ match(ctx) {
289
+ return [...ctx.matchAll(/pattern/g)]
290
+ .filter((m) => !ctx.isMasked(m.index))
291
+ .map((m) => ({ index: m.index, message: "What is wrong here." }));
292
+ },
293
+ };
294
+ ```
295
+
296
+ The context gives you `ctx.text` (raw), `ctx.code` (comments blanked),
297
+ `ctx.structure` (literals blanked), `ctx.isMasked(index)`, `ctx.matchAll`,
298
+ `ctx.findCalls`, `ctx.window`, `ctx.lineOf` and `ctx.isTestPath`. Add fixtures
299
+ under `test/fixtures/bad/` and `test/fixtures/good/` and a row in the `CASES`
300
+ table in `test/rules.test.js` — the suite fails if a rule has no fixture.
301
+
302
+ Prefer a lower confidence over a louder rule: a scanner people mute is worth
303
+ nothing.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "ecdsa-scan",
3
+ "version": "0.1.0",
4
+ "description": "Static analysis for digital-signature code: JWT algorithm confusion, curve mix-ups, signature-encoding bugs, weak nonces, hardcoded keys and disabled certificate checks.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://ecdsa.com/scanner",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+ssh://git@bitbucket.org/hub2026/ecdsa.git",
11
+ "directory": "cli"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "bin": {
17
+ "ecdsa-scan": "src/index.js",
18
+ "ecdsa": "src/index.js"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "test": "node --test test/*.test.js",
27
+ "scan": "node ./src/index.js scan"
28
+ },
29
+ "keywords": [
30
+ "ecdsa",
31
+ "jwt",
32
+ "signature",
33
+ "static-analysis",
34
+ "security",
35
+ "linter",
36
+ "sarif",
37
+ "cbom",
38
+ "cryptography",
39
+ "secp256k1",
40
+ "p-256",
41
+ "post-quantum"
42
+ ]
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ // ecdsa scan — static analysis for digital-signature code.
3
+ //
4
+ // Read-only by design: the scanner opens files and writes nothing except the
5
+ // report you ask for (--sarif writes the report file itself).
6
+
7
+ import { existsSync, realpathSync } from "node:fs";
8
+ import { writeFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import process from "node:process";
11
+ import { pathToFileURL } from "node:url";
12
+ import { parseArgs } from "node:util";
13
+ import { CONFIDENCE_LEVELS, filterByConfidence, scan } from "./scan.js";
14
+ import { formatJson, formatSarif, formatText, TOOL_VERSION } from "./report.js";
15
+ import { defectRules, rules as allRules } from "./rules/index.js";
16
+
17
+ const DEFAULT_SARIF_PATH = "ecdsa-scan.sarif";
18
+
19
+ const OPTIONS = {
20
+ json: { type: "boolean", default: false },
21
+ sarif: { type: "string" },
22
+ "min-confidence": { type: "string", default: "advisory" },
23
+ ignore: { type: "string", multiple: true, default: [] },
24
+ "no-color": { type: "boolean", default: false },
25
+ help: { type: "boolean", short: "h", default: false },
26
+ version: { type: "boolean", short: "v", default: false },
27
+ };
28
+
29
+ function helpText() {
30
+ return `ecdsa-scan — find digital-signature defects in source code
31
+
32
+ USAGE
33
+ ecdsa-scan [path] scan a directory or a single file (default: .)
34
+ ecdsa-scan rules list the rules and their default confidence
35
+ ecdsa-scan --help | --version
36
+ (installed as both \`ecdsa-scan\` and \`ecdsa\`; \`ecdsa scan [path]\` works too)
37
+
38
+ OPTIONS
39
+ --json machine-readable report on stdout
40
+ --sarif [file] write SARIF 2.1.0 (default: ${DEFAULT_SARIF_PATH}, "-" for stdout)
41
+ --min-confidence <level> confirmed | suspected | advisory (default: advisory)
42
+ --ignore <glob> skip paths matching a glob; repeatable
43
+ --no-color plain output (also honours NO_COLOR)
44
+ -h, --help this text
45
+ -v, --version print version
46
+
47
+ CONFIDENCE
48
+ confirmed an unambiguous defect — fix it
49
+ suspected very likely wrong, but the surrounding code decides
50
+ advisory worth a look; expect legitimate matches here
51
+
52
+ EXIT CODES
53
+ 0 no confirmed findings
54
+ 1 at least one confirmed finding (use this to gate CI)
55
+ 2 usage error, or the path could not be read
56
+
57
+ EXAMPLES
58
+ ecdsa scan
59
+ ecdsa scan ./src --min-confidence suspected
60
+ ecdsa scan . --ignore "**/*.test.ts" --ignore examples
61
+ ecdsa scan . --sarif results.sarif # upload to GitHub Code Scanning
62
+ ecdsa scan . --json | jq '.inventory'
63
+
64
+ Languages: JavaScript, TypeScript, Python, Go (plus key files: .pem/.key/.p12).
65
+ Skipped automatically: node_modules, .git, dist, build, vendor, .next, and more.
66
+ Always: static pattern matching. It misses things, and it flags correct code.
67
+ `;
68
+ }
69
+
70
+ function rulesText() {
71
+ const lines = ["ID CONFIDENCE SEVERITY TITLE"];
72
+ for (const rule of allRules) {
73
+ const confidence = rule.kind === "inventory" ? "n/a" : rule.confidence;
74
+ const severity = rule.kind === "inventory" ? "n/a" : rule.severity;
75
+ lines.push(`${rule.id.padEnd(35)} ${confidence.padEnd(11)} ${severity.padEnd(9)} ${rule.title}`);
76
+ }
77
+ lines.push("");
78
+ lines.push(`${defectRules.length} defect rules + 1 inventory collector.`);
79
+ lines.push("Some rules downgrade or upgrade a finding's confidence based on the surrounding code.");
80
+ return lines.join("\n");
81
+ }
82
+
83
+ /**
84
+ * `--sarif` may be used without a value; parseArgs would reject that, so the
85
+ * default path is injected before parsing.
86
+ */
87
+ function normalizeArgv(argv) {
88
+ const out = [];
89
+ for (let i = 0; i < argv.length; i++) {
90
+ out.push(argv[i]);
91
+ if (argv[i] === "--sarif") {
92
+ const next = argv[i + 1];
93
+ // `-` means stdout and is a real value; any other flag means the value
94
+ // was omitted, so the default file name is inserted.
95
+ if (next === undefined || (next.startsWith("-") && next !== "-")) out.push(DEFAULT_SARIF_PATH);
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function colorEnabled(values) {
102
+ if (values["no-color"] || process.env.NO_COLOR) return false;
103
+ return Boolean(process.stdout.isTTY);
104
+ }
105
+
106
+ export async function main(argv = process.argv.slice(2)) {
107
+ let parsed;
108
+ try {
109
+ parsed = parseArgs({ args: normalizeArgv(argv), options: OPTIONS, allowPositionals: true });
110
+ } catch (err) {
111
+ process.stderr.write(`ecdsa: ${err.message}\n\nRun \`ecdsa --help\`.\n`);
112
+ return 2;
113
+ }
114
+ const { values, positionals } = parsed;
115
+
116
+ if (values.version) {
117
+ process.stdout.write(`${TOOL_VERSION}\n`);
118
+ return 0;
119
+ }
120
+
121
+ let command = positionals[0] ?? (values.help ? "help" : "scan");
122
+ let targetArg = positionals[1];
123
+ // `ecdsa-scan .` / `ecdsa-scan ./src`: an existing path in command position
124
+ // implies the scan command, so the published binary works without a verb.
125
+ if (!["scan", "rules", "help"].includes(command) && existsSync(path.resolve(command))) {
126
+ targetArg = command;
127
+ command = "scan";
128
+ }
129
+ if (command === "help" || values.help) {
130
+ process.stdout.write(helpText());
131
+ return 0;
132
+ }
133
+ if (command === "rules") {
134
+ process.stdout.write(`${rulesText()}\n`);
135
+ return 0;
136
+ }
137
+ if (command !== "scan") {
138
+ process.stderr.write(`ecdsa: unknown command "${command}". Run \`ecdsa --help\`.\n`);
139
+ return 2;
140
+ }
141
+
142
+ const minConfidence = values["min-confidence"];
143
+ if (!CONFIDENCE_LEVELS.includes(minConfidence)) {
144
+ process.stderr.write(
145
+ `ecdsa: --min-confidence must be one of ${CONFIDENCE_LEVELS.join(", ")} (got "${minConfidence}").\n`
146
+ );
147
+ return 2;
148
+ }
149
+
150
+ const target = path.resolve(targetArg ?? ".");
151
+ let result;
152
+ try {
153
+ result = await scan(target, { ignore: values.ignore });
154
+ } catch (err) {
155
+ process.stderr.write(`ecdsa: cannot scan ${target}: ${err.message}\n`);
156
+ return 2;
157
+ }
158
+
159
+ result.findings = filterByConfidence(result.findings, minConfidence);
160
+ const confirmed = result.findings.filter((f) => f.confidence === "confirmed").length;
161
+
162
+ if (values.sarif) {
163
+ const sarif = formatSarif(result);
164
+ if (values.sarif === "-") {
165
+ process.stdout.write(`${sarif}\n`);
166
+ } else {
167
+ const sarifPath = path.resolve(values.sarif);
168
+ try {
169
+ await writeFile(sarifPath, `${sarif}\n`, "utf8");
170
+ } catch (err) {
171
+ process.stderr.write(`ecdsa: cannot write ${sarifPath}: ${err.message}\n`);
172
+ return 2;
173
+ }
174
+ if (!values.json) process.stderr.write(`SARIF written to ${sarifPath}\n`);
175
+ }
176
+ }
177
+
178
+ if (values.json) {
179
+ process.stdout.write(`${formatJson(result)}\n`);
180
+ } else if (values.sarif !== "-") {
181
+ process.stdout.write(
182
+ `${formatText(result, { color: colorEnabled(values), width: process.stdout.columns ?? 100 })}\n`
183
+ );
184
+ }
185
+
186
+ return confirmed > 0 ? 1 : 0;
187
+ }
188
+
189
+ // Only run when executed directly, so tests can import main(). argv[1] is
190
+ // resolved through realpath because npm installs the bin as a symlink in
191
+ // node_modules/.bin (and /tmp-style symlinked paths exist too): node resolves
192
+ // the real file for import.meta.url while argv[1] keeps the symlink.
193
+ function isMainModule() {
194
+ if (!process.argv[1]) return false;
195
+ try {
196
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ if (isMainModule()) {
203
+ main().then(
204
+ (code) => {
205
+ process.exitCode = code;
206
+ },
207
+ (err) => {
208
+ process.stderr.write(`ecdsa: unexpected failure: ${err?.stack ?? err}\n`);
209
+ process.exitCode = 2;
210
+ }
211
+ );
212
+ }
@@ -0,0 +1,62 @@
1
+ // Minimal glob matching for --ignore. Supports `*`, `**`, `?` and `{a,b}`.
2
+ // Patterns are matched against the path relative to the scan root (always with
3
+ // forward slashes); a pattern without a slash also matches a bare file name,
4
+ // which is what people expect from `--ignore "*.test.js"`.
5
+
6
+ function toRegExpSource(pattern) {
7
+ let out = "";
8
+ for (let i = 0; i < pattern.length; i++) {
9
+ const ch = pattern[i];
10
+ if (ch === "*") {
11
+ if (pattern[i + 1] === "*") {
12
+ // `**/` swallows any number of directories, including none.
13
+ if (pattern[i + 2] === "/") {
14
+ out += "(?:.*/)?";
15
+ i += 2;
16
+ } else {
17
+ out += ".*";
18
+ i += 1;
19
+ }
20
+ } else {
21
+ out += "[^/]*";
22
+ }
23
+ continue;
24
+ }
25
+ if (ch === "?") {
26
+ out += "[^/]";
27
+ continue;
28
+ }
29
+ if (ch === "{") {
30
+ const close = pattern.indexOf("}", i);
31
+ if (close !== -1) {
32
+ const options = pattern.slice(i + 1, close).split(",");
33
+ out += `(?:${options.map((o) => toRegExpSource(o)).join("|")})`;
34
+ i = close;
35
+ continue;
36
+ }
37
+ }
38
+ out += ch.replace(/[.+^$()|[\]\\]/g, "\\$&");
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * Compile ignore patterns into a predicate over the root-relative posix path.
45
+ * A pattern also matches everything below a directory it names.
46
+ */
47
+ export function compileIgnore(patterns) {
48
+ const compiled = (patterns ?? []).filter(Boolean).map((pattern) => {
49
+ const clean = pattern.replace(/^\.\//, "").replace(/\/+$/, "");
50
+ const src = toRegExpSource(clean);
51
+ return {
52
+ pattern,
53
+ bare: !clean.includes("/"),
54
+ // The pattern itself, or the pattern used as a directory prefix.
55
+ re: new RegExp(`^(?:${src})(?:/.*)?$`),
56
+ };
57
+ });
58
+ return (relPath) => {
59
+ const base = relPath.split("/").pop();
60
+ return compiled.some((c) => c.re.test(relPath) || (c.bare && c.re.test(base)));
61
+ };
62
+ }